summaryrefslogtreecommitdiffhomepage
path: root/packages/openai-stream/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/openai-stream/src')
-rw-r--r--packages/openai-stream/src/__fixtures__/flash-text-turn.json48
-rw-r--r--packages/openai-stream/src/__fixtures__/tool-call-turn.json46
-rw-r--r--packages/openai-stream/src/convert-messages.test.ts810
-rw-r--r--packages/openai-stream/src/convert-messages.ts203
-rw-r--r--packages/openai-stream/src/convert-tools.test.ts188
-rw-r--r--packages/openai-stream/src/convert-tools.ts30
-rw-r--r--packages/openai-stream/src/getUsage.test.ts139
-rw-r--r--packages/openai-stream/src/getUsage.ts73
-rw-r--r--packages/openai-stream/src/index.ts11
-rw-r--r--packages/openai-stream/src/listModels.test.ts219
-rw-r--r--packages/openai-stream/src/listModels.ts114
-rw-r--r--packages/openai-stream/src/parse-sse.test.ts456
-rw-r--r--packages/openai-stream/src/parse-sse.ts222
-rw-r--r--packages/openai-stream/src/provider.test.ts279
-rw-r--r--packages/openai-stream/src/provider.ts113
-rw-r--r--packages/openai-stream/src/stream.test.ts1652
-rw-r--r--packages/openai-stream/src/stream.ts786
17 files changed, 2922 insertions, 2467 deletions
diff --git a/packages/openai-stream/src/__fixtures__/flash-text-turn.json b/packages/openai-stream/src/__fixtures__/flash-text-turn.json
index d7e71cb..34d0d33 100644
--- a/packages/openai-stream/src/__fixtures__/flash-text-turn.json
+++ b/packages/openai-stream/src/__fixtures__/flash-text-turn.json
@@ -1,26 +1,26 @@
{
- "request": {
- "method": "POST",
- "url": "https://opencode.ai/zen/go/v1/chat/completions",
- "headers": {
- "Content-Type": "application/json",
- "Authorization": "Bearer sk-…redacted…UN0"
- },
- "body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in exactly three words.\"},{\"role\":\"assistant\",\"content\":\"The user asks: \\\"Say hello in exactly three words.\\\" So I need to respond with exactly three words. The obvious answer is \\\"Hello, how are you?\\\" But that's four words. Actually \\\"Hello there friend\\\" is three words. Or \\\"Hello, my friend\\\"? That's three words if we count \\\"Hello,\\\" as one word? Better to be simple: \\\"Hello, world!\\\" is two words. \\\"Hello everyone here\\\" is three. I'll go with \\\"Hello there friend.\\\" But let's make sure it's exactly three words. I'll respond: \\\"Hello, my friend.\\\" That's three words: Hello, my, friend. But includes a comma? Usually words mean space-separated tokens. So \\\"Hello my friend\\\" is three words. I'll respond with that.\\n\\nBut the instruction is to say hello in exactly three words. I'll just output: \\\"Hello my friend\\\"Hello my friend\"},{\"role\":\"user\",\"content\":\"Say hello in exactly three words.\"}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read_file\",\"description\":\"Read the contents of a file. Returns lines with 1-indexed line numbers. Supports offset/limit for reading specific sections of large files.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file, relative to the working directory.\"},\"offset\":{\"type\":\"number\",\"description\":\"1-indexed start line number (default: 1).\",\"default\":1},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to return (default: 500, hard cap: 5000).\",\"default\":500}},\"required\":[\"path\"]}}}]}"
- },
- "response": {
- "status": 200,
- "statusText": "OK",
- "headers": {
- "cache-control": "no-cache",
- "cf-placement": "remote-ORD",
- "cf-ray": "a06dc6711bb51f51-DEN",
- "connection": "keep-alive",
- "content-type": "text/event-stream; charset=utf-8",
- "date": "Fri, 05 Jun 2026 08:23:26 GMT",
- "server": "cloudflare",
- "transfer-encoding": "chunked"
- },
- "body": "data: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"reasoning_content\":\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" user\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" asks\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Say\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" need\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" respond\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" already\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" said\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" my\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" but\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" that\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" might\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" have\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" been\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" cut\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" off\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" Let\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" me\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" respond\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" again\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\":\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" world\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"!\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" there\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" you\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" everyone\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"'ll\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" use\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" there\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" there\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" friend\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":665,\"completion_tokens\":90,\"total_tokens\":755,\"prompt_tokens_details\":{\"cached_tokens\":384},\"completion_tokens_details\":{\"reasoning_tokens\":86},\"prompt_cache_hit_tokens\":384,\"prompt_cache_miss_tokens\":281}}\n\ndata: [DONE]\n\ndata: {\"choices\":[],\"cost\":\"0\"}\n\n"
- }
+ "request": {
+ "method": "POST",
+ "url": "https://opencode.ai/zen/go/v1/chat/completions",
+ "headers": {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer sk-…redacted…UN0"
+ },
+ "body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in exactly three words.\"},{\"role\":\"assistant\",\"content\":\"The user asks: \\\"Say hello in exactly three words.\\\" So I need to respond with exactly three words. The obvious answer is \\\"Hello, how are you?\\\" But that's four words. Actually \\\"Hello there friend\\\" is three words. Or \\\"Hello, my friend\\\"? That's three words if we count \\\"Hello,\\\" as one word? Better to be simple: \\\"Hello, world!\\\" is two words. \\\"Hello everyone here\\\" is three. I'll go with \\\"Hello there friend.\\\" But let's make sure it's exactly three words. I'll respond: \\\"Hello, my friend.\\\" That's three words: Hello, my, friend. But includes a comma? Usually words mean space-separated tokens. So \\\"Hello my friend\\\" is three words. I'll respond with that.\\n\\nBut the instruction is to say hello in exactly three words. I'll just output: \\\"Hello my friend\\\"Hello my friend\"},{\"role\":\"user\",\"content\":\"Say hello in exactly three words.\"}],\"stream\":true,\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read_file\",\"description\":\"Read the contents of a file. Returns lines with 1-indexed line numbers. Supports offset/limit for reading specific sections of large files.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to the file, relative to the working directory.\"},\"offset\":{\"type\":\"number\",\"description\":\"1-indexed start line number (default: 1).\",\"default\":1},\"limit\":{\"type\":\"number\",\"description\":\"Maximum number of lines to return (default: 500, hard cap: 5000).\",\"default\":500}},\"required\":[\"path\"]}}}]}"
+ },
+ "response": {
+ "status": 200,
+ "statusText": "OK",
+ "headers": {
+ "cache-control": "no-cache",
+ "cf-placement": "remote-ORD",
+ "cf-ray": "a06dc6711bb51f51-DEN",
+ "connection": "keep-alive",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Fri, 05 Jun 2026 08:23:26 GMT",
+ "server": "cloudflare",
+ "transfer-encoding": "chunked"
+ },
+ "body": "data: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"reasoning_content\":\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" user\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" asks\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Say\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" need\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" respond\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" already\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" said\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" my\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" but\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" that\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" might\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" have\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" been\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" cut\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" off\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" Let\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" me\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" respond\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" again\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" exactly\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\":\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\",\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" world\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"!\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" words\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" there\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" to\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" you\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" three\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" everyone\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" two\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"'ll\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" use\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" \\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" there\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\" friend\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"reasoning_content\":\".\\\"\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" there\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" friend\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"c45495a0-befb-441f-9222-a3109fbbfa78\",\"object\":\"chat.completion.chunk\",\"created\":1780647805,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_8b330d02d0_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"reasoning_content\":null},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":665,\"completion_tokens\":90,\"total_tokens\":755,\"prompt_tokens_details\":{\"cached_tokens\":384},\"completion_tokens_details\":{\"reasoning_tokens\":86},\"prompt_cache_hit_tokens\":384,\"prompt_cache_miss_tokens\":281}}\n\ndata: [DONE]\n\ndata: {\"choices\":[],\"cost\":\"0\"}\n\n"
+ }
}
diff --git a/packages/openai-stream/src/__fixtures__/tool-call-turn.json b/packages/openai-stream/src/__fixtures__/tool-call-turn.json
index 48bdb8d..6f61ac9 100644
--- a/packages/openai-stream/src/__fixtures__/tool-call-turn.json
+++ b/packages/openai-stream/src/__fixtures__/tool-call-turn.json
@@ -1,25 +1,25 @@
{
- "request": {
- "method": "POST",
- "url": "https://api.example.com/v1/chat/completions",
- "headers": {
- "content-type": "application/json",
- "authorization": "Bearer sk-…redacted…xyz"
- },
- "body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"What is the weather in Tokyo?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a location\",\"parameters\":{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\"}},\"required\":[\"location\"]}}}],\"stream\":true}"
- },
- "response": {
- "status": 200,
- "statusText": "OK",
- "headers": {
- "content-type": "text/event-stream",
- "cache-control": "no-cache"
- },
- "body": "data: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_abc123\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"locat\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ion\\\":\\\"Tokyo\\\"}\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":45,\"completion_tokens\":12,\"cache_read_tokens\":30,\"cache_write_tokens\":5}}\n\ndata: [DONE]\n"
- },
- "meta": {
- "description": "Tool-call fixture: user asks about weather → model calls get_weather tool with split argument chunks.",
- "captured_by": "provider-openai-compat record mode",
- "version": 1
- }
+ "request": {
+ "method": "POST",
+ "url": "https://api.example.com/v1/chat/completions",
+ "headers": {
+ "content-type": "application/json",
+ "authorization": "Bearer sk-…redacted…xyz"
+ },
+ "body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"What is the weather in Tokyo?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a location\",\"parameters\":{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\"}},\"required\":[\"location\"]}}}],\"stream\":true}"
+ },
+ "response": {
+ "status": 200,
+ "statusText": "OK",
+ "headers": {
+ "content-type": "text/event-stream",
+ "cache-control": "no-cache"
+ },
+ "body": "data: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_abc123\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"locat\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ion\\\":\\\"Tokyo\\\"}\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-fixture-002\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":45,\"completion_tokens\":12,\"cache_read_tokens\":30,\"cache_write_tokens\":5}}\n\ndata: [DONE]\n"
+ },
+ "meta": {
+ "description": "Tool-call fixture: user asks about weather → model calls get_weather tool with split argument chunks.",
+ "captured_by": "provider-openai-compat record mode",
+ "version": 1
+ }
}
diff --git a/packages/openai-stream/src/convert-messages.test.ts b/packages/openai-stream/src/convert-messages.test.ts
index 004a6b7..57c7d81 100644
--- a/packages/openai-stream/src/convert-messages.test.ts
+++ b/packages/openai-stream/src/convert-messages.test.ts
@@ -3,362 +3,456 @@ import { describe, expect, it } from "vitest";
import { convertMessages } from "./convert-messages.js";
describe("convertMessages", () => {
- it("converts a system message with text chunks", () => {
- const messages: ChatMessage[] = [
- {
- role: "system",
- chunks: [
- { type: "system", text: "You are a helpful assistant." },
- { type: "text", text: " Additional context." },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- { role: "system", content: "You are a helpful assistant. Additional context." },
- ]);
- });
-
- it("converts a user message with text chunks", () => {
- const messages: ChatMessage[] = [
- {
- role: "user",
- chunks: [
- { type: "text", text: "Hello, " },
- { type: "text", text: "world!" },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([{ role: "user", content: "Hello, world!" }]);
- });
-
- it("converts an assistant message with text only", () => {
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- { type: "text", text: "I can help " },
- { type: "text", text: "with that." },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([{ role: "assistant", content: "I can help with that." }]);
- });
-
- it("converts an assistant message with tool calls", () => {
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- { type: "text", text: "Let me check that." },
- {
- type: "tool-call",
- toolCallId: "call_1",
- toolName: "read_file",
- input: { path: "/src/main.ts" },
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- {
- role: "assistant",
- content: "Let me check that.",
- tool_calls: [
- {
- id: "call_1",
- type: "function",
- function: {
- name: "read_file",
- arguments: JSON.stringify({ path: "/src/main.ts" }),
- },
- },
- ],
- },
- ]);
- });
-
- it("converts an assistant message with tool calls but no text", () => {
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- {
- type: "tool-call",
- toolCallId: "call_2",
- toolName: "run_shell",
- input: { command: "ls" },
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- {
- role: "assistant",
- content: null,
- tool_calls: [
- {
- id: "call_2",
- type: "function",
- function: {
- name: "run_shell",
- arguments: JSON.stringify({ command: "ls" }),
- },
- },
- ],
- },
- ]);
- });
-
- it("converts tool result messages", () => {
- const messages: ChatMessage[] = [
- {
- role: "tool",
- chunks: [
- {
- type: "tool-result",
- toolCallId: "call_1",
- toolName: "read_file",
- content: "file contents here",
- isError: false,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- {
- role: "tool",
- content: "file contents here",
- tool_call_id: "call_1",
- },
- ]);
- });
-
- it("converts a full multi-turn history with tool round-trip", () => {
- const messages: ChatMessage[] = [
- {
- role: "system",
- chunks: [{ type: "system", text: "You are helpful." }],
- },
- {
- role: "user",
- chunks: [{ type: "text", text: "Read main.ts" }],
- },
- {
- role: "assistant",
- chunks: [
- { type: "text", text: "Sure." },
- {
- type: "tool-call",
- toolCallId: "call_1",
- toolName: "read_file",
- input: { path: "main.ts" },
- },
- ],
- },
- {
- role: "tool",
- chunks: [
- {
- type: "tool-result",
- toolCallId: "call_1",
- toolName: "read_file",
- content: "console.log('hello')",
- isError: false,
- },
- ],
- },
- {
- role: "assistant",
- chunks: [{ type: "text", text: "The file logs hello." }],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- { role: "system", content: "You are helpful." },
- { role: "user", content: "Read main.ts" },
- {
- role: "assistant",
- content: "Sure.",
- tool_calls: [
- {
- id: "call_1",
- type: "function",
- function: {
- name: "read_file",
- arguments: JSON.stringify({ path: "main.ts" }),
- },
- },
- ],
- },
- {
- role: "tool",
- content: "console.log('hello')",
- tool_call_id: "call_1",
- },
- { role: "assistant", content: "The file logs hello." },
- ]);
- });
-
- it("handles multiple tool results in one tool message", () => {
- const messages: ChatMessage[] = [
- {
- role: "tool",
- chunks: [
- {
- type: "tool-result",
- toolCallId: "call_1",
- toolName: "read_file",
- content: "file1",
- isError: false,
- },
- {
- type: "tool-result",
- toolCallId: "call_2",
- toolName: "read_file",
- content: "file2",
- isError: false,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([
- { role: "tool", content: "file1", tool_call_id: "call_1" },
- { role: "tool", content: "file2", tool_call_id: "call_2" },
- ]);
- });
-
- it("includes thinking chunks in assistant content", () => {
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- { type: "thinking", text: "Let me think..." },
- { type: "text", text: "Here is my answer." },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- expect(result).toEqual([{ role: "assistant", content: "Let me think...Here is my answer." }]);
- });
-
- it("arguments is valid JSON when input is a malformed string", () => {
- // Production seq-134 shape: the model emitted broken JSON as the tool
- // arguments and it was stored verbatim. Unquoted key fails JSON.parse at
- // some column (position 1 here).
- const malformed = '{path: "/src/main.ts"}';
- expect(() => JSON.parse(malformed)).toThrow();
-
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- {
- type: "tool-call",
- toolCallId: "call_bad",
- toolName: "read_file",
- input: malformed,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- const args = result[0]?.tool_calls?.[0]?.function.arguments;
- expect(args).toBeDefined();
- // The output MUST parse without throwing — the provider receives valid JSON.
- expect(() => JSON.parse(args as string)).not.toThrow();
- // And it is the fallback object preserving a truncated hint.
- expect(JSON.parse(args as string)).toEqual({
- _malformed_arguments: malformed.slice(0, 200),
- });
- });
-
- it("arguments passes through valid string input", () => {
- const validJson = '{"path":"/src/main.ts"}';
- expect(() => JSON.parse(validJson)).not.toThrow();
-
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- {
- type: "tool-call",
- toolCallId: "call_str",
- toolName: "read_file",
- input: validJson,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- const args = result[0]?.tool_calls?.[0]?.function.arguments;
- // A valid-JSON string round-trips to a canonical JSON string.
- expect(args).toBe(JSON.stringify(JSON.parse(validJson)));
- expect(args).toBe('{"path":"/src/main.ts"}');
- });
-
- it("stringifies object input", () => {
- const input = { path: "/src/main.ts", line: 42 };
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- {
- type: "tool-call",
- toolCallId: "call_obj",
- toolName: "read_file",
- input,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- const args = result[0]?.tool_calls?.[0]?.function.arguments;
- expect(args).toBe(JSON.stringify(input));
- });
-
- it("truncates the malformed input hint to 200 characters", () => {
- // A long bare run of letters is not valid JSON (no quotes/braces).
- const malformed = "x".repeat(500);
- expect(() => JSON.parse(malformed)).toThrow();
-
- const messages: ChatMessage[] = [
- {
- role: "assistant",
- chunks: [
- {
- type: "tool-call",
- toolCallId: "call_long",
- toolName: "read_file",
- input: malformed,
- },
- ],
- },
- ];
-
- const result = convertMessages(messages);
- const args = result[0]?.tool_calls?.[0]?.function.arguments;
- expect(() => JSON.parse(args as string)).not.toThrow();
- const parsed = JSON.parse(args as string);
- expect(parsed).toEqual({ _malformed_arguments: malformed.slice(0, 200) });
- expect(parsed._malformed_arguments.length).toBe(200);
- });
+ it("converts a system message with text chunks", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "system",
+ chunks: [
+ { type: "system", text: "You are a helpful assistant." },
+ { type: "text", text: " Additional context." },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ { role: "system", content: "You are a helpful assistant. Additional context." },
+ ]);
+ });
+
+ it("converts a user message with text chunks", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "user",
+ chunks: [
+ { type: "text", text: "Hello, " },
+ { type: "text", text: "world!" },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([{ role: "user", content: "Hello, world!" }]);
+ });
+
+ it("converts a user message with a text + image chunk to a multimodal content array", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "user",
+ chunks: [
+ { type: "text", text: "What is in this image?" },
+ { type: "image", url: "data:image/png;base64,iVBORw0KGgo=" },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "What is in this image?" },
+ { type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo=" } },
+ ],
+ },
+ ]);
+ });
+
+ it("converts an image-only user message (no text) to a content array with just the image", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "user",
+ chunks: [{ type: "image", url: "https://example.com/cat.png" }],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ {
+ role: "user",
+ content: [{ type: "image_url", image_url: { url: "https://example.com/cat.png" } }],
+ },
+ ]);
+ });
+
+ it("converts a user message with multiple images interspersed with text", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "user",
+ chunks: [
+ { type: "text", text: "Compare these:" },
+ { type: "image", url: "data:image/png;base64,aaa" },
+ { type: "text", text: "and" },
+ { type: "image", url: "data:image/jpeg;base64,bbb" },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toHaveLength(1);
+ const content = result[0]?.content;
+ expect(Array.isArray(content)).toBe(true);
+ if (Array.isArray(content)) {
+ expect(content).toHaveLength(4);
+ expect(content[0]).toEqual({ type: "text", text: "Compare these:" });
+ expect(content[1]).toEqual({
+ type: "image_url",
+ image_url: { url: "data:image/png;base64,aaa" },
+ });
+ expect(content[2]).toEqual({ type: "text", text: "and" });
+ expect(content[3]).toEqual({
+ type: "image_url",
+ image_url: { url: "data:image/jpeg;base64,bbb" },
+ });
+ }
+ });
+
+ it("skips empty text parts in a multimodal message but keeps images", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "user",
+ chunks: [
+ { type: "text", text: "" },
+ { type: "image", url: "data:image/png;base64,x" },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ const content = result[0]?.content;
+ expect(Array.isArray(content)).toBe(true);
+ if (Array.isArray(content)) {
+ // Empty text part is dropped; only the image remains.
+ expect(content).toEqual([
+ { type: "image_url", image_url: { url: "data:image/png;base64,x" } },
+ ]);
+ }
+ });
+
+ it("converts an assistant message with text only", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ { type: "text", text: "I can help " },
+ { type: "text", text: "with that." },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([{ role: "assistant", content: "I can help with that." }]);
+ });
+
+ it("converts an assistant message with tool calls", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ { type: "text", text: "Let me check that." },
+ {
+ type: "tool-call",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ input: { path: "/src/main.ts" },
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ {
+ role: "assistant",
+ content: "Let me check that.",
+ tool_calls: [
+ {
+ id: "call_1",
+ type: "function",
+ function: {
+ name: "read_file",
+ arguments: JSON.stringify({ path: "/src/main.ts" }),
+ },
+ },
+ ],
+ },
+ ]);
+ });
+
+ it("converts an assistant message with tool calls but no text", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ {
+ type: "tool-call",
+ toolCallId: "call_2",
+ toolName: "run_shell",
+ input: { command: "ls" },
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: "call_2",
+ type: "function",
+ function: {
+ name: "run_shell",
+ arguments: JSON.stringify({ command: "ls" }),
+ },
+ },
+ ],
+ },
+ ]);
+ });
+
+ it("converts tool result messages", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "tool",
+ chunks: [
+ {
+ type: "tool-result",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ content: "file contents here",
+ isError: false,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ {
+ role: "tool",
+ content: "file contents here",
+ tool_call_id: "call_1",
+ },
+ ]);
+ });
+
+ it("converts a full multi-turn history with tool round-trip", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "system",
+ chunks: [{ type: "system", text: "You are helpful." }],
+ },
+ {
+ role: "user",
+ chunks: [{ type: "text", text: "Read main.ts" }],
+ },
+ {
+ role: "assistant",
+ chunks: [
+ { type: "text", text: "Sure." },
+ {
+ type: "tool-call",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ input: { path: "main.ts" },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ chunks: [
+ {
+ type: "tool-result",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ content: "console.log('hello')",
+ isError: false,
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ chunks: [{ type: "text", text: "The file logs hello." }],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ { role: "system", content: "You are helpful." },
+ { role: "user", content: "Read main.ts" },
+ {
+ role: "assistant",
+ content: "Sure.",
+ tool_calls: [
+ {
+ id: "call_1",
+ type: "function",
+ function: {
+ name: "read_file",
+ arguments: JSON.stringify({ path: "main.ts" }),
+ },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ content: "console.log('hello')",
+ tool_call_id: "call_1",
+ },
+ { role: "assistant", content: "The file logs hello." },
+ ]);
+ });
+
+ it("handles multiple tool results in one tool message", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "tool",
+ chunks: [
+ {
+ type: "tool-result",
+ toolCallId: "call_1",
+ toolName: "read_file",
+ content: "file1",
+ isError: false,
+ },
+ {
+ type: "tool-result",
+ toolCallId: "call_2",
+ toolName: "read_file",
+ content: "file2",
+ isError: false,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([
+ { role: "tool", content: "file1", tool_call_id: "call_1" },
+ { role: "tool", content: "file2", tool_call_id: "call_2" },
+ ]);
+ });
+
+ it("includes thinking chunks in assistant content", () => {
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ { type: "thinking", text: "Let me think..." },
+ { type: "text", text: "Here is my answer." },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ expect(result).toEqual([{ role: "assistant", content: "Let me think...Here is my answer." }]);
+ });
+
+ it("arguments is valid JSON when input is a malformed string", () => {
+ // Production seq-134 shape: the model emitted broken JSON as the tool
+ // arguments and it was stored verbatim. Unquoted key fails JSON.parse at
+ // some column (position 1 here).
+ const malformed = '{path: "/src/main.ts"}';
+ expect(() => JSON.parse(malformed)).toThrow();
+
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ {
+ type: "tool-call",
+ toolCallId: "call_bad",
+ toolName: "read_file",
+ input: malformed,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ const args = result[0]?.tool_calls?.[0]?.function.arguments;
+ expect(args).toBeDefined();
+ // The output MUST parse without throwing — the provider receives valid JSON.
+ expect(() => JSON.parse(args as string)).not.toThrow();
+ // And it is the fallback object preserving a truncated hint.
+ expect(JSON.parse(args as string)).toEqual({
+ _malformed_arguments: malformed.slice(0, 200),
+ });
+ });
+
+ it("arguments passes through valid string input", () => {
+ const validJson = '{"path":"/src/main.ts"}';
+ expect(() => JSON.parse(validJson)).not.toThrow();
+
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ {
+ type: "tool-call",
+ toolCallId: "call_str",
+ toolName: "read_file",
+ input: validJson,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ const args = result[0]?.tool_calls?.[0]?.function.arguments;
+ // A valid-JSON string round-trips to a canonical JSON string.
+ expect(args).toBe(JSON.stringify(JSON.parse(validJson)));
+ expect(args).toBe('{"path":"/src/main.ts"}');
+ });
+
+ it("stringifies object input", () => {
+ const input = { path: "/src/main.ts", line: 42 };
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ {
+ type: "tool-call",
+ toolCallId: "call_obj",
+ toolName: "read_file",
+ input,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ const args = result[0]?.tool_calls?.[0]?.function.arguments;
+ expect(args).toBe(JSON.stringify(input));
+ });
+
+ it("truncates the malformed input hint to 200 characters", () => {
+ // A long bare run of letters is not valid JSON (no quotes/braces).
+ const malformed = "x".repeat(500);
+ expect(() => JSON.parse(malformed)).toThrow();
+
+ const messages: ChatMessage[] = [
+ {
+ role: "assistant",
+ chunks: [
+ {
+ type: "tool-call",
+ toolCallId: "call_long",
+ toolName: "read_file",
+ input: malformed,
+ },
+ ],
+ },
+ ];
+
+ const result = convertMessages(messages);
+ const args = result[0]?.tool_calls?.[0]?.function.arguments;
+ expect(() => JSON.parse(args as string)).not.toThrow();
+ const parsed = JSON.parse(args as string);
+ expect(parsed).toEqual({ _malformed_arguments: malformed.slice(0, 200) });
+ expect(parsed._malformed_arguments.length).toBe(200);
+ });
});
diff --git a/packages/openai-stream/src/convert-messages.ts b/packages/openai-stream/src/convert-messages.ts
index 76badc8..eba3575 100644
--- a/packages/openai-stream/src/convert-messages.ts
+++ b/packages/openai-stream/src/convert-messages.ts
@@ -1,101 +1,144 @@
import type { ChatMessage, Chunk } from "@dispatch/kernel";
+/** A text part within a multimodal OpenAI content array. */
+export interface OpenAITextPart {
+ readonly type: "text";
+ readonly text: string;
+}
+
+/** An image part within a multimodal OpenAI content array (OpenAI vision format). */
+export interface OpenAIImagePart {
+ readonly type: "image_url";
+ readonly image_url: { readonly url: string };
+}
+
+/**
+ * A part of a multimodal message content array. When a message has mixed text
+ * and image chunks, the content is serialized as an array of these parts
+ * (OpenAI's vision format). Plain-text messages keep a string `content` for
+ * byte-stability with providers that only accept strings.
+ */
+export type OpenAIContentPart = OpenAITextPart | OpenAIImagePart;
+
export interface OpenAIMessage {
- readonly role: "system" | "user" | "assistant" | "tool";
- readonly content: string | null;
- readonly tool_calls?: readonly OpenAIToolCall[];
- readonly tool_call_id?: string;
+ readonly role: "system" | "user" | "assistant" | "tool";
+ readonly content: string | null | readonly OpenAIContentPart[];
+ readonly tool_calls?: readonly OpenAIToolCall[];
+ readonly tool_call_id?: string;
}
export interface OpenAIToolCall {
- readonly id: string;
- readonly type: "function";
- readonly function: { readonly name: string; readonly arguments: string };
+ readonly id: string;
+ readonly type: "function";
+ readonly function: { readonly name: string; readonly arguments: string };
}
export function convertMessages(messages: readonly ChatMessage[]): OpenAIMessage[] {
- const result: OpenAIMessage[] = [];
- for (const msg of messages) {
- const converted = convertMessage(msg);
- for (const m of converted) {
- result.push(m);
- }
- }
- return result;
+ const result: OpenAIMessage[] = [];
+ for (const msg of messages) {
+ const converted = convertMessage(msg);
+ for (const m of converted) {
+ result.push(m);
+ }
+ }
+ return result;
}
function convertMessage(msg: ChatMessage): OpenAIMessage[] {
- switch (msg.role) {
- case "system":
- return [convertSystemMessage(msg)];
- case "user":
- return [convertUserMessage(msg)];
- case "assistant":
- return [convertAssistantMessage(msg)];
- case "tool":
- return convertToolResultMessages(msg);
- }
+ switch (msg.role) {
+ case "system":
+ return [convertSystemMessage(msg)];
+ case "user":
+ return [convertUserMessage(msg)];
+ case "assistant":
+ return [convertAssistantMessage(msg)];
+ case "tool":
+ return convertToolResultMessages(msg);
+ }
}
function convertSystemMessage(msg: ChatMessage): OpenAIMessage {
- const text = msg.chunks
- .filter(
- (c): c is Extract<Chunk, { type: "text" | "system" }> =>
- c.type === "text" || c.type === "system",
- )
- .map((c) => c.text)
- .join("");
- return { role: "system", content: text };
+ const text = msg.chunks
+ .filter(
+ (c): c is Extract<Chunk, { type: "text" | "system" }> =>
+ c.type === "text" || c.type === "system",
+ )
+ .map((c) => c.text)
+ .join("");
+ return { role: "system", content: text };
}
function convertUserMessage(msg: ChatMessage): OpenAIMessage {
- const text = msg.chunks
- .filter((c): c is Extract<Chunk, { type: "text" }> => c.type === "text")
- .map((c) => c.text)
- .join("");
- return { role: "user", content: text };
+ // If the message has image chunks, serialize as a multimodal content array
+ // (OpenAI vision format): text parts + image_url parts in chunk order.
+ // Plain text-only messages keep a string `content` for byte-stability with
+ // providers that only accept a string (and to keep prompt-cache prefixes
+ // unchanged for the common no-image case).
+ const hasImage = msg.chunks.some((c) => c.type === "image");
+ if (hasImage) {
+ const parts: OpenAIContentPart[] = [];
+ for (const chunk of msg.chunks) {
+ if (chunk.type === "text") {
+ if (chunk.text.length > 0) {
+ parts.push({ type: "text", text: chunk.text });
+ }
+ } else if (chunk.type === "image") {
+ parts.push({ type: "image_url", image_url: { url: chunk.url } });
+ }
+ // Non-text/non-image chunks (tool-call, thinking, etc.) are not part of a
+ // user message's provider content and are skipped here.
+ }
+ // An image-only message (no text) still needs at least the image part.
+ return { role: "user", content: parts.length > 0 ? parts : "" };
+ }
+
+ const text = msg.chunks
+ .filter((c): c is Extract<Chunk, { type: "text" }> => c.type === "text")
+ .map((c) => c.text)
+ .join("");
+ return { role: "user", content: text };
}
function convertAssistantMessage(msg: ChatMessage): OpenAIMessage {
- const textChunks = msg.chunks.filter(
- (c): c is Extract<Chunk, { type: "text" | "thinking" }> =>
- c.type === "text" || c.type === "thinking",
- );
- const content = textChunks.map((c) => c.text).join("");
+ const textChunks = msg.chunks.filter(
+ (c): c is Extract<Chunk, { type: "text" | "thinking" }> =>
+ c.type === "text" || c.type === "thinking",
+ );
+ const content = textChunks.map((c) => c.text).join("");
- const toolCalls = msg.chunks
- .filter((c): c is Extract<Chunk, { type: "tool-call" }> => c.type === "tool-call")
- .map(
- (c): OpenAIToolCall => ({
- id: c.toolCallId,
- type: "function",
- function: {
- name: c.toolName,
- arguments: serializeToolArguments(c.input),
- },
- }),
- );
+ const toolCalls = msg.chunks
+ .filter((c): c is Extract<Chunk, { type: "tool-call" }> => c.type === "tool-call")
+ .map(
+ (c): OpenAIToolCall => ({
+ id: c.toolCallId,
+ type: "function",
+ function: {
+ name: c.toolName,
+ arguments: serializeToolArguments(c.input),
+ },
+ }),
+ );
- if (toolCalls.length > 0) {
- return {
- role: "assistant",
- content: content || null,
- tool_calls: toolCalls,
- };
- }
- return { role: "assistant", content };
+ if (toolCalls.length > 0) {
+ return {
+ role: "assistant",
+ content: content || null,
+ tool_calls: toolCalls,
+ };
+ }
+ return { role: "assistant", content };
}
function convertToolResultMessages(msg: ChatMessage): OpenAIMessage[] {
- return msg.chunks
- .filter((c): c is Extract<Chunk, { type: "tool-result" }> => c.type === "tool-result")
- .map(
- (c): OpenAIMessage => ({
- role: "tool",
- content: c.content,
- tool_call_id: c.toolCallId,
- }),
- );
+ return msg.chunks
+ .filter((c): c is Extract<Chunk, { type: "tool-result" }> => c.type === "tool-result")
+ .map(
+ (c): OpenAIMessage => ({
+ role: "tool",
+ content: c.content,
+ tool_call_id: c.toolCallId,
+ }),
+ );
}
/**
@@ -116,12 +159,12 @@ function convertToolResultMessages(msg: ChatMessage): OpenAIMessage[] {
* Pure: input → output, no I/O.
*/
function serializeToolArguments(input: unknown): string {
- if (typeof input === "string") {
- try {
- return JSON.stringify(JSON.parse(input));
- } catch {
- return JSON.stringify({ _malformed_arguments: input.slice(0, 200) });
- }
- }
- return JSON.stringify(input);
+ if (typeof input === "string") {
+ try {
+ return JSON.stringify(JSON.parse(input));
+ } catch {
+ return JSON.stringify({ _malformed_arguments: input.slice(0, 200) });
+ }
+ }
+ return JSON.stringify(input);
}
diff --git a/packages/openai-stream/src/convert-tools.test.ts b/packages/openai-stream/src/convert-tools.test.ts
index d739652..907ea15 100644
--- a/packages/openai-stream/src/convert-tools.test.ts
+++ b/packages/openai-stream/src/convert-tools.test.ts
@@ -3,104 +3,104 @@ import { describe, expect, it } from "vitest";
import { convertTools } from "./convert-tools.js";
describe("convertTools", () => {
- it("converts a single tool to OpenAI function format", () => {
- const tools: ToolContract[] = [
- {
- name: "read_file",
- description: "Read a file from disk",
- parameters: {
- type: "object",
- properties: {
- path: { type: "string", description: "File path" },
- },
- required: ["path"],
- additionalProperties: false,
- },
- execute: async () => ({ content: "" }),
- },
- ];
+ it("converts a single tool to OpenAI function format", () => {
+ const tools: ToolContract[] = [
+ {
+ name: "read_file",
+ description: "Read a file from disk",
+ parameters: {
+ type: "object",
+ properties: {
+ path: { type: "string", description: "File path" },
+ },
+ required: ["path"],
+ additionalProperties: false,
+ },
+ execute: async () => ({ content: "" }),
+ },
+ ];
- const result = convertTools(tools);
- expect(result).toEqual([
- {
- type: "function",
- function: {
- name: "read_file",
- description: "Read a file from disk",
- parameters: {
- type: "object",
- properties: {
- path: { type: "string", description: "File path" },
- },
- required: ["path"],
- additionalProperties: false,
- },
- },
- },
- ]);
- });
+ const result = convertTools(tools);
+ expect(result).toEqual([
+ {
+ type: "function",
+ function: {
+ name: "read_file",
+ description: "Read a file from disk",
+ parameters: {
+ type: "object",
+ properties: {
+ path: { type: "string", description: "File path" },
+ },
+ required: ["path"],
+ additionalProperties: false,
+ },
+ },
+ },
+ ]);
+ });
- it("converts multiple tools", () => {
- const tools: ToolContract[] = [
- {
- name: "read_file",
- description: "Read a file",
- parameters: { type: "object" },
- execute: async () => ({ content: "" }),
- },
- {
- name: "run_shell",
- description: "Run a shell command",
- parameters: {
- type: "object",
- properties: {
- command: { type: "string", description: "The command" },
- },
- required: ["command"],
- },
- execute: async () => ({ content: "" }),
- },
- ];
+ it("converts multiple tools", () => {
+ const tools: ToolContract[] = [
+ {
+ name: "read_file",
+ description: "Read a file",
+ parameters: { type: "object" },
+ execute: async () => ({ content: "" }),
+ },
+ {
+ name: "run_shell",
+ description: "Run a shell command",
+ parameters: {
+ type: "object",
+ properties: {
+ command: { type: "string", description: "The command" },
+ },
+ required: ["command"],
+ },
+ execute: async () => ({ content: "" }),
+ },
+ ];
- const result = convertTools(tools);
- expect(result).toHaveLength(2);
- expect(result[0]?.function.name).toBe("read_file");
- expect(result[1]?.function.name).toBe("run_shell");
- });
+ const result = convertTools(tools);
+ expect(result).toHaveLength(2);
+ expect(result[0]?.function.name).toBe("read_file");
+ expect(result[1]?.function.name).toBe("run_shell");
+ });
- it("returns empty array for no tools", () => {
- const result = convertTools([]);
- expect(result).toEqual([]);
- });
+ it("returns empty array for no tools", () => {
+ const result = convertTools([]);
+ expect(result).toEqual([]);
+ });
- it("preserves nested parameter schema properties", () => {
- const tools: ToolContract[] = [
- {
- name: "search",
- description: "Search code",
- parameters: {
- type: "object",
- properties: {
- query: { type: "string", description: "Search query" },
- options: {
- type: "object",
- properties: {
- limit: { type: "number", description: "Max results", default: 10 },
- },
- },
- },
- required: ["query"],
- },
- execute: async () => ({ content: "" }),
- },
- ];
+ it("preserves nested parameter schema properties", () => {
+ const tools: ToolContract[] = [
+ {
+ name: "search",
+ description: "Search code",
+ parameters: {
+ type: "object",
+ properties: {
+ query: { type: "string", description: "Search query" },
+ options: {
+ type: "object",
+ properties: {
+ limit: { type: "number", description: "Max results", default: 10 },
+ },
+ },
+ },
+ required: ["query"],
+ },
+ execute: async () => ({ content: "" }),
+ },
+ ];
- const result = convertTools(tools);
- expect(result[0]?.function.parameters.properties?.options).toEqual({
- type: "object",
- properties: {
- limit: { type: "number", description: "Max results", default: 10 },
- },
- });
- });
+ const result = convertTools(tools);
+ expect(result[0]?.function.parameters.properties?.options).toEqual({
+ type: "object",
+ properties: {
+ limit: { type: "number", description: "Max results", default: 10 },
+ },
+ });
+ });
});
diff --git a/packages/openai-stream/src/convert-tools.ts b/packages/openai-stream/src/convert-tools.ts
index 65416bb..198232c 100644
--- a/packages/openai-stream/src/convert-tools.ts
+++ b/packages/openai-stream/src/convert-tools.ts
@@ -1,25 +1,25 @@
import type { ToolContract, ToolParameterSchema } from "@dispatch/kernel";
export interface OpenAITool {
- readonly type: "function";
- readonly function: {
- readonly name: string;
- readonly description: string;
- readonly parameters: ToolParameterSchema;
- };
+ readonly type: "function";
+ readonly function: {
+ readonly name: string;
+ readonly description: string;
+ readonly parameters: ToolParameterSchema;
+ };
}
export function convertTools(tools: readonly ToolContract[]): OpenAITool[] {
- return tools.map(convertTool);
+ return tools.map(convertTool);
}
function convertTool(tool: ToolContract): OpenAITool {
- return {
- type: "function",
- function: {
- name: tool.name,
- description: tool.description,
- parameters: tool.parameters,
- },
- };
+ return {
+ type: "function",
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: tool.parameters,
+ },
+ };
}
diff --git a/packages/openai-stream/src/getUsage.test.ts b/packages/openai-stream/src/getUsage.test.ts
new file mode 100644
index 0000000..c319b73
--- /dev/null
+++ b/packages/openai-stream/src/getUsage.test.ts
@@ -0,0 +1,139 @@
+import type { ProviderUsage } from "@dispatch/kernel";
+import type { FetchLike } from "@dispatch/trace-replay";
+import { describe, expect, it, vi } from "vitest";
+import { getUsage } from "./getUsage.js";
+
+function jsonResponse(body: unknown, status = 200): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
+describe("getUsage", () => {
+ it("extracts concurrent_sessions from the Umans /v1/usage shape", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ jsonResponse({
+ usage: { concurrent_sessions: 3 },
+ limits: { concurrency: { limit: 4, hard_cap: 5 } },
+ }) as unknown as ReturnType<FetchLike>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.umans.ai/v1",
+ apiKey: "sk-test-1234567890abcdef",
+ providerId: "umans",
+ fetchFn,
+ });
+
+ expect(result).toEqual<ProviderUsage>({ concurrentSessions: 3 });
+ expect(fetchFn).toHaveBeenCalledOnce();
+ const call = fetchFn.mock.calls[0];
+ expect(call?.[0]).toBe("https://api.umans.ai/v1/usage");
+ const init = call?.[1] as RequestInit;
+ expect(init.method).toBe("GET");
+ expect((init.headers as Record<string, string>).Authorization).toBe(
+ "Bearer sk-test-1234567890abcdef",
+ );
+ });
+
+ it("returns undefined on non-200 (endpoint unsupported)", async () => {
+ const fetchFn = vi.fn(
+ () => jsonResponse({ error: "not found" }, 404) as unknown as ReturnType<FetchLike>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ fetchFn,
+ });
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined on a network error (never throws)", async () => {
+ const fetchFn = vi.fn(
+ () => Promise.reject(new Error("ECONNREFUSED")) as unknown as Promise<Response>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ fetchFn,
+ });
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when the response shape is unexpected", async () => {
+ const fetchFn = vi.fn(
+ () => jsonResponse({ unexpected: true }) as unknown as ReturnType<FetchLike>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ fetchFn,
+ });
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined when concurrent_sessions is not a number", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ jsonResponse({
+ usage: { concurrent_sessions: "three" },
+ }) as unknown as ReturnType<FetchLike>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ fetchFn,
+ });
+
+ expect(result).toBeUndefined();
+ });
+
+ it("truncates a fractional concurrent_sessions to an integer", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ jsonResponse({ usage: { concurrent_sessions: 2.9 } }) as unknown as ReturnType<FetchLike>,
+ );
+
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ fetchFn,
+ });
+
+ expect(result).toEqual<ProviderUsage>({ concurrentSessions: 2 });
+ });
+
+ it("falls back to globalThis.fetch when fetchFn is absent", async () => {
+ const original = globalThis.fetch;
+ const stub = vi.fn(
+ () => jsonResponse({ usage: { concurrent_sessions: 1 } }) as unknown as ReturnType<FetchLike>,
+ );
+ globalThis.fetch = stub as unknown as typeof globalThis.fetch;
+
+ try {
+ const result = await getUsage({
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test",
+ providerId: "openai",
+ });
+ expect(result).toEqual<ProviderUsage>({ concurrentSessions: 1 });
+ expect(stub).toHaveBeenCalledOnce();
+ } finally {
+ globalThis.fetch = original;
+ }
+ });
+});
diff --git a/packages/openai-stream/src/getUsage.ts b/packages/openai-stream/src/getUsage.ts
new file mode 100644
index 0000000..5da7fd7
--- /dev/null
+++ b/packages/openai-stream/src/getUsage.ts
@@ -0,0 +1,73 @@
+import type { ProviderUsage } from "@dispatch/kernel";
+import type { FetchLike } from "@dispatch/trace-replay";
+
+/**
+ * Generic OpenAI-compatible usage fetch. The Umans `/v1/usage` endpoint returns:
+ *
+ * { usage: { concurrent_sessions: number }, limits: { concurrency: { limit, hard_cap } } }
+ *
+ * We extract only `concurrent_sessions` (the count a concurrency limiter gates
+ * on). Lives in this library (`@dispatch/openai-stream`) so any OpenAI-compatible
+ * provider extension reuses it without cross-extension code import
+ * (isolation-over-DRY: coupling is via this typed library surface). A provider
+ * supplies its own `id` (used in error labels) via `createOpenAICompatProvider`.
+ */
+
+/** The raw shape of the `/v1/usage` response (only the fields we read). */
+interface UsageResponse {
+ readonly usage?: {
+ readonly concurrent_sessions?: number;
+ };
+}
+
+export interface GetUsageConfig {
+ readonly baseURL: string;
+ readonly apiKey: string;
+ readonly fetchFn?: FetchLike;
+ readonly providerId: string;
+}
+
+/**
+ * Fetch + map the upstream usage snapshot. Returns `undefined` on any error,
+ * non-200, or unexpected shape so the caller (the concurrency limiter) falls
+ * back to cooldown-only slot recycling (no usage gate) — never throws.
+ *
+ * Pure-ish I/O wrapper: the only effect is the injected fetch. Extracted for
+ * direct unit testing with a fake fetch.
+ */
+export async function getUsage(config: GetUsageConfig): Promise<ProviderUsage | undefined> {
+ const effectiveFetch: FetchLike = config.fetchFn ?? fetch;
+ const url = `${config.baseURL}/usage`;
+
+ let response: Response;
+ try {
+ response = await effectiveFetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ },
+ });
+ } catch {
+ // Network error — the upstream is unreachable; treat as "no usage info".
+ return undefined;
+ }
+
+ if (!response.ok) {
+ // 404 / 401 / 5xx — the endpoint is unsupported or rejected the request.
+ return undefined;
+ }
+
+ let body: UsageResponse;
+ try {
+ body = (await response.json()) as UsageResponse;
+ } catch {
+ return undefined;
+ }
+
+ const raw = body.usage?.concurrent_sessions;
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
+ return undefined;
+ }
+
+ return { concurrentSessions: Math.trunc(raw) };
+}
diff --git a/packages/openai-stream/src/index.ts b/packages/openai-stream/src/index.ts
index bd2f673..ff6b4f5 100644
--- a/packages/openai-stream/src/index.ts
+++ b/packages/openai-stream/src/index.ts
@@ -1,8 +1,15 @@
-export type { OpenAIMessage, OpenAIToolCall } from "./convert-messages.js";
+export type {
+ OpenAIContentPart,
+ OpenAIImagePart,
+ OpenAIMessage,
+ OpenAITextPart,
+ OpenAIToolCall,
+} from "./convert-messages.js";
export { convertMessages } from "./convert-messages.js";
export type { OpenAITool } from "./convert-tools.js";
export { convertTools } from "./convert-tools.js";
-export { parseModelList } from "./listModels.js";
+export { getUsage } from "./getUsage.js";
+export { isVisionModelId, parseModelList } from "./listModels.js";
export { parseSSELines } from "./parse-sse.js";
export type { CreateOpenAICompatProviderOpts } from "./provider.js";
export { createOpenAICompatProvider } from "./provider.js";
diff --git a/packages/openai-stream/src/listModels.test.ts b/packages/openai-stream/src/listModels.test.ts
index 63c95fc..2e3b1a3 100644
--- a/packages/openai-stream/src/listModels.test.ts
+++ b/packages/openai-stream/src/listModels.test.ts
@@ -1,102 +1,149 @@
import type { ApiKeyCredentials, ModelInfo, ProviderContract } from "@dispatch/kernel";
import type { FetchLike } from "@dispatch/trace-replay";
import { describe, expect, it, vi } from "vitest";
-import { parseModelList } from "./listModels.js";
+import { isVisionModelId, parseModelList } from "./listModels.js";
import { createOpenAICompatProvider } from "./provider.js";
function makeProvider(fetchFn: FetchLike, apiKey = "sk-test-1234567890abcdef"): ProviderContract {
- const creds: ApiKeyCredentials = {
- type: "api-key",
- apiKey,
- baseURL: "https://api.example.com/v1",
- };
- return createOpenAICompatProvider({
- credentials: creds,
- model: "test-model",
- id: "openai-compat",
- fetchFn,
- });
+ const creds: ApiKeyCredentials = {
+ type: "api-key",
+ apiKey,
+ baseURL: "https://api.example.com/v1",
+ };
+ return createOpenAICompatProvider({
+ credentials: creds,
+ model: "test-model",
+ id: "openai-compat",
+ fetchFn,
+ });
}
function jsonResponse(body: unknown, status = 200): Response {
- return new Response(JSON.stringify(body), {
- status,
- headers: { "Content-Type": "application/json" },
- });
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { "Content-Type": "application/json" },
+ });
}
describe("listModels — pure mapping (parseModelList)", () => {
- it("maps OpenAI model entries to ModelInfo", () => {
- const result = parseModelList([{ id: "a" }, { id: "b" }]);
- expect(result).toEqual([{ id: "a" }, { id: "b" }]);
- });
-
- it("returns empty array for empty input", () => {
- const result = parseModelList([]);
- expect(result).toEqual([]);
- });
+ it("maps OpenAI model entries to ModelInfo", () => {
+ const result = parseModelList([{ id: "a" }, { id: "b" }]);
+ expect(result).toEqual([{ id: "a" }, { id: "b" }]);
+ });
+
+ it("returns empty array for empty input", () => {
+ const result = parseModelList([]);
+ expect(result).toEqual([]);
+ });
+
+ it("extracts contextWindow from common field names", () => {
+ const result = parseModelList([
+ { id: "m1", context_length: 128000 },
+ { id: "m2", context_window: 200000 },
+ { id: "m3", max_context_length: 64000 },
+ { id: "m4", max_tokens: 8000 },
+ ]);
+ expect(result).toEqual([
+ { id: "m1", contextWindow: 128000 },
+ { id: "m2", contextWindow: 200000 },
+ { id: "m3", contextWindow: 64000 },
+ { id: "m4", contextWindow: 8000 },
+ ]);
+ });
+});
+
+describe("listModels — vision capability detection", () => {
+ it("isVisionModelId returns true for umans kimi and qwen model ids", () => {
+ expect(isVisionModelId("umans-kimi-k2.7")).toBe(true);
+ expect(isVisionModelId("Umans-Kimi-K2.7")).toBe(true); // case-insensitive
+ expect(isVisionModelId("umans-qwen3.6-35b-a3b")).toBe(true);
+ });
+
+ it("isVisionModelId returns false for non-vision model ids", () => {
+ expect(isVisionModelId("umans-glm-5.2")).toBe(false);
+ expect(isVisionModelId("umans-coder")).toBe(false);
+ expect(isVisionModelId("umans-flash")).toBe(false);
+ expect(isVisionModelId("kimi-k2.7-code")).toBe(false); // opencode kimi, not umans
+ expect(isVisionModelId("qwen3.7-max")).toBe(false); // opencode qwen, not umans
+ expect(isVisionModelId("deepseek-v4-flash")).toBe(false);
+ });
+
+ it("parseModelList sets vision: true on umans kimi and qwen models only", () => {
+ const result = parseModelList([
+ { id: "umans-kimi-k2.7", context_length: 262144 },
+ { id: "umans-qwen3.6-35b-a3b", context_length: 262144 },
+ { id: "umans-glm-5.2", context_length: 405504 },
+ { id: "umans-coder" },
+ ]);
+ expect(result).toEqual([
+ { id: "umans-kimi-k2.7", contextWindow: 262144, vision: true },
+ { id: "umans-qwen3.6-35b-a3b", contextWindow: 262144, vision: true },
+ { id: "umans-glm-5.2", contextWindow: 405504 },
+ { id: "umans-coder" },
+ ]);
+ });
});
describe("listModels — provider contract", () => {
- it("GETs models endpoint with bearer key and returns mapped ModelInfo[]", async () => {
- const fetchFn = vi.fn(
- () => jsonResponse({ data: [{ id: "a" }, { id: "b" }] }) as unknown as ReturnType<FetchLike>,
- );
- const provider = makeProvider(fetchFn);
- const listModels = provider.listModels;
- if (!listModels) throw new Error("listModels not defined");
-
- const models = await listModels();
-
- expect(fetchFn).toHaveBeenCalledOnce();
- const callArgs = fetchFn.mock.calls[0];
- if (!callArgs) throw new Error("no call args");
- const [url, init] = callArgs as unknown as [string, RequestInit];
- expect(url).toBe("https://api.example.com/v1/models");
- expect(init.method).toBe("GET");
- expect(init.headers).toEqual({ Authorization: "Bearer sk-test-1234567890abcdef" });
-
- expect(models).toEqual([{ id: "a" }, { id: "b" }] as readonly ModelInfo[]);
- });
-
- it("throws on non-OK HTTP status with a clear message", async () => {
- const fetchFn = vi.fn(
- () =>
- new Response("Unauthorized", {
- status: 401,
- headers: { "Content-Type": "text/plain" },
- }) as unknown as ReturnType<FetchLike>,
- );
- const provider = makeProvider(fetchFn);
- const listModels = provider.listModels;
- if (!listModels) throw new Error("listModels not defined");
-
- await expect(listModels()).rejects.toThrow(
- "listModels[openai-compat]: HTTP 401 — Unauthorized",
- );
- });
-
- it("throws on network error with a clear message", async () => {
- const fetchFn = vi.fn(() => {
- throw new Error("connection refused");
- }) as unknown as FetchLike;
- const provider = makeProvider(fetchFn);
- const listModels = provider.listModels;
- if (!listModels) throw new Error("listModels not defined");
-
- await expect(listModels()).rejects.toThrow(
- "listModels[openai-compat]: network error — connection refused",
- );
- });
-
- it("throws when response shape is missing data array", async () => {
- const fetchFn = vi.fn(() => jsonResponse({ models: [] }) as unknown as ReturnType<FetchLike>);
- const provider = makeProvider(fetchFn);
- const listModels = provider.listModels;
- if (!listModels) throw new Error("listModels not defined");
-
- await expect(listModels()).rejects.toThrow(
- 'listModels[openai-compat]: unexpected response shape — missing "data" array',
- );
- });
+ it("GETs models endpoint with bearer key and returns mapped ModelInfo[]", async () => {
+ const fetchFn = vi.fn(
+ () => jsonResponse({ data: [{ id: "a" }, { id: "b" }] }) as unknown as ReturnType<FetchLike>,
+ );
+ const provider = makeProvider(fetchFn);
+ const listModels = provider.listModels;
+ if (!listModels) throw new Error("listModels not defined");
+
+ const models = await listModels();
+
+ expect(fetchFn).toHaveBeenCalledOnce();
+ const callArgs = fetchFn.mock.calls[0];
+ if (!callArgs) throw new Error("no call args");
+ const [url, init] = callArgs as unknown as [string, RequestInit];
+ expect(url).toBe("https://api.example.com/v1/models");
+ expect(init.method).toBe("GET");
+ expect(init.headers).toEqual({ Authorization: "Bearer sk-test-1234567890abcdef" });
+
+ expect(models).toEqual([{ id: "a" }, { id: "b" }] as readonly ModelInfo[]);
+ });
+
+ it("throws on non-OK HTTP status with a clear message", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ new Response("Unauthorized", {
+ status: 401,
+ headers: { "Content-Type": "text/plain" },
+ }) as unknown as ReturnType<FetchLike>,
+ );
+ const provider = makeProvider(fetchFn);
+ const listModels = provider.listModels;
+ if (!listModels) throw new Error("listModels not defined");
+
+ await expect(listModels()).rejects.toThrow(
+ "listModels[openai-compat]: HTTP 401 — Unauthorized",
+ );
+ });
+
+ it("throws on network error with a clear message", async () => {
+ const fetchFn = vi.fn(() => {
+ throw new Error("connection refused");
+ }) as unknown as FetchLike;
+ const provider = makeProvider(fetchFn);
+ const listModels = provider.listModels;
+ if (!listModels) throw new Error("listModels not defined");
+
+ await expect(listModels()).rejects.toThrow(
+ "listModels[openai-compat]: network error — connection refused",
+ );
+ });
+
+ it("throws when response shape is missing data array", async () => {
+ const fetchFn = vi.fn(() => jsonResponse({ models: [] }) as unknown as ReturnType<FetchLike>);
+ const provider = makeProvider(fetchFn);
+ const listModels = provider.listModels;
+ if (!listModels) throw new Error("listModels not defined");
+
+ await expect(listModels()).rejects.toThrow(
+ 'listModels[openai-compat]: unexpected response shape — missing "data" array',
+ );
+ });
});
diff --git a/packages/openai-stream/src/listModels.ts b/packages/openai-stream/src/listModels.ts
index 8a76c67..df116b0 100644
--- a/packages/openai-stream/src/listModels.ts
+++ b/packages/openai-stream/src/listModels.ts
@@ -12,69 +12,91 @@ import type { FetchLike } from "@dispatch/trace-replay";
*/
interface OpenAIModelEntry {
- readonly id: string;
- readonly context_length?: number;
- readonly context_window?: number;
- readonly max_context_length?: number;
- readonly max_tokens?: number;
+ readonly id: string;
+ readonly context_length?: number;
+ readonly context_window?: number;
+ readonly max_context_length?: number;
+ readonly max_tokens?: number;
}
interface OpenAIModelListResponse {
- readonly data: readonly OpenAIModelEntry[];
+ readonly data: readonly OpenAIModelEntry[];
+}
+
+/**
+ * Whether a model id is vision-capable (can natively accept image input).
+ *
+ * The OpenAI-compatible `/models` endpoint does not reliably report image
+ * capabilities, so this is a hardcoded heuristic by model id: the Umans Kimi
+ * (`umans-kimi-k2.7`) and Umans Qwen (`umans-qwen3.6-35b-a3b`) models are
+ * vision-capable; all others are treated as non-vision. This is the single
+ * source of truth — the orchestrator's vision handoff and the `consult_vision`
+ * tool both consult the `ModelInfo.vision` flag this sets, so adding a model
+ * here enables vision everywhere. Pure: id → boolean, no I/O.
+ *
+ * (When an endpoint gains reliable vision reporting, this can be replaced with
+ * a real capability check without changing callers.)
+ */
+export function isVisionModelId(id: string): boolean {
+ const lower = id.toLowerCase();
+ return lower.includes("umans-kimi") || lower.includes("umans-qwen");
}
/**
* Pure mapping: raw OpenAI-compatible model list → ModelInfo[].
- * Extracts `contextWindow` from common field names (providers vary).
- * Extracted for direct unit testing with no I/O.
+ * Extracts `contextWindow` from common field names (providers vary) and
+ * detects vision capability via {@link isVisionModelId}. Extracted for direct
+ * unit testing with no I/O.
*/
export function parseModelList(data: readonly OpenAIModelEntry[]): readonly ModelInfo[] {
- return data.map((entry) => {
- const contextWindow =
- entry.context_length ?? entry.context_window ?? entry.max_context_length ?? entry.max_tokens;
- return {
- id: entry.id,
- ...(contextWindow !== undefined ? { contextWindow } : {}),
- };
- });
+ return data.map((entry) => {
+ const contextWindow =
+ entry.context_length ?? entry.context_window ?? entry.max_context_length ?? entry.max_tokens;
+ const vision = isVisionModelId(entry.id);
+ return {
+ id: entry.id,
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
+ ...(vision ? { vision } : {}),
+ };
+ });
}
export interface ListModelsConfig {
- readonly baseURL: string;
- readonly apiKey: string;
- readonly fetchFn?: FetchLike;
- readonly providerId: string;
+ readonly baseURL: string;
+ readonly apiKey: string;
+ readonly fetchFn?: FetchLike;
+ readonly providerId: string;
}
export async function listModels(config: ListModelsConfig): Promise<readonly ModelInfo[]> {
- const effectiveFetch: FetchLike = config.fetchFn ?? fetch;
- const url = `${config.baseURL}/models`;
+ const effectiveFetch: FetchLike = config.fetchFn ?? fetch;
+ const url = `${config.baseURL}/models`;
- let response: Response;
- try {
- response = await effectiveFetch(url, {
- method: "GET",
- headers: {
- Authorization: `Bearer ${config.apiKey}`,
- },
- });
- } catch (err) {
- throw new Error(
- `listModels[${config.providerId}]: network error — ${err instanceof Error ? err.message : String(err)}`,
- );
- }
+ let response: Response;
+ try {
+ response = await effectiveFetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ },
+ });
+ } catch (err) {
+ throw new Error(
+ `listModels[${config.providerId}]: network error — ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
- if (!response.ok) {
- const text = await response.text().catch(() => "unknown");
- throw new Error(`listModels[${config.providerId}]: HTTP ${response.status} — ${text}`);
- }
+ if (!response.ok) {
+ const text = await response.text().catch(() => "unknown");
+ throw new Error(`listModels[${config.providerId}]: HTTP ${response.status} — ${text}`);
+ }
- const body = (await response.json()) as OpenAIModelListResponse;
- if (!Array.isArray(body.data)) {
- throw new Error(
- `listModels[${config.providerId}]: unexpected response shape — missing "data" array`,
- );
- }
+ const body = (await response.json()) as OpenAIModelListResponse;
+ if (!Array.isArray(body.data)) {
+ throw new Error(
+ `listModels[${config.providerId}]: unexpected response shape — missing "data" array`,
+ );
+ }
- return parseModelList(body.data);
+ return parseModelList(body.data);
}
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" },
+ ]);
+ });
});
diff --git a/packages/openai-stream/src/parse-sse.ts b/packages/openai-stream/src/parse-sse.ts
index cfeb5b0..112a418 100644
--- a/packages/openai-stream/src/parse-sse.ts
+++ b/packages/openai-stream/src/parse-sse.ts
@@ -1,130 +1,130 @@
import type { ProviderEvent } from "@dispatch/kernel";
interface ToolCallAccumulator {
- id: string;
- name: string;
- arguments: string;
+ id: string;
+ name: string;
+ arguments: string;
}
interface SSEChunkDelta {
- content?: string;
- reasoning_content?: string;
- tool_calls?: Array<{
- index: number;
- id?: string;
- function?: { name?: string; arguments?: string };
- }>;
+ content?: string;
+ reasoning_content?: string;
+ tool_calls?: Array<{
+ index: number;
+ id?: string;
+ function?: { name?: string; arguments?: string };
+ }>;
}
interface SSEChunkChoice {
- delta: SSEChunkDelta;
- finish_reason?: string | null;
- index: number;
+ delta: SSEChunkDelta;
+ finish_reason?: string | null;
+ index: number;
}
interface SSEChunkUsageDetails {
- cached_tokens?: number;
+ cached_tokens?: number;
}
interface SSEChunk {
- id?: string;
- choices?: SSEChunkChoice[];
- usage?: {
- prompt_tokens?: number;
- completion_tokens?: number;
- cache_read_tokens?: number;
- cache_write_tokens?: number;
- prompt_tokens_details?: SSEChunkUsageDetails;
- completion_tokens_details?: Record<string, unknown>;
- };
+ id?: string;
+ choices?: SSEChunkChoice[];
+ usage?: {
+ prompt_tokens?: number;
+ completion_tokens?: number;
+ cache_read_tokens?: number;
+ cache_write_tokens?: number;
+ prompt_tokens_details?: SSEChunkUsageDetails;
+ completion_tokens_details?: Record<string, unknown>;
+ };
}
export function parseSSELines(lines: readonly string[]): ProviderEvent[] {
- const events: ProviderEvent[] = [];
- const toolCalls = new Map<number, ToolCallAccumulator>();
-
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed.startsWith("data:")) continue;
-
- const data = trimmed.slice(5).trim();
- if (data === "[DONE]") break;
-
- let chunk: SSEChunk;
- try {
- chunk = JSON.parse(data) as SSEChunk;
- } catch {
- events.push({ type: "error", message: `Invalid JSON in SSE data: ${data}` });
- continue;
- }
-
- if (chunk.choices) {
- for (const choice of chunk.choices) {
- const delta = choice.delta;
-
- if (delta.content) {
- events.push({ type: "text-delta", delta: delta.content });
- }
-
- if (delta.reasoning_content) {
- events.push({ type: "reasoning-delta", delta: delta.reasoning_content });
- }
-
- if (delta.tool_calls) {
- for (const tc of delta.tool_calls) {
- const existing = toolCalls.get(tc.index);
- if (existing) {
- if (tc.function?.arguments) {
- existing.arguments += tc.function.arguments;
- }
- } else {
- toolCalls.set(tc.index, {
- id: tc.id ?? "",
- name: tc.function?.name ?? "",
- arguments: tc.function?.arguments ?? "",
- });
- }
- }
- }
-
- if (choice.finish_reason) {
- const sortedIndices = [...toolCalls.keys()].sort((a, b) => a - b);
- for (const idx of sortedIndices) {
- const acc = toolCalls.get(idx);
- if (!acc) continue;
- let input: unknown;
- try {
- input = JSON.parse(acc.arguments);
- } catch {
- input = acc.arguments;
- }
- events.push({
- type: "tool-call",
- toolCallId: acc.id,
- toolName: acc.name,
- input,
- });
- }
- events.push({ type: "finish", reason: choice.finish_reason });
- }
- }
- }
-
- if (chunk.usage) {
- const cacheRead =
- chunk.usage.cache_read_tokens ?? chunk.usage.prompt_tokens_details?.cached_tokens;
- const cacheWrite = chunk.usage.cache_write_tokens;
- events.push({
- type: "usage",
- usage: {
- inputTokens: chunk.usage.prompt_tokens ?? 0,
- outputTokens: chunk.usage.completion_tokens ?? 0,
- ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
- ...(cacheWrite !== undefined ? { cacheWriteTokens: cacheWrite } : {}),
- },
- });
- }
- }
-
- return events;
+ const events: ProviderEvent[] = [];
+ const toolCalls = new Map<number, ToolCallAccumulator>();
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed.startsWith("data:")) continue;
+
+ const data = trimmed.slice(5).trim();
+ if (data === "[DONE]") break;
+
+ let chunk: SSEChunk;
+ try {
+ chunk = JSON.parse(data) as SSEChunk;
+ } catch {
+ events.push({ type: "error", message: `Invalid JSON in SSE data: ${data}` });
+ continue;
+ }
+
+ if (chunk.choices) {
+ for (const choice of chunk.choices) {
+ const delta = choice.delta;
+
+ if (delta.content) {
+ events.push({ type: "text-delta", delta: delta.content });
+ }
+
+ if (delta.reasoning_content) {
+ events.push({ type: "reasoning-delta", delta: delta.reasoning_content });
+ }
+
+ if (delta.tool_calls) {
+ for (const tc of delta.tool_calls) {
+ const existing = toolCalls.get(tc.index);
+ if (existing) {
+ if (tc.function?.arguments) {
+ existing.arguments += tc.function.arguments;
+ }
+ } else {
+ toolCalls.set(tc.index, {
+ id: tc.id ?? "",
+ name: tc.function?.name ?? "",
+ arguments: tc.function?.arguments ?? "",
+ });
+ }
+ }
+ }
+
+ if (choice.finish_reason) {
+ const sortedIndices = [...toolCalls.keys()].sort((a, b) => a - b);
+ for (const idx of sortedIndices) {
+ const acc = toolCalls.get(idx);
+ if (!acc) continue;
+ let input: unknown;
+ try {
+ input = JSON.parse(acc.arguments);
+ } catch {
+ input = acc.arguments;
+ }
+ events.push({
+ type: "tool-call",
+ toolCallId: acc.id,
+ toolName: acc.name,
+ input,
+ });
+ }
+ events.push({ type: "finish", reason: choice.finish_reason });
+ }
+ }
+ }
+
+ if (chunk.usage) {
+ const cacheRead =
+ chunk.usage.cache_read_tokens ?? chunk.usage.prompt_tokens_details?.cached_tokens;
+ const cacheWrite = chunk.usage.cache_write_tokens;
+ events.push({
+ type: "usage",
+ usage: {
+ inputTokens: chunk.usage.prompt_tokens ?? 0,
+ outputTokens: chunk.usage.completion_tokens ?? 0,
+ ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
+ ...(cacheWrite !== undefined ? { cacheWriteTokens: cacheWrite } : {}),
+ },
+ });
+ }
+ }
+
+ return events;
}
diff --git a/packages/openai-stream/src/provider.test.ts b/packages/openai-stream/src/provider.test.ts
index 434c405..13a303e 100644
--- a/packages/openai-stream/src/provider.test.ts
+++ b/packages/openai-stream/src/provider.test.ts
@@ -4,151 +4,172 @@ import { describe, expect, it, vi } from "vitest";
import { createOpenAICompatProvider } from "./provider.js";
function makeCreds(): ApiKeyCredentials {
- return {
- type: "api-key",
- apiKey: "sk-test-1234567890abcdef",
- baseURL: "https://api.example.com/v1",
- };
+ return {
+ type: "api-key",
+ apiKey: "sk-test-1234567890abcdef",
+ baseURL: "https://api.example.com/v1",
+ };
}
function makeMessages(): readonly ChatMessage[] {
- return [{ role: "user", chunks: [{ type: "text", text: "Hello" }] }];
+ return [{ role: "user", chunks: [{ type: "text", text: "Hello" }] }];
}
function sseBody(...lines: string[]): ReadableStream<Uint8Array> {
- const encoder = new TextEncoder();
- const chunks = lines.map((l) => encoder.encode(`${l}\n`));
- let index = 0;
- return new ReadableStream<Uint8Array>({
- pull(controller) {
- if (index < chunks.length) {
- const chunk = chunks[index];
- if (chunk === undefined) throw new Error("empty chunk");
- controller.enqueue(chunk);
- index++;
- } else {
- controller.close();
- }
- },
- });
+ const encoder = new TextEncoder();
+ const chunks = lines.map((l) => encoder.encode(`${l}\n`));
+ let index = 0;
+ return new ReadableStream<Uint8Array>({
+ pull(controller) {
+ if (index < chunks.length) {
+ const chunk = chunks[index];
+ if (chunk === undefined) throw new Error("empty chunk");
+ controller.enqueue(chunk);
+ index++;
+ } else {
+ controller.close();
+ }
+ },
+ });
}
function okSseResponse(): Response {
- return new Response(
- sseBody(
- 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
- 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- );
+ return new Response(
+ sseBody(
+ 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
+ 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ );
}
async function collectEvents(iter: AsyncIterable<unknown>): Promise<unknown[]> {
- const events: unknown[] = [];
- for await (const event of iter) {
- events.push(event);
- }
- return events;
+ const events: unknown[] = [];
+ for await (const event of iter) {
+ events.push(event);
+ }
+ return events;
}
describe("createOpenAICompatProvider stamps the given id on the ProviderContract + listModels", () => {
- it("stamps opts.id on ProviderContract.id", () => {
- const provider = createOpenAICompatProvider({
- credentials: makeCreds(),
- model: "test-model",
- id: "my-custom-id",
- });
- expect(provider.id).toBe("my-custom-id");
- });
-
- it("uses opts.id in listModels error labels (was hardcoded 'openai-compat')", async () => {
- const fetchFn = vi.fn(
- () =>
- new Response("Unauthorized", {
- status: 401,
- headers: { "Content-Type": "text/plain" },
- }) as unknown as ReturnType<FetchLike>,
- );
- const provider = createOpenAICompatProvider({
- credentials: makeCreds(),
- model: "test-model",
- id: "my-custom-id",
- fetchFn,
- });
- const listModels = provider.listModels;
- if (!listModels) throw new Error("listModels not defined");
-
- await expect(listModels()).rejects.toThrow("listModels[my-custom-id]: HTTP 401 — Unauthorized");
- });
+ it("stamps opts.id on ProviderContract.id", () => {
+ const provider = createOpenAICompatProvider({
+ credentials: makeCreds(),
+ model: "test-model",
+ id: "my-custom-id",
+ });
+ expect(provider.id).toBe("my-custom-id");
+ });
+
+ it("uses opts.id in listModels error labels (was hardcoded 'openai-compat')", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ new Response("Unauthorized", {
+ status: 401,
+ headers: { "Content-Type": "text/plain" },
+ }) as unknown as ReturnType<FetchLike>,
+ );
+ const provider = createOpenAICompatProvider({
+ credentials: makeCreds(),
+ model: "test-model",
+ id: "my-custom-id",
+ fetchFn,
+ });
+ const listModels = provider.listModels;
+ if (!listModels) throw new Error("listModels not defined");
+
+ await expect(listModels()).rejects.toThrow("listModels[my-custom-id]: HTTP 401 — Unauthorized");
+ });
+
+ it("exposes getUsage that returns the upstream concurrent_sessions", async () => {
+ const fetchFn = vi.fn(
+ () =>
+ new Response(JSON.stringify({ usage: { concurrent_sessions: 2 } }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ }) as unknown as ReturnType<FetchLike>,
+ );
+ const provider = createOpenAICompatProvider({
+ credentials: makeCreds(),
+ model: "test-model",
+ id: "umans",
+ fetchFn,
+ });
+ const getUsage = provider.getUsage;
+ if (!getUsage) throw new Error("getUsage not defined");
+
+ const usage = await getUsage();
+ expect(usage).toEqual({ concurrentSessions: 2 });
+ });
});
describe("transformBody", () => {
- it("transformBody merges its returned fields into the request body", async () => {
- let capturedInit: RequestInit | undefined;
- const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
- capturedInit = init;
- return okSseResponse();
- }) as unknown as FetchLike;
-
- let receivedBody: Record<string, unknown> | undefined;
- let receivedOpts: ProviderStreamOptions | undefined;
- const provider = createOpenAICompatProvider({
- credentials: makeCreds(),
- model: "test-model",
- id: "umans",
- fetchFn,
- transformBody: (body, opts) => {
- receivedBody = body;
- receivedOpts = opts;
- return { reasoning_effort: "high" };
- },
- });
-
- await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5 }));
-
- // The hook was called with the body built so far + the stream opts.
- expect(receivedBody).toBeDefined();
- expect(receivedOpts?.temperature).toBe(0.5);
- expect(receivedBody?.model).toBe("test-model");
-
- // The captured wire body carries the merged field.
- expect(capturedInit?.body).toBeTypeOf("string");
- const wireBody = JSON.parse(capturedInit?.body as string) as Record<string, unknown>;
- expect(wireBody.reasoning_effort).toBe("high");
- expect(wireBody.model).toBe("test-model");
- expect(wireBody.stream).toBe(true);
- expect(wireBody.temperature).toBe(0.5);
- });
-
- it("transformBody absent → body byte-identical to before (regression)", async () => {
- let capturedInit: RequestInit | undefined;
- const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
- capturedInit = init;
- return okSseResponse();
- }) as unknown as FetchLike;
-
- const provider = createOpenAICompatProvider({
- credentials: makeCreds(),
- model: "test-model",
- id: "openai-compat",
- fetchFn,
- // No transformBody — default behavior.
- });
-
- await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5, maxTokens: 42 }));
-
- expect(capturedInit?.body).toBeTypeOf("string");
- const wireBody = JSON.parse(capturedInit?.body as string) as Record<string, unknown>;
- // Exact pre-refactor shape — no extra fields, no transformBody key leakage.
- expect(wireBody).toEqual({
- model: "test-model",
- messages: [{ role: "user", content: "Hello" }],
- stream: true,
- stream_options: { include_usage: true },
- temperature: 0.5,
- max_tokens: 42,
- });
- expect("reasoning_effort" in wireBody).toBe(false);
- });
+ it("transformBody merges its returned fields into the request body", async () => {
+ let capturedInit: RequestInit | undefined;
+ const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
+ capturedInit = init;
+ return okSseResponse();
+ }) as unknown as FetchLike;
+
+ let receivedBody: Record<string, unknown> | undefined;
+ let receivedOpts: ProviderStreamOptions | undefined;
+ const provider = createOpenAICompatProvider({
+ credentials: makeCreds(),
+ model: "test-model",
+ id: "umans",
+ fetchFn,
+ transformBody: (body, opts) => {
+ receivedBody = body;
+ receivedOpts = opts;
+ return { reasoning_effort: "high" };
+ },
+ });
+
+ await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5 }));
+
+ // The hook was called with the body built so far + the stream opts.
+ expect(receivedBody).toBeDefined();
+ expect(receivedOpts?.temperature).toBe(0.5);
+ expect(receivedBody?.model).toBe("test-model");
+
+ // The captured wire body carries the merged field.
+ expect(capturedInit?.body).toBeTypeOf("string");
+ const wireBody = JSON.parse(capturedInit?.body as string) as Record<string, unknown>;
+ expect(wireBody.reasoning_effort).toBe("high");
+ expect(wireBody.model).toBe("test-model");
+ expect(wireBody.stream).toBe(true);
+ expect(wireBody.temperature).toBe(0.5);
+ });
+
+ it("transformBody absent → body byte-identical to before (regression)", async () => {
+ let capturedInit: RequestInit | undefined;
+ const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
+ capturedInit = init;
+ return okSseResponse();
+ }) as unknown as FetchLike;
+
+ const provider = createOpenAICompatProvider({
+ credentials: makeCreds(),
+ model: "test-model",
+ id: "openai-compat",
+ fetchFn,
+ // No transformBody — default behavior.
+ });
+
+ await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5, maxTokens: 42 }));
+
+ expect(capturedInit?.body).toBeTypeOf("string");
+ const wireBody = JSON.parse(capturedInit?.body as string) as Record<string, unknown>;
+ // Exact pre-refactor shape — no extra fields, no transformBody key leakage.
+ expect(wireBody).toEqual({
+ model: "test-model",
+ messages: [{ role: "user", content: "Hello" }],
+ stream: true,
+ stream_options: { include_usage: true },
+ temperature: 0.5,
+ max_tokens: 42,
+ });
+ expect("reasoning_effort" in wireBody).toBe(false);
+ });
});
diff --git a/packages/openai-stream/src/provider.ts b/packages/openai-stream/src/provider.ts
index c13d60e..9a9369f 100644
--- a/packages/openai-stream/src/provider.ts
+++ b/packages/openai-stream/src/provider.ts
@@ -1,12 +1,14 @@
import type {
- ApiKeyCredentials,
- ChatMessage,
- ModelInfo,
- ProviderContract,
- ProviderStreamOptions,
- ToolContract,
+ ApiKeyCredentials,
+ ChatMessage,
+ ModelInfo,
+ ProviderContract,
+ ProviderStreamOptions,
+ ProviderUsage,
+ ToolContract,
} from "@dispatch/kernel";
import type { FetchLike } from "@dispatch/trace-replay";
+import { getUsage as fetchUsage } from "./getUsage.js";
import { listModels as fetchModels } from "./listModels.js";
import { streamChat } from "./stream.js";
@@ -19,55 +21,62 @@ import { streamChat } from "./stream.js";
*/
export interface CreateOpenAICompatProviderOpts {
- readonly credentials: ApiKeyCredentials;
- readonly model: string;
- /** Provider id (was hardcoded "openai-compat"). Stamped on the ProviderContract.id
- * + used in listModels error labels. */
- readonly id: string;
- /**
- * Internal injectable fetch — used by tests and replay mode.
- * When absent, falls back to globalThis.fetch (production default).
- */
- readonly fetchFn?: FetchLike;
- /**
- * Optional hook a provider extension uses to add provider-specific body fields (e.g.
- * `reasoning_effort`) before the request is sent. Receives the body built so far +
- * the ProviderStreamOptions; returns ADDITIONAL fields to merge (or the full body).
- * Default (absent): no extra fields. Generic — the library names no feature.
- */
- readonly transformBody?: (
- body: Record<string, unknown>,
- opts: ProviderStreamOptions,
- ) => Record<string, unknown>;
+ readonly credentials: ApiKeyCredentials;
+ readonly model: string;
+ /** Provider id (was hardcoded "openai-compat"). Stamped on the ProviderContract.id
+ * + used in listModels error labels. */
+ readonly id: string;
+ /**
+ * Internal injectable fetch — used by tests and replay mode.
+ * When absent, falls back to globalThis.fetch (production default).
+ */
+ readonly fetchFn?: FetchLike;
+ /**
+ * Optional hook a provider extension uses to add provider-specific body fields (e.g.
+ * `reasoning_effort`) before the request is sent. Receives the body built so far +
+ * the ProviderStreamOptions; returns ADDITIONAL fields to merge (or the full body).
+ * Default (absent): no extra fields. Generic — the library names no feature.
+ */
+ readonly transformBody?: (
+ body: Record<string, unknown>,
+ opts: ProviderStreamOptions,
+ ) => Record<string, unknown>;
}
export function createOpenAICompatProvider(opts: CreateOpenAICompatProviderOpts): ProviderContract {
- const baseURL = opts.credentials.baseURL ?? "https://opencode.ai/zen/go/v1";
- const apiKey = opts.credentials.apiKey;
- const fetchFn = opts.fetchFn;
- const transformBody = opts.transformBody;
+ const baseURL = opts.credentials.baseURL ?? "https://opencode.ai/zen/go/v1";
+ const apiKey = opts.credentials.apiKey;
+ const fetchFn = opts.fetchFn;
+ const transformBody = opts.transformBody;
- const streamConfig = {
- baseURL,
- apiKey,
- model: opts.model,
- ...(fetchFn !== undefined ? { fetchFn } : {}),
- ...(transformBody !== undefined ? { transformBody } : {}),
- };
+ const streamConfig = {
+ baseURL,
+ apiKey,
+ model: opts.model,
+ ...(fetchFn !== undefined ? { fetchFn } : {}),
+ ...(transformBody !== undefined ? { transformBody } : {}),
+ };
- return {
- id: opts.id,
- stream: (
- messages: readonly ChatMessage[],
- tools: readonly ToolContract[],
- streamOpts?: ProviderStreamOptions,
- ) => streamChat(streamConfig, messages, tools, streamOpts),
- listModels: (): Promise<readonly ModelInfo[]> =>
- fetchModels({
- baseURL,
- apiKey,
- providerId: opts.id,
- ...(fetchFn !== undefined ? { fetchFn } : {}),
- }),
- };
+ return {
+ id: opts.id,
+ stream: (
+ messages: readonly ChatMessage[],
+ tools: readonly ToolContract[],
+ streamOpts?: ProviderStreamOptions,
+ ) => streamChat(streamConfig, messages, tools, streamOpts),
+ listModels: (): Promise<readonly ModelInfo[]> =>
+ fetchModels({
+ baseURL,
+ apiKey,
+ providerId: opts.id,
+ ...(fetchFn !== undefined ? { fetchFn } : {}),
+ }),
+ getUsage: (): Promise<ProviderUsage | undefined> =>
+ fetchUsage({
+ baseURL,
+ apiKey,
+ providerId: opts.id,
+ ...(fetchFn !== undefined ? { fetchFn } : {}),
+ }),
+ };
}
diff --git a/packages/openai-stream/src/stream.test.ts b/packages/openai-stream/src/stream.test.ts
index 0650153..ea8a079 100644
--- a/packages/openai-stream/src/stream.test.ts
+++ b/packages/openai-stream/src/stream.test.ts
@@ -5,860 +5,860 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type StreamConfig, streamChat } from "./stream.js";
async function collectEvents(iter: AsyncIterable<ProviderEvent>): Promise<ProviderEvent[]> {
- const events: ProviderEvent[] = [];
- for await (const event of iter) {
- events.push(event);
- }
- return events;
+ const events: ProviderEvent[] = [];
+ for await (const event of iter) {
+ events.push(event);
+ }
+ return events;
}
function assertDefined<T>(v: T, msg?: string): asserts v is NonNullable<T> {
- if (v === undefined || v === null) {
- throw new Error(msg ?? "expected defined");
- }
+ if (v === undefined || v === null) {
+ throw new Error(msg ?? "expected defined");
+ }
}
interface CapturedSpan {
- name: string;
- attrs: Record<string, string | number | boolean | null>;
- body?: string | undefined;
- endOutcome?:
- | { err?: unknown; attrs?: Record<string, string | number | boolean | null> }
- | undefined;
+ name: string;
+ attrs: Record<string, string | number | boolean | null>;
+ body?: string | undefined;
+ endOutcome?:
+ | { err?: unknown; attrs?: Record<string, string | number | boolean | null> }
+ | undefined;
}
function createFakeLogger(): { logger: Logger; spans: CapturedSpan[] } {
- const spans: CapturedSpan[] = [];
- let spanAttrBuffer: Record<string, string | number | boolean | null> = {};
- let spanBodyBuffer: string | undefined;
-
- const fakeSpan: Span = {
- id: "fake-span-id",
- log: {} as Logger,
- setAttributes(attrs) {
- Object.assign(spanAttrBuffer, attrs);
- },
- addLink() {},
- child() {
- return fakeSpan;
- },
- end(outcome?) {
- spans.push({
- name: "provider.request",
- attrs: { ...spanAttrBuffer },
- body: spanBodyBuffer,
- endOutcome: outcome as CapturedSpan["endOutcome"],
- });
- },
- };
-
- const logger: Logger = {
- debug() {},
- info() {},
- warn() {},
- error() {},
- child() {
- return logger;
- },
- span(_name, attrs, body) {
- spanAttrBuffer = attrs ? { ...attrs } : {};
- spanBodyBuffer = body;
- return fakeSpan;
- },
- };
-
- return { logger, spans };
+ const spans: CapturedSpan[] = [];
+ let spanAttrBuffer: Record<string, string | number | boolean | null> = {};
+ let spanBodyBuffer: string | undefined;
+
+ const fakeSpan: Span = {
+ id: "fake-span-id",
+ log: {} as Logger,
+ setAttributes(attrs) {
+ Object.assign(spanAttrBuffer, attrs);
+ },
+ addLink() {},
+ child() {
+ return fakeSpan;
+ },
+ end(outcome?) {
+ spans.push({
+ name: "provider.request",
+ attrs: { ...spanAttrBuffer },
+ body: spanBodyBuffer,
+ endOutcome: outcome as CapturedSpan["endOutcome"],
+ });
+ },
+ };
+
+ const logger: Logger = {
+ debug() {},
+ info() {},
+ warn() {},
+ error() {},
+ child() {
+ return logger;
+ },
+ span(_name, attrs, body) {
+ spanAttrBuffer = attrs ? { ...attrs } : {};
+ spanBodyBuffer = body;
+ return fakeSpan;
+ },
+ };
+
+ return { logger, spans };
}
function makeConfig(apiKey = "sk-test-1234567890abcdef"): StreamConfig {
- return {
- baseURL: "https://api.example.com/v1",
- apiKey,
- model: "test-model",
- };
+ return {
+ baseURL: "https://api.example.com/v1",
+ apiKey,
+ model: "test-model",
+ };
}
function mockFetch(handler: (url: string | URL | Request, init?: RequestInit) => unknown): void {
- globalThis.fetch = vi.fn(handler) as unknown as typeof globalThis.fetch;
+ globalThis.fetch = vi.fn(handler) as unknown as typeof globalThis.fetch;
}
function makeMessages(): readonly ChatMessage[] {
- return [
- {
- role: "user",
- chunks: [{ type: "text", text: "Hello" }],
- },
- ];
+ return [
+ {
+ role: "user",
+ chunks: [{ type: "text", text: "Hello" }],
+ },
+ ];
}
function sseBody(...lines: string[]): ReadableStream<Uint8Array> {
- const encoder = new TextEncoder();
- const chunks = lines.map((l) => encoder.encode(`${l}\n`));
- let index = 0;
- return new ReadableStream<Uint8Array>({
- pull(controller) {
- if (index < chunks.length) {
- const chunk = chunks[index];
- assertDefined(chunk);
- controller.enqueue(chunk);
- index++;
- } else {
- controller.close();
- }
- },
- });
+ const encoder = new TextEncoder();
+ const chunks = lines.map((l) => encoder.encode(`${l}\n`));
+ let index = 0;
+ return new ReadableStream<Uint8Array>({
+ pull(controller) {
+ if (index < chunks.length) {
+ const chunk = chunks[index];
+ assertDefined(chunk);
+ controller.enqueue(chunk);
+ index++;
+ } else {
+ controller.close();
+ }
+ },
+ });
}
describe("streamChat — provider.request AFTER capture", () => {
- let originalFetch: typeof globalThis.fetch;
-
- beforeEach(() => {
- originalFetch = globalThis.fetch;
- });
-
- afterEach(() => {
- globalThis.fetch = originalFetch;
- });
-
- it("opens a provider.request span with verbatim request body", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
- 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- expect(spans).toHaveLength(1);
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.name).toBe("provider.request");
- expect(span.attrs["request.method"]).toBe("POST");
- expect(span.attrs["request.body"]).toBeUndefined();
-
- assertDefined(span.body);
- const capturedBody = JSON.parse(span.body);
- expect(capturedBody.model).toBe("test-model");
- expect(capturedBody.stream).toBe(true);
- expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello" }]);
-
- expect(span.endOutcome?.attrs?.status).toBe(200);
- });
-
- it("redacts a long API key (≥13 chars → reveal 3 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcdefghijkmnop");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-2","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-2","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer sk-…redacted…nop");
- expect(authHeader).not.toContain("abcdefghijkm");
- });
-
- it("redacts a medium API key (8–10 chars → reveal 1 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcde");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-3","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-3","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer s…redacted…e");
- });
-
- it("redacts a short API key (≤7 chars → full mask)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("secret!");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-4","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer …redacted…");
- });
-
- it("captures cache tokens from the response", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-5","choices":[{"delta":{"content":"Hi"},"index":0}]}',
- 'data: {"id":"cmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- 'data: {"id":"cmpl-5","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":80,"cache_write_tokens":10}}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.["usage.inputTokens"]).toBe(100);
- expect(span.endOutcome?.attrs?.["usage.outputTokens"]).toBe(20);
- expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(80);
- expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBe(10);
- });
-
- it("captures cache_read_tokens alone", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-6","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- 'data: {"id":"cmpl-6","usage":{"prompt_tokens":50,"completion_tokens":5,"cache_read_tokens":45}}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(45);
- expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBeUndefined();
- });
-
- it("records HTTP error status and error body without throwing", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response("Invalid request body", {
- status: 400,
- headers: { "Content-Type": "text/plain" },
- }),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "HTTP 400: Invalid request body",
- code: "400",
- retryable: false,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.attrs?.status).toBe(400);
- expect(span.endOutcome?.attrs?.["response.error_body"]).toBe("Invalid request body");
- expect(span.endOutcome?.err).toBeInstanceOf(Error);
- });
-
- it("records network error without throwing", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(() => {
- throw new Error("connection refused");
- });
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "connection refused",
- retryable: true,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.endOutcome?.err).toBeInstanceOf(Error);
- expect((span.endOutcome?.err as Error).message).toBe("connection refused");
- });
-
- it("detects cache_control breakpoint absence in a normal request body", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-7","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-7","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs["request.cache_control_present"]).toBe(false);
- });
-
- it("does not open a span when opts.logger is absent", async () => {
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-8","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-8","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), []));
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- });
-
- it("fail-safe: logger throwing does not break stream()", async () => {
- const brokenLogger: Logger = {
- debug() {},
- info() {},
- warn() {},
- error() {},
- child() {
- return brokenLogger;
- },
- span() {
- throw new Error("logger exploded");
- },
- };
-
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-9","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-9","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- const events = await collectEvents(
- streamChat(config, makeMessages(), [], { logger: brokenLogger }),
- );
-
- expect(events.some((e) => e.type === "text-delta")).toBe(true);
- expect(events.some((e) => e.type === "finish")).toBe(true);
- });
-
- it("redacts an 11-char API key (reveal 2 each side)", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig("sk-abcde1234");
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-10","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-10","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- const authHeader = span.attrs["request.headers.authorization"] as string;
- expect(authHeader).toBe("Bearer sk…redacted…34");
- });
-
- it("records server error (500) as retryable", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response("Internal Server Error", {
- status: 500,
- headers: { "Content-Type": "text/plain" },
- }),
- );
-
- const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- expect(events).toHaveLength(1);
- expect(events[0]).toEqual({
- type: "error",
- message: "HTTP 500: Internal Server Error",
- code: "500",
- retryable: true,
- });
-
- expect(spans).toHaveLength(1);
- assertDefined(spans[0]);
- expect(spans[0].endOutcome?.attrs?.status).toBe(500);
- });
-
- it("captures model and url on the span", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-11","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-11","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(streamChat(config, makeMessages(), [], { logger }));
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs.model).toBe("test-model");
- expect(span.attrs.url).toBe("https://api.example.com/v1/chat/completions");
- });
-
- it("uses opts.model override in capture", async () => {
- const { logger, spans } = createFakeLogger();
- const config = makeConfig();
-
- mockFetch(
- () =>
- new Response(
- sseBody(
- 'data: {"id":"cmpl-12","choices":[{"delta":{"content":"ok"},"index":0}]}',
- 'data: {"id":"cmpl-12","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
- "data: [DONE]",
- ),
- { status: 200, headers: { "Content-Type": "text/event-stream" } },
- ),
- );
-
- await collectEvents(
- streamChat(config, makeMessages(), [], { logger, model: "override-model" }),
- );
-
- assertDefined(spans[0]);
- const span = spans[0];
- expect(span.attrs.model).toBe("override-model");
-
- assertDefined(span.body);
- const capturedBody = JSON.parse(span.body);
- expect(capturedBody.model).toBe("override-model");
- });
+ let originalFetch: typeof globalThis.fetch;
+
+ beforeEach(() => {
+ originalFetch = globalThis.fetch;
+ });
+
+ afterEach(() => {
+ globalThis.fetch = originalFetch;
+ });
+
+ it("opens a provider.request span with verbatim request body", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}',
+ 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ expect(spans).toHaveLength(1);
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.name).toBe("provider.request");
+ expect(span.attrs["request.method"]).toBe("POST");
+ expect(span.attrs["request.body"]).toBeUndefined();
+
+ assertDefined(span.body);
+ const capturedBody = JSON.parse(span.body);
+ expect(capturedBody.model).toBe("test-model");
+ expect(capturedBody.stream).toBe(true);
+ expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello" }]);
+
+ expect(span.endOutcome?.attrs?.status).toBe(200);
+ });
+
+ it("redacts a long API key (≥13 chars → reveal 3 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcdefghijkmnop");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-2","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-2","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer sk-…redacted…nop");
+ expect(authHeader).not.toContain("abcdefghijkm");
+ });
+
+ it("redacts a medium API key (8–10 chars → reveal 1 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcde");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-3","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-3","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer s…redacted…e");
+ });
+
+ it("redacts a short API key (≤7 chars → full mask)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("secret!");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-4","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer …redacted…");
+ });
+
+ it("captures cache tokens from the response", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-5","choices":[{"delta":{"content":"Hi"},"index":0}]}',
+ 'data: {"id":"cmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ 'data: {"id":"cmpl-5","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":80,"cache_write_tokens":10}}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.["usage.inputTokens"]).toBe(100);
+ expect(span.endOutcome?.attrs?.["usage.outputTokens"]).toBe(20);
+ expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(80);
+ expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBe(10);
+ });
+
+ it("captures cache_read_tokens alone", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-6","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ 'data: {"id":"cmpl-6","usage":{"prompt_tokens":50,"completion_tokens":5,"cache_read_tokens":45}}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(45);
+ expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBeUndefined();
+ });
+
+ it("records HTTP error status and error body without throwing", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response("Invalid request body", {
+ status: 400,
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "HTTP 400: Invalid request body",
+ code: "400",
+ retryable: false,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.attrs?.status).toBe(400);
+ expect(span.endOutcome?.attrs?.["response.error_body"]).toBe("Invalid request body");
+ expect(span.endOutcome?.err).toBeInstanceOf(Error);
+ });
+
+ it("records network error without throwing", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(() => {
+ throw new Error("connection refused");
+ });
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "connection refused",
+ retryable: true,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.endOutcome?.err).toBeInstanceOf(Error);
+ expect((span.endOutcome?.err as Error).message).toBe("connection refused");
+ });
+
+ it("detects cache_control breakpoint absence in a normal request body", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-7","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-7","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs["request.cache_control_present"]).toBe(false);
+ });
+
+ it("does not open a span when opts.logger is absent", async () => {
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-8","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-8","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), []));
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ });
+
+ it("fail-safe: logger throwing does not break stream()", async () => {
+ const brokenLogger: Logger = {
+ debug() {},
+ info() {},
+ warn() {},
+ error() {},
+ child() {
+ return brokenLogger;
+ },
+ span() {
+ throw new Error("logger exploded");
+ },
+ };
+
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-9","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-9","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ const events = await collectEvents(
+ streamChat(config, makeMessages(), [], { logger: brokenLogger }),
+ );
+
+ expect(events.some((e) => e.type === "text-delta")).toBe(true);
+ expect(events.some((e) => e.type === "finish")).toBe(true);
+ });
+
+ it("redacts an 11-char API key (reveal 2 each side)", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig("sk-abcde1234");
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-10","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-10","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ const authHeader = span.attrs["request.headers.authorization"] as string;
+ expect(authHeader).toBe("Bearer sk…redacted…34");
+ });
+
+ it("records server error (500) as retryable", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response("Internal Server Error", {
+ status: 500,
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+
+ const events = await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ expect(events).toHaveLength(1);
+ expect(events[0]).toEqual({
+ type: "error",
+ message: "HTTP 500: Internal Server Error",
+ code: "500",
+ retryable: true,
+ });
+
+ expect(spans).toHaveLength(1);
+ assertDefined(spans[0]);
+ expect(spans[0].endOutcome?.attrs?.status).toBe(500);
+ });
+
+ it("captures model and url on the span", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-11","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-11","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(streamChat(config, makeMessages(), [], { logger }));
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs.model).toBe("test-model");
+ expect(span.attrs.url).toBe("https://api.example.com/v1/chat/completions");
+ });
+
+ it("uses opts.model override in capture", async () => {
+ const { logger, spans } = createFakeLogger();
+ const config = makeConfig();
+
+ mockFetch(
+ () =>
+ new Response(
+ sseBody(
+ 'data: {"id":"cmpl-12","choices":[{"delta":{"content":"ok"},"index":0}]}',
+ 'data: {"id":"cmpl-12","choices":[{"delta":{},"finish_reason":"stop","index":0}]}',
+ "data: [DONE]",
+ ),
+ { status: 200, headers: { "Content-Type": "text/event-stream" } },
+ ),
+ );
+
+ await collectEvents(
+ streamChat(config, makeMessages(), [], { logger, model: "override-model" }),
+ );
+
+ assertDefined(spans[0]);
+ const span = spans[0];
+ expect(span.attrs.model).toBe("override-model");
+
+ assertDefined(span.body);
+ const capturedBody = JSON.parse(span.body);
+ expect(capturedBody.model).toBe("override-model");
+ });
});
describe("streamChat — hermetic replay (trace-replay)", () => {
- const testDir = new URL(".", import.meta.url).pathname;
- const fixturePath = `${testDir}__fixtures__/flash-text-turn.json`;
- const toolFixturePath = `${testDir}__fixtures__/tool-call-turn.json`;
-
- it("replays a text-turn fixture and produces correct ProviderEvents", async () => {
- const fixture = loadFixture(fixturePath);
- const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 64 });
-
- const config: StreamConfig = {
- baseURL: "https://api.example.com/v1",
- apiKey: "sk-test-1234567890abcdef",
- model: "deepseek-v4-flash",
- fetchFn: replayFetchFn,
- };
-
- const messages: ChatMessage[] = [
- { role: "user", chunks: [{ type: "text", text: "Hello, how are you?" }] },
- ];
-
- const events = await collectEvents(streamChat(config, messages, []));
-
- const textDeltas = events.filter(
- (e): e is Extract<ProviderEvent, { type: "text-delta" }> => e.type === "text-delta",
- );
- const fullText = textDeltas.map((e) => e.delta).join("");
- expect(fullText).toBe("Hello there friend");
-
- const finishEvents = events.filter((e) => e.type === "finish");
- expect(finishEvents).toHaveLength(1);
- expect(finishEvents[0]).toEqual({ type: "finish", reason: "stop" });
-
- const usageEvents = events.filter(
- (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
- );
- expect(usageEvents).toHaveLength(1);
- expect(usageEvents[0]?.usage.inputTokens).toBe(665);
- expect(usageEvents[0]?.usage.outputTokens).toBe(90);
- expect(usageEvents[0]?.usage.cacheReadTokens).toBe(384);
-
- const captured = getCapturedRequest();
- assertDefined(captured);
- expect(captured.method).toBe("POST");
- expect(captured.url).toBe("https://api.example.com/v1/chat/completions");
- expect(captured.headers["Content-Type"]).toBe("application/json");
- expect(captured.headers.Authorization).toBe("Bearer sk-test-1234567890abcdef");
-
- assertDefined(captured.body);
- const capturedBody = JSON.parse(captured.body);
- expect(capturedBody.model).toBe("deepseek-v4-flash");
- expect(capturedBody.stream).toBe(true);
- expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello, how are you?" }]);
- });
-
- it("replays a tool-call-turn fixture and produces tool-call + finish events", async () => {
- const fixture = loadFixture(toolFixturePath);
- const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 48 });
-
- const config: StreamConfig = {
- baseURL: "https://api.example.com/v1",
- apiKey: "sk-test-1234567890abcdef",
- model: "deepseek-v4-flash",
- fetchFn: replayFetchFn,
- };
-
- const messages: ChatMessage[] = [
- { role: "user", chunks: [{ type: "text", text: "What is the weather in Tokyo?" }] },
- ];
-
- const weatherTool = {
- name: "get_weather",
- description: "Get current weather for a location",
- parameters: {
- type: "object" as const,
- properties: { location: { type: "string" as const } },
- required: ["location"],
- },
- execute: async () => ({ content: "" }),
- };
-
- const events = await collectEvents(streamChat(config, messages, [weatherTool]));
-
- const toolCalls = events.filter(
- (e): e is Extract<ProviderEvent, { type: "tool-call" }> => e.type === "tool-call",
- );
- expect(toolCalls).toHaveLength(1);
- expect(toolCalls[0]?.toolCallId).toBe("call_abc123");
- expect(toolCalls[0]?.toolName).toBe("get_weather");
- expect(toolCalls[0]?.input).toEqual({ location: "Tokyo" });
-
- const finishEvents = events.filter((e) => e.type === "finish");
- expect(finishEvents).toHaveLength(1);
- expect(finishEvents[0]).toEqual({ type: "finish", reason: "tool_calls" });
-
- const usageEvents = events.filter(
- (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
- );
- expect(usageEvents).toHaveLength(1);
- expect(usageEvents[0]?.usage.inputTokens).toBe(45);
- expect(usageEvents[0]?.usage.outputTokens).toBe(12);
- expect(usageEvents[0]?.usage.cacheReadTokens).toBe(30);
- expect(usageEvents[0]?.usage.cacheWriteTokens).toBe(5);
-
- const captured = getCapturedRequest();
- assertDefined(captured);
- expect(captured.method).toBe("POST");
- assertDefined(captured.body);
- const capturedBody = JSON.parse(captured.body);
- expect(capturedBody.tools).toHaveLength(1);
- expect(capturedBody.tools[0].function.name).toBe("get_weather");
- });
+ const testDir = new URL(".", import.meta.url).pathname;
+ const fixturePath = `${testDir}__fixtures__/flash-text-turn.json`;
+ const toolFixturePath = `${testDir}__fixtures__/tool-call-turn.json`;
+
+ it("replays a text-turn fixture and produces correct ProviderEvents", async () => {
+ const fixture = loadFixture(fixturePath);
+ const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 64 });
+
+ const config: StreamConfig = {
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test-1234567890abcdef",
+ model: "deepseek-v4-flash",
+ fetchFn: replayFetchFn,
+ };
+
+ const messages: ChatMessage[] = [
+ { role: "user", chunks: [{ type: "text", text: "Hello, how are you?" }] },
+ ];
+
+ const events = await collectEvents(streamChat(config, messages, []));
+
+ const textDeltas = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "text-delta" }> => e.type === "text-delta",
+ );
+ const fullText = textDeltas.map((e) => e.delta).join("");
+ expect(fullText).toBe("Hello there friend");
+
+ const finishEvents = events.filter((e) => e.type === "finish");
+ expect(finishEvents).toHaveLength(1);
+ expect(finishEvents[0]).toEqual({ type: "finish", reason: "stop" });
+
+ const usageEvents = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
+ );
+ expect(usageEvents).toHaveLength(1);
+ expect(usageEvents[0]?.usage.inputTokens).toBe(665);
+ expect(usageEvents[0]?.usage.outputTokens).toBe(90);
+ expect(usageEvents[0]?.usage.cacheReadTokens).toBe(384);
+
+ const captured = getCapturedRequest();
+ assertDefined(captured);
+ expect(captured.method).toBe("POST");
+ expect(captured.url).toBe("https://api.example.com/v1/chat/completions");
+ expect(captured.headers["Content-Type"]).toBe("application/json");
+ expect(captured.headers.Authorization).toBe("Bearer sk-test-1234567890abcdef");
+
+ assertDefined(captured.body);
+ const capturedBody = JSON.parse(captured.body);
+ expect(capturedBody.model).toBe("deepseek-v4-flash");
+ expect(capturedBody.stream).toBe(true);
+ expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello, how are you?" }]);
+ });
+
+ it("replays a tool-call-turn fixture and produces tool-call + finish events", async () => {
+ const fixture = loadFixture(toolFixturePath);
+ const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 48 });
+
+ const config: StreamConfig = {
+ baseURL: "https://api.example.com/v1",
+ apiKey: "sk-test-1234567890abcdef",
+ model: "deepseek-v4-flash",
+ fetchFn: replayFetchFn,
+ };
+
+ const messages: ChatMessage[] = [
+ { role: "user", chunks: [{ type: "text", text: "What is the weather in Tokyo?" }] },
+ ];
+
+ const weatherTool = {
+ name: "get_weather",
+ description: "Get current weather for a location",
+ parameters: {
+ type: "object" as const,
+ properties: { location: { type: "string" as const } },
+ required: ["location"],
+ },
+ execute: async () => ({ content: "" }),
+ };
+
+ const events = await collectEvents(streamChat(config, messages, [weatherTool]));
+
+ const toolCalls = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "tool-call" }> => e.type === "tool-call",
+ );
+ expect(toolCalls).toHaveLength(1);
+ expect(toolCalls[0]?.toolCallId).toBe("call_abc123");
+ expect(toolCalls[0]?.toolName).toBe("get_weather");
+ expect(toolCalls[0]?.input).toEqual({ location: "Tokyo" });
+
+ const finishEvents = events.filter((e) => e.type === "finish");
+ expect(finishEvents).toHaveLength(1);
+ expect(finishEvents[0]).toEqual({ type: "finish", reason: "tool_calls" });
+
+ const usageEvents = events.filter(
+ (e): e is Extract<ProviderEvent, { type: "usage" }> => e.type === "usage",
+ );
+ expect(usageEvents).toHaveLength(1);
+ expect(usageEvents[0]?.usage.inputTokens).toBe(45);
+ expect(usageEvents[0]?.usage.outputTokens).toBe(12);
+ expect(usageEvents[0]?.usage.cacheReadTokens).toBe(30);
+ expect(usageEvents[0]?.usage.cacheWriteTokens).toBe(5);
+
+ const captured = getCapturedRequest();
+ assertDefined(captured);
+ expect(captured.method).toBe("POST");
+ assertDefined(captured.body);
+ const capturedBody = JSON.parse(captured.body);
+ expect(capturedBody.tools).toHaveLength(1);
+ expect(capturedBody.tools[0].function.name).toBe("get_weather");
+ });
});
describe("streamChat — record-mode redaction (trace-replay)", () => {
- /**
- * Graduated secret mask — §6 tiers. Duplicated locally (isolation-over-dry).
- * ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask.
- */
- function maskSecret(value: string): string {
- const len = value.length;
- if (len <= 7) return "…redacted…";
- let reveal: number;
- if (len >= 13) {
- reveal = 3;
- } else if (len >= 11) {
- reveal = 2;
- } else {
- reveal = 1;
- }
- return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
- }
-
- it("self-redacts auth header in onExchange and produces a secret-free fixture", async () => {
- const apiKey = "sk-abcdefghijkmnop";
- const responseBody =
- 'data: {"id":"cmpl-r","choices":[{"delta":{"content":"ok"},"index":0}],"usage":{"prompt_tokens":5,"completion_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}\n\ndata: [DONE]\n';
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () =>
- new Response(responseBody, {
- status: 200,
- headers: { "content-type": "text/event-stream" },
- }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "content-type": "application/json",
- authorization: `Bearer ${apiKey}`,
- },
- body: '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}',
- });
-
- assertDefined(capturedFixture);
-
- expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
- expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
- expect(capturedFixture.request.headers["content-type"]).toBe("application/json");
-
- expect(capturedFixture.request.body).toContain('"model":"test"');
- expect(capturedFixture.request.body).toContain('"content":"hi"');
-
- expect(capturedFixture.response.status).toBe(200);
- expect(capturedFixture.response.body).toBe(responseBody);
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).toContain("Bearer sk-…redacted…nop");
- expect(serialized).not.toContain("abcdefghijkm");
- expect(serialized).toContain("content");
- expect(serialized).toContain("hi");
- });
-
- it("redacts capitalized Authorization header (the real leak casing)", async () => {
- const apiKey = "sk-LIVEKEY1234567890abcdef";
- const responseBody = "data: [DONE]\n";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () =>
- new Response(responseBody, {
- status: 200,
- headers: { "content-type": "text/event-stream" },
- }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${apiKey}`,
- },
- body: '{"model":"test","messages":[],"stream":true}',
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.Authorization).toBe("Bearer sk-…redacted…def");
- expect(capturedFixture.request.headers.Authorization).not.toContain("LIVEKEY1234567890abc");
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).not.toContain("LIVEKEY1234567890abc");
- expect(serialized).toContain("Bearer sk-…redacted…def");
- });
-
- it("redacts lowercase authorization header", async () => {
- const apiKey = "sk-abcdefghijkmnop";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { authorization: `Bearer ${apiKey}` },
- body: null,
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
- expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
- });
-
- it("guard: no header named authorization (any case) survives with a raw sk- token", async () => {
- const apiKey = "sk-REALKEY_1234567890abcdef";
-
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { Authorization: `Bearer ${apiKey}` },
- body: null,
- });
-
- assertDefined(capturedFixture);
- for (const [key, value] of Object.entries(capturedFixture.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- expect(value).not.toContain(apiKey);
- expect(value).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
- }
- }
-
- const serialized = serializeFixture(capturedFixture);
- expect(serialized).not.toContain(apiKey);
- expect(serialized).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
- });
-
- it("redacts a short API key (≤7 chars → full mask)", async () => {
- let capturedFixture: HttpExchangeFixture | undefined;
- const wrappedFetch = recordFetch(
- async () => new Response("data: [DONE]\n", { status: 200 }),
- (fx) => {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- capturedFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- };
- },
- );
-
- await wrappedFetch("https://api.example.com/v1/chat/completions", {
- method: "POST",
- headers: { authorization: "Bearer secret!" },
- body: null,
- });
-
- assertDefined(capturedFixture);
- expect(capturedFixture.request.headers.authorization).toBe("Bearer …redacted…");
- });
+ /**
+ * Graduated secret mask — §6 tiers. Duplicated locally (isolation-over-dry).
+ * ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask.
+ */
+ function maskSecret(value: string): string {
+ const len = value.length;
+ if (len <= 7) return "…redacted…";
+ let reveal: number;
+ if (len >= 13) {
+ reveal = 3;
+ } else if (len >= 11) {
+ reveal = 2;
+ } else {
+ reveal = 1;
+ }
+ return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
+ }
+
+ it("self-redacts auth header in onExchange and produces a secret-free fixture", async () => {
+ const apiKey = "sk-abcdefghijkmnop";
+ const responseBody =
+ 'data: {"id":"cmpl-r","choices":[{"delta":{"content":"ok"},"index":0}],"usage":{"prompt_tokens":5,"completion_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}\n\ndata: [DONE]\n';
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () =>
+ new Response(responseBody, {
+ status: 200,
+ headers: { "content-type": "text/event-stream" },
+ }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ authorization: `Bearer ${apiKey}`,
+ },
+ body: '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}',
+ });
+
+ assertDefined(capturedFixture);
+
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
+ expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
+ expect(capturedFixture.request.headers["content-type"]).toBe("application/json");
+
+ expect(capturedFixture.request.body).toContain('"model":"test"');
+ expect(capturedFixture.request.body).toContain('"content":"hi"');
+
+ expect(capturedFixture.response.status).toBe(200);
+ expect(capturedFixture.response.body).toBe(responseBody);
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).toContain("Bearer sk-…redacted…nop");
+ expect(serialized).not.toContain("abcdefghijkm");
+ expect(serialized).toContain("content");
+ expect(serialized).toContain("hi");
+ });
+
+ it("redacts capitalized Authorization header (the real leak casing)", async () => {
+ const apiKey = "sk-LIVEKEY1234567890abcdef";
+ const responseBody = "data: [DONE]\n";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () =>
+ new Response(responseBody, {
+ status: 200,
+ headers: { "content-type": "text/event-stream" },
+ }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: '{"model":"test","messages":[],"stream":true}',
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.Authorization).toBe("Bearer sk-…redacted…def");
+ expect(capturedFixture.request.headers.Authorization).not.toContain("LIVEKEY1234567890abc");
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).not.toContain("LIVEKEY1234567890abc");
+ expect(serialized).toContain("Bearer sk-…redacted…def");
+ });
+
+ it("redacts lowercase authorization header", async () => {
+ const apiKey = "sk-abcdefghijkmnop";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { authorization: `Bearer ${apiKey}` },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop");
+ expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm");
+ });
+
+ it("guard: no header named authorization (any case) survives with a raw sk- token", async () => {
+ const apiKey = "sk-REALKEY_1234567890abcdef";
+
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { Authorization: `Bearer ${apiKey}` },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ for (const [key, value] of Object.entries(capturedFixture.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ expect(value).not.toContain(apiKey);
+ expect(value).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
+ }
+ }
+
+ const serialized = serializeFixture(capturedFixture);
+ expect(serialized).not.toContain(apiKey);
+ expect(serialized).not.toMatch(/sk-[A-Za-z0-9]{10,}/);
+ });
+
+ it("redacts a short API key (≤7 chars → full mask)", async () => {
+ let capturedFixture: HttpExchangeFixture | undefined;
+ const wrappedFetch = recordFetch(
+ async () => new Response("data: [DONE]\n", { status: 200 }),
+ (fx) => {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ capturedFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ };
+ },
+ );
+
+ await wrappedFetch("https://api.example.com/v1/chat/completions", {
+ method: "POST",
+ headers: { authorization: "Bearer secret!" },
+ body: null,
+ });
+
+ assertDefined(capturedFixture);
+ expect(capturedFixture.request.headers.authorization).toBe("Bearer …redacted…");
+ });
});
diff --git a/packages/openai-stream/src/stream.ts b/packages/openai-stream/src/stream.ts
index 2f594b5..a676715 100644
--- a/packages/openai-stream/src/stream.ts
+++ b/packages/openai-stream/src/stream.ts
@@ -1,36 +1,36 @@
import type {
- ChatMessage,
- ProviderEvent,
- ProviderStreamOptions,
- Span,
- ToolContract,
+ ChatMessage,
+ ProviderEvent,
+ ProviderStreamOptions,
+ Span,
+ ToolContract,
} from "@dispatch/kernel";
import type { FetchLike, HttpExchangeFixture } from "@dispatch/trace-replay";
import { convertMessages, type OpenAIMessage } from "./convert-messages.js";
import { convertTools, type OpenAITool } from "./convert-tools.js";
export interface StreamConfig {
- readonly baseURL: string;
- readonly apiKey: string;
- readonly model: string;
- /**
- * Internal injectable fetch — used by replay tests and record mode.
- * When absent, falls back to globalThis.fetch (production default).
- */
- readonly fetchFn?: FetchLike;
- /**
- * Optional hook a provider extension uses to add provider-specific body
- * fields (e.g. `reasoning_effort`) before the request is sent. Receives the
- * body built so far + the ProviderStreamOptions; returns ADDITIONAL fields
- * to merge into the body (or a full body). Generic — the library names no
- * feature. Applied AFTER building `body` and BEFORE `JSON.stringify`, so
- * the verbatim post-transform bytes are what hit the wire (and what the
- * provider.request span captures). Default (absent): no extra fields.
- */
- readonly transformBody?: (
- body: Record<string, unknown>,
- opts: ProviderStreamOptions,
- ) => Record<string, unknown>;
+ readonly baseURL: string;
+ readonly apiKey: string;
+ readonly model: string;
+ /**
+ * Internal injectable fetch — used by replay tests and record mode.
+ * When absent, falls back to globalThis.fetch (production default).
+ */
+ readonly fetchFn?: FetchLike;
+ /**
+ * Optional hook a provider extension uses to add provider-specific body
+ * fields (e.g. `reasoning_effort`) before the request is sent. Receives the
+ * body built so far + the ProviderStreamOptions; returns ADDITIONAL fields
+ * to merge into the body (or a full body). Generic — the library names no
+ * feature. Applied AFTER building `body` and BEFORE `JSON.stringify`, so
+ * the verbatim post-transform bytes are what hit the wire (and what the
+ * provider.request span captures). Default (absent): no extra fields.
+ */
+ readonly transformBody?: (
+ body: Record<string, unknown>,
+ opts: ProviderStreamOptions,
+ ) => Record<string, unknown>;
}
/**
@@ -38,379 +38,379 @@ export interface StreamConfig {
* ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask.
*/
function maskSecret(value: string): string {
- const len = value.length;
- if (len <= 7) return "…redacted…";
- let reveal: number;
- if (len >= 13) {
- reveal = 3;
- } else if (len >= 11) {
- reveal = 2;
- } else {
- reveal = 1;
- }
- return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
+ const len = value.length;
+ if (len <= 7) return "…redacted…";
+ let reveal: number;
+ if (len >= 13) {
+ reveal = 3;
+ } else if (len >= 11) {
+ reveal = 2;
+ } else {
+ reveal = 1;
+ }
+ return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`;
}
export async function* streamChat(
- config: StreamConfig,
- messages: readonly ChatMessage[],
- tools: readonly ToolContract[],
- opts?: ProviderStreamOptions,
+ config: StreamConfig,
+ messages: readonly ChatMessage[],
+ tools: readonly ToolContract[],
+ opts?: ProviderStreamOptions,
): AsyncIterable<ProviderEvent> {
- const openaiMessages = convertMessages(messages);
- const openaiTools = convertTools(tools);
-
- const systemPrompt = opts?.systemPrompt;
- const finalMessages: OpenAIMessage[] = systemPrompt
- ? [{ role: "system", content: systemPrompt }, ...openaiMessages]
- : openaiMessages;
-
- const body: Record<string, unknown> = {
- model: opts?.model ?? config.model,
- messages: finalMessages,
- stream: true,
- stream_options: { include_usage: true },
- };
-
- if (openaiTools.length > 0) {
- body.tools = openaiTools satisfies OpenAITool[];
- }
- if (opts?.temperature !== undefined) {
- body.temperature = opts.temperature;
- }
- if (opts?.maxTokens !== undefined) {
- body.max_tokens = opts.maxTokens;
- }
-
- if (config.transformBody) {
- const extra = config.transformBody(body, opts ?? {});
- Object.assign(body, extra);
- }
-
- const url = `${config.baseURL}/chat/completions`;
- const bodyString = JSON.stringify(body);
-
- let reqSpan: Span | undefined;
- let totalInputTokens: number | undefined;
- let totalOutputTokens: number | undefined;
- let totalCacheReadTokens: number | undefined;
- let totalCacheWriteTokens: number | undefined;
-
- if (opts?.logger) {
- try {
- const model = opts?.model ?? config.model;
- const hasCacheBreakpoint = bodyString.includes("cache_control");
- reqSpan = opts.logger.span(
- "provider.request",
- {
- model,
- url,
- "request.method": "POST",
- "request.cache_control_present": hasCacheBreakpoint,
- "request.headers.authorization": `Bearer ${maskSecret(config.apiKey)}`,
- },
- bodyString,
- );
- } catch {
- // Fail-safe: capture must never break stream().
- }
- }
-
- let effectiveFetch: FetchLike = config.fetchFn ?? fetch;
-
- const recordPath =
- typeof process !== "undefined" ? process.env.DISPATCH_RECORD_FIXTURE : undefined;
- if (recordPath && !config.fetchFn) {
- try {
- const { recordFetch: rf, saveFixture } = await import("@dispatch/trace-replay");
- effectiveFetch = rf(effectiveFetch, (fx: HttpExchangeFixture) => {
- try {
- const redactedHeaders: Record<string, string> = {};
- for (const [key, value] of Object.entries(fx.request.headers)) {
- if (key.toLowerCase() === "authorization") {
- const token = value.replace(/^Bearer\s+/i, "");
- redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
- } else {
- redactedHeaders[key] = value;
- }
- }
- const redacted: HttpExchangeFixture = {
- request: { ...fx.request, headers: redactedHeaders },
- response: fx.response,
- ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
- };
- saveFixture(recordPath, redacted);
- } catch {
- // Fail-safe: capture/write must never break the turn.
- }
- });
- } catch {
- // Fail-safe: dynamic import or wrapping failure must never break the turn.
- }
- }
-
- let response: Response;
- try {
- response = await effectiveFetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${config.apiKey}`,
- },
- body: bodyString,
- });
- } catch (err) {
- if (reqSpan) {
- try {
- reqSpan.end({
- err,
- attrs: { status: 0 },
- });
- } catch {
- // Fail-safe.
- }
- }
- yield {
- type: "error",
- message: err instanceof Error ? err.message : String(err),
- retryable: true,
- };
- return;
- }
-
- if (!response.ok) {
- const text = await response.text().catch(() => "unknown");
- if (reqSpan) {
- try {
- reqSpan.setAttributes({ status: response.status });
- reqSpan.end({
- err: new Error(`HTTP ${response.status}: ${text}`),
- attrs: {
- status: response.status,
- "response.error_body": text,
- },
- });
- } catch {
- // Fail-safe.
- }
- }
- yield {
- type: "error",
- message: `HTTP ${response.status}: ${text}`,
- code: String(response.status),
- retryable: response.status >= 500 || response.status === 429,
- };
- return;
- }
-
- if (!response.body) {
- if (reqSpan) {
- try {
- reqSpan.end({
- err: new Error("Response body is null"),
- attrs: { status: response.status },
- });
- } catch {
- // Fail-safe.
- }
- }
- yield { type: "error", message: "Response body is null" };
- return;
- }
-
- try {
- yield* readSSEStream(response.body, (usage) => {
- totalInputTokens = usage.inputTokens;
- totalOutputTokens = usage.outputTokens;
- totalCacheReadTokens = usage.cacheReadTokens;
- totalCacheWriteTokens = usage.cacheWriteTokens;
- });
- } catch (err) {
- if (reqSpan) {
- try {
- reqSpan.end({
- err,
- attrs: { status: response.status },
- });
- } catch {
- // Fail-safe.
- }
- }
- throw err;
- }
-
- if (reqSpan) {
- try {
- const attrs: Record<string, string | number | boolean | null> = {
- status: response.status,
- };
- if (totalInputTokens !== undefined) {
- attrs["usage.inputTokens"] = totalInputTokens;
- }
- if (totalOutputTokens !== undefined) {
- attrs["usage.outputTokens"] = totalOutputTokens;
- }
- if (totalCacheReadTokens !== undefined) {
- attrs["usage.cacheReadTokens"] = totalCacheReadTokens;
- }
- if (totalCacheWriteTokens !== undefined) {
- attrs["usage.cacheWriteTokens"] = totalCacheWriteTokens;
- }
- reqSpan.end({ attrs });
- } catch {
- // Fail-safe.
- }
- }
+ const openaiMessages = convertMessages(messages);
+ const openaiTools = convertTools(tools);
+
+ const systemPrompt = opts?.systemPrompt;
+ const finalMessages: OpenAIMessage[] = systemPrompt
+ ? [{ role: "system", content: systemPrompt }, ...openaiMessages]
+ : openaiMessages;
+
+ const body: Record<string, unknown> = {
+ model: opts?.model ?? config.model,
+ messages: finalMessages,
+ stream: true,
+ stream_options: { include_usage: true },
+ };
+
+ if (openaiTools.length > 0) {
+ body.tools = openaiTools satisfies OpenAITool[];
+ }
+ if (opts?.temperature !== undefined) {
+ body.temperature = opts.temperature;
+ }
+ if (opts?.maxTokens !== undefined) {
+ body.max_tokens = opts.maxTokens;
+ }
+
+ if (config.transformBody) {
+ const extra = config.transformBody(body, opts ?? {});
+ Object.assign(body, extra);
+ }
+
+ const url = `${config.baseURL}/chat/completions`;
+ const bodyString = JSON.stringify(body);
+
+ let reqSpan: Span | undefined;
+ let totalInputTokens: number | undefined;
+ let totalOutputTokens: number | undefined;
+ let totalCacheReadTokens: number | undefined;
+ let totalCacheWriteTokens: number | undefined;
+
+ if (opts?.logger) {
+ try {
+ const model = opts?.model ?? config.model;
+ const hasCacheBreakpoint = bodyString.includes("cache_control");
+ reqSpan = opts.logger.span(
+ "provider.request",
+ {
+ model,
+ url,
+ "request.method": "POST",
+ "request.cache_control_present": hasCacheBreakpoint,
+ "request.headers.authorization": `Bearer ${maskSecret(config.apiKey)}`,
+ },
+ bodyString,
+ );
+ } catch {
+ // Fail-safe: capture must never break stream().
+ }
+ }
+
+ let effectiveFetch: FetchLike = config.fetchFn ?? fetch;
+
+ const recordPath =
+ typeof process !== "undefined" ? process.env.DISPATCH_RECORD_FIXTURE : undefined;
+ if (recordPath && !config.fetchFn) {
+ try {
+ const { recordFetch: rf, saveFixture } = await import("@dispatch/trace-replay");
+ effectiveFetch = rf(effectiveFetch, (fx: HttpExchangeFixture) => {
+ try {
+ const redactedHeaders: Record<string, string> = {};
+ for (const [key, value] of Object.entries(fx.request.headers)) {
+ if (key.toLowerCase() === "authorization") {
+ const token = value.replace(/^Bearer\s+/i, "");
+ redactedHeaders[key] = `Bearer ${maskSecret(token)}`;
+ } else {
+ redactedHeaders[key] = value;
+ }
+ }
+ const redacted: HttpExchangeFixture = {
+ request: { ...fx.request, headers: redactedHeaders },
+ response: fx.response,
+ ...(fx.meta !== undefined ? { meta: fx.meta } : {}),
+ };
+ saveFixture(recordPath, redacted);
+ } catch {
+ // Fail-safe: capture/write must never break the turn.
+ }
+ });
+ } catch {
+ // Fail-safe: dynamic import or wrapping failure must never break the turn.
+ }
+ }
+
+ let response: Response;
+ try {
+ response = await effectiveFetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${config.apiKey}`,
+ },
+ body: bodyString,
+ });
+ } catch (err) {
+ if (reqSpan) {
+ try {
+ reqSpan.end({
+ err,
+ attrs: { status: 0 },
+ });
+ } catch {
+ // Fail-safe.
+ }
+ }
+ yield {
+ type: "error",
+ message: err instanceof Error ? err.message : String(err),
+ retryable: true,
+ };
+ return;
+ }
+
+ if (!response.ok) {
+ const text = await response.text().catch(() => "unknown");
+ if (reqSpan) {
+ try {
+ reqSpan.setAttributes({ status: response.status });
+ reqSpan.end({
+ err: new Error(`HTTP ${response.status}: ${text}`),
+ attrs: {
+ status: response.status,
+ "response.error_body": text,
+ },
+ });
+ } catch {
+ // Fail-safe.
+ }
+ }
+ yield {
+ type: "error",
+ message: `HTTP ${response.status}: ${text}`,
+ code: String(response.status),
+ retryable: response.status >= 500 || response.status === 429,
+ };
+ return;
+ }
+
+ if (!response.body) {
+ if (reqSpan) {
+ try {
+ reqSpan.end({
+ err: new Error("Response body is null"),
+ attrs: { status: response.status },
+ });
+ } catch {
+ // Fail-safe.
+ }
+ }
+ yield { type: "error", message: "Response body is null" };
+ return;
+ }
+
+ try {
+ yield* readSSEStream(response.body, (usage) => {
+ totalInputTokens = usage.inputTokens;
+ totalOutputTokens = usage.outputTokens;
+ totalCacheReadTokens = usage.cacheReadTokens;
+ totalCacheWriteTokens = usage.cacheWriteTokens;
+ });
+ } catch (err) {
+ if (reqSpan) {
+ try {
+ reqSpan.end({
+ err,
+ attrs: { status: response.status },
+ });
+ } catch {
+ // Fail-safe.
+ }
+ }
+ throw err;
+ }
+
+ if (reqSpan) {
+ try {
+ const attrs: Record<string, string | number | boolean | null> = {
+ status: response.status,
+ };
+ if (totalInputTokens !== undefined) {
+ attrs["usage.inputTokens"] = totalInputTokens;
+ }
+ if (totalOutputTokens !== undefined) {
+ attrs["usage.outputTokens"] = totalOutputTokens;
+ }
+ if (totalCacheReadTokens !== undefined) {
+ attrs["usage.cacheReadTokens"] = totalCacheReadTokens;
+ }
+ if (totalCacheWriteTokens !== undefined) {
+ attrs["usage.cacheWriteTokens"] = totalCacheWriteTokens;
+ }
+ reqSpan.end({ attrs });
+ } catch {
+ // Fail-safe.
+ }
+ }
}
async function* readSSEStream(
- body: ReadableStream<Uint8Array>,
- onUsage?: (usage: {
- inputTokens: number;
- outputTokens: number;
- cacheReadTokens?: number;
- cacheWriteTokens?: number;
- }) => void,
+ body: ReadableStream<Uint8Array>,
+ onUsage?: (usage: {
+ inputTokens: number;
+ outputTokens: number;
+ cacheReadTokens?: number;
+ cacheWriteTokens?: number;
+ }) => void,
): AsyncIterable<ProviderEvent> {
- const reader = body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
- const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
-
- try {
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
- buffer = lines.pop() ?? "";
-
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed.startsWith("data:")) continue;
-
- const data = trimmed.slice(5).trim();
- if (data === "[DONE]") return;
-
- let chunk: Record<string, unknown>;
- try {
- chunk = JSON.parse(data);
- } catch {
- yield { type: "error", message: `Invalid JSON in SSE data: ${data}` };
- continue;
- }
-
- const choices = chunk.choices as
- | Array<{
- delta: Record<string, unknown>;
- finish_reason?: string | null;
- }>
- | undefined;
-
- if (choices) {
- for (const choice of choices) {
- const delta = choice.delta;
-
- if (typeof delta.content === "string" && delta.content) {
- yield { type: "text-delta", delta: delta.content };
- }
-
- if (typeof delta.reasoning_content === "string" && delta.reasoning_content) {
- yield { type: "reasoning-delta", delta: delta.reasoning_content };
- }
-
- const tcs = delta.tool_calls as
- | Array<{
- index: number;
- id?: string;
- function?: { name?: string; arguments?: string };
- }>
- | undefined;
-
- if (tcs) {
- for (const tc of tcs) {
- const existing = toolCalls.get(tc.index);
- if (existing) {
- if (tc.function?.arguments) {
- existing.arguments += tc.function.arguments;
- }
- } else {
- toolCalls.set(tc.index, {
- id: tc.id ?? "",
- name: tc.function?.name ?? "",
- arguments: tc.function?.arguments ?? "",
- });
- }
- }
- }
-
- if (choice.finish_reason) {
- const sortedIndices = [...toolCalls.keys()].sort((a, b) => a - b);
- for (const idx of sortedIndices) {
- const acc = toolCalls.get(idx);
- if (!acc) continue;
- let input: unknown;
- try {
- input = JSON.parse(acc.arguments);
- } catch {
- input = acc.arguments;
- }
- yield {
- type: "tool-call",
- toolCallId: acc.id,
- toolName: acc.name,
- input,
- };
- }
- yield { type: "finish", reason: choice.finish_reason };
- }
- }
- }
-
- const usage = chunk.usage as
- | {
- prompt_tokens?: number;
- completion_tokens?: number;
- cache_read_tokens?: number;
- cache_write_tokens?: number;
- prompt_tokens_details?: { cached_tokens?: number };
- completion_tokens_details?: Record<string, unknown>;
- }
- | undefined;
-
- if (usage) {
- const cacheRead = usage.cache_read_tokens ?? usage.prompt_tokens_details?.cached_tokens;
- const cacheWrite = usage.cache_write_tokens;
- const usageObj: {
- inputTokens: number;
- outputTokens: number;
- cacheReadTokens?: number;
- cacheWriteTokens?: number;
- } = {
- inputTokens: usage.prompt_tokens ?? 0,
- outputTokens: usage.completion_tokens ?? 0,
- };
- if (cacheRead !== undefined) {
- usageObj.cacheReadTokens = cacheRead;
- }
- if (cacheWrite !== undefined) {
- usageObj.cacheWriteTokens = cacheWrite;
- }
- onUsage?.(usageObj);
- yield {
- type: "usage",
- usage: {
- inputTokens: usage.prompt_tokens ?? 0,
- outputTokens: usage.completion_tokens ?? 0,
- ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
- ...(cacheWrite !== undefined ? { cacheWriteTokens: cacheWrite } : {}),
- },
- };
- }
- }
- }
- } finally {
- reader.releaseLock();
- }
+ const reader = body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+ const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split("\n");
+ buffer = lines.pop() ?? "";
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed.startsWith("data:")) continue;
+
+ const data = trimmed.slice(5).trim();
+ if (data === "[DONE]") return;
+
+ let chunk: Record<string, unknown>;
+ try {
+ chunk = JSON.parse(data);
+ } catch {
+ yield { type: "error", message: `Invalid JSON in SSE data: ${data}` };
+ continue;
+ }
+
+ const choices = chunk.choices as
+ | Array<{
+ delta: Record<string, unknown>;
+ finish_reason?: string | null;
+ }>
+ | undefined;
+
+ if (choices) {
+ for (const choice of choices) {
+ const delta = choice.delta;
+
+ if (typeof delta.content === "string" && delta.content) {
+ yield { type: "text-delta", delta: delta.content };
+ }
+
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content) {
+ yield { type: "reasoning-delta", delta: delta.reasoning_content };
+ }
+
+ const tcs = delta.tool_calls as
+ | Array<{
+ index: number;
+ id?: string;
+ function?: { name?: string; arguments?: string };
+ }>
+ | undefined;
+
+ if (tcs) {
+ for (const tc of tcs) {
+ const existing = toolCalls.get(tc.index);
+ if (existing) {
+ if (tc.function?.arguments) {
+ existing.arguments += tc.function.arguments;
+ }
+ } else {
+ toolCalls.set(tc.index, {
+ id: tc.id ?? "",
+ name: tc.function?.name ?? "",
+ arguments: tc.function?.arguments ?? "",
+ });
+ }
+ }
+ }
+
+ if (choice.finish_reason) {
+ const sortedIndices = [...toolCalls.keys()].sort((a, b) => a - b);
+ for (const idx of sortedIndices) {
+ const acc = toolCalls.get(idx);
+ if (!acc) continue;
+ let input: unknown;
+ try {
+ input = JSON.parse(acc.arguments);
+ } catch {
+ input = acc.arguments;
+ }
+ yield {
+ type: "tool-call",
+ toolCallId: acc.id,
+ toolName: acc.name,
+ input,
+ };
+ }
+ yield { type: "finish", reason: choice.finish_reason };
+ }
+ }
+ }
+
+ const usage = chunk.usage as
+ | {
+ prompt_tokens?: number;
+ completion_tokens?: number;
+ cache_read_tokens?: number;
+ cache_write_tokens?: number;
+ prompt_tokens_details?: { cached_tokens?: number };
+ completion_tokens_details?: Record<string, unknown>;
+ }
+ | undefined;
+
+ if (usage) {
+ const cacheRead = usage.cache_read_tokens ?? usage.prompt_tokens_details?.cached_tokens;
+ const cacheWrite = usage.cache_write_tokens;
+ const usageObj: {
+ inputTokens: number;
+ outputTokens: number;
+ cacheReadTokens?: number;
+ cacheWriteTokens?: number;
+ } = {
+ inputTokens: usage.prompt_tokens ?? 0,
+ outputTokens: usage.completion_tokens ?? 0,
+ };
+ if (cacheRead !== undefined) {
+ usageObj.cacheReadTokens = cacheRead;
+ }
+ if (cacheWrite !== undefined) {
+ usageObj.cacheWriteTokens = cacheWrite;
+ }
+ onUsage?.(usageObj);
+ yield {
+ type: "usage",
+ usage: {
+ inputTokens: usage.prompt_tokens ?? 0,
+ outputTokens: usage.completion_tokens ?? 0,
+ ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
+ ...(cacheWrite !== undefined ? { cacheWriteTokens: cacheWrite } : {}),
+ },
+ };
+ }
+ }
+ }
+ } finally {
+ reader.releaseLock();
+ }
}