三个 thinking 整流器分别在修什么:两个是事后补救,一个是事前改写

2026-08-10

在 cc-switch 的 src-tauri/src/proxy/ 下有三个文件名里带 thinking 的模块:thinking_rectifier.rs(722 行)、thinking_budget_rectifier.rs(365 行)、thinking_optimizer.rs(338 行)。名字挨在一起,很容易被读成「同一件事的三个档位」——比如轻度整流、中度整流、深度优化。

不是。它们的触发时机相反,默认开关相反,改写动作也不是同一类。

以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用

先把三者摆在一起看

模块行数什么时候动手默认开关作用范围
thinking_rectifier.rs722上游返回签名类错误之后开(request_thinking_signature: true我们核对的范围未覆盖,本文不下结论
thinking_budget_rectifier.rs365上游返回 budget 类错误之后开(request_thinking_budget: true我们核对的范围未覆盖,本文不下结论
thinking_optimizer.rs338请求发出之前关(OptimizerConfig::enabled 默认 false)仅 Bedrock provider

最后一列这两格留白不是排版偷懒:两个 rectifier 各自对哪些 provider 生效,我们这次核对的范围里没有落到确切依据,所以这一栏就空着,不靠推测填。第三行的「仅 Bedrock」则是有明确出处的,见下。

前两个的开关都在 RectifierConfig 里,五个字段 enabledrequest_thinking_signaturerequest_thinking_budgetrequest_media_fallbackrequest_media_heuristic 默认全为 true(src-tauri/src/proxy/types.rs:231-241)。第三个的开关在 OptimizerConfig,总开关默认 false、需用户手动启用,且只对 Bedrock provider(CLAUDE_CODE_USE_BEDROCK = "1")生效(types.rs:243-269)。

一句话记法:带 rectifier 的两个是「撞了墙再修」,叫 optimizer 的那个是「出门前先改」,而且默认不出手。

反直觉的那一处:它刻意不在请求前碰 thinking

这才是这三个模块里最值得看的一行。在转发管线的请求体处理段,模型映射做完之后紧接着的是:

// 与 CCH 对齐:请求前不做 thinking 主动改写(仅保留兼容入口)

注释原文见 src-tauri/src/proxy/forwarder.rs:1171-1172,紧跟它的那一行调用的是 normalize_thinking_type

一个能解析请求体、能看懂 thinking 字段结构、手里还握着两个专门处理 thinking 的整流模块的本地代理,在请求发出之前选择了什么都不主动改。所有 thinking 相关的改写,都被推到「上游先报错」之后才发生。

这跟大多数人对「代理层做兼容」的直觉是反的。直觉版本是:代理知道下游 CLI 和上游 API 的字段差异,那就在转发前统一规范化一遍,把已知的坑填上再发。cc-switch 走的是另一条:请求体尽量按原样送,只有上游明确以错误的形式告诉你「这个字段我不接受」,才有针对性地拆掉那一处再重试。

带来的后果也很直接:这两个整流器的触发全部依赖上游返回的错误文本,而不是状态码。 下面两节会看到,两个模块的判定函数拿到的入参都是上游错误文本,做小写化之后再逐条比对关键词。

签名整流器:七类命中加一条兜底

thinking_rectifier.rs 的模块自述是「自动修复 Anthropic API 中因签名校验失败导致的请求错误」(thinking_rectifier.rs:1-4)。判定函数里一共列了七类场景,每一类都在注释里附了错误文本示例:

  1. 同时含 invalid + signature + thinking + block(:47-53
  2. “thought signature … not valid”(:57-61
  3. “must start with a thinking block”(:65-67
  4. 同时含 expected + thinking/redacted_thinking + found + tool_use(:72-78)——四个条件缺一不可,tool_use 是必须命中的那一个
  5. signature + “field required”(:82-84
  6. signature + “extra inputs are not permitted”(:88-90
  7. thinking/redacted_thinking + “cannot be modified”(:94-98

第八条是兜底:错误文本里出现「非法请求」「illegal request」「invalid request」任意一种,也判定命中(:100-106)。这条兜底比前七条宽得多——前七条里除第 3 条是一整句短语命中外,其余都要求两到四个关键词同时出现,兜底只要一个词。

命中之后的整流动作是三件(:111-189):删掉 messages 里的 thinking 与 redacted_thinking block、把残留在非 thinking block 上的 signature 字段摘掉、特定条件下把顶层的 thinking 一并删除。

注意这里的语义边界:整流器做的是减法,它把上游不接受的东西拿掉再重试,而不是补一个「正确的签名」上去。签名本身是上游侧的产物,代理没有能力重新生成。

budget 整流器:三个写死的常量

thinking_budget_rectifier.rs 开头定义了三个常量(:9-16):

  • MAX_THINKING_BUDGET = 32000
  • MAX_TOKENS_VALUE = 64000
  • MIN_MAX_TOKENS_FOR_BUDGET = 32001

这三个是 Rust 的 const,不是数据库里的可配置项,也不在 RectifierConfig 那五个开关里——你能配的只有「这个整流器开不开」,配不了「整流成多少」。

触发条件比签名整流器严格:错误文本必须同时含有 budget_tokens 相关表述、thinking,以及 1024 这个下限约束的表述(:61-69)。三个条件缺一个都不触发。

命中后的动作是固定的一套(:75-122):把 thinking.type 置为 enabledbudget_tokens 置为 32000;如果原请求的 max_tokens 小于 32001,就把它设成 64000。有一个例外分支放在最前面——如果原请求的 thinking.type 已经是 "adaptive",直接返回不改写。

所以这个模块的行为是「一刀切到一个已知可行的组合」,而不是「按上游要求微调到刚好合规」。你如果原本用的是一个更小的 budget,整流之后它会被抬到 32000,这一点在读日志时别搞混。

Bedrock 优化器:唯一的事前改写,且默认不开

thinking_optimizer.rs 是三者里唯一在请求发出前动手的。它按模型名分三条路径(thinking_optimizer.rs:6-58):

  • skip:模型名里含 haiku,直接跳过,什么都不做。
  • adaptive:属于 adaptive-thinking 一档的模型,写入 thinking={"type":"adaptive"}output_config={"effort":"max"},并追加 beta 标记 context-1m-2025-08-07
  • legacy:其余模型,取 max_tokens(缺省按 16384 算)后令 budget_target = max_tokens - 1,并追加 beta 标记 interleaved-thinking-2025-05-14

legacy 路径那个 max_tokens - 1 值得单独说一句:这个值不是某个经验数,而是紧贴 max_tokens 上界取的。至于 budget_target 最终如何写入请求体,我们核对的范围未覆盖,本文不做推断。

还有一处工程细节在转发层,而不在优化器自己文件里:当 provider 是 Bedrock 且优化器开启时,thinking_optimizer::optimize 与随后的 cache_injector::inject 是在 body 的 clone 上做的,注释说明目的是避免优化过的字段泄漏到故障转移之后的非 Bedrock provider(forwarder.rs:450-464)。

这条约束和上一节连起来看就清楚了:优化器写进去的 output_config、beta 标记这些东西是给 Bedrock 准备的,一旦这次尝试失败、故障转移换到下一家,那家未必认这些字段。所以它必须只活在这一次尝试的请求体副本里。

三个模块与故障转移的两处耦合

第一处:整流重试标记每个 provider 独立持有。 在转发的 provider 循环里,signature / budget / media 三个整流重试标记是在循环体内部声明的(forwarder.rs:417-421),注释说明这样做是为了不让首家的标记短路后续 provider 的整流流程——首家 provider 整流过一次之后被 5xx 或超时击落时,下家仍然能自己走一遍整流流程,而不是因为「已经整流过了」被跳过。

第二处:熔断器里有一个 release_half_open_permit,用 CAS 循环释放半开态的探测名额(src-tauri/src/proxy/circuit_breaker.rs:335-356),注释指明的适用场景就是「整流器等不该计入健康度」的情况。也就是说,因为整流而产生的那一次重试,不该被记成 provider 的一次健康度样本。熔断器本身的状态机与阈值另有一篇专门在讲,这里只用到这一条。

你可以怎么自己核一遍

四个动作,都不需要运行任何东西:

wc -l src-tauri/src/proxy/thinking_rectifier.rs \
      src-tauri/src/proxy/thinking_budget_rectifier.rs \
      src-tauri/src/proxy/thinking_optimizer.rs
grep -n "const MAX_THINKING_BUDGET\|const MAX_TOKENS_VALUE\|const MIN_MAX_TOKENS_FOR_BUDGET" \
      src-tauri/src/proxy/thinking_budget_rectifier.rs
grep -n "请求前不做 thinking 主动改写" src-tauri/src/proxy/forwarder.rs
grep -n "enabled" src-tauri/src/proxy/types.rs | head -40

第一条确认三个文件的体量对不对得上;第二条把那三个常量的值和行号一次性拿到手;第三条落到本文的核心那一行;第四条用来把 RectifierConfig 默认全 true 与 OptimizerConfig 默认 false 这两处放在一起看。

判定「触发条件」时的读法也给一下:两个 rectifier 的判定函数入参都是错误文本,你要确认某个错误会不会触发,就把这条错误文本小写化之后,对着上面列的关键词组合逐条比一遍。签名整流器要看是否落进七类之一或那条兜底,budget 整流器要看三个条件是否同时满足。

什么情况说明不是这三个模块的问题

排查时最容易走弯路的是把不相干的现象挂到这三个模块头上。以下几种情况可以直接排除它们:

  • 错误文本里没有对应关键词。 两个 rectifier 都不看状态码,只看文本。上游返回的错误如果被中间层改写、包装成了另一种措辞,关键词匹配不上就不会触发。
  • 请求原本就是 thinking.type = "adaptive" budget 整流器在这种情况下明确返回不改写(thinking_budget_rectifier.rs:75-122)。
  • 模型名里含 haiku 且你在讨论优化器。 优化器的第一条路径就是直接跳过(thinking_optimizer.rs:6-58)。
  • provider 不是 Bedrock。 优化器整段与你无关,types.rs:243-269 把它的作用域限定在 Bedrock 一种上。
  • 优化器总开关没开。 默认就是关的,没手动启用过就不必怀疑它改了你的请求体。
  • 现象出现在请求发出之前。forwarder.rs:1171-1172 那条注释,请求前不做 thinking 主动改写,能改的只有默认关闭的优化器那一条路径。

至于「该把这些开关调成什么」——项目没有给通用值。RectifierConfig 那五项是布尔开关不是数值,budget 那三个数是代码常量不是配置项,优化器的总开关默认关闭本身就是一种表态。本文给出的全部是快照 c39c903(2026-08-10)里的源码默认配置,不是「你用起来会怎样」的保证,也不能拿来推算任何关于成功率或稳定性的结论。

收一收

三个文件名相似的模块,实际分工是这样的:两个 rectifier 站在响应之后,靠错误文本关键词命中来做减法式的补救,默认全开;一个 optimizer 站在请求之前做加法式的改写,默认全关且只服务一种 provider。中间那条把它们分开的线,就是 forwarder.rs:1171-1172 的那句「请求前不做 thinking 主动改写」。

读这一块源码时,先认清一个模块站在这条线的哪一侧,剩下的细节就都能归位了。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、 src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。

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