Skip to content

fix(coordinator): rewrite cached tokens in streamed Responses usage - #3596

Merged
ahg-g merged 1 commit into
llm-d:mainfrom
wguo7:fix/responses-stream-cached-tokens
Oct 30, 2026
Merged

ahg-g merged 1 commit into
llm-d:mainfrom
wguo7:fix/responses-stream-cached-tokens

Conversation

@wguo7

@wguo7 wguo7 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind bug

What this PR does / why we need it:

The P/D usage rewriter, used by the coordinator decode step and the NIXL sidecar connector, reads only a top-level usage field. The response.completed event of a streamed /v1/responses reply nests usage under response, so it kept decode's cached_tokens (#3577). observe already finds that response.usage block for the prompt-token parity check (#3751), and with this change the cached-token rewrite applies to it too. The Messages message.usage block stays observe-only. response.incomplete carries usage the same way and is covered too. Events whose response.usage is null, such as response.created, are left unchanged. The rewriter already buffers an SSE line split across writes (TestResponseWriter_BuffersStreamedFramesSplitAcrossWrites), so a large response.completed event is parsed whole.

Which issue(s) this PR fixes:

Fixes #3577

Release note (write NONE if no user-facing change):

In P/D, streamed /v1/responses replies report the prefill cached-token count in the response.completed usage.

Test plan:

  • Streamed response.completed and response.incomplete events have their cached tokens rewritten (fail on main, pass here), and a response.created event with null usage passes through unchanged.
  • go test passes for pkg/common/..., pkg/coordinator/... and pkg/sidecar/proxy (TestServeMetrics_ListenError skipped, since it also hangs on macOS on main), and make lint reports 0 issues. make presubmit was not run.

@wguo7
wguo7 requested a review from a team as a code owner October 22, 2026 11:07
@github-actions github-actions Bot added size/S Denotes a PR that changes 10-29 lines, ignoring generated files. kind/bug Categorizes issue or PR as related to a bug. labels Oct 22, 2026
@wguo7
wguo7 force-pushed the fix/responses-stream-cached-tokens branch from 31a776c to 034f9db Compare October 22, 2026 11:57

@blacksoxx blacksoxx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked this at 034f9db.

setCachedTokens only looked for a top-level usage, and a streamed Responses reply carries its usage inside the response object of the response.completed and response.incomplete events, which is also where the EPP's openai parser reads it from. Recursing into response when the frame has no top-level block covers both terminal events, and a response.created frame, whose usage is null, falls out of the type assertion and is left unchanged, as the third row shows. The chat and completions paths are untouched since their frames carry usage at the top level. On the constant: "response" appears nowhere else in the request paths except as the openai parser's struct tag, so FieldResponse is its first named home.

The two terminal-event rows fail on main and pass on the branch; go test -race for pkg/common/usage, pkg/common/request and pkg/coordinator/steps, go build ./..., go vet and golangci-lint v2.14.0 are clean.

I would approve this; the decision is with the maintainers.

@wguo7

wguo7 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

@elevran, could you review this when you have time? It fixes the cached-token count in streamed Responses replies. It has one approval, all checks pass, and the branch still merges cleanly with main.

@github-actions github-actions Bot added the needs-rebase PR has merge conflicts and needs a rebase label Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@wguo7 This PR has merge conflicts with main. Please rebase on main and push to resolve them:

gh pr checkout 3596
git fetch origin main
git rebase origin/main
# resolve conflicts
git push --force-with-lease

@wguo7
wguo7 force-pushed the fix/responses-stream-cached-tokens branch from 034f9db to aa0fc90 Compare October 30, 2026 18:32
@github-actions github-actions Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. re-approve PR was rebased after prior approval and needs re-approval and removed size/S Denotes a PR that changes 10-29 lines, ignoring generated files. needs-rebase PR has merge conflicts and needs a rebase labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@blacksoxx @wguo7 has rebased this PR and it is now conflict-free. Please take another look and re-approve when ready.

@github-actions github-actions Bot added kind/bug Categorizes issue or PR as related to a bug. and removed kind/bug Categorizes issue or PR as related to a bug. labels Oct 30, 2026
@wguo7

wguo7 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main to resolve the conflict with #3636. Since setCachedTokens now receives only the usage block, the lookup of the nested response object moved into a small helper, replaceNestedCachedTokensJSON, which replaceCachedTokensJSON calls only when a frame has no top-level usage. Chat Completions and other replies with top-level usage still take the path from #3636, and a frame with neither a top-level usage nor a response.usage is left unchanged. That is the only change to the fix, and the test cases were not edited.

CI passes, and locally the pkg/common/usage tests pass, including the ones #3636 added. The streamed responses completed event and streamed responses incomplete event cases of TestReplaceCachedTokens fail on main and pass with this change. Could you take another look when you have time?

A streamed /v1/responses reply carries its usage inside the response
object of the response.completed event, so the P/D usage rewriter left
decode's cached_tokens in place of the prefill count.

Signed-off-by: William Guo <guowilliam3@gmail.com>
@wguo7
wguo7 force-pushed the fix/responses-stream-cached-tokens branch from aa0fc90 to 5e02ca7 Compare October 30, 2026 22:22
@github-actions github-actions Bot added kind/bug Categorizes issue or PR as related to a bug. and removed kind/bug Categorizes issue or PR as related to a bug. labels Oct 30, 2026
@ahg-g
ahg-g enabled auto-merge October 30, 2026 22:28
@ahg-g
ahg-g added this pull request to the merge queue Oct 30, 2026
@wguo7

wguo7 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

This replaces my earlier rebase note. #3751 conflicted, so I rebased again and dropped the replaceNestedCachedTokensJSON helper. #3751 added observe, which finds the usage block for its prompt-token parity check. It already looks in response.usage on Responses stream events.

The PR changes only pkg/common/usage/cached_tokens.go and its test. observe is unexported and has two callers, processJSON and processSSELine, both in that file. Its second return value changes from topLevel bool to the key of the object that holds a nested block: "" for a top-level block, "response" or "message" otherwise. The cached-token rewrite runs only for "" and "response". Dropping an injected usage-only frame still requires a top-level block, so a frame whose usage is only under response is never dropped.

message.usage on the Messages message_start event is only observed, not rewritten. The Messages API reports cached tokens as cache_read_input_tokens, and setCachedTokens leaves a block in that schema unchanged wherever it appears, so rewriting it would have no effect.

The two Responses event cases in TestReplaceCachedTokens are new in this PR. They fail against main with #3751 and pass with this change. The pkg/common/usage and pkg/coordinator tests pass, and golangci-lint reports 0 issues on pkg/common/usage.

Merged via the queue into llm-d:main with commit f01d548 Oct 30, 2026
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug Categorizes issue or PR as related to a bug. re-approve PR was rebased after prior approval and needs re-approval size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P/D: streamed /v1/responses usage keeps decode's cached_tokens

3 participants