Skip to content

fix(translator): keep tool arguments when openai-compat upstreams send per-chunk usage - #5430

Open
ggbdpq wants to merge 2 commits into
router-for-me:devfrom
ggbdpq:fix/stream-tool-args-per-chunk-usage
Open

fix(translator): keep tool arguments when openai-compat upstreams send per-chunk usage#5430
ggbdpq wants to merge 2 commits into
router-for-me:devfrom
ggbdpq:fix/stream-tool-args-per-chunk-usage

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Verification

Check Result
New TestStreamingTool_PerChunkUsageKeepsToolArguments (3-chunk vLLM-style sequence from #5419) fails before fix (tool arguments lost: got ""), passes after
New TestStreamingTool_PerChunkUsageWithoutFinishReasonUsesBufferedUsage fails before fix (output_tokens = 5), passes after
Existing #5308 tests (TestStreamingTool_UsageWithoutFinishReasonEmitsMessageDelta, TestStreamingTool_OmittedFinishReasonEmitsMessageDeltaOnDone, TestStreamingText_OmittedFinishReasonEmitsEndTurnOnDone) pass unchanged
go test ./internal/translator/openai/claude/ pass
go test ./internal/translator/... ./sdk/... pass
go test -p 2 ./... all packages pass except pre-existing xai executor flakes that fail identically on unmodified main on this Windows machine (parallel compile load also crashed the Go compiler 3 times there, incl. once on a clean main tree)
go build ./cmd/server pass
gofmt clean

Notes on the flake disclaimer: TestXAIExecutorExecuteImagesUsesImagesEndpointAndPublishesUsage, TestXAIExecutorExecuteVideosCreate, TestXAIWebsocketsExecuteStreamSendsResponseCreateWithPreviousResponseID, and TestAntigravityAuthHasCreditsRequiredHomeBalanceUsesKV each failed only under full-suite parallel load on this machine and pass individually; a -count=3 run of the executor package on unmodified main reproduced failures across a wider set. None of them touch this translator. Upstream CI is the authoritative gate.

AI use

Implemented by GLM-5.3-Flash (ZCode) from the root-cause analysis in #5419, reviewed and verified locally by a human.

Checklist

@github-actions
github-actions Bot changed the base branch from main to dev September 2, 2026 22:57
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

…d per-chunk usage

The OpenAI-to-Claude streaming translator finalized the whole stream as
soon as a chunk carried usage while SawToolCall was set (shortcut added
in 677dbe1). Upstreams that attach usage to every chunk (e.g. vLLM) hit
that condition on the very first tool-call chunk, which only carries id
and function.name, so the tool_use block was closed before any argument
delta arrived and every later function.arguments was dropped: clients
saw input: {} and every tool call failed.

Decouple "usage seen" from "stream terminal": while finish_reason is
still empty, buffer the latest usage values on the conversion params
instead of finalizing; finish_reason or the [DONE] marker emits the
buffered values. This preserves the router-for-me#5308 behavior of reporting real
usage when an upstream omits finish_reason, and restores full tool
arguments for per-chunk-usage upstreams.

Fixes router-for-me#5419

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq
ggbdpq force-pushed the fix/stream-tool-args-per-chunk-usage branch from 08b93b6 to 14ee0c8 Compare September 2, 2026 23:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14ee0c84de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +324 to +326
if usage := root.Get("usage"); usage.Exists() && usage.Type != gjson.Null {
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractOpenAIUsage(usage)
emitAnthropicMessageDelta(param, &results, inputTokens, outputTokens, cachedTokens, cacheWriteTokens)
emitMessageStopIfNeeded(param, &results)
if param.FinishReason != "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush buffered usage on the finish-reason chunk

When an upstream sends usage with the tool-argument chunks but puts finish_reason in a separate chunk without usage, this nested check never emits the buffered values even though the stream is now known to be terminal. The translator waits until [DONE], so direct SDK consumers—or compatible upstreams that delay or omit that marker—never receive message_delta/message_stop; the finish-reason path should flush Last*Tokens when LastUsageSeen is set.

Useful? React with 👍 / 👎.

…hunk

When an upstream attaches usage to the tool-argument chunks but sends
finish_reason on a separate chunk without usage, the buffered values were
only flushed at [DONE]. Direct Anthropic SSE consumers rely on
message_delta/message_stop to detect the end of the stream, so they waited
for a marker compatible upstreams may delay or omit.

Close the stream on the finish_reason chunk when its own usage or the
buffered values are available; without either, keep waiting for the
usage-only chunk that OpenAI's include_usage mode sends afterwards.

Fixes the review comment on router-for-me#5430

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 2, 2026

Copy link
Copy Markdown
Author

Fixed in 2974d85: the finish_reason chunk now closes the stream immediately, flushing the buffered values when its own usage is absent. One guard from the original suggestion: when neither is available, the translator keeps waiting for the usage-only chunk that OpenAI's include_usage mode sends after finish_reason (locked in by the existing TestStreamingUsage_PreservesCacheWriteTokens, which has usage arriving after finish_reason). Added TestStreamingTool_FinishReasonChunkFlushesBufferedUsage covering the per-chunk-usage + separate finish_reason sequence, asserting message_delta/message_stop land on the terminal chunk and [DONE] re-emits nothing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2974d857fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +341 to +345
} else if hasUsage {
// The stream has not terminated yet. Ending it here would drop
// tool call arguments that arrive in later chunks, so buffer
// the latest usage values instead; finish_reason or [DONE]
// emits them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close on terminal usage-only chunks

When an upstream omits finish_reason but sends an include_usage tail chunk with choices: [] and then delays or omits [DONE], this branch buffers that unambiguously terminal chunk instead of emitting message_delta and message_stop. The previous param.SawToolCall path closed such streams immediately; the existing TestStreamingTool_UsageWithoutFinishReasonEmitsMessageDelta now masks the timing regression because runStream always appends [DONE]. Continue buffering usage attached to choice/delta chunks, but finalize when the usage-bearing chunk has no choices.

Useful? React with 👍 / 👎.

@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Author

Heads-up on the failing ❌: this PR intentionally touches internal/translator/ to fix #5419, so the ensure-no-translator-changes path guard fails by design — the build and all tests pass. I don't have write access, so per AGENTS.md the analysis and plan are on the issue first; this PR is a concrete, tested proposal (both Codex review suggestions are already addressed in 2974d85). If the maintenance team prefers to apply the change differently, feel free to use the patch or close this — happy to adjust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant