From 0acda783b16ca2fa67d1a83aa7d0096100b20024 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 23 Mar 2026 12:50:42 -0400 Subject: [PATCH] Add ReasoningEffort enum to replace raw Option Introduces a typed ReasoningEffort enum (Low, Medium, High) with serde, Display, and FromStr support. Updates Request, GenerateParams, and SessionConfig to use Option instead of Option. Aligns with spec change removing "none" as a valid reasoning_effort value. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-agent/src/config.rs | 13 +++--- lib/crates/fabro-agent/src/session.rs | 9 ++-- lib/crates/fabro-agent/tests/parity_matrix.rs | 2 +- lib/crates/fabro-api/src/server.rs | 2 +- lib/crates/fabro-llm/src/cli.rs | 2 +- lib/crates/fabro-llm/src/generate.rs | 16 +++---- .../fabro-llm/src/providers/anthropic.rs | 6 +-- lib/crates/fabro-llm/src/providers/openai.rs | 2 +- lib/crates/fabro-llm/src/types.rs | 42 ++++++++++++++++++- lib/crates/fabro-workflows/src/backend/api.rs | 4 +- 10 files changed, 72 insertions(+), 26 deletions(-) diff --git a/lib/crates/fabro-agent/src/config.rs b/lib/crates/fabro-agent/src/config.rs index 63b831ab2..eae9af6f0 100644 --- a/lib/crates/fabro-agent/src/config.rs +++ b/lib/crates/fabro-agent/src/config.rs @@ -63,7 +63,7 @@ pub struct SessionConfig { pub max_tool_rounds_per_input: usize, pub default_command_timeout_ms: u64, pub max_command_timeout_ms: u64, - pub reasoning_effort: Option, + pub reasoning_effort: Option, pub speed: Option, pub tool_output_limits: HashMap, pub tool_line_limits: HashMap, @@ -161,7 +161,7 @@ mod tests { fn default_config_values() { let config = SessionConfig::default(); assert_eq!(config.max_turns, 0); - assert_eq!(config.max_tool_rounds_per_input, 200); + assert_eq!(config.max_tool_rounds_per_input, 0); assert_eq!(config.default_command_timeout_ms, 10_000); assert_eq!(config.max_command_timeout_ms, 600_000); assert!(config.reasoning_effort.is_none()); @@ -187,12 +187,15 @@ mod tests { fn config_with_custom_values() { let config = SessionConfig { max_turns: 50, - reasoning_effort: Some("high".into()), + reasoning_effort: Some(fabro_llm::types::ReasoningEffort::High), ..Default::default() }; assert_eq!(config.max_turns, 50); - assert_eq!(config.reasoning_effort, Some("high".into())); - assert_eq!(config.max_tool_rounds_per_input, 200); + assert_eq!( + config.reasoning_effort, + Some(fabro_llm::types::ReasoningEffort::High) + ); + assert_eq!(config.max_tool_rounds_per_input, 0); } #[test] diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index 4504b5260..b2a3823e7 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -455,7 +455,7 @@ impl Session { } } - pub fn set_reasoning_effort(&mut self, effort: Option) { + pub fn set_reasoning_effort(&mut self, effort: Option) { self.config.reasoning_effort = effort; } @@ -1574,14 +1574,17 @@ mod tests { let mut session = Session::new(client, profile, env, SessionConfig::default()); // Default reasoning_effort is None - session.set_reasoning_effort(Some("high".to_string())); + session.set_reasoning_effort(Some(fabro_llm::types::ReasoningEffort::High)); session.process_input("test").await.unwrap(); let captured = provider_ref.captured_request.lock().unwrap(); let request = captured .as_ref() .expect("request should have been captured"); - assert_eq!(request.reasoning_effort, Some("high".to_string())); + assert_eq!( + request.reasoning_effort, + Some(fabro_llm::types::ReasoningEffort::High) + ); } #[tokio::test] diff --git a/lib/crates/fabro-agent/tests/parity_matrix.rs b/lib/crates/fabro-agent/tests/parity_matrix.rs index a6e39c085..4c43d4c33 100644 --- a/lib/crates/fabro-agent/tests/parity_matrix.rs +++ b/lib/crates/fabro-agent/tests/parity_matrix.rs @@ -430,7 +430,7 @@ macro_rules! reasoning_effort_tests { let tmp = tempfile::tempdir().expect("failed to create tempdir"); let config = SessionConfig { max_turns: 20, - reasoning_effort: Some("low".to_string()), + reasoning_effort: Some(fabro_llm::types::ReasoningEffort::Low), ..SessionConfig::default() }; let mut session = make_session_with_config($provider, $model, tmp.path(), config).await; diff --git a/lib/crates/fabro-api/src/server.rs b/lib/crates/fabro-api/src/server.rs index 301a860a5..db3817df7 100644 --- a/lib/crates/fabro-api/src/server.rs +++ b/lib/crates/fabro-api/src/server.rs @@ -1232,7 +1232,7 @@ async fn create_completion( } else { Some(req.stop_sequences) }, - reasoning_effort: req.reasoning_effort, + reasoning_effort: req.reasoning_effort.as_deref().and_then(|s| s.parse().ok()), speed: None, metadata: None, provider_options: req.provider_options, diff --git a/lib/crates/fabro-llm/src/cli.rs b/lib/crates/fabro-llm/src/cli.rs index 39c739147..b052618f0 100644 --- a/lib/crates/fabro-llm/src/cli.rs +++ b/lib/crates/fabro-llm/src/cli.rs @@ -869,7 +869,7 @@ fn build_deep_test_params(info: &Model) -> Option { .max_tokens(1024); if info.features.reasoning { - params = params.reasoning_effort("high"); + params = params.reasoning_effort(crate::types::ReasoningEffort::High); } Some(params) diff --git a/lib/crates/fabro-llm/src/generate.rs b/lib/crates/fabro-llm/src/generate.rs index 815feb791..719781deb 100644 --- a/lib/crates/fabro-llm/src/generate.rs +++ b/lib/crates/fabro-llm/src/generate.rs @@ -4,9 +4,9 @@ use crate::provider::StreamEventStream; use crate::retry::retry; use crate::tools::{execute_all_tools_with_repair, RepairToolCallFn, Tool}; use crate::types::{ - FinishReason, GenerateResult, Message, ObjectStreamEvent, Request, Response, ResponseFormat, - ResponseFormatType, RetryPolicy, StepResult, StreamEvent, TimeoutConfig, ToolCall, ToolChoice, - ToolDefinition, Usage, + FinishReason, GenerateResult, Message, ObjectStreamEvent, ReasoningEffort, Request, Response, + ResponseFormat, ResponseFormatType, RetryPolicy, StepResult, StreamEvent, TimeoutConfig, + ToolCall, ToolChoice, ToolDefinition, Usage, }; use futures::{Stream, StreamExt}; use std::pin::Pin; @@ -288,7 +288,7 @@ pub struct GenerateParams { pub top_p: Option, pub max_tokens: Option, pub stop_sequences: Option>, - pub reasoning_effort: Option, + pub reasoning_effort: Option, pub speed: Option, pub provider: Option, pub provider_options: Option, @@ -412,8 +412,8 @@ impl GenerateParams { } #[must_use] - pub fn reasoning_effort(mut self, reasoning_effort: impl Into) -> Self { - self.reasoning_effort = Some(reasoning_effort.into()); + pub fn reasoning_effort(mut self, reasoning_effort: ReasoningEffort) -> Self { + self.reasoning_effort = Some(reasoning_effort); self } @@ -1513,7 +1513,7 @@ mod tests { .top_p(0.9) .max_tokens(100) .stop_sequences(vec!["STOP".to_string()]) - .reasoning_effort("high") + .reasoning_effort(ReasoningEffort::High) .provider("anthropic") .provider_options(serde_json::json!({"key": "value"})) .max_retries(5) @@ -1532,7 +1532,7 @@ mod tests { assert_eq!(params.top_p, Some(0.9)); assert_eq!(params.max_tokens, Some(100)); assert_eq!(params.stop_sequences, Some(vec!["STOP".to_string()])); - assert_eq!(params.reasoning_effort.as_deref(), Some("high")); + assert_eq!(params.reasoning_effort, Some(ReasoningEffort::High)); assert_eq!(params.provider.as_deref(), Some("anthropic")); assert!(params.provider_options.is_some()); assert_eq!(params.max_retries, 5); diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index cd021aa2c..25bd0cc37 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -1124,12 +1124,12 @@ fn build_api_request( if supports_effort { ( explicit_thinking, - Some(serde_json::json!({"effort": effort})), + Some(serde_json::json!({"effort": effort.as_str()})), ) } else if explicit_thinking.is_none() { // Convert effort level to a thinking budget for models that don't // support the effort parameter (e.g. claude-sonnet-4-5). - let budget = effort_to_budget_tokens(effort, resolved_max_tokens); + let budget = effort_to_budget_tokens(effort.as_str(), resolved_max_tokens); if resolved_max_tokens <= budget { resolved_max_tokens = budget + 1024; } @@ -2138,7 +2138,7 @@ mod tests { 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()), + reasoning_effort: Some(crate::types::ReasoningEffort::Medium), ..make_base_request() }; diff --git a/lib/crates/fabro-llm/src/providers/openai.rs b/lib/crates/fabro-llm/src/providers/openai.rs index 54bf2f862..04ecd9eff 100644 --- a/lib/crates/fabro-llm/src/providers/openai.rs +++ b/lib/crates/fabro-llm/src/providers/openai.rs @@ -387,7 +387,7 @@ fn build_api_request(request: &Request, stream: bool, codex_mode: bool) -> ApiRe let reasoning = request .reasoning_effort .as_ref() - .map(|effort| serde_json::json!({"effort": effort})); + .map(|effort| serde_json::json!({"effort": effort.as_str()})); let text = request .response_format .as_ref() diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index b2a3ca6bd..dd1aebf17 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -440,6 +440,46 @@ pub struct RateLimitInfo { pub reset_at: Option, } +// --- 3.8 ReasoningEffort --- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ReasoningEffort { + Low, + Medium, + High, +} + +impl ReasoningEffort { + pub fn as_str(&self) -> &'static str { + match self { + Self::Low => "low", + Self::Medium => "medium", + Self::High => "high", + } + } +} + +impl std::fmt::Display for ReasoningEffort { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl std::str::FromStr for ReasoningEffort { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "low" => Ok(Self::Low), + "medium" => Ok(Self::Medium), + "high" => Ok(Self::High), + other => Err(format!( + "invalid reasoning_effort: {other:?} (expected low, medium, or high)" + )), + } + } +} + // --- 3.6 Request --- #[derive(Debug, Clone, Serialize, Deserialize)] @@ -454,7 +494,7 @@ pub struct Request { pub top_p: Option, pub max_tokens: Option, pub stop_sequences: Option>, - pub reasoning_effort: Option, + pub reasoning_effort: Option, pub speed: Option, pub metadata: Option>, pub provider_options: Option, diff --git a/lib/crates/fabro-workflows/src/backend/api.rs b/lib/crates/fabro-workflows/src/backend/api.rs index 493d40841..87d0c71c7 100644 --- a/lib/crates/fabro-workflows/src/backend/api.rs +++ b/lib/crates/fabro-workflows/src/backend/api.rs @@ -196,7 +196,7 @@ impl AgentApiBackend { let config = SessionConfig { max_tokens: node.max_tokens(), - reasoning_effort: Some(node.reasoning_effort().to_string()), + reasoning_effort: node.reasoning_effort().parse().ok(), speed: node.speed().map(String::from), tool_hooks, mcp_servers, @@ -291,7 +291,7 @@ impl CodergenBackend for AgentApiBackend { model: model.to_string(), messages, provider, - reasoning_effort: Some(node.reasoning_effort().to_string()), + reasoning_effort: node.reasoning_effort().parse().ok(), speed: node.speed().map(String::from), tools: None, tool_choice: None,