Skip to content

fix(chat): buffer streamed tool call until name arrives - #577

Merged
FrozenArcher404 merged 1 commit into
mainfrom
fix/chat-stream-late-tool-name
Jun 18, 2026
Merged

FrozenArcher404 merged 1 commit into
mainfrom
fix/chat-stream-late-tool-name

Conversation

@FrozenArcher404

Copy link
Copy Markdown
Contributor

Summary

Fixes #560. Streaming chat completions broke tool-calling for Vercel-AI-SDK clients (e.g. opencode) with:

AI_InvalidResponseDataError: Expected 'function.name' to be a string.

Plain text generation was unaffected — only the streamed tool_call path.

Root cause

Some OpenAI-compatible upstreams (e.g. deepseek/deepseek-v4-pro, moonshotai/kimi-k2.7-code) stream a tool call whose first tool_calls delta carries only id + partial arguments, delivering function.name in a later delta:

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"arguments":""}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"bash","arguments":"{"}}]}}]}

ChatStreamEncoder forwarded each delta verbatim, so the first chunk a client saw had no function.name. The Vercel AI SDK (@ai-sdk/openai-compatible) rejects the first streamed chunk for a new tool-call index unless function.name is a string. (Same root cause as opencode#24137.)

PR #552 hardened the Responses / Messages / Generate Content encoders and the chat decoder against this upstream quirk, but left the chat encoder naive — that gap is this bug.

Fix

ChatStreamEncoder now holds a tool call's opening chunk back — buffering argument fragments per call — until a non-empty function.name arrives, then emits one named opening chunk and streams the remaining arguments. A call whose name never arrives is dropped, consistent with the server-tool loop's handling of nameless calls. The change is encoder-only; the decoder and the other encoders are untouched.

Test

Adds chat_encode_late_tool_name_first_chunk_carries_name, which replays the captured wire shape through the real chat decode → encode round trip and asserts the first emitted chunk names the function. It fails on main (function.name is null) and passes with the fix.

Verification

  • cargo nextest run --all-features — 985 passed, 1 skipped
  • cargo clippy --all-features — clean
  • cargo fmt -- --check — clean

🤖 Generated with Claude Code

Some OpenAI-compatible upstreams (e.g. DeepSeek / Kimi) stream a tool
call whose FIRST `tool_calls` delta carries only `id` + partial
`arguments`, delivering `function.name` in a later delta. The Chat
Completions stream encoder forwarded each delta verbatim, so the first
chunk a client saw had no `function.name`. Strict clients — the Vercel
AI SDK `@ai-sdk/openai-compatible`, and thus opencode — reject the first
streamed chunk for a new tool-call index unless `function.name` is a
string, aborting with:

    AI_InvalidResponseDataError: Expected 'function.name' to be a string.

Plain (non-tool) generation was unaffected. PR #552 hardened the
Responses / Messages / Generate Content encoders and the chat decoder
against the same upstream quirk but left the chat encoder naive.

`ChatStreamEncoder` now holds a tool call's opening chunk back —
buffering argument fragments per call — until a non-empty name arrives,
then emits one named opening chunk and streams the remaining arguments.
A call whose name never arrives is dropped, consistent with the
server-tool loop's handling of nameless calls. Encoder-only; the decoder
and the other encoders are untouched.

Adds a regression test that replays the captured wire shape through the
chat decode -> encode round trip and asserts the first emitted chunk
names the function.

Ref: anomalyco/opencode#24137

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@github-actions github-actions Bot added the fix label Jun 17, 2026
@FrozenArcher404
FrozenArcher404 merged commit ab02eb6 into main Jun 18, 2026
22 checks passed
@FrozenArcher404
FrozenArcher404 deleted the fix/chat-stream-late-tool-name branch June 18, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloud streaming completions emit tool calls with non-string function.name (breaks AI SDK / opencode)

1 participant