fix(anthropic): legacy budget maps adaptive-only models to adaptive

Address codex review (new P1) on #9484:

thinking_type() returned Enabled whenever only a legacy
ANTHROPIC_THINKING_BUDGET / CLAUDE_THINKING_BUDGET was set, before
checking is_adaptive_model. For adaptive-only Opus 4.7/4.8 that
produced thinking: {"type":"enabled","budget_tokens":...}, which
Anthropic rejects with HTTP 400. Now the legacy-budget path maps
adaptive models to the adaptive payload while non-adaptive models
keep the enabled/budget behavior.

Adds a test covering the legacy-budget-without-effort path.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-05-30 16:12:42 +10:00
parent 2dab4645e3
commit 168d45d2ab
@@ -92,7 +92,13 @@ pub fn thinking_type(model_config: &ModelConfig) -> ThinkingType {
let effort = model_config.thinking_effort();
if effort.is_none() && legacy_thinking_budget_tokens().is_some() {
return ThinkingType::Enabled;
// Adaptive-only models reject manual budget_tokens, so a legacy budget
// setting must still map to the adaptive payload rather than enabled.
return if is_adaptive_model {
ThinkingType::Adaptive
} else {
ThinkingType::Enabled
};
}
match effort.unwrap_or(ThinkingEffort::Off) {
@@ -1616,6 +1622,36 @@ mod tests {
assert_eq!(thinking_budget_tokens(&config), 8192);
}
#[test]
fn test_legacy_budget_maps_adaptive_models_to_adaptive() {
let _guard = env_lock::lock_env([
("GOOSE_THINKING_EFFORT", None::<&str>),
// Set to an unrecognized value so env (read first) shadows any
// CLAUDE_THINKING_TYPE in the developer's config file, leaving
// thinking_effort() as None so the legacy-budget branch is exercised.
("CLAUDE_THINKING_TYPE", Some("unset")),
("CLAUDE_THINKING_ENABLED", None::<&str>),
("ANTHROPIC_THINKING_BUDGET", Some("8192")),
("CLAUDE_THINKING_BUDGET", None::<&str>),
]);
// Adaptive-only models must use the adaptive payload even when only a
// legacy budget env var is set (no thinking_effort configured).
for model in ["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"] {
assert_eq!(
thinking_type(&cfg(model)),
ThinkingType::Adaptive,
"model {model}"
);
}
// Non-adaptive models keep the legacy enabled/budget payload.
assert_eq!(
thinking_type(&cfg("claude-3-7-sonnet-20250219")),
ThinkingType::Enabled
);
}
#[test]
fn test_thinking_type_non_claude_always_disabled() {
assert_eq!(