Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions src/lib/ChatCompletionStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,27 +205,28 @@ export class ChatCompletionStream<ParsedT = null>

for (const choice of chunk.choices) {
const choiceSnapshot = completion.choices[choice.index]!;
const { delta } = choice;

if (
choice.delta.content != null &&
delta?.content != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.content
) {
this._emit('content', choice.delta.content, choiceSnapshot.message.content);
this._emit('content', delta.content, choiceSnapshot.message.content);
this._emit('content.delta', {
delta: choice.delta.content,
delta: delta.content,
snapshot: choiceSnapshot.message.content,
parsed: choiceSnapshot.message.parsed,
});
}

if (
choice.delta.refusal != null &&
delta?.refusal != null &&
choiceSnapshot.message?.role === 'assistant' &&
choiceSnapshot.message?.refusal
) {
this._emit('refusal.delta', {
delta: choice.delta.refusal,
delta: delta.refusal,
snapshot: choiceSnapshot.message.refusal,
});
}
Expand Down Expand Up @@ -254,7 +255,7 @@ export class ChatCompletionStream<ParsedT = null>
}
}

for (const toolCall of choice.delta.tool_calls ?? []) {
for (const toolCall of delta?.tool_calls ?? []) {
if (state.current_tool_call_index !== toolCall.index) {
this.#emitContentDoneEvents(choiceSnapshot);

Expand All @@ -267,7 +268,7 @@ export class ChatCompletionStream<ParsedT = null>
state.current_tool_call_index = toolCall.index;
}

for (const toolCallDelta of choice.delta.tool_calls ?? []) {
for (const toolCallDelta of delta?.tool_calls ?? []) {
const toolCallSnapshot = choiceSnapshot.message.tool_calls?.[toolCallDelta.index];
if (!toolCallSnapshot?.type) {
continue;
Expand Down Expand Up @@ -401,13 +402,13 @@ export class ChatCompletionStream<ParsedT = null>
const stream = Stream.fromReadableStream<ChatCompletionChunk>(readableStream, this.controller);
let chatId;
for await (const chunk of stream) {
if (chatId && chatId !== chunk.id) {
if (chatId && chunk.id && chatId !== chunk.id) {
Comment thread
HAYDEN-OAI marked this conversation as resolved.
// A new request has been made.
this._addChatCompletion(this.#endRequest());
}

this.#addChunk(chunk);
chatId = chunk.id;
if (chunk.id) chatId = chunk.id;
}
if (stream.controller.signal?.aborted) {
throw new APIUserAbortError();
Expand All @@ -432,7 +433,7 @@ export class ChatCompletionStream<ParsedT = null>
...rest,
choices: [],
};
} else {
} else if (chunk.id) {
Object.assign(snapshot, rest);
}

Expand Down
60 changes: 60 additions & 0 deletions tests/lib/ChatCompletionStream.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import OpenAI from 'openai';
import { zodResponseFormat } from 'openai/helpers/zod';
import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
import { ChatCompletionStreamingRunner } from 'openai/lib/ChatCompletionStreamingRunner';
import { ChatCompletionTokenLogprob } from 'openai/resources';
import { Stream } from 'openai/streaming';
import { z } from 'zod/v4';
import { makeStreamSnapshotRequest } from '../utils/mock-snapshots';

Expand Down Expand Up @@ -58,6 +60,64 @@ describe('.stream()', () => {
expect(removeEventListenerSpy).toHaveBeenCalledWith('abort', abortListener);
});

it('handles Azure async filter chunks without deltas', async () => {
const chunks: OpenAI.Chat.ChatCompletionChunk[] = [
{
id: 'chatcmpl-test',
object: 'chat.completion.chunk',
created: 1,
model: 'gpt-test',
choices: [
{
index: 0,
delta: { role: 'assistant', content: 'hello' },
finish_reason: null,
logprobs: null,
},
],
},
{
id: 'chatcmpl-test',
object: 'chat.completion.chunk',
created: 1,
model: 'gpt-test',
choices: [
{
index: 0,
delta: {},
finish_reason: 'stop',
logprobs: null,
},
],
},
{
id: '',
object: '',
created: 0,
model: '',
choices: [
{
index: 0,
finish_reason: null,
content_filter_results: {},
},
],
} as unknown as OpenAI.Chat.ChatCompletionChunk,
];
const readable = new Stream(async function* () {
for (const chunk of chunks) yield chunk;
}, new AbortController()).toReadableStream();

const stream = ChatCompletionStreamingRunner.fromReadableStream(readable);

await expect(stream.finalChatCompletion()).resolves.toMatchObject({
id: 'chatcmpl-test',
created: 1,
model: 'gpt-test',
choices: [{ message: { content: 'hello' } }],
});
});

it('works', async () => {
const stream = await makeStreamSnapshotRequest((openai) =>
openai.chat.completions.stream({
Expand Down
Loading