diff --git a/crates/lingua/src/providers/anthropic/adapter.rs b/crates/lingua/src/providers/anthropic/adapter.rs index ea431eb6..5ad1bb38 100644 --- a/crates/lingua/src/providers/anthropic/adapter.rs +++ b/crates/lingua/src/providers/anthropic/adapter.rs @@ -565,10 +565,25 @@ impl ProviderAdapter for AnthropicAdapter { }); let thinking_val = if use_adaptive_thinking { + // Same-provider Anthropic round-trips carry the original `thinking` in extras. + // Preserve its `display` so an explicit "summarized" opt-in survives; models + // that default `display` to "omitted" would otherwise return empty thinking. + let display = anthropic_extras_view + .thinking + .as_ref() + .map(|raw| serde_json::from_value::(raw.clone())) + .transpose() + .map_err(|e| { + TransformError::FromUniversalFailed(format!( + "invalid Anthropic thinking extras: {}", + e + )) + })? + .and_then(|thinking| thinking.display); Some( serde_json::to_value(&Thinking { budget_tokens: None, - display: None, + display, thinking_type: ThinkingType::Adaptive, }) .map_err(|e| TransformError::SerializationFailed(e.to_string()))?, @@ -1736,7 +1751,7 @@ fn parse_content_block_start_event(payload: &Value) -> ContentBlockStartEventVie #[cfg(test)] mod tests { use super::*; - use crate::providers::anthropic::generated::System; + use crate::providers::anthropic::generated::{System, ThinkingDisplayMode}; use crate::serde_json::json; use crate::universal::UniversalReasoningSignature; use serde::Deserialize; @@ -2595,6 +2610,44 @@ mod tests { } } + #[test] + fn test_anthropic_preserves_adaptive_thinking_display_round_trip() { + // Same-provider Anthropic round-trip: an explicit `thinking.display` must survive + // the adaptive-thinking rebuild. + for (model, display) in [ + ("claude-sonnet-5", Some("summarized")), + ("claude-opus-5", Some("summarized")), + ("claude-opus-4-7", Some("omitted")), + ("claude-sonnet-5", None), + ] { + let adapter = AnthropicAdapter; + let mut thinking = json!({"type": "adaptive"}); + if let Some(display) = display { + thinking["display"] = json!(display); + } + let payload = json!({ + "model": model, + "max_tokens": 4096, + "messages": [{"role": "user", "content": "What is 2+2?"}], + "thinking": thinking + }); + + let universal = adapter.request_to_universal(payload).unwrap(); + let result: CreateMessageParams = + serde_json::from_value(adapter.request_from_universal(&universal).unwrap()) + .unwrap(); + + let thinking = result.thinking.expect("thinking should be present"); + assert_eq!(thinking.thinking_type, ThinkingType::Adaptive); + let expected_display: Option = + display.map(|d| serde_json::from_value(json!(d)).unwrap()); + assert_eq!( + thinking.display, expected_display, + "{model}: thinking.display should round-trip {display:?}" + ); + } + } + #[test] fn test_anthropic_opus_5_preserves_disabled_thinking_with_effort_round_trip() { let adapter = AnthropicAdapter; diff --git a/crates/lingua/src/providers/bedrock_anthropic/adapter.rs b/crates/lingua/src/providers/bedrock_anthropic/adapter.rs index cf4348a8..565de1ce 100644 --- a/crates/lingua/src/providers/bedrock_anthropic/adapter.rs +++ b/crates/lingua/src/providers/bedrock_anthropic/adapter.rs @@ -368,4 +368,37 @@ mod tests { assert_flat_invoke_body(transformed); } + + #[test] + fn anthropic_messages_to_bedrock_preserves_adaptive_thinking_display() { + use crate::providers::anthropic::generated::{Thinking, ThinkingDisplayMode, ThinkingType}; + + #[derive(Deserialize)] + struct ThinkingBodyView { + thinking: Option, + } + + for model in ["eu.anthropic.claude-sonnet-5", "eu.anthropic.claude-opus-5"] { + let input = json!({ + "model": model, + "max_tokens": 1024, + "messages": [{"role": "user", "content": "hi"}], + "thinking": {"type": "adaptive", "display": "summarized"} + }); + + let universal = AnthropicAdapter.request_to_universal(input).unwrap(); + let transformed = BedrockAnthropicAdapter::new() + .request_from_universal(&universal) + .unwrap(); + + let body: ThinkingBodyView = crate::serde_json::from_value(transformed).unwrap(); + let thinking = body.thinking.expect("thinking should be present"); + assert_eq!(thinking.thinking_type, ThinkingType::Adaptive, "{model}"); + assert_eq!( + thinking.display, + Some(ThinkingDisplayMode::Summarized), + "{model}: thinking.display should be preserved" + ); + } + } }