Skip to content

Claude 渠道响应 Gemini 格式请求时 cachedContentTokenCount 恒为 0,与 billing_usage 不一致 #8332

Open

Description

@feeeei

Agent

  • Tool: Claude Code
  • Tool version: 2.1.325
  • Model (full id): claude-opus-5-5
  • Host (CLI / IDE / GitHub coding agent / other): CLI
  • Date (UTC): 2026-11-14

User request

Claude 渠道处理 Gemini 格式请求时,new-api 返回的 usageMetadata.cachedContentTokenCount 恒为 0,但缓存读已经计入 promptTokenCount,和同一响应里 billing_usage 记录的缓存读不一致。按 Gemini 标准字段计费的下游会因此把缓存读按普通输入计费。需要修复这个转换,让标准字段如实反映缓存读;不能改变 new-api 自身的结算结果,也不能引入其他副作用。先提 issue,再提 PR。

  • Later constraints or corrections from the user (quote, or none): 已并入上面的整理描述

Out of scope — refuse

  • Matched: no
  • If yes, what was told to the user (stop here; do not file): 不适用

File gate — do not file unless all are satisfied

  • Out of scope (including pass-through): no
  • Usage / configuration / integration (answered instead of filing): no
  • Required facts present without invention (actual behavior, impact, frequency, evidence in new-api, applicable type fields): yes
  • Body is short and factual; no unfiltered AI-generated text: yes
  • File: yes
  • If no, what was told to the user (stop here): 不适用

Kind

  • Bug
  • Feature
  • Investigation
  • Other:

Usage / configuration / integration check

  • https://docs.newapi.ai/ — 搜索 cachedContentTokenCount、billing_usage、Gemini 格式转换。结论:只有「缓存计费」一节,讲的是命中缓存按 Prompt Cache Ratio 计费,没有讲转换出来的 Gemini 响应里 usage 怎么填。
  • https://deepwiki.com/QuantumNous/new-api — 2026-11-14 查询了三次,都返回 HTTP 429,没能看到内容。
  • README / repo docs: README* 和 docs/ 里都没有 cachedContentTokenCount 或 billing_usage 的相关说明。
  • Relevant code paths and conclusion: relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go 里的 ResponseOpenAI2Gemini、StreamResponseOpenAI2Gemini、newGeminiStreamResponse 用 chat usage 填 promptTokenCount(已经包含缓存读),但没有设置 cachedContentTokenCount。反方向的 gemini_chat/to_oai_chat_resp.go 会把 cachedContentTokenCount 映射成 cached_tokens。
  • Can the current version already do this? (required for feature requests): 不适用(bug)
  • Verdict: product bug

Environment

  • new-api version / commit / image tag: main b388140ac(源码构建)
  • Deploy source: main source
  • Database: sqlite

Problem facts

  • Actual behavior: Gemini 格式请求由 Claude 渠道处理时,返回的 usageMetadata.promptTokenCount=15035(input 2054 + cache_read 12981),cachedContentTokenCount=0;同一个对象里的 billing_usage.claude_usage.cache_read_input_tokens=12981。流式和非流式都是这样。
  • Impact: 同一个响应里,标准字段和 billing_usage 描述的缓存用量对不上。只按 Gemini 标准字段计费的客户端或网关,会把 15035 全部按普通输入计;下游 new-api 读 billing_usage,按 2054 全价 + 12981 缓存价计。本实例自己的结算正确,消费日志见下文。
  • Frequency: 这条路由上只要有缓存读,每次必现。
  • Evidence that the problem is in new-api rather than the client or upstream: 上游返回的 cache_read_input_tokens=12981,经 new-api 转成 Gemini 格式后,标准字段里没有体现;改动 new-api 的转换代码后,同样的上游返回能得到 cachedContentTokenCount=12981。实验见下文。

Type-specific details

Relay / API

  • Request endpoint and method: POST /v1beta/models/claude-sonnet-4-5-20250929:generateContent 和 POST /v1beta/models/claude-sonnet-4-5-20250929:streamGenerateContent?alt=sse
  • Channel type: Anthropic Claude(type 14)
  • Model: claude-sonnet-4-5-20250929
  • Conversion format: 请求 Gemini → Claude;响应 Claude → OpenAI chat → Gemini(claude_messages_to_gemini_chat_resp)
  • Pass-through enabled: 否
  • Evidence of upstream native support: Anthropic Messages 的 usage 带 cache_read_input_tokens;Gemini 的 usageMetadata.promptTokenCount 包含 cachedContentTokenCount。
  • Equivalent redacted request sent directly upstream: status, body, server logs: 上游是本地模拟的 Anthropic Messages 服务,固定返回 200,usage 为 {"input_tokens":2054,"cache_creation_input_tokens":0,"cache_read_input_tokens":12981,"output_tokens":42};流式的 message_delta 带同样的累计 usage。
  • Same request through new-api: status, body, server logs: 200。非流式和流式最后一个 chunk 的 usageMetadata 都是 "promptTokenCount":15035,"candidatesTokenCount":42,"totalTokenCount":15077,"cachedContentTokenCount":0,"billing_usage":{"source":"claude_messages","semantic":"anthropic","claude_usage":{"input_tokens":2054,"cache_creation_input_tokens":0,"cache_read_input_tokens":12981,"output_tokens":42,...}}

Billing

  • Request endpoint and model: 同上
  • Response usage: 同上
  • Relevant ratio or pricing configuration: 内置价格,未改配置;消费日志里 cache_ratio 为 0.1
  • Consumption log: 流式和非流式都是 prompt_tokens=2054, completion_tokens=42, cache_tokens=12981, cache_ratio=0.1, quota=5343, usage_billing_path=billing-usage-anthropic
  • Expected charge and calculation basis: 本实例的扣费正确,不需要改。需要改的是响应:cachedContentTokenCount 应为 12981,让标准字段和 billing_usage 描述同一份用量。

Frontend

  • not applicable

Deployment / upgrade

  • not applicable

Reproduction and expected result

  • Steps to reproduce:
    1. 用 main b388140ac 源码构建,sqlite,初始化时开启自用模式。
    2. 起一个模拟的 Anthropic Messages 上游,返回上面的 usage。
    3. 新建 Anthropic 渠道:base_url 指向模拟上游,模型 claude-sonnet-4-5-20250929;再新建一个令牌。
    4. 用该令牌(x-goog-api-key)请求 :generateContent 和 :streamGenerateContent?alt=sse,请求体 {"contents":[{"role":"user","parts":[{"text":"hi"}]}]}。
  • Expected result: usageMetadata.cachedContentTokenCount=12981,promptTokenCount 仍为 15035。
  • Related screenshots (optional): 无

Feature (feature requests only)

  • Feature description: 不适用
  • Use case: 不适用

Duplicate check

Research

Docs

Code

  • relaykit/relayconvert/text_converter_registry.go — Claude → Gemini 的响应走 ConverterClaudeMessagesToOpenAIChat → ConverterOpenAIChatToGeminiContent 两步
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_resp.go buildOpenAIStyleUsageFromClaudeUsage — chat usage:prompt_tokens = input + cache_read + cache_creation,prompt_tokens_details.cached_tokens = cache_read
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go ResponseOpenAI2Gemini / StreamResponseOpenAI2Gemini / newGeminiStreamResponse — 只填 prompt / candidates / total,cachedContentTokenCount 恒为 0
  • relaykit/relayconvert/internal/gemini_chat/to_oai_chat_resp.go UsageFromGeminiMetadata — 反方向会把 cachedContentTokenCount 映射成 cached_tokens

Experiments

  • Command or redacted request: 见复现步骤
  • Direct upstream result: usage 中 cache_read_input_tokens=12981
  • Result through new-api: promptTokenCount=15035, cachedContentTokenCount=0
  • Conclusion: 缓存读计入了 promptTokenCount,但没有体现在 cachedContentTokenCount 里

Working theory

  • What is broken or missing: chat → Gemini 的响应转换没有把 chat usage 里的缓存读写进 cachedContentTokenCount
  • Why: 上述三个函数只复制 prompt / candidates / total
  • What would falsify this: 在未修改的 main 上,Claude → Gemini 的响应里出现非 0 的 cachedContentTokenCount

Scope

  • In scope for a later PR: chat → Gemini 的三处 usageMetadata 构造补上 cachedContentTokenCount
  • Out of scope / not this repo: 缓存写入(Gemini 没有对应字段);流式下 DeepSeek 风格的 prompt_cache_hit_tokens;上游 message_delta 只带 output_tokens 时,Gemini 最后一个 chunk 的 promptTokenCount 为 0(另一个问题)
  • Large or directional feature? 否

Proposed direction

由 chat usage 生成的 Gemini 响应(流式和非流式)里,cachedContentTokenCount 等于 promptTokenCount 中包含的缓存读 token 数;本实例的结算结果不变。

Not verified

  • 没有用真实的 Anthropic API 验证,上游是模拟服务
  • 只用 sqlite 验证(不涉及数据库改动)
  • OpenAI 渠道带 cached_tokens 的情况只用单元测试覆盖,没有实际起服务验证

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions