Add ReasoningEffort enum to replace raw Option<String>

Introduces a typed ReasoningEffort enum (Low, Medium, High) with
serde, Display, and FromStr support. Updates Request, GenerateParams,
and SessionConfig to use Option<ReasoningEffort> instead of
Option<String>. Aligns with spec change removing "none" as a valid
reasoning_effort value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-23 12:50:42 -04:00
parent 0c649ea482
commit 0acda783b1
No known key found for this signature in database
10 changed files with 72 additions and 26 deletions

View file

@ -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<String>,
pub reasoning_effort: Option<fabro_llm::types::ReasoningEffort>,
pub speed: Option<String>,
pub tool_output_limits: HashMap<String, usize>,
pub tool_line_limits: HashMap<String, usize>,
@ -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]

View file

@ -455,7 +455,7 @@ impl Session {
}
}
pub fn set_reasoning_effort(&mut self, effort: Option<String>) {
pub fn set_reasoning_effort(&mut self, effort: Option<fabro_llm::types::ReasoningEffort>) {
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]

View file

@ -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;

View file

@ -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,

View file

@ -869,7 +869,7 @@ fn build_deep_test_params(info: &Model) -> Option<GenerateParams> {
.max_tokens(1024);
if info.features.reasoning {
params = params.reasoning_effort("high");
params = params.reasoning_effort(crate::types::ReasoningEffort::High);
}
Some(params)

View file

@ -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<f64>,
pub max_tokens: Option<i64>,
pub stop_sequences: Option<Vec<String>>,
pub reasoning_effort: Option<String>,
pub reasoning_effort: Option<ReasoningEffort>,
pub speed: Option<String>,
pub provider: Option<String>,
pub provider_options: Option<serde_json::Value>,
@ -412,8 +412,8 @@ impl GenerateParams {
}
#[must_use]
pub fn reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> 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);

View file

@ -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()
};

View file

@ -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()

View file

@ -440,6 +440,46 @@ pub struct RateLimitInfo {
pub reset_at: Option<String>,
}
// --- 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<Self, Self::Err> {
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<f64>,
pub max_tokens: Option<i64>,
pub stop_sequences: Option<Vec<String>>,
pub reasoning_effort: Option<String>,
pub reasoning_effort: Option<ReasoningEffort>,
pub speed: Option<String>,
pub metadata: Option<HashMap<String, String>>,
pub provider_options: Option<serde_json::Value>,

View file

@ -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,