mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Use the lithos control enum API instead of local spellings
lithos-llm now exposes `ReasoningEffort::ALL`, `Speed::ALL`, `as_str`, `Display`, and `FromStr` on its request-control enums. Fabro's `controls` module kept parallel name tables and parsers for them; only the nearest-supported-effort rule is Fabro's own, so that is what stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2a2fc41807
commit
b82b3dd48e
8 changed files with 47 additions and 119 deletions
|
|
@ -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<ReasoningEffort, String> {
|
||||
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::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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::<ReasoningEffort>() {
|
||||
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::<Vec<_>>()
|
||||
.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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<ReasoningEffort, Error> {
|
||||
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<Speed, Error> {
|
||||
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)),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<H: std::hash::Hasher>(&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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReasoningEffort> {
|
||||
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<Speed> {
|
||||
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<ReasoningEffort> {
|
||||
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::<ReasoningEffort>().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::<Speed>().unwrap(), speed);
|
||||
}
|
||||
assert_eq!(parse_reasoning_effort("standard"), None);
|
||||
assert_eq!(parse_speed("standard"), None);
|
||||
assert!("standard".parse::<ReasoningEffort>().is_err());
|
||||
assert!("standard".parse::<Speed>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue