diff --git a/lib/apps/fabro-cli/src/args.rs b/lib/apps/fabro-cli/src/args.rs index 8e78809f0..f44d554f5 100644 --- a/lib/apps/fabro-cli/src/args.rs +++ b/lib/apps/fabro-cli/src/args.rs @@ -7,9 +7,9 @@ use fabro_agent::cli::AgentArgs; use fabro_config::{CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer}; use fabro_server::serve::DEFAULT_TCP_PORT; use fabro_static::EnvVars; +use fabro_types::ReasoningEffort; use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_types::settings::run::MergeStrategy; -use fabro_types::{ReasoningEffort, controls}; use fabro_util::printer::Printer; pub(crate) const LONG_VERSION: &str = concat!( @@ -1857,13 +1857,12 @@ pub(crate) struct CompletionArgs { } fn parse_reasoning_effort_arg(value: &str) -> Result { - controls::parse_reasoning_effort(value).ok_or_else(|| { + value.parse().map_err(|_| { format!( "unknown reasoning effort '{value}'; expected one of: {}", - controls::REASONING_EFFORTS - .iter() - .copied() - .map(controls::reasoning_effort_name) + ReasoningEffort::ALL + .into_iter() + .map(ReasoningEffort::as_str) .collect::>() .join(", ") ) diff --git a/lib/apps/fabro-server/src/server/handler/models.rs b/lib/apps/fabro-server/src/server/handler/models.rs index bdc23a652..cb47cea3f 100644 --- a/lib/apps/fabro-server/src/server/handler/models.rs +++ b/lib/apps/fabro-server/src/server/handler/models.rs @@ -6,7 +6,7 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; use fabro_llm::{ModelSelectionError, api, catalog, selection}; use fabro_redact::redact_string; -use fabro_types::controls; +use fabro_types::ReasoningEffort; use super::super::{ ApiError, AppState, FromStr, IntoResponse, Json, MAX_PAGE_OFFSET, ModelTestMode, Path, @@ -204,9 +204,9 @@ async fn test_model( Err(error) => return error.into_response(), }; let reasoning_effort = match params.reasoning_effort.as_deref() { - Some(value) => match controls::parse_reasoning_effort(value) { - Some(effort) => Some(effort), - None => { + Some(value) => match value.parse::() { + Ok(effort) => Some(effort), + Err(_) => { return ApiError::new( StatusCode::BAD_REQUEST, format!("invalid reasoning effort: {value}"), @@ -264,16 +264,14 @@ async fn test_model( if let Some(effort) = reasoning_effort { let capabilities = info.model.capabilities(); if !capabilities.reasoning_effort(effort).is_supported() { - let allowed = controls::REASONING_EFFORTS - .iter() - .copied() + let allowed = ReasoningEffort::ALL + .into_iter() .filter(|candidate| capabilities.reasoning_effort(*candidate).is_supported()) - .map(controls::reasoning_effort_name) + .map(ReasoningEffort::as_str) .collect::>() .join(", "); return ApiError::bad_request(format!( - "model '{model_id}' does not support reasoning_effort '{}'; allowed values: {allowed}", - controls::reasoning_effort_name(effort) + "model '{model_id}' does not support reasoning_effort '{effort}'; allowed values: {allowed}" )) .into_response(); } diff --git a/lib/components/fabro-agent/src/types.rs b/lib/components/fabro-agent/src/types.rs index 897ff6410..cf1627e03 100644 --- a/lib/components/fabro-agent/src/types.rs +++ b/lib/components/fabro-agent/src/types.rs @@ -4,8 +4,8 @@ use chrono::{DateTime, Utc}; use fabro_llm::LlmError; use fabro_types::{ CommandTermination, ContentPart, Cost, ExecOutputTail, LlmOutputKind, LlmRetryPhase, - Message as LlmMessage, ModelRef, ReasoningOutput, Role, SessionMessage, - StageContextWindowProjection, TokenCounts, ToolCall, ToolResult, controls, + Message as LlmMessage, ModelRef, ReasoningOutput, Role, SessionMessage, Speed, + StageContextWindowProjection, TokenCounts, ToolCall, ToolResult, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -511,7 +511,7 @@ impl AgentEvent { session_id, provider = %requested_model.provider, model = %requested_model.model_id, - speed = requested_model.speed.map_or("", controls::speed_name), + speed = requested_model.speed.map_or("", Speed::as_str), "LLM request started" ); } diff --git a/lib/components/fabro-llm/src/api.rs b/lib/components/fabro-llm/src/api.rs index b4aa4024e..e08f6ec7b 100644 --- a/lib/components/fabro-llm/src/api.rs +++ b/lib/components/fabro-llm/src/api.rs @@ -5,9 +5,9 @@ use std::collections::HashSet; -use fabro_types::controls::REASONING_EFFORTS; use fabro_types::{ Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider, ProviderId, + ReasoningEffort, }; use lithos_llm::catalog::{Catalog, CatalogProvider}; @@ -62,9 +62,8 @@ fn model_view(entry: &ModelEntry<'_>, configured: bool) -> Model { sampling: capabilities.sampling().is_supported(), }, controls: ModelControls { - reasoning_effort: REASONING_EFFORTS - .iter() - .copied() + reasoning_effort: ReasoningEffort::ALL + .into_iter() .filter(|effort| capabilities.reasoning_effort(*effort).is_supported()) .collect(), }, diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index 412e76ede..ab140a10c 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -392,29 +392,25 @@ fn control_attr<'a>(node: &'a Node, key: &str) -> Option<&'a str> { } fn parse_reasoning_effort(node: &Node, value: &str) -> Result { - controls::parse_reasoning_effort(value).ok_or_else(|| { + value.parse().map_err(|_| { Error::handler(format!( "Invalid reasoning_effort \"{value}\" for node \"{}\"; expected one of: {}", node.id, expected_values( - controls::REASONING_EFFORTS - .iter() - .map(|effort| controls::reasoning_effort_name(*effort)) + ReasoningEffort::ALL + .into_iter() + .map(ReasoningEffort::as_str) ), )) }) } fn parse_speed(node: &Node, value: &str) -> Result { - controls::parse_speed(value).ok_or_else(|| { + value.parse().map_err(|_| { Error::handler(format!( "Invalid speed \"{value}\" for node \"{}\"; expected one of: {}", node.id, - expected_values( - controls::SPEEDS - .iter() - .map(|speed| controls::speed_name(*speed)) - ), + expected_values(Speed::ALL.into_iter().map(Speed::as_str)), )) }) } diff --git a/lib/components/fabro-workflow/src/model_fallback.rs b/lib/components/fabro-workflow/src/model_fallback.rs index c4462acb8..4a9d3c73f 100644 --- a/lib/components/fabro-workflow/src/model_fallback.rs +++ b/lib/components/fabro-workflow/src/model_fallback.rs @@ -4,7 +4,7 @@ use fabro_llm::catalog::ModelEntry; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{FallbackTarget, ModelSelectionError, catalog, selection}; use fabro_types::settings::{ModelRef, ResolvedModelRef}; -use fabro_types::{ProviderId, ReasoningEffort, RunNoticeCode, RunNoticeLevel, controls}; +use fabro_types::{ProviderId, ReasoningEffort, RunNoticeCode, RunNoticeLevel}; use crate::Error; @@ -180,7 +180,6 @@ impl ModelFallbackNotice { target, requested_effort, } => { - let requested_effort = controls::reasoning_effort_name(*requested_effort); format!( "Model fallback `{target}` for requested model `{requested_model}` was skipped because it has no reasoning level near `{requested_effort}`." ) diff --git a/lib/foundation/fabro-types/src/billing.rs b/lib/foundation/fabro-types/src/billing.rs index 88b7b27ef..1fe4c77ae 100644 --- a/lib/foundation/fabro-types/src/billing.rs +++ b/lib/foundation/fabro-types/src/billing.rs @@ -10,8 +10,6 @@ use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; pub use lithos_llm::types::{Cost, CostSource, Speed, TokenCounts}; use serde::{Deserialize, Serialize}; -use crate::controls; - const USD_MICROS_PER_USD_F64: f64 = 1_000_000.0; #[allow( @@ -156,7 +154,7 @@ impl ModelRef { ( self.provider.as_str(), self.model_id.as_str(), - self.speed.map_or("", controls::speed_name), + self.speed.map_or("", Speed::as_str), ) } } @@ -165,7 +163,7 @@ impl std::hash::Hash for ModelRef { fn hash(&self, state: &mut H) { self.provider.hash(state); self.model_id.hash(state); - self.speed.map(controls::speed_name).hash(state); + self.speed.map(Speed::as_str).hash(state); } } @@ -173,7 +171,7 @@ impl std::fmt::Display for ModelRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}/{}", self.provider, self.model_id)?; if let Some(speed) = self.speed { - write!(f, " ({})", controls::speed_name(speed))?; + write!(f, " ({speed})")?; } Ok(()) } diff --git a/lib/foundation/fabro-types/src/controls.rs b/lib/foundation/fabro-types/src/controls.rs index 9b3a54e6d..0017f6bc7 100644 --- a/lib/foundation/fabro-types/src/controls.rs +++ b/lib/foundation/fabro-types/src/controls.rs @@ -1,74 +1,17 @@ //! Helpers over the lithos request-control enums. //! -//! lithos owns [`ReasoningEffort`] and [`Speed`] and marks both -//! `#[non_exhaustive]`. Fabro needs to list, name, and parse them for -//! settings, graph attributes, and CLI flags, so the spellings live here in -//! one place. The names match the lithos serde form. +//! lithos owns [`ReasoningEffort`] and [`Speed`], their spellings, and their +//! parsing (`ALL`, `as_str`, `Display`, `FromStr`). What stays here is +//! Fabro's own rule for substituting a reasoning level a model lacks. pub use lithos_llm::types::{ReasoningEffort, Speed}; -/// Every reasoning effort, least to most. -pub const REASONING_EFFORTS: &[ReasoningEffort] = &[ - ReasoningEffort::Minimal, - ReasoningEffort::Low, - ReasoningEffort::Medium, - ReasoningEffort::High, - ReasoningEffort::Xhigh, - ReasoningEffort::Max, -]; - -/// Every speed tier. -pub const SPEEDS: &[Speed] = &[Speed::Fast, Speed::Balanced, Speed::Economical]; - -/// The wire spelling of a reasoning effort. -#[must_use] -pub fn reasoning_effort_name(effort: ReasoningEffort) -> &'static str { - match effort { - ReasoningEffort::Minimal => "minimal", - ReasoningEffort::Low => "low", - ReasoningEffort::Medium => "medium", - ReasoningEffort::High => "high", - ReasoningEffort::Xhigh => "xhigh", - ReasoningEffort::Max => "max", - _ => "unknown", - } -} - -/// The wire spelling of a speed tier. -#[must_use] -pub fn speed_name(speed: Speed) -> &'static str { - match speed { - Speed::Fast => "fast", - Speed::Balanced => "balanced", - Speed::Economical => "economical", - _ => "unknown", - } -} - -/// Parses a reasoning effort from its wire spelling. -#[must_use] -pub fn parse_reasoning_effort(value: &str) -> Option { - REASONING_EFFORTS - .iter() - .copied() - .find(|effort| reasoning_effort_name(*effort) == value) -} - -/// Parses a speed tier from its wire spelling. -#[must_use] -pub fn parse_speed(value: &str) -> Option { - SPEEDS - .iter() - .copied() - .find(|speed| speed_name(*speed) == value) -} - /// Position of an effort in the least-to-most ordering. fn effort_rank(effort: ReasoningEffort) -> usize { - REASONING_EFFORTS + ReasoningEffort::ALL .iter() .position(|candidate| *candidate == effort) - .unwrap_or(REASONING_EFFORTS.len()) + .unwrap_or(ReasoningEffort::ALL.len()) } /// Selects the supported effort nearest to `requested`. @@ -81,9 +24,8 @@ pub fn closest_supported_effort( supported: impl Fn(ReasoningEffort) -> bool, ) -> Option { let target = effort_rank(requested); - REASONING_EFFORTS - .iter() - .copied() + ReasoningEffort::ALL + .into_iter() .filter(|effort| supported(*effort)) .min_by_key(|effort| { let rank = effort_rank(*effort); @@ -96,22 +38,19 @@ mod tests { use super::*; #[test] - fn names_round_trip_through_serde() { - for effort in REASONING_EFFORTS { - let json = serde_json::to_string(effort).unwrap(); - assert_eq!(json, format!("\"{}\"", reasoning_effort_name(*effort))); - assert_eq!( - parse_reasoning_effort(reasoning_effort_name(*effort)), - Some(*effort) - ); + fn lithos_spellings_match_serde() { + for effort in ReasoningEffort::ALL { + let json = serde_json::to_string(&effort).unwrap(); + assert_eq!(json, format!("\"{effort}\"")); + assert_eq!(effort.as_str().parse::().unwrap(), effort); } - for speed in SPEEDS { - let json = serde_json::to_string(speed).unwrap(); - assert_eq!(json, format!("\"{}\"", speed_name(*speed))); - assert_eq!(parse_speed(speed_name(*speed)), Some(*speed)); + for speed in Speed::ALL { + let json = serde_json::to_string(&speed).unwrap(); + assert_eq!(json, format!("\"{speed}\"")); + assert_eq!(speed.as_str().parse::().unwrap(), speed); } - assert_eq!(parse_reasoning_effort("standard"), None); - assert_eq!(parse_speed("standard"), None); + assert!("standard".parse::().is_err()); + assert!("standard".parse::().is_err()); } #[test]