From 599db8050c550a0140fdbc42ad30ee23d65d3c2c Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Fri, 24 Jul 2026 23:27:06 +0530 Subject: [PATCH] feat: add Claude Opus 5 support for claude_code and anthropic providers - Add claude-opus-5 model to claude_code provider in provider.json (1M context) - Classify opus-5 as AdaptiveOnly in ModelSpecificReasoning (adaptive thinking only; drops budget_tokens and sampling params like Opus 4.7/4.8) - Skip redundant interleaved-thinking beta header for Opus 5 - Map claude-opus-5 to 1M context length in Anthropic response DTO Co-Authored-By: ForgeCode --- .../forge_app/src/dto/anthropic/response.rs | 4 +-- .../transformers/model_specific_reasoning.rs | 28 +++++++++++++++++-- crates/forge_repo/src/provider/anthropic.rs | 7 +++-- crates/forge_repo/src/provider/provider.json | 10 +++++++ 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/crates/forge_app/src/dto/anthropic/response.rs b/crates/forge_app/src/dto/anthropic/response.rs index ef0cd78ac6..6d3b89c759 100644 --- a/crates/forge_app/src/dto/anthropic/response.rs +++ b/crates/forge_app/src/dto/anthropic/response.rs @@ -84,8 +84,8 @@ fn get_context_length(model_id: &str) -> Option { return Some(1_000_000); } - // Claude Sonnet 5 (1M context) - if model_id.starts_with("claude-sonnet-5") { + // Claude Sonnet 5 and Opus 5 (1M context) + if model_id.starts_with("claude-sonnet-5") || model_id.starts_with("claude-opus-5") { return Some(1_000_000); } diff --git a/crates/forge_app/src/transformers/model_specific_reasoning.rs b/crates/forge_app/src/transformers/model_specific_reasoning.rs index 4c74b595b8..43f25c7539 100644 --- a/crates/forge_app/src/transformers/model_specific_reasoning.rs +++ b/crates/forge_app/src/transformers/model_specific_reasoning.rs @@ -27,14 +27,15 @@ impl ModelSpecificReasoning { fn family(&self) -> AnthropicModelFamily { let id = self.model_id.to_lowercase(); - if id.contains("opus-4-8") + if id.contains("opus-5") + || id.contains("opus-4-8") || id.contains("48-opus") || id.contains("opus-4-7") || id.contains("47-opus") || id.contains("mythos") || id.contains("fable") { - // Opus 4.8 shares Opus 4.7's API contract: adaptive thinking only + // Opus 5 and Opus 4.8 share Opus 4.7's API contract: adaptive thinking only // (legacy `budget_tokens` returns 400) and non-default sampling // params (`temperature`/`top_p`/`top_k`) return 400. AnthropicModelFamily::AdaptiveOnly @@ -188,6 +189,29 @@ mod tests { assert_eq!(actual, expected); } + #[test] + fn test_opus_5_drops_max_tokens_and_sampling_params() { + // Opus 5 shares the Opus 4.7/4.8 API contract: adaptive thinking only, + // legacy `budget_tokens` and non-default sampling params return 400. + let fixture = fixture_context_with_sampling().reasoning(ReasoningConfig { + enabled: Some(true), + max_tokens: Some(8000), + effort: Some(Effort::XHigh), + exclude: Some(true), + }); + + let actual = ModelSpecificReasoning::new("claude-opus-5").transform(fixture); + + let expected = Context::default().reasoning(ReasoningConfig { + enabled: Some(true), + max_tokens: None, + effort: Some(Effort::XHigh), + exclude: Some(true), + }); + + assert_eq!(actual, expected); + } + #[test] fn test_opus_4_8_strips_sampling_even_without_reasoning() { let fixture = fixture_context_with_sampling(); diff --git a/crates/forge_repo/src/provider/anthropic.rs b/crates/forge_repo/src/provider/anthropic.rs index e6c360afd6..9bcb906cb9 100644 --- a/crates/forge_repo/src/provider/anthropic.rs +++ b/crates/forge_repo/src/provider/anthropic.rs @@ -92,13 +92,15 @@ impl Anthropic { } /// Returns false when the model auto-enables interleaved thinking through -/// adaptive thinking (Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 5, Sonnet 4.6). When +/// adaptive thinking (Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 5, Sonnet +/// 4.6). When /// the model is unknown (e.g., listing endpoints), the flag is included because /// it is harmless on non-chat endpoints and necessary on older chat models. fn interleaved_thinking_required(model: Option<&ModelId>) -> bool { let Some(model) = model else { return true }; let id = model.as_str().to_lowercase(); - !(id.contains("opus-4-8") + !(id.contains("opus-5") + || id.contains("opus-4-8") || id.contains("opus-4-7") || id.contains("opus-4-6") || id.contains("sonnet-4-6") @@ -837,6 +839,7 @@ mod tests { ); for model_id in [ + "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", diff --git a/crates/forge_repo/src/provider/provider.json b/crates/forge_repo/src/provider/provider.json index bd49a3b8ce..b5e9aec958 100644 --- a/crates/forge_repo/src/provider/provider.json +++ b/crates/forge_repo/src/provider/provider.json @@ -765,6 +765,16 @@ "supports_reasoning": true, "input_modalities": ["text", "image"] }, + { + "id": "claude-opus-5", + "name": "Claude Opus 5", + "description": "Thoughtful and proactive model approaching frontier intelligence at half the price", + "context_length": 1000000, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text", "image"] + }, { "id": "claude-opus-4-8", "name": "Claude Opus 4.8",