Skip to content

fix(provider): stop committing half-streamed tool-call args#3957

Merged
esengine merged 1 commit into
main-v2from
fix/stream-truncated-tool-args
Jun 11, 2026
Merged

fix(provider): stop committing half-streamed tool-call args#3957
esengine merged 1 commit into
main-v2from
fix/stream-truncated-tool-args

Conversation

@esengine

Copy link
Copy Markdown
Owner

A clean-FIN idle close from a proxy ends the SSE scan loop with no scanner error, so readStream treated the stream as complete and the agent committed the turn — including half-streamed tool-call arguments ("{", {"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:

  • Detect the cut at the source. readStream now treats a clean EOF without [DONE] and without any finish_reason as 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 as StreamInterruptedError (agent recovery, partial calls discarded) if output already streamed. Gateways that omit [DONE] but send finish_reason keep working.
  • Repair already-poisoned histories. SanitizeToolPairing now 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 the finish_reason: length mid-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_reason without [DONE] still completes; arg-repair table incl. copy-on-write (stored history never mutated).

Closes #3953

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.
@esengine esengine requested a review from SivanCola as a code owner June 11, 2026 05:38
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development provider Model providers & selection (internal/provider) labels Jun 11, 2026
@esengine esengine merged commit adde2d3 into main-v2 Jun 11, 2026
14 checks passed
@esengine esengine deleted the fix/stream-truncated-tool-args branch June 11, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

provider Model providers & selection (internal/provider) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Tool call arguments JSON serialization truncation causing HTTP 400 errors

1 participant