Skip to content

fix(billing): count cache tokens in Anthropic consume log input totals - #7291

Open
TomJKS wants to merge 1 commit into
QuantumNous:mainfrom
TomJKS:fix/anthropic-consume-log-input-tokens
Open

fix(billing): count cache tokens in Anthropic consume log input totals#7291
TomJKS wants to merge 1 commit into
QuantumNous:mainfrom
TomJKS:fix/anthropic-consume-log-input-tokens

Conversation

@TomJKS

@TomJKS TomJKS commented Sep 9, 2026

Copy link
Copy Markdown

Agent

  • Tool: Claude Code
  • Tool version: 未知(运行环境未向 agent 暴露版本号)
  • Model (full id): claude-opus-5[1m]
  • Host (CLI / IDE / GitHub coding agent / other): IDE(VS Code 扩展)
  • Date (UTC): 2026-09-09

声明:本 PR 的代码由 AI 生成/AI 辅助完成。 提交者 TomJKS 不是本仓库的历史核心开发者(在 upstream/main 上提交数为 0),按项目约定在此显式说明。变更已经过人工复核与真实环境验证,详见 Verification。

Links

User 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

  • Bug fix
  • New feature
  • Performance / refactor
  • Docs
  • Other:

Issue facts

  • Actual behavior: Anthropic 语义的消耗日志把 prompt_tokens 写成 Claude 原始 input_tokens,即仅未命中缓存的 fresh token。一次命中 589,733 缓存读取、1,088 缓存写入的请求,prompt_tokens 只有 2。
  • Impact: ①展示——使用日志 Tokens 列、详情弹窗 "Input Tokens"、移动端卡片均按「总输入」读取该字段并把缓存作为其明细,Claude 行严重偏小且无法与其他模型横向比较;②统计——model/log.go:625 的 RPM/TPM 与 model/log.go:684SumUsedToken 都按 sum(prompt_tokens) + sum(completion_tokens) 聚合,Claude 请求静默漏掉全部缓存输入,缓存密集场景下 TPM 可低估两个数量级;③看板——LogQuotaDataTokenUsed 取同一表达式,导出同样偏小。计费金额不受影响。
  • Frequency: 确定性发生,100%。任何走 Anthropic 语义且使用 prompt caching 的请求皆然。
  • Evidence that the problem is in new-api rather than the client or upstream: 上游 usage 符合 Anthropic 规范且被 new-api 完整接收——同一条日志的 other.cache_tokens=589733other.cache_write_tokens=1088 均正确落库。更直接的内部证据:new-api 自身在别处已实现正确归一化,service/tiered_settle.go:45-48 用同一个 isClaudeUsageSemantic 标记计算 len = p + cr + cc5m + cc1hpkg/billingexpr/expr.md:354 将该公式写成规范。仓库既知道公式也拿到了数据,只是写日志时未套用。属纯内部不一致。
  • Applicable types and their fields (relay / billing / frontend / deployment; write "not applicable" otherwise): 详见所链 issue 的 Type-specific details 段(Relay/API、Billing、Frontend 已填;Deployment 不适用)。

Change

在写消耗日志的边界补齐 Anthropic 语义的 token 归一化,使 logs.prompt_tokens 对所有供应商统一表示「总输入 token」。

  1. textQuotaSummary 新增 PromptTokensExcludeCache 字段,取值为计费逻辑已在使用的同一判定 summary.IsClaudeUsageSemantic || legacyClaudeDerivedtext_quota.go:284)。复用同源条件,而非另写一份判断,避免日志与计费口径日后分叉。
  2. 新增 logInputTokens(summary):该标记为真时返回 PromptTokens + CacheTokens + cacheWriteTokensTotal(summary),否则原样返回 PromptTokens
  3. RecordConsumeLogPromptTokens 改为传 logInputTokens(summary)

为什么成立:

  • 计费金额不变。 summary.PromptTokens 本身未被修改,配额在 calculateTextQuotaSummary 内部早已算完并结算,本变更只改变此后传给 RecordConsumeLog 的值。原有配额断言 1488 / 1624 未做任何调整即通过。
  • 不重复累加。 OpenAI 语义下 PromptTokens 已含缓存,PromptTokensExcludeCache 为假,直接返回原值。
  • 缓存写入取的是实际计费量。 选用 cacheWriteTokensTotal 而非 summary.CacheCreationTokens:Claude 语义的计费分支实际按 max(总量, 5m+1h) 计价(text_quota.go:322-325remaining = max(总量-5m-1h, 0) 再加 5m、1h),cacheWriteTokensTotal 正是该量;且当上游只给 5m/1h 拆分而缺总量字段时仍然准确。
  • 既有特殊路径能正确还原。 OpenRouter 的 Claude 计费路径先从 prompt 中减去缓存(text_quota.go:273-281),加回后即为上游原始的含缓存总输入;legacyClaudeDerived 旧路径的 PromptTokens 同样不含缓存,故沿用同一条件一并归一化。
  • 与既有规范一致。 该公式与 expr.md:354 记载的 len = input_tokens + cache_read_tokens + cache_creation_tokens 相同,不引入新概念,只是把既有归一化补齐到缺失的那条路径。

Research

Duplicate / prior art

Docs and code

  • https://docs.newapi.ai/ : 无法访问(网络策略拦截,Unable to verify if domain ... is safe to fetch),未取得结论。
  • https://deepwiki.com/QuantumNous/new-api : 无法访问(同上),未取得结论。
  • README / repo docs: 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
  • Code paths and what they imply for this change:
    • relaykit/dto/billing_usage.go:298-327canonicalClaudeUsage()PromptTokens = InputTokens(fresh),同时另算含缓存的 InputTokens,并把缓存放入 PromptTokensDetails。含缓存总输入在这一层就已存在。
    • service/text_quota.go:258-262summary.PromptTokens 取 fresh 值。
    • service/text_quota.go:309-327 — 计费分支对 Anthropic 语义不减缓存、按各自倍率单独计价,证明计费侧对语义差异知情且正确。
    • service/tiered_settle.go:45-48 — 同一标记已在计费 len 上做正确归一化,与 expr.md:354 一致。
    • model/log.go:625model/log.go:684model/log.go:393 — 三处按「总输入」消费 prompt_tokens
    • web/.../common-logs-columns.tsx:703-735web/.../details-dialog.tsx:373-403 — 前端按「总输入」渲染并把缓存作为明细,前提对 OpenAI 语义成立、对 Anthropic 不成立。
    • 结论:写入方与所有消费方对该列的解释相互矛盾,修正点应在写入边界。

Alternatives considered

  • Option A(本 PR):在写日志边界归一化,使该列对所有供应商统一表示总输入。
  • Option B:保留列为 fresh 语义,改为在消费侧补偿——前端三处展示与 model/log.go 两处 SQL 聚合都读取并累加缓存字段。
  • Option C:不动该列,另加 input_tokens_total 之类的新列供 UI 与统计使用。
  • Why this approach: B 需要改动五个消费点(含两处 SQL 聚合,且 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

Path Why
service/text_quota.go 新增 PromptTokensExcludeCache 标记与 logInputTokens(),并把 RecordConsumeLogPromptTokens 改为归一化后的总输入
service/text_quota_test.go 新增 TestLogInputTokensNormalizesCacheAcrossSemantics;在既有的 Claude 语义与 legacyClaudeDerived 测试中补充归一化断言

Behavior

  • Before: Anthropic 语义记录写入 prompt_tokens = input_tokens(fresh)。示例记录写入 2,而同条记录的 other.cache_tokens=589733other.cache_write_tokens=1088。TPM/SumUsedToken/看板 TokenUsed 均漏掉缓存输入。
  • After: 同类记录写入 prompt_tokens = 5908232 + 589733 + 1088),与缓存明细自洽,并与 OpenAI 语义模型口径一致;TPM 与 token 统计包含缓存输入。OpenAI 语义记录数值不变。计费金额、quota 字段、预扣费与结算逻辑不变。
  • Explicit non-goals / leftover work:
    • 不追溯修正历史日志,仅影响变更后新产生的记录。存量数据需独立的回填迁移,本 PR 不含。
    • 不改动前端与 SQL 聚合(采用本方案后它们本已正确)。
    • 观察到一处相邻的潜在缺陷,本次刻意不修以保持单一聚焦:service/tiered_settle.go:47lencc5m + cc1h 加回缓存写入,而 Anthropic 语义下这两个值仅取自 ClaudeCacheCreation5m/1hTokens;若上游只给了 cache_creation_input_tokens 总量而未给 cache_creation 子对象,len 会漏掉该部分。建议另开条目。

Verification

  • Commands and results:
    • 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,与本变更无关。
  • Manual steps and observed result: 以本变更构建 Docker 镜像部署到实际服务器,通过 Claude Code 客户端向 POST /v1/messages 发起会命中缓存的连续对话(claude-opus-5claude-sonnet-5),在 /usage-logs 观察。六条记录逐条精确自洽:261,283 = 2 + 260,411 + 870678,781 = 2 + 677,478 + 1,301677,480 = 2 + 676,858 + 62072,230 = 2 + 29,717 + 42,511260,413 = 2 + 257,765 + 2,64672,037 = 2 + 29,261 + 42,774。TPM 从修复前的 0 变为 941,332,恰等于最近 60 秒两条记录之和 (678,781 + 178) + (261,283 + 1,090) = 941,332,分毫不差——这直接证明修复贯通到了 SumUsedQuota 的聚合。同一页面中切换容器前写入的最后一条仍为 2 / 2,568,构成同图前后对照。
  • UI: screenshot or recording (or why none): 有修复前后各一张 /usage-logs 截图(由报告者提供)。如需附在 PR 中请告知。
  • Tests added or updated, or why none: 新增 TestLogInputTokensNormalizesCacheAcrossSemantics(OpenAI 语义不累加、Anthropic 语义加回缓存读写含真实数据 2 → 590,823、仅有 5m/1h 拆分时按实际计费量加回、Anthropic 无缓存时不变);并在既有 TestCalculateTextQuotaSummaryUnifiedForClaudeSemanticTestCalculateTextQuotaSummaryHandlesLegacyClaudeDerivedOpenAIUsage 中补充断言(分别为 1150 与 4192),未新建测试文件。
  • Databases / providers / platforms exercised: postgres 15(Docker);Anthropic 原生渠道,claude-opus-5claude-sonnet-5;linux/amd64。
  • Not verified: MySQL、SQLite 未实测——本变更不含 schema、GORM tag、迁移或 SQL 变更,仅改变写入既有 prompt_tokens 列的数值,故三库行为应一致,但未逐一验证。OpenRouter Claude 计费路径、legacyClaudeDerived 旧路径、Claude←OpenAI 跨格式转换路径仅有单元测试覆盖,无真实上游实测。Bedrock、Vertex 等其他承载 Claude 的渠道类型未实测。历史日志回填未验证(本 PR 不涉及)。docs.newapi.aideepwiki.comgithub.com 在本工作环境无法访问,文档与重复排查依赖站外搜索摘要。

Risks

  • Failure modes: 若某条路径的 PromptTokens 实际已经含缓存却被判定为 Anthropic 语义,会导致重复累加、prompt_tokens 偏大。该判定沿用计费侧同源条件 IsClaudeUsageSemantic || legacyClaudeDerived,若它误判,现有计费也会同时算错,因此不引入新的误判面。OpenRouter 路径已在单元测试中覆盖往返一致性。
  • Billing / quota / auth impact: 计费与配额无影响。 summary.PromptTokens 未被改动,配额在写日志之前已计算并结算完毕;原有配额断言未做调整即通过。不涉及认证。唯一的口径变化是 logs.prompt_tokens 及由其派生的 RPM/TPM、SumUsedToken、看板 TokenUsed 对 Claude 请求开始包含缓存输入——这正是本 PR 的目的,但会造成修复前后同一模型的统计数字出现台阶,且与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案取向需要协调。
  • Follow-ups: ①历史日志回填迁移(可选,独立 PR);②tiered_settle.go:47len 在缺少 5m/1h 拆分时漏算缓存写入(见 Behavior 段);③与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案协调。

Scope check

  • Single focused change: yes — 仅修正写入消耗日志时的 token 归一化,两个文件,不含无关重构。
  • Secrets included: no
  • Out of scope (Coding Plan / reverse-engineered channel / third-party wrapper / Codex): no

Agent

  • Tool: Claude Code
  • Tool version: 未知(运行环境未向 agent 暴露版本号)
  • Model (full id): claude-opus-5[1m]
  • Host (CLI / IDE / GitHub coding agent / other): IDE(VS Code 扩展)
  • Date (UTC): 2026-09-09

声明:本 PR 的代码由 AI 生成/AI 辅助完成。 提交者 TomJKS 不是本仓库的历史核心开发者(在 upstream/main 上提交数为 0),按项目约定在此显式说明。变更已经过人工复核与真实环境验证,详见 Verification。

Links

User 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

  • Bug fix
  • New feature
  • Performance / refactor
  • Docs
  • Other:

Issue facts

  • Actual behavior: Anthropic 语义的消耗日志把 prompt_tokens 写成 Claude 原始 input_tokens,即仅未命中缓存的 fresh token。一次命中 589,733 缓存读取、1,088 缓存写入的请求,prompt_tokens 只有 2。
  • Impact: ①展示——使用日志 Tokens 列、详情弹窗 "Input Tokens"、移动端卡片均按「总输入」读取该字段并把缓存作为其明细,Claude 行严重偏小且无法与其他模型横向比较;②统计——model/log.go:625 的 RPM/TPM 与 model/log.go:684SumUsedToken 都按 sum(prompt_tokens) + sum(completion_tokens) 聚合,Claude 请求静默漏掉全部缓存输入,缓存密集场景下 TPM 可低估两个数量级;③看板——LogQuotaDataTokenUsed 取同一表达式,导出同样偏小。计费金额不受影响。
  • Frequency: 确定性发生,100%。任何走 Anthropic 语义且使用 prompt caching 的请求皆然。
  • Evidence that the problem is in new-api rather than the client or upstream: 上游 usage 符合 Anthropic 规范且被 new-api 完整接收——同一条日志的 other.cache_tokens=589733other.cache_write_tokens=1088 均正确落库。更直接的内部证据:new-api 自身在别处已实现正确归一化,service/tiered_settle.go:45-48 用同一个 isClaudeUsageSemantic 标记计算 len = p + cr + cc5m + cc1hpkg/billingexpr/expr.md:354 将该公式写成规范。仓库既知道公式也拿到了数据,只是写日志时未套用。属纯内部不一致。
  • Applicable types and their fields (relay / billing / frontend / deployment; write "not applicable" otherwise): 详见所链 issue 的 Type-specific details 段(Relay/API、Billing、Frontend 已填;Deployment 不适用)。

Change

在写消耗日志的边界补齐 Anthropic 语义的 token 归一化,使 logs.prompt_tokens 对所有供应商统一表示「总输入 token」。

  1. textQuotaSummary 新增 PromptTokensExcludeCache 字段,取值为计费逻辑已在使用的同一判定 summary.IsClaudeUsageSemantic || legacyClaudeDerivedtext_quota.go:284)。复用同源条件,而非另写一份判断,避免日志与计费口径日后分叉。
  2. 新增 logInputTokens(summary):该标记为真时返回 PromptTokens + CacheTokens + cacheWriteTokensTotal(summary),否则原样返回 PromptTokens
  3. RecordConsumeLogPromptTokens 改为传 logInputTokens(summary)

为什么成立:

  • 计费金额不变。 summary.PromptTokens 本身未被修改,配额在 calculateTextQuotaSummary 内部早已算完并结算,本变更只改变此后传给 RecordConsumeLog 的值。原有配额断言 1488 / 1624 未做任何调整即通过。
  • 不重复累加。 OpenAI 语义下 PromptTokens 已含缓存,PromptTokensExcludeCache 为假,直接返回原值。
  • 缓存写入取的是实际计费量。 选用 cacheWriteTokensTotal 而非 summary.CacheCreationTokens:Claude 语义的计费分支实际按 max(总量, 5m+1h) 计价(text_quota.go:322-325remaining = max(总量-5m-1h, 0) 再加 5m、1h),cacheWriteTokensTotal 正是该量;且当上游只给 5m/1h 拆分而缺总量字段时仍然准确。
  • 既有特殊路径能正确还原。 OpenRouter 的 Claude 计费路径先从 prompt 中减去缓存(text_quota.go:273-281),加回后即为上游原始的含缓存总输入;legacyClaudeDerived 旧路径的 PromptTokens 同样不含缓存,故沿用同一条件一并归一化。
  • 与既有规范一致。 该公式与 expr.md:354 记载的 len = input_tokens + cache_read_tokens + cache_creation_tokens 相同,不引入新概念,只是把既有归一化补齐到缺失的那条路径。

Research

Duplicate / prior art

Docs and code

  • https://docs.newapi.ai/ : 无法访问(网络策略拦截,Unable to verify if domain ... is safe to fetch),未取得结论。
  • https://deepwiki.com/QuantumNous/new-api : 无法访问(同上),未取得结论。
  • README / repo docs: 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
  • Code paths and what they imply for this change:
    • relaykit/dto/billing_usage.go:298-327canonicalClaudeUsage()PromptTokens = InputTokens(fresh),同时另算含缓存的 InputTokens,并把缓存放入 PromptTokensDetails。含缓存总输入在这一层就已存在。
    • service/text_quota.go:258-262summary.PromptTokens 取 fresh 值。
    • service/text_quota.go:309-327 — 计费分支对 Anthropic 语义不减缓存、按各自倍率单独计价,证明计费侧对语义差异知情且正确。
    • service/tiered_settle.go:45-48 — 同一标记已在计费 len 上做正确归一化,与 expr.md:354 一致。
    • model/log.go:625model/log.go:684model/log.go:393 — 三处按「总输入」消费 prompt_tokens
    • web/.../common-logs-columns.tsx:703-735web/.../details-dialog.tsx:373-403 — 前端按「总输入」渲染并把缓存作为明细,前提对 OpenAI 语义成立、对 Anthropic 不成立。
    • 结论:写入方与所有消费方对该列的解释相互矛盾,修正点应在写入边界。

Alternatives considered

  • Option A(本 PR):在写日志边界归一化,使该列对所有供应商统一表示总输入。
  • Option B:保留列为 fresh 语义,改为在消费侧补偿——前端三处展示与 model/log.go 两处 SQL 聚合都读取并累加缓存字段。
  • Option C:不动该列,另加 input_tokens_total 之类的新列供 UI 与统计使用。
  • Why this approach: B 需要改动五个消费点(含两处 SQL 聚合,且 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

Path Why
service/text_quota.go 新增 PromptTokensExcludeCache 标记与 logInputTokens(),并把 RecordConsumeLogPromptTokens 改为归一化后的总输入
service/text_quota_test.go 新增 TestLogInputTokensNormalizesCacheAcrossSemantics;在既有的 Claude 语义与 legacyClaudeDerived 测试中补充归一化断言

Behavior

  • Before: Anthropic 语义记录写入 prompt_tokens = input_tokens(fresh)。示例记录写入 2,而同条记录的 other.cache_tokens=589733other.cache_write_tokens=1088。TPM/SumUsedToken/看板 TokenUsed 均漏掉缓存输入。
  • After: 同类记录写入 prompt_tokens = 5908232 + 589733 + 1088),与缓存明细自洽,并与 OpenAI 语义模型口径一致;TPM 与 token 统计包含缓存输入。OpenAI 语义记录数值不变。计费金额、quota 字段、预扣费与结算逻辑不变。
  • Explicit non-goals / leftover work:
    • 不追溯修正历史日志,仅影响变更后新产生的记录。存量数据需独立的回填迁移,本 PR 不含。
    • 不改动前端与 SQL 聚合(采用本方案后它们本已正确)。
    • 观察到一处相邻的潜在缺陷,本次刻意不修以保持单一聚焦:service/tiered_settle.go:47lencc5m + cc1h 加回缓存写入,而 Anthropic 语义下这两个值仅取自 ClaudeCacheCreation5m/1hTokens;若上游只给了 cache_creation_input_tokens 总量而未给 cache_creation 子对象,len 会漏掉该部分。建议另开条目。

Verification

  • Commands and results:
    • 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,与本变更无关。
  • Manual steps and observed result: 以本变更构建 Docker 镜像部署到实际服务器,通过 Claude Code 客户端向 POST /v1/messages 发起会命中缓存的连续对话(claude-opus-5claude-sonnet-5),在 /usage-logs 观察。六条记录逐条精确自洽:261,283 = 2 + 260,411 + 870678,781 = 2 + 677,478 + 1,301677,480 = 2 + 676,858 + 62072,230 = 2 + 29,717 + 42,511260,413 = 2 + 257,765 + 2,64672,037 = 2 + 29,261 + 42,774。TPM 从修复前的 0 变为 941,332,恰等于最近 60 秒两条记录之和 (678,781 + 178) + (261,283 + 1,090) = 941,332,分毫不差——这直接证明修复贯通到了 SumUsedQuota 的聚合。同一页面中切换容器前写入的最后一条仍为 2 / 2,568,构成同图前后对照。
  • UI: screenshot or recording (or why none): 有修复前后各一张 /usage-logs 截图(由报告者提供)。如需附在 PR 中请告知。
  • Tests added or updated, or why none: 新增 TestLogInputTokensNormalizesCacheAcrossSemantics(OpenAI 语义不累加、Anthropic 语义加回缓存读写含真实数据 2 → 590,823、仅有 5m/1h 拆分时按实际计费量加回、Anthropic 无缓存时不变);并在既有 TestCalculateTextQuotaSummaryUnifiedForClaudeSemanticTestCalculateTextQuotaSummaryHandlesLegacyClaudeDerivedOpenAIUsage 中补充断言(分别为 1150 与 4192),未新建测试文件。
  • Databases / providers / platforms exercised: postgres 15(Docker);Anthropic 原生渠道,claude-opus-5claude-sonnet-5;linux/amd64。
  • Not verified: MySQL、SQLite 未实测——本变更不含 schema、GORM tag、迁移或 SQL 变更,仅改变写入既有 prompt_tokens 列的数值,故三库行为应一致,但未逐一验证。OpenRouter Claude 计费路径、legacyClaudeDerived 旧路径、Claude←OpenAI 跨格式转换路径仅有单元测试覆盖,无真实上游实测。Bedrock、Vertex 等其他承载 Claude 的渠道类型未实测。历史日志回填未验证(本 PR 不涉及)。docs.newapi.aideepwiki.comgithub.com 在本工作环境无法访问,文档与重复排查依赖站外搜索摘要。

Risks

  • Failure modes: 若某条路径的 PromptTokens 实际已经含缓存却被判定为 Anthropic 语义,会导致重复累加、prompt_tokens 偏大。该判定沿用计费侧同源条件 IsClaudeUsageSemantic || legacyClaudeDerived,若它误判,现有计费也会同时算错,因此不引入新的误判面。OpenRouter 路径已在单元测试中覆盖往返一致性。
  • Billing / quota / auth impact: 计费与配额无影响。 summary.PromptTokens 未被改动,配额在写日志之前已计算并结算完毕;原有配额断言未做调整即通过。不涉及认证。唯一的口径变化是 logs.prompt_tokens 及由其派生的 RPM/TPM、SumUsedToken、看板 TokenUsed 对 Claude 请求开始包含缓存输入——这正是本 PR 的目的,但会造成修复前后同一模型的统计数字出现台阶,且与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案取向需要协调。
  • Follow-ups: ①历史日志回填迁移(可选,独立 PR);②tiered_settle.go:47len 在缺少 5m/1h 拆分时漏算缓存写入(见 Behavior 段);③与 feat: data panel tokens 统计增加 cache_tokens 和 cache_creation_tokens #5069 的方案协调。

Scope check

  • Single focused change: yes — 仅修正写入消耗日志时的 token 归一化,两个文件,不含无关重构。
  • Secrets included: no
  • Out of scope (Coding Plan / reverse-engineered channel / third-party wrapper / Codex): no
image

Summary by CodeRabbit

  • Bug Fixes
    • Improved usage and quota accounting for requests with cached input.
    • Input token totals now consistently include fresh, cache-read, and cache-write usage where applicable.
    • Corrected handling across Anthropic-style, Claude-derived, and standard OpenAI usage formats.
    • Cache-excluded prompt counts are preserved while recorded consumption reflects the total billable input.

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>
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 6191713f-52bc-4186-9957-b65a80b6f4c3

📥 Commits

Reviewing files that changed from the base of the PR and between bdef117 and 0369461.

📒 Files selected for processing (2)
  • service/text_quota.go
  • service/text_quota_test.go

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

Claude-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.

Changes

Token log normalization

Layer / File(s) Summary
Classify cache-excluded prompt tokens
service/text_quota.go
The quota summary records whether prompt tokens exclude cached input for Anthropic-semantic and legacy Claude-derived OpenAI usage.
Normalize and validate logged input tokens
service/text_quota.go, service/text_quota_test.go
logInputTokens restores cache reads and normalized cache writes for flagged usage. Consume logs use this total. Tests cover OpenAI, Anthropic, and legacy Claude-derived usage.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 03694

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: calcium-ion

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: including Anthropic cache tokens in consume-log input totals.
Linked Issues check ✅ Passed The changes satisfy issue #7290 by adding cache-read and billed cache-write tokens to Anthropic consume-log input totals, preserving OpenAI behavior, and adding coverage for the relevant usage semanti…
Out of Scope Changes check ✅ Passed The changes are limited to the logging normalization logic and related unit tests. No unrelated code or behavior changes are present.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

⚠️ This pull request shows signs of AI-generated slop (ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.


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.

❤️ Share

A rabbit counts fresh tokens bright
Cache hops back into the sum tonight
Read and write join the trail
Claude logs now tell the tale
Bouncy numbers land just right

Comment @coderabbitai help to get the list of available commands.

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