fix(provider): stop committing half-streamed tool-call args#3957
Merged
Conversation
A proxy that idle-closes the SSE connection with a clean FIN ends the scan loop with no error, so the turn was committed as complete -- with whatever fraction of the tool-call arguments had streamed. DeepSeek then rejects every subsequent request that replays the truncated JSON with HTTP 400, bricking the session. Two layers: - readStream now requires [DONE] or a finish_reason; a clean EOF without either is a connection cut, which the existing replay / StreamInterrupted recovery handles (partial calls are never emitted). - SanitizeToolPairing closes truncated argument JSON before requests are built, so sessions already poisoned by this bug resume working.
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.
A clean-FIN idle close from a proxy ends the SSE scan loop with no scanner error, so
readStreamtreated the stream as complete and the agent committed the turn — including half-streamed tool-callarguments("{",{"time": 2, …). Every later request replays that truncated JSON and DeepSeek rejects it with HTTP 400, so the session is permanently stuck until the user abandons it.Two layers:
readStreamnow treats a clean EOF without[DONE]and without anyfinish_reasonas a connection cut. That routes it through the existing machinery from [Feature]: Reasonix 用的是长连接(SSE 流式请求),sing-box 可能对这种连接处理有问题。 #3148: replayed transparently if nothing was emitted yet, surfaced asStreamInterruptedError(agent recovery, partial calls discarded) if output already streamed. Gateways that omit[DONE]but sendfinish_reasonkeep working.SanitizeToolPairingnow best-effort closes truncated argument JSON (unterminated string, open braces, dangling comma/colon; anything unrecoverable degrades to{}) when building the wire request, for both the OpenAI-compatible and Anthropic providers. The stored session keeps the original bytes; this also un-bricks sessions users already have on disk, and covers thefinish_reason: lengthmid-call truncation case.Tests: clean-EOF-before-output replays and yields the full call; clean EOF mid-call surfaces as interruption with no partial
ChunkToolCall;finish_reasonwithout[DONE]still completes; arg-repair table incl. copy-on-write (stored history never mutated).Closes #3953