feat(proxy): thinking effort 冲突整流器——兼容 DeepSeek/MiMo 子 agent 思考关闭报错 (#1257)#1269
Conversation
When Anthropic-compatible providers like DeepSeek or MiMo receive a request with thinking disabled but reasoning_effort or output_config.effort set, they return a 400 error. This rectifier detects the specific error message, strips the conflicting effort fields while preserving the disabled thinking state, and retries the request once against the same provider. Adds a new system setting enableThinkingEffortConflictRectifier (enabled by default) with full UI, API, and database migration support to control this behavior.
📝 WalkthroughWalkthrough该PR实现了完整的 thinking effort 冲突自动整流与重试:当 Anthropic 兼容供应商因 thinking disabled 与 reasoning_effort 并存返回 400 时,系统可按配置移除冲突字段并对同一提供方重试,变更涵盖数据库、API/验证、缓存与仓库、核心 rectifier、Proxy 集成、UI 与测试。 ChangesThinking Effort 冲突整流器完整实现
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces the 'Thinking Effort Conflict Rectifier', a feature designed to resolve 400 Bad Request errors from strict Anthropic-compatible providers (such as DeepSeek or MiMo) when thinking is disabled but reasoning effort configurations are still present in the payload. The changes include database schema updates, UI settings, localization, and proxy forwarder integration to strip conflicting effort fields and retry requests. Feedback on the changes highlights a critical database fallback compatibility issue in src/repository/system-config.ts that could cause errors during rolling updates before migrations run, as well as a recommendation to refine the rectifier logic to selectively delete the effort field rather than the entire output_config object to ensure future compatibility.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, | ||
| enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, | ||
| enableThinkingBudgetRectifier: systemSettings.enableThinkingBudgetRectifier, | ||
| enableThinkingEffortConflictRectifier: systemSettings.enableThinkingEffortConflictRectifier, |
There was a problem hiding this comment.
在 getSystemSettings 中,新引入的 enableThinkingEffortConflictRectifier 字段被直接加入到了 selectionWithoutHighConcurrencyMode 中。
然而,在 selectSettingsRow 的数据库降级兼容链(fallback chain)中,最外层的降级(第 309 行左右)仍然是针对上一次新增的 billHedgeLosers 进行的:
const { billHedgeLosers: _omitBillHedgeLosers, ...selectionWithoutBillHedgeLosers } = fullSelection;由于 fullSelection 包含了 selectionWithoutHighConcurrencyMode,如果数据库尚未执行本次迁移(例如在滚动更新或尚未运行 db:generate 的环境中),查询 enableThinkingEffortConflictRectifier 会直接报错。此时降级链会尝试移除 billHedgeLosers,但由于新字段 enableThinkingEffortConflictRectifier 依然存在于 selectionWithoutBillHedgeLosers 中,后续的所有降级尝试都将持续报错,最终一路溃退到最底层的 minimalSelection。
这会导致在数据库迁移完成前,所有中间版本引入的系统设置(如高并发模式、IP 提取配置等)全部失效。
建议:
在 selectSettingsRow 的降级链最顶端,优先剥离本次新增的 enableThinkingEffortConflictRectifier 字段,然后再依次向下剥离旧字段。例如:
// 最新降级:移除最近新增的 enableThinkingEffortConflictRectifier 列
const { enableThinkingEffortConflictRectifier: _omitThinkingEffort, ...selectionWithoutThinkingEffort } = fullSelection;
// 随后在 selectionWithoutThinkingEffort 的基础上继续剥离旧列...(注:由于该函数未在本次 Diff 的修改范围内,无法直接提供行内代码建议,请手动在 selectSettingsRow 中进行调整。)
| export function rectifyThinkingEffortConflict( | ||
| message: Record<string, unknown> | ||
| ): ThinkingEffortConflictRectifierResult { | ||
| const thinking = message.thinking; | ||
| const thinkingType = | ||
| thinking && typeof thinking === "object" && !Array.isArray(thinking) | ||
| ? typeof (thinking as Record<string, unknown>).type === "string" | ||
| ? ((thinking as Record<string, unknown>).type as string) | ||
| : null | ||
| : null; | ||
|
|
||
| const result: ThinkingEffortConflictRectifierResult = { | ||
| applied: false, | ||
| removedOutputConfig: false, | ||
| removedReasoningEffort: false, | ||
| thinkingType, | ||
| effort: null, | ||
| }; | ||
|
|
||
| // thinking 显式启用(enabled/adaptive 等)时不属于该冲突,保持原样 | ||
| const thinkingDisabled = thinkingType === null || thinkingType === "disabled"; | ||
| if (!thinkingDisabled) { | ||
| return result; | ||
| } | ||
|
|
||
| const outputConfig = message.output_config; | ||
| const outputConfigEffort = | ||
| outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig) | ||
| ? (outputConfig as Record<string, unknown>).effort | ||
| : undefined; | ||
|
|
||
| if (outputConfigEffort !== undefined) { | ||
| result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null; | ||
| delete message.output_config; | ||
| result.removedOutputConfig = true; | ||
| result.applied = true; | ||
| } | ||
|
|
||
| const reasoningEffort = message.reasoning_effort; | ||
| if (reasoningEffort !== undefined) { | ||
| if (result.effort === null && typeof reasoningEffort === "string") { | ||
| result.effort = reasoningEffort; | ||
| } | ||
| delete message.reasoning_effort; | ||
| result.removedReasoningEffort = true; | ||
| result.applied = true; | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
在整流 output_config 时,直接使用 delete message.output_config 会将整个 output_config 对象删除。如果未来 Anthropic 或兼容上游在 output_config 中引入了除 effort 之外的其他配置字段,这种暴力的删除方式会导致其他有用的配置字段丢失。
此外,为了增强代码的健壮性,建议在函数入口处添加对 message 的防御性空值与类型校验,防止在运行时因传入非对象而抛出 TypeError。
建议对该函数进行重构:
- 在入口处添加对
message的防御性校验。 - 仅删除
output_config中的effort属性。如果删除后output_config变为空对象,再将其从message中彻底删除。这样可以保证更好的前向兼容性和鲁棒性。
export function rectifyThinkingEffortConflict(
message: Record<string, unknown>
): ThinkingEffortConflictRectifierResult {
const result: ThinkingEffortConflictRectifierResult = {
applied: false,
removedOutputConfig: false,
removedReasoningEffort: false,
thinkingType: null,
effort: null,
};
if (!message || typeof message !== "object") {
return result;
}
const thinking = message.thinking;
const thinkingType =
thinking && typeof thinking === "object" && !Array.isArray(thinking)
? typeof (thinking as Record<string, unknown>).type === "string"
? ((thinking as Record<string, unknown>).type as string)
: null
: null;
result.thinkingType = thinkingType;
// thinking 显式启用(enabled/adaptive 等)时不属于该冲突,保持原样
const thinkingDisabled = thinkingType === null || thinkingType === "disabled";
if (!thinkingDisabled) {
return result;
}
const outputConfig = message.output_config;
const outputConfigEffort =
outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig)
? (outputConfig as Record<string, unknown>).effort
: undefined;
if (outputConfigEffort !== undefined) {
result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null;
const outputConfigObj = { ...(outputConfig as Record<string, unknown>) };
delete outputConfigObj.effort;
if (Object.keys(outputConfigObj).length === 0) {
delete message.output_config;
} else {
message.output_config = outputConfigObj;
}
result.removedOutputConfig = true;
result.applied = true;
}
const reasoningEffort = message.reasoning_effort;
if (reasoningEffort !== undefined) {
if (result.effort === null && typeof reasoningEffort === "string") {
result.effort = reasoningEffort;
}
delete message.reasoning_effort;
result.removedReasoningEffort = true;
result.applied = true;
}
return result;
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f70b7832c9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| if (outputConfigEffort !== undefined) { | ||
| result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null; | ||
| delete message.output_config; |
There was a problem hiding this comment.
Preserve other output_config fields when stripping effort
When a retried request has output_config containing effort plus any other option, this deletes the entire object even though the rectifier only needs to remove the effort carrier. The rest of the codebase explicitly preserves unknown output_config properties when applying Anthropic overrides, so this retry can silently drop provider/client parameters on the second attempt instead of only resolving the thinking/effort conflict.
Useful? React with 👍 / 👎.
| interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests, | ||
| enableThinkingSignatureRectifier: systemSettings.enableThinkingSignatureRectifier, | ||
| enableThinkingBudgetRectifier: systemSettings.enableThinkingBudgetRectifier, | ||
| enableThinkingEffortConflictRectifier: systemSettings.enableThinkingEffortConflictRectifier, |
There was a problem hiding this comment.
Add the new column to the unmigrated DB fallback path
If the app starts with code from this commit before migration 0105 has run, the first select fails because this new column is missing; the undefined-column fallback below removes older columns such as billHedgeLosers first but keeps enableThinkingEffortConflictRectifier in every wide fallback, so it keeps failing until the minimal selection and loses persisted settings by replacing them with defaults. Please omit this new column in the first fallback tier so older system_settings rows still load all existing columns during rolling deploys or manual migrations.
Useful? React with 👍 / 👎.
| if (outputConfigEffort !== undefined) { | ||
| result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null; | ||
| delete message.output_config; | ||
| result.removedOutputConfig = true; | ||
| result.applied = true; | ||
| } |
There was a problem hiding this comment.
delete message.output_config removes the entire object when any effort key is found. If a provider ever puts fields other than effort inside output_config (e.g. a future extension), those extra fields are silently discarded. Surgically removing only the effort key and then conditionally cleaning up an empty shell is safer.
| if (outputConfigEffort !== undefined) { | |
| result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null; | |
| delete message.output_config; | |
| result.removedOutputConfig = true; | |
| result.applied = true; | |
| } | |
| if (outputConfigEffort !== undefined) { | |
| result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null; | |
| delete (outputConfig as Record<string, unknown>).effort; | |
| if (Object.keys(outputConfig as Record<string, unknown>).length === 0) { | |
| delete message.output_config; | |
| } | |
| result.removedOutputConfig = true; | |
| result.applied = true; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.ts
Line: 95-100
Comment:
`delete message.output_config` removes the entire object when any `effort` key is found. If a provider ever puts fields other than `effort` inside `output_config` (e.g. a future extension), those extra fields are silently discarded. Surgically removing only the `effort` key and then conditionally cleaning up an empty shell is safer.
```suggestion
if (outputConfigEffort !== undefined) {
result.effort = typeof outputConfigEffort === "string" ? outputConfigEffort : null;
delete (outputConfig as Record<string, unknown>).effort;
if (Object.keys(outputConfig as Record<string, unknown>).length === 0) {
delete message.output_config;
}
result.removedOutputConfig = true;
result.applied = true;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/types/system-config.ts (1)
198-206:⚠️ Potential issue | 🔴 Critical | ⚡ Quick win
UpdateSystemSettingsInput缺少新字段,导致类型检查失败(构建阻断)
SystemSettings已在 Line 94-97 增加enableThinkingEffortConflictRectifier,但UpdateSystemSettingsInput未同步增加该可选字段,直接触发src/actions/system-config.tsLine 129 的 TS2353。请在输入类型中补齐该字段,保持跨层契约一致。🔧 建议修复
export interface UpdateSystemSettingsInput { @@ // thinking budget 整流器(可选) enableThinkingBudgetRectifier?: boolean; + + // thinking effort 冲突整流器(可选) + enableThinkingEffortConflictRectifier?: boolean; // billing header 整流器(可选) enableBillingHeaderRectifier?: boolean;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/types/system-config.ts` around lines 198 - 206, SystemSettings 增加了 enableThinkingEffortConflictRectifier,但 UpdateSystemSettingsInput 未同步,导致类型不匹配;在类型定义 UpdateSystemSettingsInput 中新增可选字段 enableThinkingEffortConflictRectifier?: boolean,使其与 SystemSettings 对齐,修复使用该输入类型(例如在 system-config actions 中处理更新设置的函数)时触发的 TS2353 类型错误。Sources: Linters/SAST tools, Pipeline failures
🧹 Nitpick comments (1)
src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.test.ts (1)
60-77: ⚡ Quick win建议补一个“
output_config含多字段”回归用例。当前用例没有覆盖
output_config: { effort: "...", other: ... }场景。建议断言整流后仅移除effort,其余字段保留,防止后续回归成整块删除。可补充的测试示例
+ test("only removes output_config.effort and preserves other output_config fields", () => { + const message: Record<string, unknown> = { + thinking: { type: "disabled" }, + output_config: { effort: "max", keep_me: true }, + messages: [], + }; + + const result = rectifyThinkingEffortConflict(message); + + expect(result.applied).toBe(true); + expect(result.removedOutputConfig).toBe(true); + expect(message.output_config).toEqual({ keep_me: true }); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.test.ts` around lines 60 - 77, Add a regression test for rectifyThinkingEffortConflict that uses an input message where output_config has multiple fields (e.g., { effort: "max", other: "keep" }); call rectifyThinkingEffortConflict and assert it applied, that only the reasoning effort was removed (removedReasoningEffort true) while the output_config key itself remains (removedOutputConfig false), the remaining fields (other) are preserved, the effort value is reported in the result, and message.thinking remains { type: "disabled" }; reference rectifyThinkingEffortConflict and the existing test name "removes output_config when thinking is disabled (Claude Code subagent shape)" to add a similar test covering the multi-field output_config case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.ts`:
- Around line 95-99: The code currently deletes message.output_config entirely
when outputConfigEffort is detected; instead only remove the conflicting effort
field: if outputConfigEffort !== undefined set result.effort appropriately,
delete message.output_config.effort (not message.output_config), set
result.removedOutputConfig (or preferably rename/update it to indicate the
effort field was removed) and result.applied = true; update any downstream logic
that relied on whole-object deletion to tolerate the partial removal
(references: outputConfigEffort, message.output_config, result.effort,
result.removedOutputConfig, result.applied).
In `@src/repository/system-config.ts`:
- Around line 759-762: The code reads
payload.enableThinkingEffortConflictRectifier but the UpdateSystemSettingsInput
type is missing that optional field, causing type-check failures; add
enableThinkingEffortConflictRectifier?: boolean to the UpdateSystemSettingsInput
definition and any upstream API/input types (and corresponding schema/action
types) so the new boolean flag is accepted through the type chain, then update
any related input/interface (e.g., the payload consumer and system settings
update handlers) to match the new property name.
---
Outside diff comments:
In `@src/types/system-config.ts`:
- Around line 198-206: SystemSettings 增加了
enableThinkingEffortConflictRectifier,但 UpdateSystemSettingsInput
未同步,导致类型不匹配;在类型定义 UpdateSystemSettingsInput 中新增可选字段
enableThinkingEffortConflictRectifier?: boolean,使其与 SystemSettings
对齐,修复使用该输入类型(例如在 system-config actions 中处理更新设置的函数)时触发的 TS2353 类型错误。
---
Nitpick comments:
In `@src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.test.ts`:
- Around line 60-77: Add a regression test for rectifyThinkingEffortConflict
that uses an input message where output_config has multiple fields (e.g., {
effort: "max", other: "keep" }); call rectifyThinkingEffortConflict and assert
it applied, that only the reasoning effort was removed (removedReasoningEffort
true) while the output_config key itself remains (removedOutputConfig false),
the remaining fields (other) are preserved, the effort value is reported in the
result, and message.thinking remains { type: "disabled" }; reference
rectifyThinkingEffortConflict and the existing test name "removes output_config
when thinking is disabled (Claude Code subagent shape)" to add a similar test
covering the multi-field output_config case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 844fca13-9133-4071-b0dc-3827913e84ff
📒 Files selected for processing (28)
drizzle/0105_chief_rocket_racer.sqldrizzle/meta/0105_snapshot.jsondrizzle/meta/_journal.jsonmessages/en/settings/config.jsonmessages/ja/settings/config.jsonmessages/ru/settings/config.jsonmessages/zh-CN/settings/config.jsonmessages/zh-TW/settings/config.jsonpackage.jsonsrc/actions/system-config.tssrc/app/[locale]/settings/config/_components/system-settings-form.tsxsrc/app/[locale]/settings/config/page.tsxsrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.test.tssrc/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.tssrc/drizzle/schema.tssrc/lib/api-client/v1/openapi-types.gen.tssrc/lib/api/v1/schemas/system-config.tssrc/lib/config/system-settings-cache.tssrc/lib/utils/special-settings.tssrc/lib/validation/schemas.tssrc/repository/_shared/transformers.tssrc/repository/system-config.tssrc/types/special-settings.tssrc/types/system-config.tstests/configs/thinking-effort-conflict-rectifier.config.tstests/unit/actions/system-config-thinking-effort-conflict-setting.test.tstests/unit/proxy/proxy-forwarder-thinking-effort-conflict-rectifier.test.ts
There was a problem hiding this comment.
Code Review Summary
This PR adds a reactive "thinking effort conflict rectifier" that handles the incompatibility between thinking: { type: "disabled" } and output_config: { effort } / reasoning_effort — a combination that strict Anthropic-compatible providers (DeepSeek, MiMo) reject with 400. The implementation is clean, follows existing rectifier patterns precisely, and has comprehensive test coverage (96.9% statements). No issues meeting the reporting threshold were found.
PR Size: XL
- Lines changed: 5,327 (note: ~4,500 are auto-generated Drizzle snapshot; actual meaningful changes ~500 lines)
- Files changed: 28 (note: 3 are Drizzle auto-generated metadata)
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Observations (below reporting threshold)
delete message.output_configremoves the entire object (confidence: ~70): Whenoutput_configcontainseffortalongside other keys, the whole object is deleted rather than just theeffortkey. The PR author acknowledges this in the description and Greptile's review also flagged it. In practice, DeepSeek only putseffortinoutput_config, so this is a theoretical concern for future providers — not a current bug. The test suite explicitly verifies that effort-lessoutput_configobjects are preserved, which covers the main alternative case.
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - Clean
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Excellent (96.9% stmts, 89.5% branches, 100% functions, 19 new tests)
- Code clarity - Clean
Key Verification Points
- Rectifier ordering in forwarder: effort conflict checked before signature rectifier (correct — more specific detection wins)
- Retry-once guard via
thinkingEffortConflictRetriedflag (consistent with existing rectifiers) - All 5 i18n locales (en, ja, ru, zh-CN, zh-TW) have matching key pairs
- Settings infrastructure: DB migration, schema, transformer, cache defaults, validation schema, API schema, server action, UI toggle — all consistent
- Feature toggle defaults to enabled with proper fallback in all code paths (cache miss, DB read failure, transformer)
Automated review by Claude AI
Add missing enableThinkingEffortConflictRectifier field to UpdateSystemSettingsInput to resolve a typecheck failure. Strip only the effort key from output_config in the thinking effort conflict rectifier instead of deleting the entire object, preserving any sibling configuration fields. Extend the system_settings database degradation fallback chain to handle missing enableThinkingEffortConflictRectifier columns in un-migrated databases for both read and update operations.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
The previous assertion only checked the transformer default value for the new column, which did not guarantee the fallback query actually stripped the correct column. The test now inspects the second select call to ensure the newly added column is removed while older fallback columns remain, catching regressions in the degradation chain order.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
评审驱动的修复(deep-review + /code-review max effort)经多代理评审 + 机器人评论交叉核对,本 PR 已修复以下真实问题(均已 push,CI 全绿):
未在本 PR 内处理(说明)
|
…nflict rectifier) - 迁移序号冲突:将本 PR 的 system-message-rectifier 列迁移移至 0106 (drizzle 重新生成为 0106_stiff_dormammu.sql),让出 0105 给 dev 已合入 的 0105_chief_rocket_racer.sql(thinking-effort-conflict rectifier)。 - src/repository/system-config.ts 降级链堆叠: 最新层先剥离 enableSystemMessageRectifier,次层再剥离 enableThinkingEffortConflictRectifier,再回到原有 billHedgeLosers 等 既有降级路径,确保两轮新增列对老库的兼容性。 - tests/unit/repository/system-config-update-missing-columns.test.ts: 原 "仅缺 enable_thinking_effort_conflict_rectifier 列" 测试改为 覆盖 enableSystemMessageRectifier(最外层),新增一项覆盖连续剥离两 个新列直到 billHedgeLosers 层的回归断言。
Closes #1257
背景
Claude Code v2.1.166+ 为子 agent 任务主动关闭思考(
thinking: {type: "disabled"}),但未剥离全局推理参数(output_config: {effort})。Anthropic 官方 API 忽略这种矛盾组合;DeepSeek 等严格校验的 Anthropic 兼容上游直接 400:issue 中引用的社区 proxy.js 即为此而生。本 PR 将其能力集成进 CCH 的整流器体系。
文档核实(国产模型可用性)
thinkingSupported(忽略 budget_tokens);output_config"Onlyeffortis supported"——effort内部映射为 reasoning_effort,即冲突组合确为「thinking.disabled + output_config.effort」,剥离output_config与文档语义一致。模型:deepseek-v4-pro / deepseek-v4-flash。/anthropic,mimo-v2.5-pro),thinking 经 reasoning_effort 启用;文档未记录该冲突的行为。由于整流器仅在上游真实返回该错误时触发,MiMo 若不报错则零影响,若报同类错误则同样被修复——被动触发天然保证了兼容安全。anthropic_effort特殊设置与 provider 参数覆写均围绕output_config.effort字段,与本整流器审计字段闭环。实现(完全对齐 thinking-signature-rectifier 的被动触发模式)
核心(
src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.ts):detect*Trigger(errorMessage):仅字符串匹配("cannot be disabled" + reasoning_effort/output_config 变体),不依赖错误规则开关rectify*(message):仅当 thinking 关闭(或缺省)时,剥离output_config(携带 effort 时)与顶层reasoning_effort透传;保留 thinking 关闭状态(尊重客户端对子 agent 关闭思考的意图);thinking enabled/adaptive 或无 effort 字段时applied=false不重试forwarder 集成:
tryApplyReactiveAnthropicRectifier中置于签名整流器之前(更具体的错误检测优先,避免被其通用 invalid request 兜底吞掉);命中→整流→同供应商重试一次(retryState防重复);hedge 分支自动复用thinking_effort_conflict_rectifier类型(hit/trigger/removedOutputConfig/removedReasoningEffort/thinkingType/effort),日志可回溯系统设置(
enableThinkingEffortConflictRectifier,默认开启):drizzle/0105_chief_rocket_racer.sql(bun run db:generate生成并已审查)测试(TDD,先红后绿;19 个新用例)
src/app/v1/_lib/proxy/thinking-effort-conflict-rectifier.test.ts(12):错误检测各变体与反例(签名/预算错误不误触)、整流各形态(含 thinking 缺省视为关闭、enabled/adaptive 不触碰、无 effort no-op)tests/unit/proxy/proxy-forwarder-thinking-effort-conflict-rectifier.test.ts(4):命中→剥离→重试成功(且不误触签名整流器)、开关关闭不整流、not_applicable 不重试、同供应商最多重试一次tests/unit/actions/system-config-thinking-effort-conflict-setting.test.ts(3):transformer 默认值、validation schema、v1 响应 schematests/configs/thinking-effort-conflict-rectifier.config.ts+ package.json 脚本:96.9% Stmts / 89.5% Branch / 100% Funcs / 96.7% Lines(阈值 80/70/80/80)验证
bun run build/typecheck/lint/test(根配置全量)/test:v1全部通过logs-sessionid-time-filter在干净 dev 基线(499d925)上以相同数字(branches 85.52%,65/76)失败,为存量问题与本 PR 无关openapi:check/openapi:lint通过;i18n 键名守卫(sync-settings-keys / settings-split-guards)通过🤖 Generated with Claude Code
Greptile Summary
This PR adds a new reactive "Thinking Effort Conflict Rectifier" that handles a specific 400 error from strict Anthropic-compatible providers (DeepSeek, MiMo) which reject the combination of
thinking: {type: "disabled"}andreasoning_effort/output_config.effort. The fix detects the error by string-matching the upstream message, surgically strips only the conflicting effort fields, and retries once against the same provider.thinking-effort-conflict-rectifier.ts) is placed before the existing signature rectifier inforwarder.tsto prevent the more specific error from being swallowed by the genericinvalid_requestfallback; the database column, schema, transformers, fallback-chain, settings cache, settings UI, and i18n strings are all updated consistently.output_configobject has been addressed — the new implementation surgically removes only theeffortkey and drops the object only when it becomes empty.Confidence Score: 5/5
Safe to merge — the rectifier is passively triggered only on an exact 400 error string, touches no normal-path requests, and the one retry is guarded against repetition.
All changed paths are well-isolated: the new rectifier fires only after an upstream 400 containing the specific DeepSeek/MiMo error wording, leaves thinking-disabled intact, and retries at most once. The database fallback chain is correctly extended with the new column at the outermost tier. Tests cover detection variants, rectification edge-cases (sibling-key preservation, enabled/adaptive no-ops, effort-less no-ops), the disabled-switch path, and the retry-once guard. No existing behavior is altered for providers that don't return this specific error.
No files require special attention.
Important Files Changed
Sequence Diagram
sequenceDiagram participant CC as Claude Code (subagent) participant PF as ProxyForwarder participant TECR as ThinkingEffortConflictRectifier participant DS as DeepSeek / MiMo CC->>PF: "POST /v1/messages {thinking:{type:"disabled"}, output_config:{effort:"max"}}" PF->>DS: Forward request (attempt 1) DS-->>PF: 400 thinking options type cannot be disabled when reasoning_effort is set PF->>TECR: detectThinkingEffortConflictRectifierTrigger(errorMsg) TECR-->>PF: thinking_disabled_with_reasoning_effort PF->>TECR: rectifyThinkingEffortConflict(message) Note over TECR: Remove output_config.effort, keep sibling keys, remove top-level reasoning_effort, keep thinking disabled TECR-->>PF: "applied=true, removedOutputConfig=true" PF->>PF: "retryState.thinkingEffortConflictRetried = true" PF->>PF: persist special setting audit log PF->>DS: "Forward request (attempt 2) {thinking:{type:"disabled"}, messages:[...]}" DS-->>PF: 200 OK PF-->>CC: 200 OKReviews (3): Last reviewed commit: "test(repository): assert stripped select..." | Re-trigger Greptile