Repository navigation
fix(qwen): stop counting cached and reasoning tokens twice - #1527
Merged
junhoyeo merged 1 commit intoOct 23, 2026
Merged
Conversation
Qwen Code records Gemini-shaped `usageMetadata` for every provider, but the fields overlap: `promptTokenCount` already includes `cachedContentTokenCount`, and on OpenAI-compatible providers (Qwen OAuth, DashScope, OpenRouter and similar) `candidatesTokenCount` is `completion_tokens`, which already includes `thoughtsTokenCount`. The parser fed all four into separate buckets, so every cached prompt token was counted twice and priced at both the input and the cache-read rate, and reasoning was counted twice on OpenAI-compatible providers. Split the cached count out of the prompt when it is a valid subset, and split the thoughts out of the candidates when `totalTokenCount` equals prompt + candidates. The Gemini API adds thoughts on top of the candidates in its total, so those records keep reasoning as a separate bucket. Bump the Qwen parser version so cached parses of unchanged transcripts are rebuilt. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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
Qwen CLI usage is overcounted. Every cached prompt token is counted twice, once inside
inputand once ascache_read, so it is priced at both the input rate and the cache-read rate. On OpenAI-compatible providers (Qwen OAuth, DashScope, OpenRouter and similar) every reasoning token is counted twice as well. A turn with a 10,000-token prompt (9,000 of them cached) and 300 completion tokens (120 of them reasoning) is reported as 19,420 tokens, while Qwen Code recordedtotalTokenCount: 10300for it.Cause
parse_qwen_fileincrates/tokscale-core/src/sessions/qwen.rsmapspromptTokenCounttoinput,cachedContentTokenCounttocache_read,candidatesTokenCounttooutputandthoughtsTokenCounttoreasoning, as if the four were disjoint. In Qwen Code's chat recordings they overlap:promptTokenCountincludes the cached prefix for every provider. The OpenAI-compatible converter copiesprompt_tokens(converter.ts, streaming path L2085-L2135). The Responses converter copiesinput_tokens(responses-converter.ts). The Anthropic converter adds cache reads and writes toinput_tokensunless the provider already counted them there (usage.ts). The Gemini API counts cached content inside the prompt.candidatesTokenCountiscompletion_tokens, which already includes the reasoning thatthoughtsTokenCountreports again. Qwen Code treats it that way itself: when it has to rebuild a missing total,getEventTotalTokenCountaddsthoughts_token_counton top of the output only for auth types other thanopenaiandqwen-oauth(uiTelemetry.ts). Only the Gemini API reports thoughts beside the candidates, and the parser assumed that for every provider.recordAssistantTurnstores that object unchanged as the record'susageMetadata(chatRecordingService.ts).TokenBreakdown::total()andcompute_costadd every bucket, so the overlapping tokens were counted and billed twice. The Gemini parser got the same fix for its ownpromptTokenCountin #452, and Hindsight got it in #1393.Fix
A new
qwen_token_breakdownhelper removes each overlap once:input = promptTokenCount - cachedContentTokenCountwhen the cached count is a valid subset of the prompt. A cached count above the prompt is kept as reported, as fix(hindsight): avoid counting cached input twice #1393 does, so the existingtest_parse_qwen_with_cache_and_reasoningfixture is unchanged.totalTokenCountequalspromptTokenCount + candidatesTokenCount(OpenAI-compatible and Responses providers), the thoughts come out of the candidates:output = candidates - thoughts, clamped like the Codex split. The Gemini API's total adds thoughts on top of the candidates, so those records keep reasoning as a separate bucket. Records without a total also keep the current split.For both the OpenAI-compatible and the Gemini shape, a parsed record's total now equals the
totalTokenCountQwen Code recorded.ClientId::Qwenmoves to parser version 2 so cached parses of unchanged transcripts are rebuilt rather than replaying the old buckets.Verification
test_parse_qwen_counts_cached_and_reasoning_subsets_once(OpenAI-compatible shape) andtest_parse_qwen_keeps_gemini_thoughts_beside_candidates(Gemini shape) assert every bucket and thattokens.total()equals the recordedtotalTokenCount.test_qwen_parser_version_invalidates_v1_entriespins the version bump.test_parse_qwen_multi_turn,test_parse_qwen_malformed_lines,test_parse_qwen_unknown_model_fallback). They now expectinputwithout the cached tokens: 100 becomes 95 and 300 becomes 290.cargo fmt,cargo clippyandcargo testhave not been run, and CI will be the first build. To check the expected values, I ran every fixture in theqwen.rstest module through a line-by-line Python port of the new mapping. All 49 assertions held. With the old mapping, the two new tests and the three updated ones fail, and every other assertion still passes.🤖 Generated with Claude Code
Summary by cubic
Fixes Qwen CLI usage overcounting: cached prompt tokens were counted twice (in both
inputandcache_read) and priced at both rates, and on OpenAI-compatible providers reasoning tokens were counted twice as well. A 10,000-token prompt (9,000 cached) with 300 completion tokens (120 reasoning) was reported as 19,420 tokens instead of 10,300.inputnow excludescachedContentTokenCountwhen the cached count is a valid subset of the prompt; counts above the prompt are kept as reported.outputnow excludesthoughtsTokenCountwhen the recorded total shows the thoughts inside the candidates, while Gemini API records keep reasoning as a separate bucket.Written for commit 6ad8ec6. Summary will update on new commits.