Skip to content

Commit b3454cb

Browse files
fix: apply Opus 4.7 API contract to Claude Opus 4.8 (#3418)
Co-authored-by: ForgeCode <noreply@forgecode.dev>
1 parent 118a582 commit b3454cb

2 files changed

Lines changed: 78 additions & 8 deletions

File tree

crates/forge_app/src/transformers/model_specific_reasoning.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ impl ModelSpecificReasoning {
2727

2828
fn family(&self) -> AnthropicModelFamily {
2929
let id = self.model_id.to_lowercase();
30-
if id.contains("opus-4-7") || id.contains("47-opus") {
30+
if id.contains("opus-4-8")
31+
|| id.contains("48-opus")
32+
|| id.contains("opus-4-7")
33+
|| id.contains("47-opus")
34+
{
35+
// Opus 4.8 shares Opus 4.7's API contract: adaptive thinking only
36+
// (legacy `budget_tokens` returns 400) and non-default sampling
37+
// params (`temperature`/`top_p`/`top_k`) return 400.
3138
AnthropicModelFamily::AdaptiveOnly
3239
} else if id.contains("opus-4-6")
3340
|| id.contains("46-opus")
@@ -82,7 +89,7 @@ impl Transformer for ModelSpecificReasoning {
8289
warn!(
8390
model = %self.model_id,
8491
dropped_max_tokens = max_tokens,
85-
"Dropping `reasoning.max_tokens` for Opus 4.7: extended thinking budgets are unsupported. Use `reasoning.effort` to control thinking depth instead."
92+
"Dropping `reasoning.max_tokens` for Opus 4.7/4.8: extended thinking budgets are unsupported. Use `reasoning.effort` to control thinking depth instead."
8693
);
8794
}
8895
context.temperature = None;
@@ -108,7 +115,7 @@ impl Transformer for ModelSpecificReasoning {
108115
{
109116
warn!(
110117
model = %self.model_id,
111-
"Dropping `reasoning.effort`: the effort parameter is only supported on Opus 4.5, Opus 4.6, Sonnet 4.6, and Opus 4.7."
118+
"Dropping `reasoning.effort`: the effort parameter is only supported on Opus 4.5, Opus 4.6, Sonnet 4.6, Opus 4.7, and Opus 4.8."
112119
);
113120
reasoning.effort = None;
114121
}
@@ -155,6 +162,64 @@ mod tests {
155162
assert_eq!(actual, expected);
156163
}
157164

165+
#[test]
166+
fn test_opus_4_8_drops_max_tokens_and_sampling_params() {
167+
// Opus 4.8 shares Opus 4.7's API contract: legacy `budget_tokens` and
168+
// non-default sampling params both return 400, so they must be dropped.
169+
let fixture = fixture_context_with_sampling().reasoning(ReasoningConfig {
170+
enabled: Some(true),
171+
max_tokens: Some(8000),
172+
effort: Some(Effort::XHigh),
173+
exclude: Some(true),
174+
});
175+
176+
let actual = ModelSpecificReasoning::new("claude-opus-4-8").transform(fixture);
177+
178+
let expected = Context::default().reasoning(ReasoningConfig {
179+
enabled: Some(true),
180+
max_tokens: None,
181+
effort: Some(Effort::XHigh),
182+
exclude: Some(true),
183+
});
184+
185+
assert_eq!(actual, expected);
186+
}
187+
188+
#[test]
189+
fn test_opus_4_8_strips_sampling_even_without_reasoning() {
190+
let fixture = fixture_context_with_sampling();
191+
192+
let actual = ModelSpecificReasoning::new("claude-opus-4-8").transform(fixture);
193+
194+
let expected = Context::default();
195+
196+
assert_eq!(actual, expected);
197+
}
198+
199+
#[test]
200+
fn test_opus_4_8_bedrock_prefix_still_matches() {
201+
// Bedrock region prefixes (`us.anthropic.claude-...`) must still be
202+
// classified as AdaptiveOnly so sampling params are stripped and
203+
// `max_tokens` is dropped.
204+
let fixture = fixture_context_with_sampling().reasoning(ReasoningConfig {
205+
enabled: Some(true),
206+
max_tokens: Some(8000),
207+
effort: Some(Effort::XHigh),
208+
exclude: None,
209+
});
210+
211+
let actual = ModelSpecificReasoning::new("us.anthropic.claude-opus-4-8").transform(fixture);
212+
213+
let expected = Context::default().reasoning(ReasoningConfig {
214+
enabled: Some(true),
215+
max_tokens: None,
216+
effort: Some(Effort::XHigh),
217+
exclude: None,
218+
});
219+
220+
assert_eq!(actual, expected);
221+
}
222+
158223
#[test]
159224
fn test_opus_4_7_strips_sampling_even_without_reasoning() {
160225
let fixture = fixture_context_with_sampling();

crates/forge_repo/src/provider/anthropic.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,16 @@ impl<H: HttpInfra> Anthropic<H> {
9292
}
9393

9494
/// Returns false when the model auto-enables interleaved thinking through
95-
/// adaptive thinking (Opus 4.7, Opus 4.6, Sonnet 4.6). When the model is
96-
/// unknown (e.g., listing endpoints), the flag is included because it is
95+
/// adaptive thinking (Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6). When the model
96+
/// is unknown (e.g., listing endpoints), the flag is included because it is
9797
/// harmless on non-chat endpoints and necessary on older chat models.
9898
fn interleaved_thinking_required(model: Option<&ModelId>) -> bool {
9999
let Some(model) = model else { return true };
100100
let id = model.as_str().to_lowercase();
101-
!(id.contains("opus-4-7") || id.contains("opus-4-6") || id.contains("sonnet-4-6"))
101+
!(id.contains("opus-4-8")
102+
|| id.contains("opus-4-7")
103+
|| id.contains("opus-4-6")
104+
|| id.contains("sonnet-4-6"))
102105
}
103106

104107
impl<T: HttpInfra> Anthropic<T> {
@@ -801,8 +804,8 @@ mod tests {
801804

802805
#[test]
803806
fn test_get_headers_drops_interleaved_thinking_for_4_6_plus_models() {
804-
// Adaptive thinking auto-enables interleaved thinking on Opus 4.7,
805-
// Opus 4.6, and Sonnet 4.6; the beta header is redundant there.
807+
// Adaptive thinking auto-enables interleaved thinking on Opus 4.8,
808+
// Opus 4.7, Opus 4.6, and Sonnet 4.6; the beta header is redundant there.
806809
let chat_url = Url::parse("https://api.anthropic.com/v1/messages").unwrap();
807810
let model_url = Url::parse("https://api.anthropic.com/v1/models").unwrap();
808811

@@ -832,9 +835,11 @@ mod tests {
832835
);
833836

834837
for model_id in [
838+
"claude-opus-4-8",
835839
"claude-opus-4-7",
836840
"claude-opus-4-6",
837841
"claude-sonnet-4-6",
842+
"us.anthropic.claude-opus-4-8",
838843
"us.anthropic.claude-opus-4-7",
839844
"global.anthropic.claude-sonnet-4-6",
840845
] {

0 commit comments

Comments
 (0)