summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src/parse-sse.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/openai-stream/src/parse-sse.test.ts')
-rw-r--r--packages/openai-stream/src/parse-sse.test.ts456
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" },
+ ]);
+ });
});