From fb01944e3ad0f47f53d26b7fb39798a9fc2e3fdd Mon Sep 17 00:00:00 2001 From: Till Kolter Date: Thu, 5 Dec 2024 16:36:45 +0100 Subject: [PATCH] fix: Leading whitespace chunks break partial parser --- src/lib/ChatCompletionStream.ts | 4 +- tests/lib/ChatCompletionStream.test.ts | 41 +++++++++++++++++++ .../ChatCompletionStream.test.ts.snap | 34 +++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/lib/ChatCompletionStream.ts b/src/lib/ChatCompletionStream.ts index a88f8a23b..80bac5607 100644 --- a/src/lib/ChatCompletionStream.ts +++ b/src/lib/ChatCompletionStream.ts @@ -509,7 +509,9 @@ export class ChatCompletionStream choice.message.content = (choice.message.content || '') + content; if (!choice.message.refusal && this.#getAutoParseableResponseFormat()) { - choice.message.parsed = partialParse(choice.message.content); + // Even a partial parser does not accept empty string + const trimmed = choice.message.content.trimStart(); + choice.message.parsed = trimmed ? partialParse(trimmed) : null; } } diff --git a/tests/lib/ChatCompletionStream.test.ts b/tests/lib/ChatCompletionStream.test.ts index e5ef20c9e..89bc4ba8e 100644 --- a/tests/lib/ChatCompletionStream.test.ts +++ b/tests/lib/ChatCompletionStream.test.ts @@ -45,6 +45,47 @@ describe('.stream()', () => { `); }); + it('is robust against leading newline chunks', async () => { + const stream = await makeStreamSnapshotRequest((openai) => + openai.beta.chat.completions.stream({ + model: 'gpt-4o-2024-08-06', + messages: [ + { + role: 'user', + content: "What's the weather like in SF?", + }, + ], + response_format: zodResponseFormat( + z.object({ + city: z.string(), + units: z.enum(['c', 'f']).default('f'), + }), + 'location', + ), + }), + ); + + expect((await stream.finalChatCompletion()).choices[0]).toMatchInlineSnapshot(` + { + "finish_reason": "stop", + "index": 0, + "logprobs": null, + "message": { + "content": " + + {"city":"San Francisco","units":"c"}", + "parsed": { + "city": "San Francisco", + "units": "c", + }, + "refusal": null, + "role": "assistant", + "tool_calls": [], + }, + } + `); + }); + it('emits content logprobs events', async () => { var capturedLogProbs: ChatCompletionTokenLogprob[] | undefined; diff --git a/tests/lib/__snapshots__/ChatCompletionStream.test.ts.snap b/tests/lib/__snapshots__/ChatCompletionStream.test.ts.snap index 65740382e..d40d60362 100644 --- a/tests/lib/__snapshots__/ChatCompletionStream.test.ts.snap +++ b/tests/lib/__snapshots__/ChatCompletionStream.test.ts.snap @@ -99,3 +99,37 @@ data: [DONE] " `; + +exports[`.stream() is robust against leading newline chunks 1`] = ` +"data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"\\n\\n"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"{\\""},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"city"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"\\":\\""},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"San"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":" Francisco"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"\\",\\""},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"units"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"\\":\\""},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{"content":"\\"}"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + +data: {"id":"chatcmpl-9tZXEmwtoDf6vqCqEWSvDP8jx9OXe","object":"chat.completion.chunk","created":1723031664,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_845eaabc1f","choices":[],"usage":{"prompt_tokens":17,"completion_tokens":10,"total_tokens":27}} + +data: [DONE] + +" +`;