diff options
Diffstat (limited to 'packages/openai-stream/src/parse-sse.test.ts')
| -rw-r--r-- | packages/openai-stream/src/parse-sse.test.ts | 456 |
1 files changed, 228 insertions, 228 deletions
diff --git a/packages/openai-stream/src/parse-sse.test.ts b/packages/openai-stream/src/parse-sse.test.ts index 1910833..dcc7d2b 100644 --- a/packages/openai-stream/src/parse-sse.test.ts +++ b/packages/openai-stream/src/parse-sse.test.ts @@ -3,261 +3,261 @@ import { describe, expect, it } from "vitest"; import { parseSSELines } from "./parse-sse.js"; describe("parseSSELines", () => { - it("parses text delta events", () => { - const lines = [ - 'data: {"id":"chatcmpl-1","choices":[{"delta":{"content":"Hello"},"index":0}]}', - 'data: {"id":"chatcmpl-1","choices":[{"delta":{"content":" world"},"index":0}]}', - 'data: {"id":"chatcmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - "data: [DONE]", - ]; + it("parses text delta events", () => { + const lines = [ + 'data: {"id":"chatcmpl-1","choices":[{"delta":{"content":"Hello"},"index":0}]}', + 'data: {"id":"chatcmpl-1","choices":[{"delta":{"content":" world"},"index":0}]}', + 'data: {"id":"chatcmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "text-delta", delta: "Hello" }, - { type: "text-delta", delta: " world" }, - { type: "finish", reason: "stop" }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "text-delta", delta: "Hello" }, + { type: "text-delta", delta: " world" }, + { type: "finish", reason: "stop" }, + ]); + }); - it("parses a fragmented tool_call across chunks", () => { - const lines = [ - 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_abc","function":{"name":"read_file","arguments":""}}]},"index":0}]}', - 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\""}}]},"index":0}]}', - 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":":\\"main.ts\\"}"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-2","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', - "data: [DONE]", - ]; + it("parses a fragmented tool_call across chunks", () => { + const lines = [ + 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_abc","function":{"name":"read_file","arguments":""}}]},"index":0}]}', + 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\""}}]},"index":0}]}', + 'data: {"id":"chatcmpl-2","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":":\\"main.ts\\"}"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-2","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { - type: "tool-call", - toolCallId: "call_abc", - toolName: "read_file", - input: { path: "main.ts" }, - }, - { type: "finish", reason: "tool_calls" }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { + type: "tool-call", + toolCallId: "call_abc", + toolName: "read_file", + input: { path: "main.ts" }, + }, + { type: "finish", reason: "tool_calls" }, + ]); + }); - it("parses multiple tool_calls in one response", () => { - const lines = [ - 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"read_file","arguments":""}}]},"index":0}]}', - 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\":\\"a.ts\\"}"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":1,"id":"call_2","function":{"name":"read_file","arguments":""}}]},"index":0}]}', - 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"path\\":\\"b.ts\\"}"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-3","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', - "data: [DONE]", - ]; + it("parses multiple tool_calls in one response", () => { + const lines = [ + 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"read_file","arguments":""}}]},"index":0}]}', + 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"path\\":\\"a.ts\\"}"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":1,"id":"call_2","function":{"name":"read_file","arguments":""}}]},"index":0}]}', + 'data: {"id":"chatcmpl-3","choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"path\\":\\"b.ts\\"}"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-3","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "tool-call", toolCallId: "call_1", toolName: "read_file", input: { path: "a.ts" } }, - { type: "tool-call", toolCallId: "call_2", toolName: "read_file", input: { path: "b.ts" } }, - { type: "finish", reason: "tool_calls" }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "tool-call", toolCallId: "call_1", toolName: "read_file", input: { path: "a.ts" } }, + { type: "tool-call", toolCallId: "call_2", toolName: "read_file", input: { path: "b.ts" } }, + { type: "finish", reason: "tool_calls" }, + ]); + }); - it("parses usage from the final chunk", () => { - const lines = [ - 'data: {"id":"chatcmpl-4","choices":[{"delta":{"content":"Hi"},"index":0}]}', - 'data: {"id":"chatcmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-4","usage":{"prompt_tokens":10,"completion_tokens":5}}', - "data: [DONE]", - ]; + it("parses usage from the final chunk", () => { + const lines = [ + 'data: {"id":"chatcmpl-4","choices":[{"delta":{"content":"Hi"},"index":0}]}', + 'data: {"id":"chatcmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-4","usage":{"prompt_tokens":10,"completion_tokens":5}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "text-delta", delta: "Hi" }, - { type: "finish", reason: "stop" }, - { - type: "usage", - usage: { - inputTokens: 10, - outputTokens: 5, - cacheReadTokens: undefined, - cacheWriteTokens: undefined, - }, - }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "text-delta", delta: "Hi" }, + { type: "finish", reason: "stop" }, + { + type: "usage", + usage: { + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + }, + ]); + }); - it("parses reasoning_content deltas", () => { - const lines = [ - 'data: {"id":"chatcmpl-5","choices":[{"delta":{"reasoning_content":"Let me think..."},"index":0}]}', - 'data: {"id":"chatcmpl-5","choices":[{"delta":{"content":"Here is my answer."},"index":0}]}', - 'data: {"id":"chatcmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - "data: [DONE]", - ]; + it("parses reasoning_content deltas", () => { + const lines = [ + 'data: {"id":"chatcmpl-5","choices":[{"delta":{"reasoning_content":"Let me think..."},"index":0}]}', + 'data: {"id":"chatcmpl-5","choices":[{"delta":{"content":"Here is my answer."},"index":0}]}', + 'data: {"id":"chatcmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "reasoning-delta", delta: "Let me think..." }, - { type: "text-delta", delta: "Here is my answer." }, - { type: "finish", reason: "stop" }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "reasoning-delta", delta: "Let me think..." }, + { type: "text-delta", delta: "Here is my answer." }, + { type: "finish", reason: "stop" }, + ]); + }); - it("handles invalid JSON gracefully", () => { - const lines = [ - "data: {invalid json}", - 'data: {"id":"chatcmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}', - "data: [DONE]", - ]; + it("handles invalid JSON gracefully", () => { + const lines = [ + "data: {invalid json}", + 'data: {"id":"chatcmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toHaveLength(2); - expect(events[0]?.type).toBe("error"); - expect(events[1]).toEqual({ type: "text-delta", delta: "ok" }); - }); + const events = parseSSELines(lines); + expect(events).toHaveLength(2); + expect(events[0]?.type).toBe("error"); + expect(events[1]).toEqual({ type: "text-delta", delta: "ok" }); + }); - it("ignores non-data lines", () => { - const lines = [ - "event: message", - ": comment line", - 'data: {"id":"chatcmpl-7","choices":[{"delta":{"content":"hi"},"index":0}]}', - "", - "data: [DONE]", - ]; + it("ignores non-data lines", () => { + const lines = [ + "event: message", + ": comment line", + 'data: {"id":"chatcmpl-7","choices":[{"delta":{"content":"hi"},"index":0}]}', + "", + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([{ type: "text-delta", delta: "hi" }]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([{ type: "text-delta", delta: "hi" }]); + }); - it("stops at [DONE] sentinel", () => { - const lines = [ - 'data: {"id":"chatcmpl-8","choices":[{"delta":{"content":"before"},"index":0}]}', - "data: [DONE]", - 'data: {"id":"chatcmpl-8","choices":[{"delta":{"content":"after"},"index":0}]}', - ]; + it("stops at [DONE] sentinel", () => { + const lines = [ + 'data: {"id":"chatcmpl-8","choices":[{"delta":{"content":"before"},"index":0}]}', + "data: [DONE]", + 'data: {"id":"chatcmpl-8","choices":[{"delta":{"content":"after"},"index":0}]}', + ]; - const events = parseSSELines(lines); - expect(events).toEqual([{ type: "text-delta", delta: "before" }]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([{ type: "text-delta", delta: "before" }]); + }); - it("parses nested prompt_tokens_details.cached_tokens → cacheReadTokens", () => { - const lines = [ - 'data: {"id":"chatcmpl-nested","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-nested","usage":{"prompt_tokens":665,"completion_tokens":90,"prompt_tokens_details":{"cached_tokens":384},"completion_tokens_details":{"reasoning_tokens":86}}}', - "data: [DONE]", - ]; + it("parses nested prompt_tokens_details.cached_tokens → cacheReadTokens", () => { + const lines = [ + 'data: {"id":"chatcmpl-nested","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-nested","usage":{"prompt_tokens":665,"completion_tokens":90,"prompt_tokens_details":{"cached_tokens":384},"completion_tokens_details":{"reasoning_tokens":86}}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - const usageEvent = events.find((e) => e.type === "usage") as Extract< - ProviderEvent, - { type: "usage" } - >; - expect(usageEvent.usage.inputTokens).toBe(665); - expect(usageEvent.usage.outputTokens).toBe(90); - expect(usageEvent.usage.cacheReadTokens).toBe(384); - expect(usageEvent.usage.cacheWriteTokens).toBeUndefined(); - }); + const events = parseSSELines(lines); + const usageEvent = events.find((e) => e.type === "usage") as Extract< + ProviderEvent, + { type: "usage" } + >; + expect(usageEvent.usage.inputTokens).toBe(665); + expect(usageEvent.usage.outputTokens).toBe(90); + expect(usageEvent.usage.cacheReadTokens).toBe(384); + expect(usageEvent.usage.cacheWriteTokens).toBeUndefined(); + }); - it("flat cache_read_tokens takes precedence over nested cached_tokens", () => { - const lines = [ - 'data: {"id":"chatcmpl-both","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-both","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":50,"prompt_tokens_details":{"cached_tokens":99}}}', - "data: [DONE]", - ]; + it("flat cache_read_tokens takes precedence over nested cached_tokens", () => { + const lines = [ + 'data: {"id":"chatcmpl-both","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-both","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":50,"prompt_tokens_details":{"cached_tokens":99}}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - const usageEvent = events.find((e) => e.type === "usage") as Extract< - ProviderEvent, - { type: "usage" } - >; - expect(usageEvent.usage.cacheReadTokens).toBe(50); - }); + const events = parseSSELines(lines); + const usageEvent = events.find((e) => e.type === "usage") as Extract< + ProviderEvent, + { type: "usage" } + >; + expect(usageEvent.usage.cacheReadTokens).toBe(50); + }); - it("returns undefined for cacheReadTokens when neither flat nor nested present", () => { - const lines = [ - 'data: {"id":"chatcmpl-none","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-none","usage":{"prompt_tokens":10,"completion_tokens":5}}', - "data: [DONE]", - ]; + it("returns undefined for cacheReadTokens when neither flat nor nested present", () => { + const lines = [ + 'data: {"id":"chatcmpl-none","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-none","usage":{"prompt_tokens":10,"completion_tokens":5}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - const usageEvent = events.find((e) => e.type === "usage") as Extract< - ProviderEvent, - { type: "usage" } - >; - expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); - expect(usageEvent.usage.cacheWriteTokens).toBeUndefined(); - }); + const events = parseSSELines(lines); + const usageEvent = events.find((e) => e.type === "usage") as Extract< + ProviderEvent, + { type: "usage" } + >; + expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); + expect(usageEvent.usage.cacheWriteTokens).toBeUndefined(); + }); - it("handles missing/partial prompt_tokens_details safely", () => { - const lines = [ - 'data: {"id":"chatcmpl-partial","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-partial","usage":{"prompt_tokens":50,"completion_tokens":10,"prompt_tokens_details":{}}}', - "data: [DONE]", - ]; + it("handles missing/partial prompt_tokens_details safely", () => { + const lines = [ + 'data: {"id":"chatcmpl-partial","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-partial","usage":{"prompt_tokens":50,"completion_tokens":10,"prompt_tokens_details":{}}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - const usageEvent = events.find((e) => e.type === "usage") as Extract< - ProviderEvent, - { type: "usage" } - >; - expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); - }); + const events = parseSSELines(lines); + const usageEvent = events.find((e) => e.type === "usage") as Extract< + ProviderEvent, + { type: "usage" } + >; + expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); + }); - it("handles empty prompt_tokens_details object safely", () => { - const lines = [ - 'data: {"id":"chatcmpl-empty","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', - 'data: {"id":"chatcmpl-empty","usage":{"prompt_tokens":30,"completion_tokens":8,"prompt_tokens_details":null}}', - "data: [DONE]", - ]; + it("handles empty prompt_tokens_details object safely", () => { + const lines = [ + 'data: {"id":"chatcmpl-empty","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', + 'data: {"id":"chatcmpl-empty","usage":{"prompt_tokens":30,"completion_tokens":8,"prompt_tokens_details":null}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - const usageEvent = events.find((e) => e.type === "usage") as Extract< - ProviderEvent, - { type: "usage" } - >; - expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); - }); + const events = parseSSELines(lines); + const usageEvent = events.find((e) => e.type === "usage") as Extract< + ProviderEvent, + { type: "usage" } + >; + expect(usageEvent.usage.cacheReadTokens).toBeUndefined(); + }); - it("handles a complete turn with text, tool call, usage, and finish", () => { - const lines = [ - 'data: {"id":"chatcmpl-9","choices":[{"delta":{"content":"Let me check."},"index":0}]}', - 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_xyz","function":{"name":"search","arguments":""}}]},"index":0}]}', - 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"query\\":"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"dispatch\\"}"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-9","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', - 'data: {"id":"chatcmpl-9","usage":{"prompt_tokens":50,"completion_tokens":20}}', - "data: [DONE]", - ]; + it("handles a complete turn with text, tool call, usage, and finish", () => { + const lines = [ + 'data: {"id":"chatcmpl-9","choices":[{"delta":{"content":"Let me check."},"index":0}]}', + 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_xyz","function":{"name":"search","arguments":""}}]},"index":0}]}', + 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"query\\":"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-9","choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"dispatch\\"}"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-9","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', + 'data: {"id":"chatcmpl-9","usage":{"prompt_tokens":50,"completion_tokens":20}}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "text-delta", delta: "Let me check." }, - { - type: "tool-call", - toolCallId: "call_xyz", - toolName: "search", - input: { query: "dispatch" }, - }, - { type: "finish", reason: "tool_calls" }, - { - type: "usage", - usage: { - inputTokens: 50, - outputTokens: 20, - cacheReadTokens: undefined, - cacheWriteTokens: undefined, - }, - }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "text-delta", delta: "Let me check." }, + { + type: "tool-call", + toolCallId: "call_xyz", + toolName: "search", + input: { query: "dispatch" }, + }, + { type: "finish", reason: "tool_calls" }, + { + type: "usage", + usage: { + inputTokens: 50, + outputTokens: 20, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + }, + ]); + }); - it("handles tool_call with unparseable arguments as raw string", () => { - const lines = [ - 'data: {"id":"chatcmpl-10","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_bad","function":{"name":"foo","arguments":"not-json"}}]},"index":0}]}', - 'data: {"id":"chatcmpl-10","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', - "data: [DONE]", - ]; + it("handles tool_call with unparseable arguments as raw string", () => { + const lines = [ + 'data: {"id":"chatcmpl-10","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_bad","function":{"name":"foo","arguments":"not-json"}}]},"index":0}]}', + 'data: {"id":"chatcmpl-10","choices":[{"delta":{},"finish_reason":"tool_calls","index":0}]}', + "data: [DONE]", + ]; - const events = parseSSELines(lines); - expect(events).toEqual([ - { type: "tool-call", toolCallId: "call_bad", toolName: "foo", input: "not-json" }, - { type: "finish", reason: "tool_calls" }, - ]); - }); + const events = parseSSELines(lines); + expect(events).toEqual([ + { type: "tool-call", toolCallId: "call_bad", toolName: "foo", input: "not-json" }, + { type: "finish", reason: "tool_calls" }, + ]); + }); }); |
