OpenRouter 集成与实践

OpenRouter 集成与实践

推理 Token

13 分钟阅读

推理 Token

对于支持该功能的模型,OpenRouter API 可以返回推理 Token(Reasoning Tokens),也称为思考 Token(thinking tokens)。OpenRouter 会将各模型自定义推理 Token 用量的不同方式加以归一化,从而在不同模型服务提供商之间提供统一接口。

推理 Token 让你可以透明地查看模型采取的推理步骤。推理 Token 计为输出 Token,并按输出计费。

默认情况下,如果模型决定输出推理 Token,它们会包含在响应中。除非你选择排除,否则推理 Token 会出现在每条消息的 reasoning 字段中。

部分推理模型不会返回其推理 Token

虽然大多数模型和模型服务提供商会在响应中提供推理 Token,但有些(例如 OpenAI 的 o 系列)不会。

控制推理 Token

你可以在请求中使用 reasoning 参数来控制推理 Token:

{
  "model": "your-model",
  "messages": [],
  "reasoning": {
    // 以下二者择一(不可同时使用):
    "effort": "high", // 可以为 "max"、"xhigh"、"high"、"medium"、"low"、"minimal" 或 "none"(OpenAI 风格)
    "max_tokens": 2000, // 具体 Token 上限(Anthropic 风格)

    // 可选:默认为 false。所有模型都支持此项。
    "exclude": false, // 设为 true 可从响应中排除推理 Token

    // 或者使用默认参数启用推理:
    "enabled": true // 默认:从 `effort` 或 `max_tokens` 推断
  }
}

reasoning 配置对象汇总了跨不同模型控制推理强度的设置。请参阅下方各选项的 Note,了解哪些模型受支持,以及其他模型会如何表现。

发现各模型的推理选项

GET /api/v1/models 中的每个模型都可能包含一个 reasoning 对象,描述它接受哪些 effort 级别,以及推理是否强制:

{
  "id": "google/gemini-3.5-flash",
  "reasoning": {
    "supported_efforts": ["high", "medium", "low", "minimal"],
    "default_effort": "medium",
    "default_enabled": true,
    "mandatory": true
  }
}

在构建客户端界面时使用这些字段:

  • supported_efforts:将 effort 选择器过滤为这些值,按降序返回(最高在前)。为 null 时,接受所有网关 effort 值。省略时,该模型不暴露 effort 选择。
  • default_effort:启用推理时预选此 effort。映射到聊天请求中的 reasoning.effort。如果值为 "none",应将其视为“默认关闭推理”,而不是在用户显式打开推理时预选禁用。
  • default_enabled:用户尚未设置 reasoning.enabled 时的默认开/关状态。
  • supports_max_tokens:当存在且为 true 时,显示 Token 预算控件,并发送 reasoning.max_tokens 以替代(或同时发送)reasoning.effort。模型不支持按 Token 预算推理时省略。
  • mandatory:为 true 时,隐藏禁用控件,并且不要发送 effort: "none"——模型会拒绝它。

非推理模型和动态路由模型(openrouter/autoopenrouter/free)会省略 reasoning 字段。

推理的最大 Token 数

支持的模型

目前支持:

  • Gemini 思考(thinking)模型
  • Anthropic 推理模型(通过 reasoning.max_tokens 参数)

  • 部分 Alibaba Qwen 思考(thinking)模型(映射到 thinking_budget

对 Alibaba 而言,支持情况因模型而异——请查看各模型说明以确认 reasoning.max_tokens(通过 thinking_budget)是否可用。

对于支持推理 Token 分配的模型,你可以这样控制:

  • "max_tokens": 2000 - 直接指定用于推理的最大 Token 数

对于仅支持 reasoning.effort 的模型(见下文),max_tokens 值将用于确定 effort 级别。

推理强度级别

支持的模型

目前由 OpenAI 推理模型(o1 系列、o3 系列、GPT-5 系列)和 Grok 模型支持

  • "effort": "max" - 为推理分配最大比例的 Token(约为 max_tokens 的 95%)
  • "effort": "xhigh" - 与 max 相同的分配(约为 max_tokens 的 95%)
  • "effort": "high" - 为推理分配较大比例的 Token(约为 max_tokens 的 80%)
  • "effort": "medium" - 分配中等比例的 Token(约为 max_tokens 的 50%)
  • "effort": "low" - 分配较小比例的 Token(约为 max_tokens 的 20%)
  • "effort": "minimal" - 分配更小比例的 Token(约为 max_tokens 的 10%)
  • "effort": "none" - 完全禁用推理

对于仅支持 reasoning.max_tokens 的模型,effort 级别将按上述百分比设置。

排除推理 Token

如果你希望模型在内部使用推理,但不将其包含在响应中:

  • "exclude": true - 模型仍会使用推理,但不会在响应中返回

推理 Token 会出现在每条消息的 reasoning 字段中。

使用默认配置启用推理

要使用默认参数启用推理:

  • "enabled": true - 以 "medium" effort 级别启用推理,且不排除。

示例

推理 Token 的基本用法

import { OpenRouter } from '@openrouter/sdk';

const openRouter = new OpenRouter({
  apiKey: '<OPENROUTER_API_KEY>',
});

const response = await openRouter.chat.send({
  model: 'openai/o3-mini',
  messages: [
    {
      role: 'user',
      content: "How would you build the world's tallest skyscraper?",
    },
  ],
  reasoning: {
    effort: 'high',
  },
  stream: false,
});

console.log('REASONING:', response.choices[0].message.reasoning);
console.log('CONTENT:', response.choices[0].message.content);

使用最大 Token 数进行推理

对于支持直接 Token 分配的模型(如 Anthropic 模型),你可以指定用于推理的精确 Token 数:

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

response = client.chat.completions.create(
    model="~anthropic/claude-sonnet-latest",
    messages=[
        {"role": "user", "content": "What's the most efficient algorithm for sorting a large dataset?"}
    ],
    extra_body={
        "reasoning": {
            "max_tokens": 2000
        }
    },
)

msg = response.choices[0].message
print(getattr(msg, "reasoning", None))
print(getattr(msg, "content", None))

从响应中排除推理 Token

如果你希望模型在内部使用推理,但不将其包含在响应中:

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

response = client.chat.completions.create(
    model="deepseek/deepseek-r1",
    messages=[
        {"role": "user", "content": "Explain quantum computing in simple terms."}
    ],
    extra_body={
        "reasoning": {
            "effort": "high",
            "exclude": True
        }
    },
)

msg = response.choices[0].message
print(getattr(msg, "content", None))

高级用法:推理思维链

此示例展示如何在更复杂的工作流中使用推理 Token。它将一个模型的推理注入另一个模型,以提升其回复质量:

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

question = "Which is bigger: 9.11 or 9.9?"

def do_req(model: str, content: str, reasoning_config: dict | None = None):
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": content}],
        "stop": "</think>",
    }
    if reasoning_config:
        payload.update(reasoning_config)
    return client.chat.completions.create(**payload)

# 从能力较强的模型获取推理
content = f"{question} Please think this through, but don't output an answer"
reasoning_response = do_req("deepseek/deepseek-r1", content)
reasoning = getattr(reasoning_response.choices[0].message, "reasoning", "")

# 来测试一下!以下是未经处理的朴素回复:
simple_response = do_req("~openai/gpt-mini-latest", question)
print(getattr(simple_response.choices[0].message, "content", None))

# 以下是注入推理 Token 后的回复:
content = f"{question}. Here is some context to help you: {reasoning}"
smart_response = do_req("~openai/gpt-mini-latest", content)
print(getattr(smart_response.choices[0].message, "content", None))

保留推理

要在多轮之间保留推理上下文,你可以通过以下两种方式之一将其传回 API:

  1. message.reasoning(字符串):将纯文本推理作为 assistant 消息上的字符串字段传入
  2. message.reasoning_details(数组):传入完整的 reasoning_details 块

在处理返回特殊推理类型(例如加密或摘要)的模型时使用 reasoning_details——这会保留这些模型所需的完整结构。

对于只返回原始推理字符串的模型,你可以使用更简单的 reasoning 字段。你也可以使用 reasoning_content 作为别名——它与 reasoning 功能相同。

reasoning_details 功能在所有受支持的推理模型上工作方式相同。你可以在 OpenAI 推理模型(如 ~openai/gpt-latest)和 Anthropic 推理模型(如 ~anthropic/claude-sonnet-latest)之间轻松切换,而无需更改代码结构。

保留推理块对工具调用特别有用。当 Claude 这类模型调用工具时,它会暂停构建回复以等待外部信息。当工具结果返回时,模型会继续构建那份已有回复。因此在工具使用期间有必要保留推理块,原因有二:

推理连续性:推理块捕获了导致工具请求的逐步推理。当你提交工具结果时,包含原始推理可确保模型能从中断处继续推理。

上下文维持:虽然工具结果在 API 结构中表现为 user 消息,但它们属于连续的推理流。保留推理块可以在多次 API 调用之间维持这一概念流。

对推理模型很重要

在提供 reasoning_details 块时,连续推理块的整个序列必须与模型在 原始请求期间生成的输出匹配;你不能重新排列或修改这些块的顺序。

示例:使用 OpenRouter 和 Claude 保留推理块

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

# 定义一次工具并复用
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"}
            },
            "required": ["location"]
        }
    }
}]

# 带工具的第一次 API 调用
# 注意:你可以使用 '~openai/gpt-latest' 代替 '~anthropic/claude-sonnet-latest'——它们完全可互换
response = client.chat.completions.create(
    model="~anthropic/claude-sonnet-latest",
    messages=[
        {"role": "user", "content": "What's the weather like in Boston? Then recommend what to wear."}
    ],
    tools=tools,
    extra_body={"reasoning": {"max_tokens": 2000}}
)

# 提取带 reasoning_details 的 assistant 消息
message = response.choices[0].message

# 回传时保留完整的 reasoning_details
messages = [
    {"role": "user", "content": "What's the weather like in Boston? Then recommend what to wear."},
    {
        "role": "assistant",
        "content": message.content,
        "tool_calls": message.tool_calls,
        "reasoning_details": message.reasoning_details  # 原样回传
    },
    {
        "role": "tool",
        "tool_call_id": message.tool_calls[0].id,
        "content": '{"temperature": 45, "condition": "rainy", "humidity": 85}'
    }
]

# 第二次 API 调用——Claude 从中断处继续推理
response2 = client.chat.completions.create(
    model="~anthropic/claude-sonnet-latest",
    messages=messages,  # 包含保留的思考块
    tools=tools
)

关于思考加密、已屏蔽块和高级用例的更详细信息,见 Anthropic 关于扩展思考的文档

关于 OpenAI 推理模型的更多信息,见 OpenAI 的推理文档

推理上下文模式

当你在对话历史中回显推理条目时,可以使用 reasoning.context 参数控制模型可以访问哪些推理:

  • auto:模型使用其默认上下文模式。省略 reasoning.context 效果相同。
  • all_turns:模型可以引用输入中所有轮次的推理。适用于你希望模型基于先前思维链继续构建的多轮对话。
  • current_turn:模型仅使用当前轮次的推理。输入中先前的推理条目会被忽略。适用于你希望进行一次不受先前轮次影响的全新推理。
模型服务提供商支持

reasoning.context 仅由 OpenAI GPT-5.6 及更新版本支持。

在 Responses API 中使用手动状态管理(将输出条目作为输入回显)时,请将 reasoning.contextinclude 一起设置:

{
  "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..." }]
    },
    {
      "type": "message",
      "role": "assistant",
      "content": [{ "type": "output_text", "text": "The answer is 42." }]
    },
    { "role": "user", "content": "Now explain it differently." }
  ],
  "reasoning": {
    "effort": "high",
    "context": "all_turns"
  },
  "include": ["reasoning.encrypted_content"]
}
行为

context 的默认值可能因模型而异。如果你的用例需要特定行为,请显式设置它。

推理模式

对于提供专业推理变体的模型,reasoning.mode 参数控制由哪个变体处理你的请求:

  • standard:模型的标准推理行为。省略 reasoning.mode 效果相同。
  • pro:将请求路由到模型的专业变体,该变体对更难的问题使用更深的多遍推理。
模型服务提供商支持

reasoning.mode 仅在由 OpenAI 或 Azure 提供服务时,由 OpenAI GPT-5.6 及更新版本支持。Amazon Bedrock 的 OpenAI 兼容 API 接受该字段 但会静默忽略,因此 Bedrock 上不可用专业推理—— OpenRouter 只会将 pro 请求路由到会遵守模式选择的模型服务提供商。

对每个受支持的模型,在 OpenRouter 上请求专业模式有两种等价方式:

  1. 对标准模型发送 reasoning.mode: "pro"——OpenRouter 会将请求重新路由到匹配的 *-pro 模型。
  2. 直接调用 *-pro 模型列表项。
{
  "model": "~openai/gpt-latest",
  "input": "Prove that there are infinitely many primes.",
  "reasoning": {
    "mode": "pro"
  }
}
行为
  • mode 独立于 effort:你可以将 mode: "pro" 与任何受支持的 effort 级别组合。
  • 专业模式按与标准模式相同的每 Token 费率计费,但通常会消耗更多 Token。

推理详情 API 形态

当推理模型生成响应时,推理信息通过 reasoning_details 数组以标准化格式组织。本节记录流式和非流式响应中推理详情的 API 响应结构。

reasoning_details 数组结构

reasoning_details 字段包含推理详情对象的数组。数组中的每个对象表示一条特定的推理信息,并遵循三种可能类型之一。该数组的位置在流式和非流式响应之间有所不同。

  • 非流式响应reasoning_details 出现在 choices[].message.reasoning_details
  • 流式响应reasoning_details 出现在每个分块的 choices[].delta.reasoning_details

公共字段

所有推理详情对象共享这些公共字段:

  • id(string | null):推理详情的唯一标识符
  • format(string):推理详情的格式,可能的值为:
    • "unknown" - 未指定格式
    • "openai-responses-v1" - OpenAI 响应格式版本 1
    • "azure-openai-responses-v1" - Azure OpenAI 响应格式版本 1
    • "bedrock-openai-responses-v1" - Amazon Bedrock OpenAI 响应格式版本 1
    • "xai-responses-v1" - SpaceXAI 响应格式版本 1
    • "meta-responses-v1" - Meta 响应格式版本 1
    • "anthropic-claude-v1" - Anthropic Claude 格式版本 1(默认)
    • "google-gemini-v1" - Google Gemini 格式版本 1
  • index(number,可选):推理详情的顺序索引

推理详情类型

1. 摘要类型(reasoning.summary

包含推理过程的高层摘要:

{
  "type": "reasoning.summary",
  "summary": "The model analyzed the problem by first identifying key constraints, then evaluating possible solutions...",
  "id": "reasoning-summary-1",
  "format": "anthropic-claude-v1",
  "index": 0
}

2. 加密类型(reasoning.encrypted

包含可能被屏蔽或受保护的加密推理数据:

{
  "type": "reasoning.encrypted",
  "data": "eyJlbmNyeXB0ZWQiOiJ0cnVlIiwiY29udGVudCI6IltSRURBQ1RFRF0ifQ==",
  "id": "reasoning-encrypted-1",
  "format": "anthropic-claude-v1",
  "index": 1
}

3. 文本类型(reasoning.text

包含带有可选签名验证的原始文本推理:

{
  "type": "reasoning.text",
  "text": "Let me think through this step by step:\n1. First, I need to understand the user's question...",
  "signature": "sha256:abc123def456...",
  "id": "reasoning-text-1",
  "format": "anthropic-claude-v1",
  "index": 2
}

响应示例

非流式响应

在非流式响应中,reasoning_details 出现在消息中:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "Based on my analysis, I recommend the following approach...",
        "reasoning_details": [
          {
            "type": "reasoning.summary",
            "summary": "Analyzed the problem by breaking it into components",
            "id": "reasoning-summary-1",
            "format": "anthropic-claude-v1",
            "index": 0
          },
          {
            "type": "reasoning.text",
            "text": "Let me work through this systematically:\n1. First consideration...\n2. Second consideration...",
            "signature": null,
            "id": "reasoning-text-1",
            "format": "anthropic-claude-v1",
            "index": 1
          }
        ]
      }
    }
  ]
}

流式响应

在流式响应中,reasoning_details 会在推理生成时出现在 delta 分块中:

{
  "choices": [
    {
      "delta": {
        "reasoning_details": [
          {
            "type": "reasoning.text",
            "text": "Let me think about this step by step...",
            "signature": null,
            "id": "reasoning-text-1",
            "format": "anthropic-claude-v1",
            "index": 0
          }
        ]
      }
    }
  ]
}

流式行为说明:

  • 每个推理详情分块在可用时立即发送
  • 每个分块中的 reasoning_details 数组可能包含一个或多个推理对象
  • 对于加密推理,内容在流式响应中可能显示为 [REDACTED]
  • 完整推理序列通过按顺序拼接所有分块构建

旧版参数

为保持向后兼容,OpenRouter 仍支持以下旧版参数:

  • include_reasoning: true - 等价于 reasoning: {}
  • include_reasoning: false - 等价于 reasoning: { exclude: true }

不过,我们建议使用新的统一 reasoning 参数,以获得更好的控制和未来兼容性。

特定于模型服务提供商的推理实现

带推理 Token 的 Anthropic 模型

最新的 Claude 模型,例如 ~anthropic/claude-sonnet-latest,支持处理并返回推理 Token。

只能通过统一的 reasoning 参数,使用 effortmax_tokens 在 Anthropic 模型上启用推理。

注意: Anthropic 模型不再支持 :thinking 变体。请改用 reasoning 参数。

Anthropic 模型的推理最大 Token 数

在 Anthropic 模型上使用推理时:

  • 使用 reasoning.max_tokens 参数时,该值会直接使用,最小为 1024 Token。
  • 使用 reasoning.effort 参数时,budget_tokens 会基于 max_tokens 值计算。

推理 Token 分配上限为最多 128,000 Token,下限为最少 1024 Token。计算 budget_tokens 的公式为:budget_tokens = max(min(max_tokens * {effort_ratio}, 128000), 1024)

effort_ratio 对 max 和 xhigh effort 为 0.95,对 high effort 为 0.8,对 medium effort 为 0.5,对 low effort 为 0.2,对 minimal effort 为 0.1。

重要max_tokens 必须严格高于推理预算,以确保思考之后仍有 Token 可用于最终回复。

Token 用量与计费

推理 Token 作为输出 Token 计入计费。使用 推理 Token 会增加你的 Token 用量,但可以显著提升 模型回复的质量。

摘要思考

对于支持 thinking.display 字段的 Claude 模型,OpenRouter 默认使用摘要思考thinking.display: 'summarized'),因此在 Anthropic 默认省略思考的较新模型上,你不会丢失推理轨迹。

display 设置只控制响应中可见的思考轨迹。无论哪种方式,模型消耗的 Token 数量相同,用量按模型实际生成的 Token 计费。由于可见摘要是压缩过的,它包含的 Token 可能少于 usage 中报告的推理 Token 数。

如果你直接使用 Anthropic Messages API 格式,可以用 thinking.display 控制这一点:

  • 'summarized'(默认):返回推理的压缩摘要
  • 'omitted':不返回思考轨迹

示例:使用 Anthropic 推理 Token 进行流式传输

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

def chat_completion_with_reasoning(messages):
    response = client.chat.completions.create(
        model="~anthropic/claude-sonnet-latest",
        messages=messages,
        max_tokens=10000,
        extra_body={
            "reasoning": {
                "max_tokens": 8000
            }
        },
        stream=True
    )
    return response

for chunk in chat_completion_with_reasoning([
    {"role": "user", "content": "What's bigger, 9.9 or 9.11?"}
]):
    if hasattr(chunk.choices[0].delta, 'reasoning_details') and chunk.choices[0].delta.reasoning_details:
        print(f"REASONING_DETAILS: {chunk.choices[0].delta.reasoning_details}")
    elif getattr(chunk.choices[0].delta, 'content', None):
        print(f"CONTENT: {chunk.choices[0].delta.content}")

带思考级别的 Google Gemini 3 模型

Gemini 3 模型(例如 google/gemini-3.1-pro-previewgoogle/gemini-3-flash-preview)使用 Google 的 thinkingLevel API,而不是 Gemini 2.5 模型使用的较旧 thinkingBudget API。

OpenRouter 将 reasoning.effort 参数直接映射到 Google 的 thinkingLevel 值:

OpenRouter reasoning.effortGoogle thinkingLevel
"minimal""minimal"
"low""low"
"medium""medium"
"high""high"
"xhigh""high"(向下映射)

Token 消耗由 Google 决定

使用 thinkingLevel 时,实际消耗的推理 Token 数量由 Google 内部决定。各级别没有公开记录的 Token 上限断点。例如,设置 effort: "low" 可能因任务复杂度而产生数百个推理 Token。这是预期行为,反映了 Google 在内部实现思考级别的方式。

如果模型不支持特定的 effort 级别(例如,某模型只支持 lowhigh),OpenRouter 会将你请求的 effort 映射到最接近的受支持级别。

在 Gemini 3 上使用 max_tokens

如果你显式指定 reasoning.max_tokens,OpenRouter 会将其作为 thinkingBudget 透传给 Google 的 API。不过,对 Gemini 3 模型,Google 会在内部将该预算值映射到 thinkingLevel,因此你无法获得精确的 Token 控制。实际 Token 消耗仍由 Google 的 thinkingLevel 实现决定,而不是由你提供的具体预算值决定。

示例:在 Gemini 3 上使用思考级别

from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key="<OPENROUTER_API_KEY>",
)

response = client.chat.completions.create(
    model="google/gemini-3.1-pro-preview",
    messages=[
        {"role": "user", "content": "Explain the implications of quantum entanglement."}
    ],
    extra_body={
        "reasoning": {
            "effort": "low"  # 映射为 thinkingLevel: "low"
        }
    },
)

msg = response.choices[0].message
print(getattr(msg, "reasoning", None))
print(getattr(msg, "content", None))