OpenRouter 推理 token 排查:思维链拿不到、用量算不清

2026-08-18

接入 OpenRouter 的推理模型时,最容易卡住的不是把请求发出去,而是发出去之后发现:想拿的思维链没有出现在你以为的位置上,或者拿到了,但 usage 里报的推理 token 数跟你眼睛看到的那段文本怎么算都对不上。这两件事都不是玄学,官方文档《Reasoning Tokens》与 Responses API 的《Reasoning》两页里,字段位置、类型枚举和几处「换个供应商行为就变」的说明都写得挺清楚,只是分散在不同段落,一次读不全就会踩。

下面按排查顺序走一遍。所有依据都来自这两页官方文档,该平台迭代频繁,字段与默认值随版本变动,以官方文档最新内容为准。

现象一:reasoning 是空的

先说文档给的基线:官方文档写明,如果模型决定输出推理 token,它们默认就包含在响应里,出现在每条 message 的 reasoning 字段中,除非你自己排除掉。所以拿不到,通常是下面几种情况之一。

怎么确认

按这个顺序查,四步之内基本能定位:

第一步,查请求体里有没有把它关掉。 reasoning.exclude 的文档默认值是 false(这是文档写明的默认值,随版本可能变动),置为 true 时模型照样在内部推理,只是不返回。另外还有一组遗留参数:文档在「Legacy Parameters」一节写明,include_reasoning: true 等价于 reasoning: {}include_reasoning: false 等价于 reasoning: { exclude: true }。老代码里如果留着一个 include_reasoning: false,它表达的意思跟你新写的 reasoning 配置就是相反的;两者同时出现时以哪个为准,官方文档没有说明这一点,所以别让它们共存。文档同时建议改用统一的 reasoning 参数。

第二步,查这个模型到底返不返。 文档有一条独立的 Note 明说:虽然大多数模型和供应商会在响应里提供推理 token,但有些不返回,文档举的例子是 OpenAI 的 o 系列。这一条是能力差异,不是配置问题,改参数没用。

第三步,查模型自己声明的推理能力。 文档写明,GET /api/v1/models 返回的每个模型可能包含一个 reasoning 对象,描述它接受哪些 effort 档位以及推理是否强制。这些字段的语义文档逐条给了:

字段文档写明的语义
supported_efforts该模型接受的 effort 取值,按 effort 从高到低返回;为 null 时接受网关的全部 effort 值;字段缺失表示该模型不暴露 effort 选择
default_effort启用推理时应预选的档位,对应请求里的 reasoning.effort
default_enabled用户没设 reasoning.enabled 时的默认开关状态
supports_max_tokens出现且为 true 时才该发 reasoning.max_tokens;模型不支持 token 预算式推理时该字段被省略
mandatorytrue 时模型会拒绝 effort: "none",不要发

这里有个排查上很实用的点:文档写明,非推理模型以及动态路由模型(openrouter/autoopenrouter/free)会省略 reasoning 字段。注意这句话的范围:它说的是模型列表里那个描述用的 reasoning 对象不存在,也就是你没法从列表里读出这个模型收哪些 effort 档位、推理是否强制;至于这类模型在响应里到底会不会带推理内容,官方文档没有说明这一点,别把两件事混成一件。实际的排查价值在于:模型列表里查不到 reasoning 描述时,你照着 supported_efforts 调档位这条路就走不通了,得换判断依据。

第四步,查你是在哪个响应形状里找的。 这是最常见的一种自找麻烦。文档明确区分了两处位置:非流式响应里,reasoning_detailschoices[].message.reasoning_details;流式响应里,它在每个 chunk 的 choices[].delta.reasoning_details。用流式却只在 message 上取,当然是空。

Responses API 的形状又是另一回事。它的 reasoning 不在 message 上,而是作为 output 数组里 type"reasoning" 的一项,带 idencrypted_contentsummary;流式时对应的事件类型是 response.reasoning.delta。在 Responses API 上找 message.reasoning 找不到,属于正常。

处置

确认了是哪一类,处置就直白了。配置类的把 exclude 与遗留的 include_reasoning 清干净;能力类的换模型或接受拿不到。

Anthropic 侧还有两条要单独记:文档写明 :thinking 变体对 Anthropic 模型已不再支持(no longer supported),要启用推理只能用统一的 reasoning 参数配 effortmax_tokens。另外,对支持 thinking.display 字段的 Claude 模型,OpenRouter 默认走摘要式思维链thinking.display: 'summarized'),文档自述这样做是为了在 Anthropic 默认省略 thinking 的新模型上不丢掉推理痕迹;直接用 Anthropic Messages API 格式时,该字段可取 'summarized'(默认)或 'omitted',后者不返回任何 thinking 痕迹。

处置后怎么验证

非流式先把每一项的类型和格式打出来。文档写明 reasoning_details 里的对象只会是三种类型之一,共有字段是 idformat、可选的 index

type内容字段文档描述
reasoning.summarysummary推理过程的高层摘要
reasoning.encrypteddata加密的推理数据,可能被脱敏或保护
reasoning.texttext、可选 signature原始文本推理,带可选签名校验

format 字段标的是这段推理来自哪种响应格式,文档列出的取值包括 unknownopenai-responses-v1azure-openai-responses-v1bedrock-openai-responses-v1xai-responses-v1meta-responses-v1anthropic-claude-v1(文档标为默认)、google-gemini-v1。做跨供应商的解析时,别按字符串猜,按 type 分支再看 format——平台上的供应商与端点随时在变,取值集合以官方文档最新内容为准。

流式验证要多注意一句:文档写明每个推理片段可用即发,一个 chunk 里的数组可能包含一个或多个推理对象,完整序列靠按序拼接所有 chunk 得到;而且对于加密推理,流式响应里内容可能显示为 [REDACTED]。看到 [REDACTED] 别当成 bug。

Responses API 侧则去看 output 里那一项,以及 usage.output_tokens_details.reasoning_tokens

什么情况说明不是这个原因

  • GET /api/v1/models 里该模型压根没有 reasoning 对象——文档写明非推理模型与动态路由模型会省略它,这说明你手上缺的是档位描述,不是请求配置写错了,继续调 effort 没有意义。
  • 能拿到 reasoning_details,但多轮对话里模型「忘了」上一轮的思路——这不是返回缺失,是回传与上下文模式的问题,见下一节。
  • 你给一个 mandatory: true 的模型发了 effort: "none" 被拒——这是参数非法,不是返回被吞。

现象二:多轮里推理断了

文档把回传方式写得很明确,只有两种:把纯文本推理作为 assistant 消息上的 message.reasoning 字符串传回,或者传回完整的 message.reasoning_details 数组。文档还提到 reasoning_contentreasoning 的别名,功能完全相同。涉及加密或摘要这类特殊推理类型的模型,要用 reasoning_details,因为它保留了这些模型需要的完整结构。

有一条 Note 是硬约束,值得单独抄出来:提供 reasoning_details 块时,连续推理块的整个序列必须与模型在原始请求中生成的输出一致,不能重排或修改。工具调用场景尤其要守这条——文档解释说,像 Claude 这类模型调用工具时是暂停了响应的构建去等外部信息,工具结果回来后它会接着构建同一个响应,所以原样带回推理块才能让它从断点继续。

还有一个参数容易被忽略:reasoning.context,控制模型能引用回传历史里的哪些推理,取值是 auto(默认行为,等同于不传)、all_turns(可引用输入中所有轮次的推理)、current_turn(只用当前轮,忽略先前的推理项)。文档带 Provider Support 提示:该参数只有 OpenAI GPT-5.6 及更新版本支持;另一条 Note 说 context 的默认值可能因模型而异,需要确定行为就显式设置。

{
  "model": "~openai/gpt-latest",
  "input": [
    { "role": "user", "content": "Solve this math problem step by step: ..." },
    {
      "type": "reasoning",
      "id": "rs_abc123",
      "encrypted_content": "...",
      "summary": [{ "type": "summary_text", "text": "Analyzed the equation..." }]
    },
    { "role": "user", "content": "Now explain it differently." }
  ],
  "reasoning": {
    "effort": "high",
    "context": "all_turns"
  },
  "include": ["reasoning.encrypted_content"]
}

上面这段按官方文档示例节选(原示例里还有一条 assistant message)。示例里的 model 值只是文档当时的写法,平台上有哪些模型随时在变,不要当清单用。

现象三:用量对不上

先把计费口径摆平:文档写明推理 token 按输出 token 计入用量。本文不涉及任何价格与额度数值。真正会让人算错的是下面三处。

摘要式思维链天生对不上。 文档在讲 thinking.display 时说得很直白:display 只控制响应里可见的思考痕迹,模型两种方式下消耗的 token 数一样,计费按模型实际生成的 token 走;而因为可见摘要是压缩过的,它包含的 token 可能少于 usage 里报告的推理 token 数。注意文档用的词是「可能少于」,它既没有说一定少,也没有给出偏差幅度。所以拿可见文本去反推用量本身就不成立,对不上不等于计量出了 bug——想核用量只能看 usage 里报告的值。

Gemini 3 那一档你控制不了精确值。 文档写明 Gemini 3 系列用 Google 的 thinkingLevel API,而不是 Gemini 2.5 用的 thinkingBudget;OpenRouter 把 reasoning.effort 直接映射过去,其中 xhigh 会被向下映射为 high,其余同名对应。关键在那条 Note:使用 thinkingLevel 时,实际消耗多少推理 token 由 Google 内部决定,各档位没有公开的 token 断点。就算你显式给了 reasoning.max_tokens,文档也说它会作为 thinkingBudget 透传,但 Google 内部仍会把它映射成一个 thinkingLevel,所以拿不到精确的 token 控制。另外,模型若不支持你请求的档位,OpenRouter 会映射到最接近的受支持档位——你以为发的是 A,实际生效的可能是 B。

Anthropic 侧 effort 与 max_tokens 是换算关系。 文档写明,用 reasoning.max_tokens 时该值直接使用;用 reasoning.effort 时,budget 由 max_tokens 按各档位对应的比例换算,并被夹在文档给出的上下限之间。这里我不抄具体比例和上下限数值,它们随版本可能调整,请直接看官方文档那一段公式。要记住的是那句加粗的限制:max_tokens 必须严格大于推理 budget,否则思考完就没有 token 留给最终回答了。

顺带一提 reasoning.mode:取值 standard(默认行为,等同不传)与 pro,后者把请求路由到模型的 pro 变体做更深的多轮推理。文档 Note 写明它同样只有 OpenAI GPT-5.6 及更新版本、且由 OpenAI 或 Azure 提供服务时支持;Amazon Bedrock 的 OpenAI 兼容 API 接受这个字段但会静默忽略,因此 Bedrock 上没有 pro 推理,OpenRouter 只把 pro 请求路由到会遵守 mode 选择的 provider。「接受但静默忽略」这种行为在排查时最难受——请求不报错,你却拿不到你以为的东西。文档另外提到 pro 模式与 effort 相互独立,可任意组合,且通常消耗更多 token。

一处口径差异

顺手记一条:《Reasoning Tokens》页的 JSON 注释里,effort 的取值写的是 maxxhighhighmediumlowminimalnone;而 Responses API 的《Reasoning》页那张表只列了 minimallowmediumhigh 四档。两页口径不一致,写代码前按你实际调用的那个端点的文档来,并结合 GET /api/v1/models 返回的 supported_efforts 做过滤。至于差异的成因,官方文档没有说明这一点。

用量这一侧,什么情况说明不是这个原因

  • 差值方向是「可见文本比报告值少」,且模型走的是摘要式思维链——这是文档写明的正常表现。
  • 你在 Gemini 3 上设了 max_tokens 却发现消耗不按它走——文档已明说该档不提供精确 token 控制。
  • 请求直接被拒而不是数值偏差——那是参数校验(比如给 mandatory: true 的模型发 none),不是计量问题。

Windows 侧的一点提醒

这两页文档给的都是 HTTP API 示例,没有命令行工具,所以不存在 CLI 差异。但官方的 cURL 示例是 POSIX shell 写法,用单引号包住整段 JSON:

curl -X POST https://openrouter.ai/api/v1/responses \
  -H "Authorization: Bearer YOUR_OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/o4-mini",
    "input": "What is the meaning of life?",
    "reasoning": {
      "effort": "high"
    },
    "max_output_tokens": 9000
  }'

以上为官方文档中的示例原样抄录,其中的模型值与 max_output_tokens 的取值都只是文档当时的示例,不是平台上限也不是推荐值,按你自己的需要设。在 Linux/macOS 的 bash/zsh 里可以直接粘。Windows 的 PowerShell 引号规则不同,单引号内的这段 JSON 不能照搬;把请求体存成文件再用 curl.exe 引用文件,或者干脆用文档给的 Python / TypeScript 示例,是更省事的路子——这一段是通用的命令行使用做法,不是 OpenRouter 官方文档的内容。密钥请用环境变量注入,不要写进脚本,示例里的 YOUR_OPENROUTER_API_KEY 换成 <YOUR_API_KEY> 对应的实际值来源。

以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。


本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。 该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。 该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单; 价格、额度与限流的具体数值请以官方定价页与用量说明为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。