fix(billing): count cache tokens in Anthropic consume log input totals - #7291
fix(billing): count cache tokens in Anthropic consume log input totals#7291TomJKS wants to merge 1 commit into
Conversation
The consume log stored summary.PromptTokens verbatim, but that field means different things per provider. Anthropic reports input_tokens as the fresh, non-cached input only, with cache_read_input_tokens and cache_creation_input_tokens counted separately, while OpenAI-style prompt_tokens already includes both. A Claude request that served 589,733 cached and 1,088 cache-written tokens was therefore logged with prompt_tokens=2. Every consumer reads that column as the whole input and renders the cache counts as its breakdown: the usage log Tokens column, the details dialog, the mobile card, the RPM/TPM query and SumUsedToken in model/log.go, and the data export TokenUsed. Claude rows looked wrong next to OpenAI-style models and, more importantly, TPM and token totals silently dropped all cached input. Normalize at the log boundary rather than at parse time: - Record PromptTokensExcludeCache on textQuotaSummary, derived from the same condition the quota math already uses (IsClaudeUsageSemantic || legacyClaudeDerived), so logging and billing cannot drift apart. - Add logInputTokens, which adds CacheTokens and cacheWriteTokensTotal back when that flag is set. cacheWriteTokensTotal is the cache-write amount actually billed as input, so the total stays correct when upstream sends only the 5m/1h split without an aggregate. - Pass logInputTokens(summary) to RecordConsumeLog. Charged quota is unchanged: summary.PromptTokens itself is untouched and the quota is computed before the log is written. The OpenRouter Claude path, which subtracts cache from prompt tokens before billing, and the legacy Claude-derived OpenAI usage path both round-trip correctly. The realtime WSS and audio consume logs already use OpenAI-style inclusive input counts and need no change. Historical rows are not rewritten. Validation: gofmt clean; go vet ./service/... ./model/... clean; go test ./service/... ./model/... with the new TestLogInputTokensNormalizesCacheAcrossSemantics plus assertions added to the existing Claude-semantic and legacy-derived tests. The two TestObserveChannelAffinityUsageCacheByRelayFormat failures in service/ are pre-existing and reproduce on unmodified 064ed94. go build ./... fails only on the unbuilt web/dist embed. No schema, GORM tag, migration, or SQL change, so the three-database verification matrix does not apply. 中文说明: 消耗日志此前直接写入 summary.PromptTokens,但该字段在不同供应商下含义不同。 Anthropic 的 input_tokens 只表示未命中缓存的 fresh token,cache_read_input_tokens 与 cache_creation_input_tokens 单独上报;OpenAI 语义的 prompt_tokens 本身已包含 缓存。因此一次命中 589,733 缓存、写入 1,088 缓存的 Claude 请求,日志里 prompt_tokens 只有 2。 而所有下游都按「总输入」读这一列,并把缓存数量当作它的明细展示:使用日志的 Tokens 列、详情弹窗、移动端卡片、model/log.go 中的 RPM/TPM 查询与 SumUsedToken, 以及数据看板导出的 TokenUsed。结果不仅 Claude 行与 OpenAI 系模型并列时显示异常, 更关键的是 TPM 与 token 总量统计静默漏掉了全部缓存输入。 修复方式是在写日志的边界统一口径,而非改动解析层: - 在 textQuotaSummary 上记录 PromptTokensExcludeCache,取值沿用计费逻辑已在使用 的判断条件(IsClaudeUsageSemantic || legacyClaudeDerived),避免日志与计费口径 分叉。 - 新增 logInputTokens,在该标记为真时把 CacheTokens 与 cacheWriteTokensTotal 加 回。选用 cacheWriteTokensTotal 是因为它正是实际按输入计费的缓存写入量,在上游 只给 5m/1h 拆分值而缺总量时仍然准确。 - 写日志时传入 logInputTokens(summary)。 计费金额不变:summary.PromptTokens 本身未改动,配额在写日志之前即已算完。 OpenRouter 的 Claude 计费路径(先从 prompt 中减去缓存)与 legacyClaudeDerived 旧 路径均能正确还原。realtime WSS 与 audio 两处日志用的是 OpenAI 口径的含缓存输入, 无需改动。历史日志不会被追溯修正。 验证:gofmt 无输出;go vet ./service/... ./model/... 无告警;go test ./service/... ./model/... 通过新增的 TestLogInputTokensNormalizesCacheAcrossSemantics, 并在既有的 Claude 语义与 legacyClaudeDerived 测试中补充断言。service 包内两个 TestObserveChannelAffinityUsageCacheByRelayFormat 失败为上游既有问题,在未修改的 064ed94 上可复现。go build ./... 仅因未构建前端 web/dist 而失败。本次没有 schema、GORM tag、迁移或 SQL 变更,故三数据库验证矩阵不适用。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. WalkthroughClaude-style usage now marks prompt tokens that exclude cached input. Consume logs use normalized totals that include cache reads and writes, with tests covering Anthropic, OpenAI, and legacy Claude-derived semantics. ChangesToken log normalization
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Claude consume logs now include cached input tokens in displayed and aggregated input totals while preserving OpenAI-style totals and leaving billing behavior unchanged. The covered usage variants indicate no remaining merge-blocking risk. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning 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. A rabbit counts fresh tokens bright Comment |
Agent
Links
7290User request
用户原话:「Claude模型的tokens统计还是不准确,参考其他模型是正常的,修复统计错误的bug」,并附使用日志截图:同一页面 qwen3.8-max 显示
70,397 / 2,228(缓存↓ 67,584),claude-opus-5 却显示2 / 1,117(缓存↓ 589,733 ↑ 1,088)。Out of scope — refuse
Coding Plan
Reverse-engineered channels
Third-party API wrappers
Codex channel-type changes, or compatibility from exposing Codex as a general-purpose API
Codex API-specific protocol or behavior treated as standard OpenAI API behavior
Pass-through-only forwarding
Third-party hosting sites, relay services, or API services
Usage, configuration, or integration
Matched: no
If yes, what was told to the user (stop here; do not open a PR): 不适用。本变更修复 new-api 自身写入消耗日志时缺失的 token 归一化,不涉及上述任何一类。
Kind
Issue facts
prompt_tokens写成 Claude 原始input_tokens,即仅未命中缓存的 fresh token。一次命中 589,733 缓存读取、1,088 缓存写入的请求,prompt_tokens只有 2。model/log.go:625的 RPM/TPM 与model/log.go:684的SumUsedToken都按sum(prompt_tokens) + sum(completion_tokens)聚合,Claude 请求静默漏掉全部缓存输入,缓存密集场景下 TPM 可低估两个数量级;③看板——LogQuotaData的TokenUsed取同一表达式,导出同样偏小。计费金额不受影响。other.cache_tokens=589733、other.cache_write_tokens=1088均正确落库。更直接的内部证据:new-api 自身在别处已实现正确归一化,service/tiered_settle.go:45-48用同一个isClaudeUsageSemantic标记计算len = p + cr + cc5m + cc1h,pkg/billingexpr/expr.md:354将该公式写成规范。仓库既知道公式也拿到了数据,只是写日志时未套用。属纯内部不一致。Change
在写消耗日志的边界补齐 Anthropic 语义的 token 归一化,使
logs.prompt_tokens对所有供应商统一表示「总输入 token」。textQuotaSummary新增PromptTokensExcludeCache字段,取值为计费逻辑已在使用的同一判定summary.IsClaudeUsageSemantic || legacyClaudeDerived(text_quota.go:284)。复用同源条件,而非另写一份判断,避免日志与计费口径日后分叉。logInputTokens(summary):该标记为真时返回PromptTokens + CacheTokens + cacheWriteTokensTotal(summary),否则原样返回PromptTokens。RecordConsumeLog的PromptTokens改为传logInputTokens(summary)。为什么成立:
summary.PromptTokens本身未被修改,配额在calculateTextQuotaSummary内部早已算完并结算,本变更只改变此后传给RecordConsumeLog的值。原有配额断言 1488 / 1624 未做任何调整即通过。PromptTokens已含缓存,PromptTokensExcludeCache为假,直接返回原值。cacheWriteTokensTotal而非summary.CacheCreationTokens:Claude 语义的计费分支实际按max(总量, 5m+1h)计价(text_quota.go:322-325中remaining = max(总量-5m-1h, 0)再加 5m、1h),cacheWriteTokensTotal正是该量;且当上游只给 5m/1h 拆分而缺总量字段时仍然准确。text_quota.go:273-281),加回后即为上游原始的含缓存总输入;legacyClaudeDerived旧路径的PromptTokens同样不含缓存,故沿用同一条件一并归一化。expr.md:354记载的len = input_tokens + cache_read_tokens + cache_creation_tokens相同,不引入新概念,只是把既有归一化补齐到缺失的那条路径。Research
Duplicate / prior art
new-api QuantumNous Claude cache_read_input_tokens prompt_tokens 日志 统计 issue;QuantumNous/new-api pull request text_quota.go prompt_tokens anthropic cache tokens log statisticsother.claude === true门控(本变更是后端写入的 token 数值口径);/v1/messages 格式请求 OpenAI Compatible 上游返回的usage #4395 是 Claude←OpenAI 的反向转换且不影响计费;[Bug] Streaming 模式下 prompt_tokens 比 non-streaming 多 400-1000,导致上游隐式缓存 0% 命中 #6212 是流式与非流式的 token 计数差异。feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 关系最近但取向不同:它把「看板统计不含缓存」当作功能缺失,提议在quota_data侧新增cache_tokens等拆分字段;本 PR 主张同一列语义随供应商而变本身是 bug。两者可并存——本 PR 修正列语义,feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 仍可提供明细拆分。若维护者倾向统一到 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的拆分字段方案,请直接说明,我按该方向重做。github.com在本工作环境无法访问,上述 issue 内容来自站外搜索摘要,未逐一打开原页核对。Docs and code
Unable to verify if domain ... is safe to fetch),未取得结论。pkg/billingexpr/expr.md是直接相关的仓库内规范。第 17 行说明表达式无需知道上游是 OpenAI 格式(prompt_tokens含缓存)还是 Claude 格式(input_tokens不含缓存),「系统会在求值前按上游响应格式归一化」;第 34 行定义len「非 Claude:等于原始prompt_tokens;Claude:等于文本输入 + 缓存读取 + 缓存创建」;第 338–340 行列出两种语义差异;第 354 行给出len = input_tokens + cache_read_tokens + cache_creation_tokens。relaykit/dto/billing_usage.go:298-327—canonicalClaudeUsage()令PromptTokens = InputTokens(fresh),同时另算含缓存的InputTokens,并把缓存放入PromptTokensDetails。含缓存总输入在这一层就已存在。service/text_quota.go:258-262—summary.PromptTokens取 fresh 值。service/text_quota.go:309-327— 计费分支对 Anthropic 语义不减缓存、按各自倍率单独计价,证明计费侧对语义差异知情且正确。service/tiered_settle.go:45-48— 同一标记已在计费len上做正确归一化,与expr.md:354一致。model/log.go:625、model/log.go:684、model/log.go:393— 三处按「总输入」消费prompt_tokens。web/.../common-logs-columns.tsx:703-735、web/.../details-dialog.tsx:373-403— 前端按「总输入」渲染并把缓存作为明细,前提对 OpenAI 语义成立、对 Anthropic 不成立。Alternatives considered
model/log.go两处 SQL 聚合都读取并累加缓存字段。input_tokens_total之类的新列供 UI 与统计使用。quota_data侧还需再算一次),面更大且此后每新增一个消费方都要记得补偿;C 会让两个含义相近的列长期并存,而other.input_tokens_total已存在却恰好对 Claude 格式不写入(text_quota.go:541-546)且前端从未读取,说明该方向此前已尝试并搁置。A 只改一处写入、与expr.md既有规范同源、且让所有现有消费方自动正确,是最小且最不易再次分叉的做法。不过 A 会改变token_used的口径,这一点与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 存在取向分歧,需维护者确认。Files
service/text_quota.goPromptTokensExcludeCache标记与logInputTokens(),并把RecordConsumeLog的PromptTokens改为归一化后的总输入service/text_quota_test.goTestLogInputTokensNormalizesCacheAcrossSemantics;在既有的 Claude 语义与legacyClaudeDerived测试中补充归一化断言Behavior
prompt_tokens = input_tokens(fresh)。示例记录写入2,而同条记录的other.cache_tokens=589733、other.cache_write_tokens=1088。TPM/SumUsedToken/看板TokenUsed均漏掉缓存输入。prompt_tokens = 590823(2 + 589733 + 1088),与缓存明细自洽,并与 OpenAI 语义模型口径一致;TPM 与 token 统计包含缓存输入。OpenAI 语义记录数值不变。计费金额、quota字段、预扣费与结算逻辑不变。service/tiered_settle.go:47的len用cc5m + cc1h加回缓存写入,而 Anthropic 语义下这两个值仅取自ClaudeCacheCreation5m/1hTokens;若上游只给了cache_creation_input_tokens总量而未给cache_creation子对象,len会漏掉该部分。建议另开条目。Verification
gofmt -l service/text_quota.go service/text_quota_test.go— 无输出。go vet ./service/... ./model/...— 无告警。go test ./service/ -run 'TestLogInputTokens|TestCacheWriteTokensTotal|TestCalculateTextQuotaSummary' -v— 全部 PASS,含新增 4 个子用例。go test ./service/... ./model/...—model全绿;service包内TestObserveChannelAffinityUsageCacheByRelayFormat_MixedMode与_UnsupportedModeKeepsEmpty失败(expected: int(2), actual: int64(3))。已用git stash在未修改的064ed943e上复现同样失败,确认为既有问题,与本 PR 无关。go build ./...— 仅因未构建前端而报pattern web/dist: no matching files found,与本变更无关。POST /v1/messages发起会命中缓存的连续对话(claude-opus-5、claude-sonnet-5),在/usage-logs观察。六条记录逐条精确自洽:261,283 = 2 + 260,411 + 870、678,781 = 2 + 677,478 + 1,301、677,480 = 2 + 676,858 + 620、72,230 = 2 + 29,717 + 42,511、260,413 = 2 + 257,765 + 2,646、72,037 = 2 + 29,261 + 42,774。TPM 从修复前的 0 变为 941,332,恰等于最近 60 秒两条记录之和(678,781 + 178) + (261,283 + 1,090) = 941,332,分毫不差——这直接证明修复贯通到了SumUsedQuota的聚合。同一页面中切换容器前写入的最后一条仍为2 / 2,568,构成同图前后对照。/usage-logs截图(由报告者提供)。如需附在 PR 中请告知。TestLogInputTokensNormalizesCacheAcrossSemantics(OpenAI 语义不累加、Anthropic 语义加回缓存读写含真实数据2 → 590,823、仅有 5m/1h 拆分时按实际计费量加回、Anthropic 无缓存时不变);并在既有TestCalculateTextQuotaSummaryUnifiedForClaudeSemantic与TestCalculateTextQuotaSummaryHandlesLegacyClaudeDerivedOpenAIUsage中补充断言(分别为 1150 与 4192),未新建测试文件。claude-opus-5与claude-sonnet-5;linux/amd64。prompt_tokens列的数值,故三库行为应一致,但未逐一验证。OpenRouter Claude 计费路径、legacyClaudeDerived旧路径、Claude←OpenAI 跨格式转换路径仅有单元测试覆盖,无真实上游实测。Bedrock、Vertex 等其他承载 Claude 的渠道类型未实测。历史日志回填未验证(本 PR 不涉及)。docs.newapi.ai、deepwiki.com、github.com在本工作环境无法访问,文档与重复排查依赖站外搜索摘要。Risks
PromptTokens实际已经含缓存却被判定为 Anthropic 语义,会导致重复累加、prompt_tokens偏大。该判定沿用计费侧同源条件IsClaudeUsageSemantic || legacyClaudeDerived,若它误判,现有计费也会同时算错,因此不引入新的误判面。OpenRouter 路径已在单元测试中覆盖往返一致性。summary.PromptTokens未被改动,配额在写日志之前已计算并结算完毕;原有配额断言未做调整即通过。不涉及认证。唯一的口径变化是logs.prompt_tokens及由其派生的 RPM/TPM、SumUsedToken、看板TokenUsed对 Claude 请求开始包含缓存输入——这正是本 PR 的目的,但会造成修复前后同一模型的统计数字出现台阶,且与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案取向需要协调。tiered_settle.go:47的len在缺少 5m/1h 拆分时漏算缓存写入(见 Behavior 段);③与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案协调。Scope check
Agent
Links
7290User request
用户原话:「Claude模型的tokens统计还是不准确,参考其他模型是正常的,修复统计错误的bug」,并附使用日志截图:同一页面 qwen3.8-max 显示
70,397 / 2,228(缓存↓ 67,584),claude-opus-5 却显示2 / 1,117(缓存↓ 589,733 ↑ 1,088)。Out of scope — refuse
Coding Plan
Reverse-engineered channels
Third-party API wrappers
Codex channel-type changes, or compatibility from exposing Codex as a general-purpose API
Codex API-specific protocol or behavior treated as standard OpenAI API behavior
Pass-through-only forwarding
Third-party hosting sites, relay services, or API services
Usage, configuration, or integration
Matched: no
If yes, what was told to the user (stop here; do not open a PR): 不适用。本变更修复 new-api 自身写入消耗日志时缺失的 token 归一化,不涉及上述任何一类。
Kind
Issue facts
prompt_tokens写成 Claude 原始input_tokens,即仅未命中缓存的 fresh token。一次命中 589,733 缓存读取、1,088 缓存写入的请求,prompt_tokens只有 2。model/log.go:625的 RPM/TPM 与model/log.go:684的SumUsedToken都按sum(prompt_tokens) + sum(completion_tokens)聚合,Claude 请求静默漏掉全部缓存输入,缓存密集场景下 TPM 可低估两个数量级;③看板——LogQuotaData的TokenUsed取同一表达式,导出同样偏小。计费金额不受影响。other.cache_tokens=589733、other.cache_write_tokens=1088均正确落库。更直接的内部证据:new-api 自身在别处已实现正确归一化,service/tiered_settle.go:45-48用同一个isClaudeUsageSemantic标记计算len = p + cr + cc5m + cc1h,pkg/billingexpr/expr.md:354将该公式写成规范。仓库既知道公式也拿到了数据,只是写日志时未套用。属纯内部不一致。Change
在写消耗日志的边界补齐 Anthropic 语义的 token 归一化,使
logs.prompt_tokens对所有供应商统一表示「总输入 token」。textQuotaSummary新增PromptTokensExcludeCache字段,取值为计费逻辑已在使用的同一判定summary.IsClaudeUsageSemantic || legacyClaudeDerived(text_quota.go:284)。复用同源条件,而非另写一份判断,避免日志与计费口径日后分叉。logInputTokens(summary):该标记为真时返回PromptTokens + CacheTokens + cacheWriteTokensTotal(summary),否则原样返回PromptTokens。RecordConsumeLog的PromptTokens改为传logInputTokens(summary)。为什么成立:
summary.PromptTokens本身未被修改,配额在calculateTextQuotaSummary内部早已算完并结算,本变更只改变此后传给RecordConsumeLog的值。原有配额断言 1488 / 1624 未做任何调整即通过。PromptTokens已含缓存,PromptTokensExcludeCache为假,直接返回原值。cacheWriteTokensTotal而非summary.CacheCreationTokens:Claude 语义的计费分支实际按max(总量, 5m+1h)计价(text_quota.go:322-325中remaining = max(总量-5m-1h, 0)再加 5m、1h),cacheWriteTokensTotal正是该量;且当上游只给 5m/1h 拆分而缺总量字段时仍然准确。text_quota.go:273-281),加回后即为上游原始的含缓存总输入;legacyClaudeDerived旧路径的PromptTokens同样不含缓存,故沿用同一条件一并归一化。expr.md:354记载的len = input_tokens + cache_read_tokens + cache_creation_tokens相同,不引入新概念,只是把既有归一化补齐到缺失的那条路径。Research
Duplicate / prior art
new-api QuantumNous Claude cache_read_input_tokens prompt_tokens 日志 统计 issue;QuantumNous/new-api pull request text_quota.go prompt_tokens anthropic cache tokens log statisticsother.claude === true门控(本变更是后端写入的 token 数值口径);/v1/messages 格式请求 OpenAI Compatible 上游返回的usage #4395 是 Claude←OpenAI 的反向转换且不影响计费;[Bug] Streaming 模式下 prompt_tokens 比 non-streaming 多 400-1000,导致上游隐式缓存 0% 命中 #6212 是流式与非流式的 token 计数差异。feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 关系最近但取向不同:它把「看板统计不含缓存」当作功能缺失,提议在quota_data侧新增cache_tokens等拆分字段;本 PR 主张同一列语义随供应商而变本身是 bug。两者可并存——本 PR 修正列语义,feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 仍可提供明细拆分。若维护者倾向统一到 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的拆分字段方案,请直接说明,我按该方向重做。github.com在本工作环境无法访问,上述 issue 内容来自站外搜索摘要,未逐一打开原页核对。Docs and code
Unable to verify if domain ... is safe to fetch),未取得结论。pkg/billingexpr/expr.md是直接相关的仓库内规范。第 17 行说明表达式无需知道上游是 OpenAI 格式(prompt_tokens含缓存)还是 Claude 格式(input_tokens不含缓存),「系统会在求值前按上游响应格式归一化」;第 34 行定义len「非 Claude:等于原始prompt_tokens;Claude:等于文本输入 + 缓存读取 + 缓存创建」;第 338–340 行列出两种语义差异;第 354 行给出len = input_tokens + cache_read_tokens + cache_creation_tokens。relaykit/dto/billing_usage.go:298-327—canonicalClaudeUsage()令PromptTokens = InputTokens(fresh),同时另算含缓存的InputTokens,并把缓存放入PromptTokensDetails。含缓存总输入在这一层就已存在。service/text_quota.go:258-262—summary.PromptTokens取 fresh 值。service/text_quota.go:309-327— 计费分支对 Anthropic 语义不减缓存、按各自倍率单独计价,证明计费侧对语义差异知情且正确。service/tiered_settle.go:45-48— 同一标记已在计费len上做正确归一化,与expr.md:354一致。model/log.go:625、model/log.go:684、model/log.go:393— 三处按「总输入」消费prompt_tokens。web/.../common-logs-columns.tsx:703-735、web/.../details-dialog.tsx:373-403— 前端按「总输入」渲染并把缓存作为明细,前提对 OpenAI 语义成立、对 Anthropic 不成立。Alternatives considered
model/log.go两处 SQL 聚合都读取并累加缓存字段。input_tokens_total之类的新列供 UI 与统计使用。quota_data侧还需再算一次),面更大且此后每新增一个消费方都要记得补偿;C 会让两个含义相近的列长期并存,而other.input_tokens_total已存在却恰好对 Claude 格式不写入(text_quota.go:541-546)且前端从未读取,说明该方向此前已尝试并搁置。A 只改一处写入、与expr.md既有规范同源、且让所有现有消费方自动正确,是最小且最不易再次分叉的做法。不过 A 会改变token_used的口径,这一点与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 存在取向分歧,需维护者确认。Files
service/text_quota.goPromptTokensExcludeCache标记与logInputTokens(),并把RecordConsumeLog的PromptTokens改为归一化后的总输入service/text_quota_test.goTestLogInputTokensNormalizesCacheAcrossSemantics;在既有的 Claude 语义与legacyClaudeDerived测试中补充归一化断言Behavior
prompt_tokens = input_tokens(fresh)。示例记录写入2,而同条记录的other.cache_tokens=589733、other.cache_write_tokens=1088。TPM/SumUsedToken/看板TokenUsed均漏掉缓存输入。prompt_tokens = 590823(2 + 589733 + 1088),与缓存明细自洽,并与 OpenAI 语义模型口径一致;TPM 与 token 统计包含缓存输入。OpenAI 语义记录数值不变。计费金额、quota字段、预扣费与结算逻辑不变。service/tiered_settle.go:47的len用cc5m + cc1h加回缓存写入,而 Anthropic 语义下这两个值仅取自ClaudeCacheCreation5m/1hTokens;若上游只给了cache_creation_input_tokens总量而未给cache_creation子对象,len会漏掉该部分。建议另开条目。Verification
gofmt -l service/text_quota.go service/text_quota_test.go— 无输出。go vet ./service/... ./model/...— 无告警。go test ./service/ -run 'TestLogInputTokens|TestCacheWriteTokensTotal|TestCalculateTextQuotaSummary' -v— 全部 PASS,含新增 4 个子用例。go test ./service/... ./model/...—model全绿;service包内TestObserveChannelAffinityUsageCacheByRelayFormat_MixedMode与_UnsupportedModeKeepsEmpty失败(expected: int(2), actual: int64(3))。已用git stash在未修改的064ed943e上复现同样失败,确认为既有问题,与本 PR 无关。go build ./...— 仅因未构建前端而报pattern web/dist: no matching files found,与本变更无关。POST /v1/messages发起会命中缓存的连续对话(claude-opus-5、claude-sonnet-5),在/usage-logs观察。六条记录逐条精确自洽:261,283 = 2 + 260,411 + 870、678,781 = 2 + 677,478 + 1,301、677,480 = 2 + 676,858 + 620、72,230 = 2 + 29,717 + 42,511、260,413 = 2 + 257,765 + 2,646、72,037 = 2 + 29,261 + 42,774。TPM 从修复前的 0 变为 941,332,恰等于最近 60 秒两条记录之和(678,781 + 178) + (261,283 + 1,090) = 941,332,分毫不差——这直接证明修复贯通到了SumUsedQuota的聚合。同一页面中切换容器前写入的最后一条仍为2 / 2,568,构成同图前后对照。/usage-logs截图(由报告者提供)。如需附在 PR 中请告知。TestLogInputTokensNormalizesCacheAcrossSemantics(OpenAI 语义不累加、Anthropic 语义加回缓存读写含真实数据2 → 590,823、仅有 5m/1h 拆分时按实际计费量加回、Anthropic 无缓存时不变);并在既有TestCalculateTextQuotaSummaryUnifiedForClaudeSemantic与TestCalculateTextQuotaSummaryHandlesLegacyClaudeDerivedOpenAIUsage中补充断言(分别为 1150 与 4192),未新建测试文件。claude-opus-5与claude-sonnet-5;linux/amd64。prompt_tokens列的数值,故三库行为应一致,但未逐一验证。OpenRouter Claude 计费路径、legacyClaudeDerived旧路径、Claude←OpenAI 跨格式转换路径仅有单元测试覆盖,无真实上游实测。Bedrock、Vertex 等其他承载 Claude 的渠道类型未实测。历史日志回填未验证(本 PR 不涉及)。docs.newapi.ai、deepwiki.com、github.com在本工作环境无法访问,文档与重复排查依赖站外搜索摘要。Risks
PromptTokens实际已经含缓存却被判定为 Anthropic 语义,会导致重复累加、prompt_tokens偏大。该判定沿用计费侧同源条件IsClaudeUsageSemantic || legacyClaudeDerived,若它误判,现有计费也会同时算错,因此不引入新的误判面。OpenRouter 路径已在单元测试中覆盖往返一致性。summary.PromptTokens未被改动,配额在写日志之前已计算并结算完毕;原有配额断言未做调整即通过。不涉及认证。唯一的口径变化是logs.prompt_tokens及由其派生的 RPM/TPM、SumUsedToken、看板TokenUsed对 Claude 请求开始包含缓存输入——这正是本 PR 的目的,但会造成修复前后同一模型的统计数字出现台阶,且与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案取向需要协调。tiered_settle.go:47的len在缺少 5m/1h 拆分时漏算缓存写入(见 Behavior 段);③与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案协调。Scope check
Summary by CodeRabbit