fix(xai): preserve prompt cache tokens in streaming responses#6145
Open
stofancy wants to merge 1 commit into
Open
fix(xai): preserve prompt cache tokens in streaming responses#6145stofancy wants to merge 1 commit into
stofancy wants to merge 1 commit into
Conversation
xAIStreamHandler rebuilt a bare *dto.Usage and copied only PromptTokens/TotalTokens/CompletionTokens from the upstream usage, silently discarding PromptTokensDetails.CachedTokens (and every other nested field) for every streaming request. As a result, prompt-cache hits reported by xAI via the standard path usage.prompt_tokens_details.cached_tokens were dropped before reaching PostTextConsumeQuota, so streaming requests were always logged and billed with cache_tokens=0 even when the upstream actually served cached input. The non-streaming xAIHandler was unaffected because it returns the full usage struct. Copy the whole upstream usage (mirroring openai.handleLastResponse) so CachedTokens and the rest of the details survive into billing/logs. CompletionTokens is still derived from total-prompt when the upstream omits it, preserving the previous fallback behavior.
4 tasks
Contributor
WalkthroughThe xAI streaming handler now copies the complete upstream usage object, preserving nested fields such as cached-token details. It retains a fallback calculation for missing completion tokens before downstream response conversion. ChangesxAI streaming usage
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Author
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.
Summary
Fixes #6144
The xAI channel's streaming handler split the upstream
usageacross two objects: it forwarded the complete upstream usage to the client, but fed a stripped usage (only 3 scalar fields) into billing. The net effect: every streaming request'scached_tokenswas silently zeroed in logs/billing, while the client still received the correct value — and whencache_ratio < 1, this caused real overcharging.Root cause — dual-path divergence
xAIStreamHandlerinrelay/channel/xai/text.gokeeps two usage objects:streamResponseXAI2OpenAIforwards the originalxAIResp.Usage(object B):xAIResp.Usage(upstream original, full)usage(rebuilt, stripped)cached_tokensThis is why a downstream client (e.g. LiteLLM) sees the correct
cached_tokenswhile new-api's own console logs show0for the same request.Proof via packet capture
Streaming request sent directly to new-api (
stream_options.include_usage=true), the final chunk new-api returns to the client:{"prompt_tokens": 1816, "completion_tokens": 64, "total_tokens": 1880, "prompt_tokens_details": {"cached_tokens": 1792, ...}}Client gets
cached_tokens = 1792(correct), but the same request is logged in-console withcache_tokens = 0.The non-streaming
xAIHandleris unaffected because it returns the fullxaiResponse.Usage. The OpenAI channel is also unaffected —handleLastResponse(relay/channel/openai/helper.go) does*usage = lastStreamResponse.Usage, copying the whole struct. The xAI streaming handler is the only place that rebuilds usage by hand.Why it matters — billing impact
The billing formula (
service/text_quota.go, OpenAI semantic):Cached tokens are subtracted from the base, then re-added at
CacheRatio(the discount). With the bug zeroingCacheTokens, cached tokens get billed at full price instead of the discounted rate — an overcharge of1 - cache_ratioper cached token.Verified with a 6-turn incremental conversation (same prompt, streaming vs non-streaming). Actual console logs: non-streaming recorded cache growth correctly (7424→8064→8704→9344→10112); streaming logged
0every turn and charged 2–2.6× the correct quota. Full table in #6144.No impact when
cache_ratio == 1(default for unlisted models) or when there's no cache hit.Fix
Mirror the OpenAI channel's approach: copy the full upstream usage so billing and forwarding share the same source of truth.
The
total - promptfallback is preserved for upstreams that omitCompletionTokens. No change needed toapplyUsagePostProcessing: xAI reports cache hits via the standardprompt_tokens_details.cached_tokenspath, preserved by the struct copy.Verification
go build ./...✅go vet ./relay/channel/xai/ ./relay/channel/openai/✅Reproduction / expected vs actual
See #6144 for the full reproduction steps, the dual-path source analysis, and the 6-turn console-log table.