Repository navigation
fix(chat): buffer streamed tool call until name arrives - #577
Merged
Merged
Conversation
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]>
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #560. Streaming chat completions broke tool-calling for Vercel-AI-SDK clients (e.g. opencode) with:
Plain text generation was unaffected — only the streamed
tool_callpath.Root cause
Some OpenAI-compatible upstreams (e.g.
deepseek/deepseek-v4-pro,moonshotai/kimi-k2.7-code) stream a tool call whose firsttool_callsdelta carries onlyid+ partialarguments, deliveringfunction.namein a later delta:ChatStreamEncoderforwarded each delta verbatim, so the first chunk a client saw had nofunction.name. The Vercel AI SDK (@ai-sdk/openai-compatible) rejects the first streamed chunk for a new tool-call index unlessfunction.nameis 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
ChatStreamEncodernow holds a tool call's opening chunk back — buffering argument fragments per call — until a non-emptyfunction.namearrives, 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 onmain(function.nameisnull) and passes with the fix.Verification
cargo nextest run --all-features— 985 passed, 1 skippedcargo clippy --all-features— cleancargo fmt -- --check— clean🤖 Generated with Claude Code