Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions src/llm/anthropic/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"));
}
}
16 changes: 13 additions & 3 deletions src/llm/pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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") =>
Expand Down Expand Up @@ -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);
Expand Down