refactor: adopt strum for enum string conversions

Replace hand-written Display/FromStr/as_str boilerplate with strum
derives on Provider, RunStatus, StatusReason, Speed, ReasoningEffort,
SandboxProvider, Fidelity, ModelTestMode, ModelTestStatus. Update a few
downstream callers whose FromStr::Err = String assumption no longer
holds. Net -172 lines, zero wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-22 17:16:25 -04:00
parent 3e2e1d3dbe
commit 18cfd1b92d
No known key found for this signature in database
19 changed files with 147 additions and 305 deletions

View file

@ -111,6 +111,20 @@ When interpolating values into shell command strings (in `fabro-workflow`), alwa
- **Functions**: import the parent module, call as `module::function()` — `use fabro_workflow::operations; operations::create(...)`
- **No glob imports** in production code (`use foo::*`). Globs are acceptable in test modules and preludes. Enforced by clippy `wildcard_imports` lint.
## Enum string/int conversions (strum)
For any enum where a variant maps to a fixed string or integer, derive it with `strum` instead of hand-writing `impl Display`, `impl FromStr`, `as_str()`, `fn all()`, or `const ALL: &[Self]`. Hand-written variant→string maps drift across the three impls on every rename.
- `strum::Display` replaces hand-written `impl fmt::Display` whose body is a match over string literals.
- `strum::EnumString` replaces hand-written `impl FromStr`. The `Err` type becomes `strum::ParseError` — adjust callers that assumed `Err = String`.
- `strum::IntoStaticStr` replaces `impl From<E> for &'static str`. When an existing `as_str(self) -> &'static str` is on the public API, keep it as a one-line wrapper: `pub fn as_str(self) -> &'static str { self.into() }`.
- `strum::EnumIter`, `strum::VariantArray`, `strum::VariantNames` replace hand-written `fn all()` / `const ALL` / `&[&'static str]` arrays. Do NOT use these if the hand-written list intentionally excludes variants (e.g. `Provider::ALL` skips `OpenAiCompatible`).
- `strum::FromRepr` replaces `fn from_u8`/`from_i32`. Note: it returns `Option<Self>`, so don't adopt it when the existing conversion has a `_ => default` fallback — that's a behavior change, not a cleanup.
Align strum with serde. When the enum also derives `Serialize`/`Deserialize` with `#[serde(rename_all = "...")]`, add the matching `#[strum(serialize_all = "...")]`. For variant aliases, use `#[strum(to_string = "canonical", serialize = "alias")]` — strum picks the last `serialize` for `Display`/`IntoStaticStr` otherwise, so `to_string` is needed to pin the canonical form.
Skip strum when parsing is fuzzy (URL/path detection, structured IDs, multi-token formats), when a variant carries a `String` catch-all, or when `Display` does dynamic formatting.
## Snapshot tests (insta)
Many CLI tests use `insta` inline snapshots. When a snapshot needs updating:

26
Cargo.lock generated
View file

@ -1790,6 +1790,7 @@ dependencies = [
"nom",
"regex",
"serde",
"strum",
"thiserror 2.0.18",
]
@ -1874,6 +1875,7 @@ dependencies = [
"rand 0.9.4",
"serde",
"serde_json",
"strum",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
@ -1914,6 +1916,7 @@ dependencies = [
"insta",
"serde",
"serde_json",
"strum",
]
[[package]]
@ -1984,6 +1987,7 @@ dependencies = [
"serde",
"serde_json",
"shlex",
"strum",
"tar",
"tempfile",
"tokio",
@ -2207,6 +2211,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum",
"tempfile",
"toml 0.8.23",
"ulid",
@ -6290,6 +6295,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "subtle"
version = "2.6.1"

View file

@ -80,6 +80,7 @@ percent-encoding = "2"
minijinja = "2"
fabro-http = { path = "lib/crates/fabro-http" }
graphviz-sys = { git = "https://github.com/fabro-sh/graphviz-sys" }
strum = { version = "0.28", features = ["derive"] }
[workspace.lints.rust]
unsafe_code = "deny"

View file

@ -420,12 +420,10 @@ pub async fn run_with_args_and_client(
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
// Parse provider string to enum early for compile-time safety
let provider: Provider = args
.provider
.as_deref()
.unwrap_or("anthropic")
let provider_str = args.provider.as_deref().unwrap_or("anthropic");
let provider: Provider = provider_str
.parse()
.map_err(|e: String| anyhow::anyhow!("{e}"))?;
.map_err(|_| anyhow::anyhow!("unknown provider: {provider_str}"))?;
// Build LLM client — use provided client or create from env
let mut client = if let Some(c) = llm_client {

View file

@ -19,4 +19,5 @@ fabro-types = { path = "../fabro-types" }
nom = "7"
regex = { workspace = true }
serde = { workspace = true }
strum.workspace = true
thiserror = { workspace = true }

View file

@ -1,9 +1,9 @@
use std::fmt;
use std::str::FromStr;
use strum::{Display, EnumString};
/// Fidelity mode controlling how much prior context is provided to LLM
/// sessions.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum Fidelity {
/// Complete context, no summarization — sessions share a thread.
Full,
@ -13,10 +13,13 @@ pub enum Fidelity {
#[default]
Compact,
/// Brief textual summary (~600 token target).
#[strum(serialize = "summary:low")]
SummaryLow,
/// Moderate textual summary (~1500 token target).
#[strum(serialize = "summary:medium")]
SummaryMedium,
/// Detailed per-stage Markdown report.
#[strum(serialize = "summary:high")]
SummaryHigh,
}
@ -31,36 +34,6 @@ impl Fidelity {
}
}
impl fmt::Display for Fidelity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Full => "full",
Self::Truncate => "truncate",
Self::Compact => "compact",
Self::SummaryLow => "summary:low",
Self::SummaryMedium => "summary:medium",
Self::SummaryHigh => "summary:high",
};
write!(f, "{s}")
}
}
impl FromStr for Fidelity {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"full" => Ok(Self::Full),
"truncate" => Ok(Self::Truncate),
"compact" => Ok(Self::Compact),
"summary:low" => Ok(Self::SummaryLow),
"summary:medium" => Ok(Self::SummaryMedium),
"summary:high" => Ok(Self::SummaryHigh),
other => Err(format!("unknown fidelity mode: {other}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -21,6 +21,7 @@ anyhow.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
tokio.workspace = true
uuid.workspace = true
rand.workspace = true

View file

@ -1,8 +1,8 @@
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use fabro_model::Model;
use strum::{EnumString, IntoStaticStr};
use tokio::time;
use crate::client::Client;
@ -10,7 +10,8 @@ use crate::generate::{self, GenerateParams};
use crate::tools::Tool;
use crate::types::{GenerateResult, ReasoningEffort};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumString, IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
pub enum ModelTestMode {
#[default]
Basic,
@ -19,11 +20,8 @@ pub enum ModelTestMode {
impl ModelTestMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Basic => "basic",
Self::Deep => "deep",
}
pub fn as_str(self) -> &'static str {
self.into()
}
#[must_use]
@ -35,19 +33,8 @@ impl ModelTestMode {
}
}
impl FromStr for ModelTestMode {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"basic" => Ok(Self::Basic),
"deep" => Ok(Self::Deep),
other => Err(format!("invalid model test mode: {other}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
pub enum ModelTestStatus {
Ok,
Error,
@ -55,11 +42,8 @@ pub enum ModelTestStatus {
impl ModelTestStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Error => "error",
}
pub fn as_str(self) -> &'static str {
self.into()
}
}

View file

@ -411,8 +411,21 @@ pub struct RateLimitInfo {
// --- 3.8 ReasoningEffort ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
strum::IntoStaticStr,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort {
Low,
Medium,
@ -423,35 +436,7 @@ pub enum ReasoningEffort {
impl ReasoningEffort {
pub fn as_str(&self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Max => "max",
}
}
}
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),
"xhigh" => Ok(Self::XHigh),
"max" => Ok(Self::Max),
other => Err(format!(
"invalid reasoning_effort: {other:?} (expected low, medium, high, xhigh, or max)"
)),
}
(*self).into()
}
}
@ -1318,12 +1303,8 @@ mod tests {
}
#[test]
fn reasoning_effort_from_str_rejects_unknown_with_updated_error() {
fn reasoning_effort_from_str_rejects_unknown() {
use std::str::FromStr;
let err = ReasoningEffort::from_str("bogus").expect_err("should reject");
assert!(
err.contains("low, medium, high, xhigh, or max"),
"error should list all accepted levels, got: {err}"
);
assert!(ReasoningEffort::from_str("bogus").is_err());
}
}

View file

@ -15,6 +15,7 @@ workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
[dev-dependencies]
insta.workspace = true

View file

@ -1,6 +1,5 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
use crate::{Model, Provider};
@ -96,8 +95,21 @@ impl PricePerMTok {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
IntoStaticStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Speed {
Standard,
Fast,
@ -106,28 +118,7 @@ pub enum Speed {
impl Speed {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Standard => "standard",
Self::Fast => "fast",
}
}
}
impl FromStr for Speed {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"standard" => Ok(Self::Standard),
"fast" => Ok(Self::Fast),
other => Err(format!("unknown speed: {other}")),
}
}
}
impl std::fmt::Display for Speed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
self.into()
}
}

View file

@ -1,25 +1,39 @@
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
// ---------------------------------------------------------------------------
// Provider enum — compile-time safe provider identity
// ---------------------------------------------------------------------------
/// Known LLM provider variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
IntoStaticStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Provider {
Anthropic,
#[serde(rename = "openai", alias = "open_ai")]
#[strum(to_string = "openai", serialize = "open_ai")]
OpenAi,
Gemini,
Kimi,
Zai,
Minimax,
#[strum(to_string = "inception", serialize = "inception_labs")]
Inception,
#[serde(rename = "openai_compatible", alias = "open_ai_compatible")]
#[strum(to_string = "openai_compatible", serialize = "open_ai_compatible")]
OpenAiCompatible,
}
@ -106,40 +120,7 @@ impl Provider {
/// adapter names, and other serialization boundaries.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Anthropic => "anthropic",
Self::OpenAi => "openai",
Self::Gemini => "gemini",
Self::Kimi => "kimi",
Self::Zai => "zai",
Self::Minimax => "minimax",
Self::Inception => "inception",
Self::OpenAiCompatible => "openai_compatible",
}
}
}
impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Provider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"anthropic" => Ok(Self::Anthropic),
"openai" | "open_ai" => Ok(Self::OpenAi),
"gemini" => Ok(Self::Gemini),
"kimi" => Ok(Self::Kimi),
"zai" => Ok(Self::Zai),
"minimax" => Ok(Self::Minimax),
"inception" | "inception_labs" => Ok(Self::Inception),
"openai_compatible" => Ok(Self::OpenAiCompatible),
other => Err(format!("unknown provider: {other}")),
}
self.into()
}
}

View file

@ -26,6 +26,7 @@ tokio.workspace = true
tokio-util.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
tracing.workspace = true
base64.workspace = true
fabro-proc = { path = "../fabro-proc" }

View file

@ -1,8 +1,8 @@
use std::fmt;
use std::str::FromStr;
use strum::{Display, EnumString};
/// Sandbox provider for agent tool operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, EnumString)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum SandboxProvider {
/// Run tools on the local host (default)
#[default]
@ -22,29 +22,6 @@ impl SandboxProvider {
}
}
impl fmt::Display for SandboxProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => write!(f, "local"),
Self::Docker => write!(f, "docker"),
Self::Daytona => write!(f, "daytona"),
}
}
}
impl FromStr for SandboxProvider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"docker" => Ok(Self::Docker),
"daytona" => Ok(Self::Daytona),
other => Err(format!("unknown sandbox provider: {other}")),
}
}
}
#[cfg(test)]
mod tests {
use super::SandboxProvider;

View file

@ -6751,7 +6751,13 @@ async fn list_models(
let provider = match params.provider.as_deref() {
Some(value) => match fabro_model::Provider::from_str(value) {
Ok(provider) => Some(provider),
Err(err) => return ApiError::new(StatusCode::BAD_REQUEST, err).into_response(),
Err(_) => {
return ApiError::new(
StatusCode::BAD_REQUEST,
format!("unknown provider: {value}"),
)
.into_response();
}
},
None => None,
};
@ -6799,7 +6805,13 @@ async fn test_model(
let mode = match params.mode.as_deref() {
Some(value) => match ModelTestMode::from_str(value) {
Ok(mode) => mode,
Err(err) => return ApiError::new(StatusCode::BAD_REQUEST, err).into_response(),
Err(_) => {
return ApiError::new(
StatusCode::BAD_REQUEST,
format!("invalid model test mode: {value}"),
)
.into_response();
}
},
None => ModelTestMode::Basic,
};
@ -11203,14 +11215,14 @@ timeout = "30s"
.send(test_event_envelope(
1,
run_two,
EventBody::RunQueued(Default::default()),
EventBody::RunQueued(fabro_types::run_event::RunStatusEffectProps::default()),
))
.unwrap();
event_tx
.send(test_event_envelope(
2,
run_one,
EventBody::RunQueued(Default::default()),
EventBody::RunQueued(fabro_types::run_event::RunStatusEffectProps::default()),
))
.unwrap();
drop(event_tx);

View file

@ -29,6 +29,7 @@ ipnet = { version = "2.11.0", features = ["serde"] }
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
strum.workspace = true
toml.workspace = true
ulid.workspace = true

View file

@ -63,6 +63,5 @@ pub use sandbox_record::SandboxRecord;
pub use stage_id::{ParallelBranchId, StageId};
pub use start::StartRecord;
pub use status::{
BlockedReason, InvalidTransition, ParseRunStatusError, RunControlAction, RunStatus,
RunStatusRecord, StatusReason,
BlockedReason, InvalidTransition, RunControlAction, RunStatus, RunStatusRecord, StatusReason,
};

View file

@ -1,11 +1,12 @@
use std::fmt;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RunStatus {
Submitted,
Queued,
@ -91,57 +92,6 @@ impl RunStatus {
}
}
impl fmt::Display for RunStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Submitted => "submitted",
Self::Queued => "queued",
Self::Starting => "starting",
Self::Running => "running",
Self::Blocked => "blocked",
Self::Paused => "paused",
Self::Removing => "removing",
Self::Succeeded => "succeeded",
Self::Failed => "failed",
Self::Dead => "dead",
Self::Archived => "archived",
};
f.write_str(s)
}
}
impl FromStr for RunStatus {
type Err = ParseRunStatusError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"submitted" => Ok(Self::Submitted),
"queued" => Ok(Self::Queued),
"starting" => Ok(Self::Starting),
"running" => Ok(Self::Running),
"blocked" => Ok(Self::Blocked),
"paused" => Ok(Self::Paused),
"removing" => Ok(Self::Removing),
"succeeded" => Ok(Self::Succeeded),
"failed" => Ok(Self::Failed),
"dead" => Ok(Self::Dead),
"archived" => Ok(Self::Archived),
_ => Err(ParseRunStatusError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct ParseRunStatusError(String);
impl fmt::Display for ParseRunStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid run status: {:?}", self.0)
}
}
impl std::error::Error for ParseRunStatusError {}
#[derive(Debug, Clone, PartialEq)]
pub struct InvalidTransition {
pub from: RunStatus,
@ -156,8 +106,9 @@ impl fmt::Display for InvalidTransition {
impl std::error::Error for InvalidTransition {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum StatusReason {
Completed,
PartialSuccess,
@ -172,57 +123,6 @@ pub enum StatusReason {
SandboxInitializing,
}
impl fmt::Display for StatusReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Completed => "completed",
Self::PartialSuccess => "partial_success",
Self::WorkflowError => "workflow_error",
Self::Cancelled => "cancelled",
Self::Terminated => "terminated",
Self::TransientInfra => "transient_infra",
Self::BudgetExhausted => "budget_exhausted",
Self::LaunchFailed => "launch_failed",
Self::BootstrapFailed => "bootstrap_failed",
Self::SandboxInitFailed => "sandbox_init_failed",
Self::SandboxInitializing => "sandbox_initializing",
};
f.write_str(s)
}
}
impl FromStr for StatusReason {
type Err = ParseStatusReasonError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"completed" => Ok(Self::Completed),
"partial_success" => Ok(Self::PartialSuccess),
"workflow_error" => Ok(Self::WorkflowError),
"cancelled" => Ok(Self::Cancelled),
"terminated" => Ok(Self::Terminated),
"transient_infra" => Ok(Self::TransientInfra),
"budget_exhausted" => Ok(Self::BudgetExhausted),
"launch_failed" => Ok(Self::LaunchFailed),
"bootstrap_failed" => Ok(Self::BootstrapFailed),
"sandbox_init_failed" => Ok(Self::SandboxInitFailed),
"sandbox_initializing" => Ok(Self::SandboxInitializing),
_ => Err(ParseStatusReasonError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct ParseStatusReasonError(String);
impl fmt::Display for ParseStatusReasonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid status reason: {:?}", self.0)
}
}
impl std::error::Error for ParseStatusReasonError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockedReason {

View file

@ -335,12 +335,12 @@ impl RunSession {
.map(InterpString::as_source)
.filter(|value| !value.is_empty());
let provider_enum: Provider = provider
.as_deref()
.map(str::parse::<Provider>)
.transpose()
.map_err(|err| Error::Precondition(err.clone()))?
.unwrap_or_else(|| Provider::default_for_configured(&configured));
let provider_enum: Provider = match provider.as_deref() {
Some(value) => value
.parse::<Provider>()
.map_err(|_| Error::Precondition(format!("unknown provider: {value}")))?,
None => Provider::default_for_configured(&configured),
};
let fallback_chain = resolve_fallback_chain(provider_enum, &model, &resolved.model);
let mcp_servers = resolved