diff --git a/src/llm/anthropic/params.rs b/src/llm/anthropic/params.rs index 11dcda8ef..103dff9de 100644 --- a/src/llm/anthropic/params.rs +++ b/src/llm/anthropic/params.rs @@ -19,12 +19,25 @@ pub struct AnthropicRequest { pub original_tools: Vec<(String, String)>, } -/// Adaptive thinking is only available on 4.6-generation models. +/// Adaptive thinking is available on every 4.6-generation-and-later model — +/// the API rejects the old `{type: "enabled", budget_tokens: N}` shape on +/// these and only accepts `{type: "adaptive"}` (or the field omitted, which +/// silently runs without thinking). Checked by family/prefix rather than a +/// single generation string so onboarding a new model is one line here, not +/// a parallel gating rewrite. Was previously scoped to literal "4-6"/"4.6" +/// only, which silently missed Opus 4.7, Opus 4.8, and Opus 5 — those +/// workers ran with no `thinking` param at all until this fix (2026-07-29). fn supports_adaptive_thinking(model_id: &str) -> bool { - model_id.contains("opus-4-6") - || model_id.contains("opus-4.6") - || model_id.contains("sonnet-4-6") - || model_id.contains("sonnet-4.6") + const ADAPTIVE_ONLY_FRAGMENTS: &[&str] = &[ + "opus-4-6", "opus-4.6", + "opus-4-7", "opus-4.7", + "opus-4-8", "opus-4.8", + "opus-5", + "sonnet-4-6", "sonnet-4.6", + "sonnet-5", + "fable-5", "mythos-5", + ]; + ADAPTIVE_ONLY_FRAGMENTS.iter().any(|f| model_id.contains(*f)) } fn is_opus(model_id: &str) -> bool { @@ -218,4 +231,18 @@ mod tests { assert!(!supports_adaptive_thinking("claude-opus-4-0")); assert!(!supports_adaptive_thinking("gpt-4o")); } + + #[test] + fn adaptive_thinking_detected_for_opus_5_and_the_rest_of_the_4_6plus_family() { + // Regression test: the original gate only matched literal "4-6", + // silently missing every later adaptive-only model — Opus 4.7, Opus + // 4.8, and Opus 5 (the current worker default) all ran with no + // `thinking` param at all before this fix. + assert!(supports_adaptive_thinking("anthropic/claude-opus-5")); + assert!(supports_adaptive_thinking("claude-opus-4-7")); + assert!(supports_adaptive_thinking("claude-opus-4-8")); + assert!(supports_adaptive_thinking("anthropic/claude-sonnet-5")); + assert!(supports_adaptive_thinking("claude-fable-5")); + assert!(supports_adaptive_thinking("claude-mythos-5")); + } } diff --git a/src/llm/pricing.rs b/src/llm/pricing.rs index c27384d34..7a5f754cc 100644 --- a/src/llm/pricing.rs +++ b/src/llm/pricing.rs @@ -31,7 +31,8 @@ fn lookup_pricing(model_name: &str) -> ModelPricing { // ORDER IS LOAD-BEARING for the Anthropic arms: specific prefixes // (claude-opus-4-8) must precede their generic parent (claude-opus-4) — // the first matching arm wins. - // Prices verified 2026-07-03 against platform.claude.com pricing docs. + // Prices verified 2026-07-29 against platform.claude.com pricing docs + // (Claude Opus 5 confirmed $5/$25 — same tier as Opus 4.5+). match model { m if m.starts_with("claude-fable-5") || m.starts_with("claude-mythos-5") => { ModelPricing { @@ -41,8 +42,9 @@ fn lookup_pricing(model_name: &str) -> ModelPricing { cache_write: per_m(12.5), } } - // Opus 4.5+ dropped to $5/$25 — only Opus 4.0/4.1 remain at $15/$75. - m if m.starts_with("claude-opus-4-5") + // Opus 5 and Opus 4.5+ are $5/$25 — only Opus 4.0/4.1 remain at $15/$75. + m if m.starts_with("claude-opus-5") + || m.starts_with("claude-opus-4-5") || m.starts_with("claude-opus-4-6") || m.starts_with("claude-opus-4-7") || m.starts_with("claude-opus-4-8") => @@ -269,6 +271,14 @@ mod tests { assert!((legacy - 90.0).abs() < 1e-6, "opus-4.1 should cost $15+$75, got {legacy}"); } + #[test] + fn test_opus_5_pricing() { + // Opus 5 replaced Opus 4.8 as the worker default (2026-07-29) but is + // priced the same: $5+$25, not the legacy $15+$75 claude-opus-4 arm. + let cost = estimate_cost("anthropic/claude-opus-5", 1_000_000, 1_000_000, 0); + assert!((cost - 30.0).abs() < 1e-6, "opus-5 should cost $5+$25, got {cost}"); + } + #[test] fn test_fable_5_pricing() { let cost = estimate_cost("anthropic/claude-fable-5", 1_000_000, 1_000_000, 0);