Files
PolyGateway/research-wiki/findings/2026-08-02-thinking-switch-and-reasoning-tokens.md
T
iomgaa 781579bf36 docs: record thinking-switch findings and capability design (issue #5, #6)
Findings: live-API measurements across MiniMax M3/M2.7/M2.5, qwen and
deepseek, plus a survey of how nine unified gateways model per-model
parameter divergence. Key facts: reasoning_effort is MiniMax's real
switch, M2.x reasoning is mandatory and cannot be disabled, and the
relay's local token-count fallback silently drops reasoning_tokens.

Design: keep the parameter shape at provider level, push capability
down to model level, split "unknown" / "unsupported" / "no opinion"
into three distinct values, and fail at assembly time when a model
cannot honour enable_thinking=False.
2026-08-02 05:42:05 -04:00

13 KiB
Raw Blame History

type, node_id, title, date
type node_id title date
finding finding:2026-08-02-thinking-switch-and-reasoning-tokens 推理开关与 reasoning_tokens: 供应商实测与业界做法 2026-08-02

推理开关与 reasoning_tokens:供应商实测与业界做法

类型:findings(事实基础)|日期:2026-08-02|来源:issue #5 / #6 调研 本文只记录已验证的事实与其证据,设计取舍见 designs/2026-08-02-thinking-capability-design.md。 本文的价值不限于这两条 issue——「同一语义、形态因模型而异」是本库长期要面对的一类问题,此处的结论与方法可复用。

1. 实验环境与方法

端点 自建 new-api 中转(newapi.iomgaa.online/v1OpenAI 兼容)
参数 temperature=0max_tokens=800、非流式为主,流式单独验证
题目 固定一道鸡兔同笼题,要求"只输出两个数字"
判据 首选 usage.completion_tokens_details.reasoning_tokens;该字段缺失时以 completion_tokens 兜底(关闭推理应 <30,推理中 >150
旁证 prompt_tokens 变化——注入生效的参数会改变模型侧模板,输入侧 token 数随之变化

方法论要点(可复用):判断一个参数"是否被上游真正消费",prompt_tokens 比输出长度可靠得多。输出长度受采样影响、方差大;而输入侧 token 数在同一请求体下是确定的,一旦变化就说明服务端换了模板,即参数确实到达了模型。本次三条关键结论全部由这个旁证锁定。

2. MiniMax:真开关是 reasoning_effort

2.1 M3 参数矩阵(非流式)

注入参数 prompt completion reasoning_tokens 判定
默认(不传) 194 4 无 ctd 不推理
reasoning_effort=none 194 10 无 ctd 不推理
reasoning_effort=minimal 207 129 123 推理
reasoning_effort=low 207 98 93 推理
reasoning_effort=medium 207 183 177 推理
reasoning_effort=high 207 158 142 推理
thinking={"type":"enabled"} 194 5 无 ctd 被静默丢弃
thinking={"type":"disabled"} 194 4 无 ctd 被静默丢弃
enable_thinking=true 194 5 无 ctd 被静默丢弃
enable_thinking=false 194 5 无 ctd 被静默丢弃

prompt_tokens 194→207 的 13 token 差是硬证据:reasoning_effort 被消费时模型注入了推理指令;另四种写法 prompt 恒为 194,参数根本没到达模型。

2.2 none 是被识别的真值,不是被当非法值丢弃

这是一个必须排除的伪解释——若中转把不认识的值直接丢掉,none 的表现会与"不传"无异,我们就会误以为它生效。

反证实验:传乱码值 reasoning_effort="xyzzy" → 返回 200、prompt=207、reasoning_tokens=180。未知值不但没被丢弃,反而开启了推理。 既然无效值的行为是"开推理",而 none 的行为是"不推理",两者不同,none 就必然是被识别的枚举值。

对照组:完全未知的 zzz_bogus_param=1 → prompt=194、无 ctd、无报错,确认未知才会被静默吞掉。

2.3 M2.7 / M2.5 的推理关不掉

三种参数形态各 3 次,completion_tokens 全部落在推理区间:

模型 默认(基线) reasoning_effort=none thinking:{disabled} thinking:{adaptive}
MiniMax-M2.7 372/283/285 275/301/248 310/190/219 299/269/246
MiniMax-M2.5 273//256 363/353/264 286/278/320 278/228/259

真关闭应为 510"23 12" 两个数字),实测无一接近。

三个独立外部来源与实测完全吻合

来源 M3 M2.7 / M2.5
OpenRouter /api/v1/modelsreasoning 描述符 mandatory: false mandatory: true
models.dev 的 reasoning_options [{"type":"toggle"}](二元可控) [](有推理但无控制手段)
MiniMax 官方仓库 issue #121 "M2.7 不允许关闭思考",无官方回复

结论:M2.x 的推理是模型固有属性,不是参数没找对。 任何库层改动都无法让它关闭;唯一诚实的做法是如实报错。

2.4 M3 的稳定性

同一请求打 10 次,(prompt_tokens, 是否上报 ctd) 全部为 (194, False),零跳变——enable_thinking=False 的修复可以建立在 M3 上。

3. qwen / deepseek:现有 profile 正确

模型 enable_thinking=false thinking:{disabled} reasoning_effort=none 现有 profile
qwen3.7-plus 关闭(compl 5 关闭 关闭 enable_thinking正确
deepseek-v4-pro 无效(仍推理 198 关闭(compl 3 关闭 thinking:{type}正确

两点附带事实:

  • reasoning_effort=none 在三家都有效,但这很可能是中转做了参数归一化。不可据此认为可以统一发一个参数——下游若直连供应商官方端点,该假设大概率不成立。翻译表必须一家一行。
  • qwen 的 strip_think_tags=True 已过时:实测 qwen 走 reasoning_content 字段,正文中无 <think> 标签。无害,但属于死代码。
  • 非流式没有 400DashScope 系"enable_thinking 仅支持流式"的限制经中转不存在。直连时是否仍存在未验证。

4. new-api 中转的三个行为(会污染观测)

这一节对任何经中转做实测的场景都适用,值得单独记住。

a)不校验参数值。 reasoning_effort="xyzzy" 返回 200 并当作"开推理"处理。意味着"靠上游报错兜底"的设计模式在此失效——Bedrock 式的"最小交集 + 裸逃生口"在这里等于零保护。

b)静默丢弃未知键。 默认路径是 struct round-tripConvertRequest 返回 struct 再 json.Marshal),未知键在第一次序列化就消失。new-api 有 per-channel 的 pass_through_body_enabled 开关可改变此行为。

(c)上游不返回 usage 时用本地 tokenizer 补算并整体替换。 补算出的 usage 只有三个标量,completion_tokens_details 为零值。这直接解释了实测中的双峰现象:

现象 解释
同一请求 10 次:prompt=74 者 6 次不上报 reasoning_tokensprompt=72 者 4 次上报,从不交叉 74 = 本地估算值,72 = 上游真值;补算路径吃掉了 ctd

这不是多渠道路由(MiniMax 侧为单渠道单密钥),也不是配置错误,而是上游偶发不返回 usage 时的兜底逻辑。中转日志中的 local_count_tokens 标志可现场确认。

对库的直接影响reasoning_tokens 缺失不能解释为"该源不上报这个字段",只能解释为"本次调用未上报"。下游若按前者建立统计口径会算错。

5. 业界如何建模"同一语义、形态因模型而异"

调研覆盖 LiteLLM、OpenRouter、models.dev、LangChain、Vercel AI SDK、AWS Bedrock Converse、Portkey、Helicone、LlamaIndex、new-api/one-api。

5.1 核心共识:形态按 provider,能力按 model

概念 变化频率 应归属层次
形态:参数长什么样(enable_thinking / thinking.type / reasoning_effort 协议方言,一个供应商数年不变 provider 级
能力:能否关闭、有几档、默认开不开 模型属性,同一供应商每代都变 model 级

注册单位的分布很能说明问题:LiteLLM2986 条目)、models.dev5949 条)、LangChain、OpenRouter(细到 endpoint)、Helicone 全部下沉到 model 级;仍停在 provider 级的只有 Portkey 与 LlamaIndex,而这两家恰是失败语义最差的两家(均静默丢弃)。二者相关不是偶然:注册单位不够细,就只能靠"表里没有 = 不发"来兜底,而这正是静默失效的成因。

5.2 失败语义的四种谱系

语义 代表 适用前提
默认报错 + 可配置降级开关 LiteLLMUnsupportedParamsError + drop_params 有 model 级能力表可依据
软降级 + 显式 warning 通道 Vercel AI SDK(丢弃参数并 push warnings[] 调用方愿意读 warning
静默忽略 + 可选路由过滤 OpenRouter(默认忽略;require_parameters:true 改为排除不支持的上游) 网关自己拥有路由权
硬失败(透传给上游报错) BedrockinferenceConfig 4 字段交集 + additionalModelRequestFields 裸透传) 上游会诚实报错

选型时先问"我的上游会不会诚实报错"。若不会(如本项目的中转),最后一种直接出局,静默类也不能选。

5.3 表会过期,这是公理

LiteLLM 有过真实事故(issue #27351gpt-5.1-mini 漏登记导致 temperature 被误拒)。它的应对是两种相反极性,值得直接借鉴:

  • opt-in 能力(用错会 400 或悄悄花钱):未登记 → 视作不支持 → 拒绝
  • opt-out 能力(多半支持,误拒代价大):未登记 → 放行 → 只有表里显式写 false 才拒

维护方式上,LiteLLM/models.dev 靠社区 PR + CI 校验,LangChain 靠"上游拉取 + 本地增补 + 代码生成"。对内部库而言唯一现实的答案是:谁实测出来谁登记,登记必须附实测证据与日期。

5.4 「布尔开关 → 多档旋钮」无语义共识

系统 effort → 预算的换算
LiteLLM 一组 2 的幂(1024/2048/4096/8192/16384),全部可用环境变量覆盖;gemini 各型号还另有分叉
OpenRouter max_tokens 的百分比(≈80%/50%/20%
Helicone 一律 max_tokens/2,完全不看档位
LangChain 明确不保证跨 provider 可比

唯一对齐的是"关"none / disabled / thinking:{type:"disabled"} / OpenRouter effort:"none" 语义一致。"开"那一端没有任何标准。

工程共识只有一条:这个映射必须是可覆盖的常量,不是可推导的公式。 业界所有人都在拍脑袋,区别只在拍完让不让调用方改。

5.5 Vercel AI SDK 的一处设计值得单记

它的推理档位枚举里有一个 'provider-default',与 'none'(明确关闭)严格区分。这与本库 enable_thinking 的三态(None 不干预 / True / False)是同一思想——"调用方不表态"必须是一个独立的值,不能与任何具体档位混同。本库这一点原本就做对了,应保持。

6. 附带发现(不属本次范围,建议另立 issue)

kimi-k3 拒绝 temperature=0:返回 400 invalid temperature: only 1 is supported(另有渠道回 only 0.6)。本库把 400 归入 RequestRejectedError——不重试、不换源。若下游统一下发 temperature=0,此类源会 100% 硬失败。这与本次两条 issue 同源:供应商能力差异未被建模

中转渠道可用性会波动:kimi 渠道在 429 后被中转下线,随后返回 404 Model not supported by any channel。任何依赖真实 API 的测试都必须容忍源不可用(跳过并给出明确原因),而不是失败。

7. 未能证实

  1. MiniMax 官方文档对 reasoning_effort 的一手定义:官方文档站三次抓取均失败。M2.x 关不掉有三处佐证,但官方原文未取得。另有二手来源称 MiniMax 原生开关是 thinking:{type:"adaptive"/"disabled"}——该说法已被本次实测证伪(M2.7/M2.5 上两种写法均无效),但"中转是否对 reasoning_effort 做了改写"仍未排除。直连官方端点复测可彻底澄清。
  2. qwen 直连 DashScope 时非流式 enable_thinking 是否仍报 400:仅验证了经中转的行为。
  3. new-api 走本地补算的确切触发条件:读到了补算分支与 local_count_tokens 标记,未逐条比对所有渠道类型。双峰现象与该解释高度吻合,但未在日志中直接验证。
  4. 能力表条目对非本次实测模型的正确性qwen / deepseek 只测了各一个型号,同系其他型号未验证。

8. 对后续开发的指导

  1. 判定参数是否生效,优先看 prompt_tokens 而非输出长度(§1)。
  2. 排除"无效值被静默丢弃"必须做反证实验:传一个乱码值,看它的行为是否与目标值不同(§2.2)。
  3. 经中转做的任何实测都要标注"经中转,直连未验证",并写进注释(§3、§7)。
  4. 新增供应商或模型前,先查 OpenRouter /api/v1/models 与 models.dev——它们的登记与本次实测 100% 吻合,可作为低成本预判,但不可作为运行时依赖。
  5. 能力表条目必须附实测证据与日期;表过期是必然事件,退化路径与漂移检测要一起设计(§5.3)。
  6. reasoning_tokens 缺失只能记 None,绝不可记 0(§4c)——"观测不到"与"没发生"是两件事。