import type { ProviderEvent } from "@dispatch/kernel"; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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}]}', ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; 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]", ]; const events = parseSSELines(lines); expect(events).toEqual([ { type: "tool-call", toolCallId: "call_bad", toolName: "foo", input: "not-json" }, { type: "finish", reason: "tool_calls" }, ]); }); });