diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index 638b81e95..b96b8decc 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -107,6 +107,8 @@ struct ApiRequest { #[serde(skip_serializing_if = "Option::is_none")] thinking: Option, #[serde(skip_serializing_if = "Option::is_none")] + output_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] metadata: Option>, #[serde(skip_serializing_if = "std::ops::Not::not")] stream: bool, @@ -1072,6 +1074,11 @@ fn build_api_request( let thinking = extract_thinking_config(request.provider_options.as_ref()); + let output_config = request + .reasoning_effort + .as_ref() + .map(|effort| serde_json::json!({"effort": effort})); + let api_request = ApiRequest { model: request.model.clone(), messages: api_messages, @@ -1088,6 +1095,7 @@ fn build_api_request( tools: api_tools, tool_choice: tool_choice_json, thinking, + output_config, metadata: request.metadata.clone(), stream, }; @@ -1577,6 +1585,7 @@ mod tests { tools: None, tool_choice: None, thinking: None, + output_config: None, metadata: None, stream: false, }; @@ -1592,8 +1601,21 @@ mod tests { fn build_api_request_omits_whitespace_only_system_prompt() { let adapter = Adapter::new("test-key"); let request = Request { - model: "claude-sonnet-4-20250514".to_string(), messages: vec![Message::system(" \n\t"), Message::user("Hello")], + ..make_base_request() + }; + + let (api_request, _req_builder) = build_api_request(&adapter, &request, false); + assert!( + api_request.system.is_none(), + "whitespace-only system prompts should be omitted" + ); + } + + fn make_base_request() -> Request { + Request { + model: "claude-sonnet-4-20250514".to_string(), + messages: vec![Message::user("Hello")], provider: Some("anthropic".to_string()), tools: None, tool_choice: None, @@ -1605,30 +1627,15 @@ mod tests { reasoning_effort: None, metadata: None, provider_options: None, - }; - - let (api_request, _req_builder) = build_api_request(&adapter, &request, false); - assert!( - api_request.system.is_none(), - "whitespace-only system prompts should be omitted" - ); + } } fn make_request_with_format(format: crate::types::ResponseFormat) -> Request { Request { - model: "claude-sonnet-4-20250514".to_string(), - messages: vec![Message::user("Hello")], provider: None, - tools: None, - tool_choice: None, response_format: Some(format), - temperature: None, - top_p: None, max_tokens: None, - stop_sequences: None, - reasoning_effort: None, - metadata: None, - provider_options: None, + ..make_base_request() } } @@ -1973,6 +1980,7 @@ mod tests { tools: None, tool_choice: None, thinking: None, + output_config: None, metadata: None, stream: false, }; @@ -2004,6 +2012,7 @@ mod tests { tools: None, tool_choice: None, thinking: None, + output_config: None, metadata: None, stream: false, }; @@ -2040,4 +2049,28 @@ mod tests { "[Audio content not supported by this provider]" ); } + + #[test] + fn build_api_request_maps_reasoning_effort_to_output_config() { + let adapter = Adapter::new("test-key"); + let request = Request { + reasoning_effort: Some("medium".to_string()), + ..make_base_request() + }; + + let (api_request, _req_builder) = build_api_request(&adapter, &request, false); + assert_eq!( + api_request.output_config, + Some(serde_json::json!({"effort": "medium"})) + ); + } + + #[test] + fn build_api_request_omits_output_config_when_no_reasoning_effort() { + let adapter = Adapter::new("test-key"); + let request = make_base_request(); + + let (api_request, _req_builder) = build_api_request(&adapter, &request, false); + assert!(api_request.output_config.is_none()); + } }