Skip to content

fix(billing): correct cache token double-counting in Claude relay to non-Claude providers - #3258

Closed
147API wants to merge 5401 commits into
QuantumNous:mainfrom
147API:fix/billing-claude-relay-cache-double-counting
Closed

fix(billing): correct cache token double-counting in Claude relay to non-Claude providers#3258
147API wants to merge 5401 commits into
QuantumNous:mainfrom
147API:fix/billing-claude-relay-cache-double-counting

Conversation

@147API

@147API 147API commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Problem

When a Claude-format request (/v1/messages) is relayed to a non-Claude provider (e.g. Gemini, OpenAI), PostClaudeConsumeQuota incorrectly assumes Claude usage semantics for the returned usage data.

Claude API returns input_tokens that already excludes cached tokens — so no subtraction is needed.

Gemini / OpenAI / etc. return prompt_tokens that includes cached tokens — so cacheTokens must be subtracted from promptTokens before adding cacheTokens * cacheRatio, otherwise cached tokens get billed twice.

Billing breakdown (before fix)

Item Tokens How billed
promptTokens 16202 (includes 15802 cached) × modelRatio
cacheTokens 15802 × cacheRatio

The 15802 cached tokens are charged twice: once at full input price, once at cache price.

Billing breakdown (after fix)

Item Tokens How billed
promptTokens 400 (16202 − 15802) × modelRatio
cacheTokens 15802 × cacheRatio

Root Cause

PostClaudeConsumeQuota only subtracted cache tokens for ChannelTypeOpenRouter. The existing postConsumeQuota in relay/compatible_handler.go already handles this correctly using GetFinalRequestRelayFormat() == RelayFormatClaude to distinguish usage semantics — PostClaudeConsumeQuota was missing the same guard.

Fix

Lift the cache-token subtraction out of the OpenRouter-only block and apply it to all non-Claude-semantic usage, consistent with compatible_handler.go:

isClaudeUsageSemantic := relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude
if !isClaudeUsageSemantic {
    promptTokens -= cacheTokens
    promptTokens -= cacheCreationTokens
}

The OpenRouter-specific cache-creation estimation logic is preserved as-is (it runs before the subtraction).

Affected Scenarios

Any request that:

  1. Enters via the Claude Messages endpoint (/v1/messages)
  2. Is relayed to a non-Claude provider (Gemini, OpenAI, Azure, DeepSeek, etc.)
  3. Has cached tokens returned in the response usage

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Cached tokens are no longer deducted from prompt token totals for Claude-format final requests.
    • Other formats continue to deduct cached tokens from prompt totals.
    • Cache-creation tokens still count toward quota as before.

MUTED64 and others added 30 commits February 6, 2026 21:22
fix: /v1/chat/completions -> /v1/responses json_schema
将散落在多个文件中的预扣费/结算/退款逻辑抽象为统一的 BillingSession 生命周期管理:

- 新增 BillingSettler 接口 (relay/common/billing.go) 避免循环引用
- 新增 FundingSource 接口 + WalletFunding / SubscriptionFunding 实现 (service/funding_source.go)
- 新增 BillingSession 封装预扣/结算/退款原子操作 (service/billing_session.go)
- 新增 SettleBilling 统一结算辅助函数,替换各 handler 中的 quotaDelta 模式
- 重写 PreConsumeBilling 为 BillingSession 工厂入口
- controller/relay.go 退款守卫改用 BillingSession.Refund()

修复的 Bug:
- 令牌额度泄漏:PreConsumeTokenQuota 成功但 DecreaseUserQuota 失败时未回滚
- 订阅退款遗漏:FinalPreConsumedQuota=0 但 SubscriptionPreConsumed>0 时跳过退款
- 订阅多扣费:subConsume 强制为 1 但 FinalPreConsumedQuota 不同步
- 退款路径不统一:钱包/订阅退款逻辑现统一由 FundingSource.Refund 分派
- Settle 部分失败保护:新增 fundingSettled 标记,资金来源提交后
  令牌调整失败不再导致 Refund 误退已结算的资金
- 订阅多扣费修复:trySubscription 传 subConsume 而非 preConsumedQuota
  给 preConsume,保证三者(amount/preConsume/FinalPreConsumedQuota)一致
- 令牌回滚错误记录:preConsume 中 funding 失败时令牌回滚错误不再丢弃
- 移除钱包路径死代码:用户额度不足的 strings.Contains 匹配不可能命中
- WalletFunding.Refund 不重试:IncreaseUserQuota 非幂等,重试会多退
…e recharge card tabs

- Defaulting to subscriptions when available and avoiding initial flash when no plans exist.
- Adjust the wide-screen layout to place wallet and invite sections side by side, simplify the subscription header and controls, and add padding to prevent card borders from clipping.
- Update related i18n strings by adding the new tab label and removing the obsolete subscription blurb.
…iption-card-when-no-plans

✨ refactor(wallet): Top-up layout to embed subscription plans into the recharge card tabs
…-session

refactor: 抽象统一计费会话 BillingSession
Add a lightweight active-subscription check to skip subscription pre-consume when none exist, reducing unnecessary transactions and locks. In the subscription UI, disable subscription-first options when no active plan is available, show the effective fallback to wallet with a clear notice, and distinguish “invalidated” from “expired” states. Update i18n strings across supported locales to reflect the new messages and status labels.
Aligns the error variable types in the subscription-first path so that quota fallback checks use the correct NewAPIError.
This prevents build failures and preserves the intended wallet fallback when subscription pre-consume returns an insufficient quota error.
Routes quota alerts through a subscription-specific check when billing from subscriptions, preventing wallet-based thresholds from triggering false warnings.
Updates the notification settings description and localization keys to clarify that both wallet and subscription balances are monitored.
…n-quota-notify

🔔 feat: Add subscription-aware quota notifications and update UI copy
…-preference-fallback

✨ chore: Improve subscription billing fallback and UI states
…tumNous#2881)

当上游为 AWS Bedrock 时,message_delta 的 usage 可能缺少 input_tokens、
cache_creation_input_tokens、cache_read_input_tokens 等字段,导致与原生
Anthropic 格式不一致。从 message_start 积累的 claudeInfo 中补全这些字段后
重新序列化,确保客户端收到一致的 usage 格式。
Modified the formatUserLogs function to include a startIdx parameter, allowing for more flexible log ID assignment. Updated calls to this function in GetLogByTokenId and GetUserLogs to pass the appropriate starting index.
feat: add Codex channel disclaimer (i18n, OpenAI terms)
feat: Force beta=true parameter for Anthropic channel
feat(oauth): implement custom OAuth provider
fix: Claude stream block index/type transitions
fix: add paragraph breaks between reasoning summary chunks
# Conflicts:
#	service/openaicompat/chat_to_responses.go
…t-stream

feat: channel test with stream=true
…fo-input-token

fix: 使用openai兼容接口调用部分渠道在最终端点为claude原生端点下还是走了openai扣减input_token的逻辑
somnifex and others added 22 commits March 7, 2026 14:10
为渠道参数覆盖可视化规则提供拖拽排序支持
…4f8a4248b0ab3b03ba703796ea3

fix: kling risk fail return openAIVideo error
…ride-beta-header-append

feat:support $keep_only_declared and deduped $append for header override
chore: update model lists for frequently used channels
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9eadeae-7aa4-466c-ab73-86ec52af7ad1

📥 Commits

Reviewing files that changed from the base of the PR and between ecd7203 and 2370be6.

📒 Files selected for processing (1)
  • service/quota.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • service/quota.go

Walkthrough

The PR modifies quota consumption in service/quota.go: subtraction of cacheTokens from promptTokens is now skipped when the final relay format is Claude (isClaudeUsageSemantic). cacheCreationTokens subtraction remains unchanged; non-Claude paths still deduct cached tokens. Small control-flow reorder and clarifying comment added.

Changes

Cohort / File(s) Summary
Claude Quota Caching Logic
service/quota.go
Move cache-token subtraction out of OpenRouter-specific block and guard it with isClaudeUsageSemantic; skip subtracting cacheTokens for Claude-format final requests while retaining cacheCreationTokens deduction. Add explanatory comment and minor control-flow reordering.

Sequence Diagram(s)

(Skipped — change is a small internal control-flow tweak, not a multi-component feature requiring a sequence diagram.)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • Calcium-Ion

Poem

🐰 I hopped through quotas, nibbling each line,
Claude gets a pause where cached crumbs align.
Creation crumbs stay counted, neat and bright,
While cached ones rest softly out of sight.
Tokens tiptoe onward into the night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary fix: correcting a billing issue where cached tokens were double-counted when Claude-format requests were relayed to non-Claude providers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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

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

@147API
147API force-pushed the fix/billing-claude-relay-cache-double-counting branch from 48ef46b to ecd7203 Compare March 14, 2026 17:11
…non-Claude providers

PostClaudeConsumeQuota assumed Claude usage semantics (input_tokens
excludes cached tokens) for all providers. When a Claude-format request
(/v1/messages) is relayed to a non-Claude provider (e.g. Gemini, OpenAI),
the upstream returns promptTokens that already include cached tokens.
Without subtracting them, cache tokens were billed twice: once as part
of promptTokens and again as cacheTokens with cacheRatio.

The fix aligns with the existing logic in compatible_handler.go's
postConsumeQuota: check GetFinalRequestRelayFormat() and only subtract
cached tokens from promptTokens when the usage follows non-Claude
semantics. The OpenRouter-specific cache creation estimation logic is
preserved as-is.

---

修复了 PostClaudeConsumeQuota 中缓存 token 重复计费的问题。

当 Claude 格式请求(/v1/messages)被转发到非 Claude 渠道(如 Gemini、OpenAI 等)时,
上游返回的 promptTokens 已包含缓存 token。由于未将缓存 token 从 promptTokens 中减去,
缓存 token 被计费两次:一次作为 promptTokens 的一部分,另一次按 cacheRatio 单独计费。

修复方式与 compatible_handler.go 中 postConsumeQuota 的现有逻辑保持一致:
通过 GetFinalRequestRelayFormat() 判断 usage 语义,仅在非 Claude 语义时
才从 promptTokens 中减去缓存 token。OpenRouter 特有的缓存创建 token 估算逻辑保持不变。
@147API
147API force-pushed the fix/billing-claude-relay-cache-double-counting branch from ecd7203 to 2370be6 Compare March 14, 2026 17:13
@147API

147API commented Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

@seefs001 @Calcium-Ion Could you please take a look at this fix when you have time? Thank you!

@147API

147API commented Mar 17, 2026

Copy link
Copy Markdown
Contributor Author

问题场景是:claude code中调用gemini时出现的,缓存token与输入token重复计费

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.