Use the lithos closest_supported_effort on ModelCapabilities

The reasoning-effort substitution rule now lives on lithos's
ModelCapabilities, so the workflow fallback planner calls it directly
and fabro-types drops its controls module. ReasoningEffort is re-exported
from lithos alongside the other request types.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-10 09:44:11 -06:00
parent 87e1e3a00f
commit f8df58966b
No known key found for this signature in database
3 changed files with 4 additions and 84 deletions

View file

@ -20,7 +20,7 @@ use fabro_types::settings::run::RunModelControls;
use fabro_types::{
AgentProfileKind, FailoverProps, Message, ModelHandle, ModelId, ModelRef, PermissionLevel,
ProviderId, ReasoningEffort, Role, RunId, SessionCapability, Speed, StageId, StageTiming,
TokenCounts, ToolDefinition as LlmToolDefinition, UsdMicros, billing, controls,
TokenCounts, ToolDefinition as LlmToolDefinition, UsdMicros, billing,
};
use serde::de::DeserializeOwned;
use tokio::sync::mpsc;
@ -897,9 +897,7 @@ impl AgentApiBackend {
return FallbackControls::Usable(requested);
};
let capabilities = offering.model.capabilities();
let effective_effort = controls::closest_supported_effort(requested_effort, |effort| {
capabilities.reasoning_effort(effort).is_supported()
});
let effective_effort = capabilities.closest_supported_effort(requested_effort);
match effective_effort {
Some(effort) => FallbackControls::Usable(EffectiveRequestControls {
reasoning_effort: Some(effort),

View file

@ -1,76 +0,0 @@
//! Helpers over the lithos request-control enums.
//!
//! 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};
/// Position of an effort in the least-to-most ordering.
fn effort_rank(effort: ReasoningEffort) -> usize {
ReasoningEffort::ALL
.iter()
.position(|candidate| *candidate == effort)
.unwrap_or(ReasoningEffort::ALL.len())
}
/// Selects the supported effort nearest to `requested`.
///
/// When two supported values are equally distant, the higher effort wins.
/// Returns `None` when nothing is supported.
#[must_use]
pub fn closest_supported_effort(
requested: ReasoningEffort,
supported: impl Fn(ReasoningEffort) -> bool,
) -> Option<ReasoningEffort> {
let target = effort_rank(requested);
ReasoningEffort::ALL
.into_iter()
.filter(|effort| supported(*effort))
.min_by_key(|effort| {
let rank = effort_rank(*effort);
(rank.abs_diff(target), std::cmp::Reverse(rank))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
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 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!("standard".parse::<ReasoningEffort>().is_err());
assert!("standard".parse::<Speed>().is_err());
}
#[test]
fn closest_supported_prefers_the_higher_neighbor_on_ties() {
let supported = |effort| matches!(effort, ReasoningEffort::Low | ReasoningEffort::High);
assert_eq!(
closest_supported_effort(ReasoningEffort::Medium, supported),
Some(ReasoningEffort::High)
);
assert_eq!(
closest_supported_effort(ReasoningEffort::Max, supported),
Some(ReasoningEffort::High)
);
assert_eq!(
closest_supported_effort(ReasoningEffort::Minimal, supported),
Some(ReasoningEffort::Low)
);
assert_eq!(
closest_supported_effort(ReasoningEffort::Medium, |_| false),
None
);
}
}

View file

@ -11,7 +11,6 @@ pub mod catalog_api;
pub mod checkpoint;
pub mod command_output;
pub mod conclusion;
pub mod controls;
pub mod dense;
pub mod diff;
pub mod event_envelope;
@ -76,7 +75,6 @@ pub use catalog_api::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimi
pub use checkpoint::Checkpoint;
pub use command_output::{CommandOutputStream, CommandTermination};
pub use conclusion::{Conclusion, StageSummary};
pub use controls::ReasoningEffort;
pub use dense::{ServerSettings, UserSettings, WorkflowSettings};
pub use diff::{DiffStats, DiffSummary, RunDiff};
pub use event_envelope::EventEnvelope;
@ -94,8 +92,8 @@ pub use interview::{
};
pub use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId};
pub use lithos_llm::types::{
FinishReason, ReasoningOutput, Request, RequestBuildError, RequestBuilder, Response,
ResponseFormat, StreamEvent,
FinishReason, ReasoningEffort, ReasoningOutput, Request, RequestBuildError, RequestBuilder,
Response, ResponseFormat, StreamEvent,
};
pub use llm_backend::AgentBackend;
pub use manifest_path::{ManifestPath, ManifestPathParseError};