Repository navigation
[Bug]: deepseek-v4-flash-free can emit incomplete tool calls that break streamed turns #1514
Description
Current status
This issue is now narrowed to the remaining OpenCode Zen / DeepSeek streaming compatibility defect.
The startup syntax crash reported against 2.12.0 is already resolved on current dev: the invalid continue outside a loop is no longer present. That part should not be reimplemented.
Current dev also already accepts [DONE] without a finish_reason: the [DONE] branch flushes pending tool calls and emits done. A raw EOF with neither [DONE] nor finish_reason remains a truncation condition and should stay fail-closed unless provider evidence proves a safe alternative.
Remaining defect
openai-chat can still flush a pending tool call with an empty function name.
Current flushToolCalls() emits:
yield { type: "tool_call_start", id: call.id, name: call.name };without first proving that call.name is non-empty.
The affected OpenCode Zen route can stream argument deltas while omitting the matching function.name. When that incomplete call is flushed, the downstream Codex tool-call contract receives an unusable call and the turn can fail.
This issue now tracks only the provider-compatibility question around incomplete streamed tool-call metadata.
Reproduction
- Route Codex through
opencode-free/deepseek-v4-flash-freeusing theopenai-chatadapter. - Run a turn that causes the upstream to emit tool-call argument deltas without a usable
function.name. - Allow the stream to reach a normal flush boundary such as
[DONE]. - Observe the pending call being emitted with an empty name and the downstream turn failing.
Expected behavior
OpenCodex must not emit a completed usable tool call with an empty name.
A fix must preserve the existing fail-closed rules for genuinely malformed or truncated tool calls. In particular:
- do not silently invent a function name;
- do not treat arbitrary raw EOF as successful completion;
- do not weaken the nested
tool_callsvalidation from [Bug] Nested field shapes bypass the #1219 frame guard and crash both adapter paths #1325; - do not silently drop a claimed tool call if doing so can orphan a matching result;
- provider-specific normalization is acceptable only when the upstream shape is proven semantically safe.
Resolved portion
The 2.12.0 startup failure:
Cannot use "continue" here
is no longer present on current dev and is closed as part of this issue's history.
Related
- [Bug] Nested field shapes bypass the #1219 frame guard and crash both adapter paths #1325 - generic nested
tool_callsvalidation and fail-closed behavior. - [Compatibility] Xiaomi MiMo v2.5 fails on 2.12.0 with invalid tool calls and an incorrect reasoning ladder #1483 - Xiaomi MiMo compatibility report. Similar symptom, different provider evidence and reasoning-effort scope.
- Bug: deepseek-v4-flash-free upstream returns malformed tool_calls causing "invalid tool calls" error #1472 / [Bug]: deepseek-v4-flash-free upstream returns malformed tool_calls causing "invalid tool calls" error #1473 - earlier auto-closed reports of the OpenCode Zen symptom.
Acceptance criteria
- A pending streamed tool call with no usable function name is never emitted as a valid completed tool call.
- Normal named tool calls remain unchanged.
-
[DONE]withoutfinish_reasonremains accepted. - Raw EOF without a terminal signal remains fail-closed unless separately proven safe.
- Malformed nested
tool_callsshapes remain fail-closed. - Focused regression coverage reproduces the OpenCode Zen incomplete-name shape without weakening generic tool-call safety.
Activity
[-][Bug]: ocx start crashes (continue outside loop) + deepseek-v4-flash-free streams die with "invalid tool calls" on 2.12.0[/-][+][Bug]: deepseek-v4-flash-free can emit incomplete tool calls that break streamed turns[/+]Wibias commentedon Aug 12, 2026
Triage update: I narrowed this issue to the one defect that still exists on current
dev.The startup parser crash is already fixed, and
[DONE]withoutfinish_reasonis already accepted. The remaining actionable problem is thatflushToolCalls()can emit a pending call with an empty function name.I updated the title/body and labels accordingly. Please keep any fix focused on incomplete tool-call metadata and preserve the existing fail-closed behavior for genuinely malformed or truncated streams.
github-actions commentedon Aug 12, 2026
Maintainer decision respected
A maintainer has reopened this issue. The automated closure has been deactivated.
fix(openai-chat): refuse to emit an unnamed streamed tool call (#1531)
lidge-jun commentedon Aug 12, 2026
Fixed on
devby #1531, merged as4e0ffb2cbef0d3724a3aa9ebef26279aed44b694.The narrowed defect is closed:
flushToolCalls()no longer emits a pending call whosefunction.namenever arrived. An unnamed streamed call now terminates the turn through the adapter error channel, so the downstream tool-call contract never receives a call it cannot dispatch.The constraints listed in this issue were kept:
idis still synthesized when absent because it is an opaque correlation handle, but a name is not;tool_callsvalidation is unchanged and still terminal;Two things were found while implementing it and are included:
parseResponsevalidator checked the type ofnamebut not its emptiness, so a blank name was emitted there too. It is now rejected the same way.delta.tool_callscast is a TypeScript convenience over upstream JSON, so a truthy non-stringnamereached the accumulator unvalidated and threw during string handling. Presentfunction,name,arguments, andidfields are now runtime-checked at ingest and routed to the existing invalid-tool-calls error.Verification at the merged tree:
bun x tsc --noEmitexit 0; focused suites 48/48; 11-file adapter sweep 161/161; fullbun run test11,310 pass / 0 fail across 698 files; all four Linux CI shards plus macOS green on the PR.fix(openai-chat): refuse to emit an unnamed streamed tool call (lidge…