anomalyco / anomalyco/opencode
Zen gateway (/go/v1) streaming: tool_calls continuation deltas send "id":null / "function":{"name":null} instead of omitting absent fields
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
Streaming chat completions from the Zen gateway (https://opencode.ai/zen/go/v1/chat/completions) emit tool_calls continuation deltas with explicit null values for id and function.name instead of omitting the absent fields, as the OpenAI streaming format specifies.
The first tool_calls delta of a call carries the real values, e.g.:
"delta":{"role":null,"content":"","tool_calls":[{"index":0,"id":"chatcmpl-tool-b4d3cf2ed9145904","type":"function","function":{"name":"calculator","arguments":""}}]}
but every continuation delta for the same call sends:
"delta":{"role":null,"content":"","tool_calls":[{"index":0,"id":null,"type":null,"function":{"name":null,"arguments":""}}]}
This breaks strict OpenAI-compatible clients that accumulate deltas and treat any present field as an update. Concrete case: DeepSeek Harness (dsh) accumulates if (call.function?.name !== undefined) block.name = call.function.name — the null overwrites the previously accumulated name, and every tool call ends up dispatched as unknown tool "". The same request non-streamed returns a correct message.tool_calls[] with id and function.name intact.
Observed model: deepseek-v4-flash (stream mode). The behavior is intermittent across time windows — client-side session logs show tool calls failing on 2026-08-17 ~04:34–12:08 UTC and 2026-08-19 ~02:37–02:50 UTC, while requests outside those windows (e.g., all of 2026-08-18) received spec-compliant chunks with the fields omitted. That pattern suggests different upstream backends behind the gateway, some of which emit null-filled deltas.
Plugins
n/a
OpenCode version
n/a — Zen gateway (opencode.ai/zen/go/v1), captured 2026-08-19 02:56 UTC
Steps to reproduce
curl -N https://opencode.ai/zen/go/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role":"user","content":"Use the calculator tool to compute 2+2"}],
"tools": [{"type":"function","function":{"name":"calculator","description":"compute an arithmetic expression","parameters":{"type":"object","properties":{"expr":{"type":"string"}},"required":["expr"]}}}],
"tool_choice": "auto",
"max_tokens": 200,
"stream": true
}'
The streamed SSE shows the chunk sequence quoted above: id/name present on the first tool_calls delta, explicit null on the continuation deltas.
Expected: continuation deltas omit id / type / function.name when they carry no value (per the OpenAI streaming format), so field-presence-based delta accumulators keep the values from the first delta.
Screenshot and/or share link
n/a (raw SSE text)
Operating System
macOS 25.5 (arm64), curl — issue is server-side
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the provided curl request against the Zen gateway endpoint and compare the first and continuation tool_calls SSE deltas. Trace the gateway's streaming response path and any upstream normalization; done means continuation chunks omit absent id, type, and function.name fields while preserving valid accumulated tool-call values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100