use crate::config::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProviderKey {
pub provider: &'static str,
pub model: &'static str,
}
impl ProviderKey {
pub const SELF_IMPROVEMENT: Self = Self {
provider: "self_improvement_provider",
model: "self_improvement_model",
};
pub const SUBAGENT: Self = Self {
provider: "subagent_provider",
model: "subagent_model",
};
pub const PLAN: Self = Self {
provider: "plan_provider",
model: "plan_model",
};
pub const EXECUTE: Self = Self {
provider: "execute_provider",
model: "execute_model",
};
pub const FALLBACK_PROVIDERS: Self = Self {
provider: "[providers.fallback] providers",
model: "that provider's own default_model",
};
pub const FALLBACK_VISION: Self = Self {
provider: "[providers.fallback] vision",
model: "that provider's own vision_model",
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderPair {
pub provider: String,
pub model: Option<String>,
pub note: Option<String>,
}
const CUSTOM_PREFIXES: [&str; 3] = ["custom:", "custom.", "custom/"];
pub fn normalize(
key: ProviderKey,
spec: &str,
model_key: Option<&str>,
is_custom: impl Fn(&str) -> bool,
is_declared: impl Fn(&str) -> bool,
) -> ProviderPair {
let raw = spec.trim();
let mut notes: Vec<String> = Vec::new();
let mut name = raw;
for prefix in CUSTOM_PREFIXES {
if let Some(rest) = name.strip_prefix(prefix) {
name = rest.trim();
notes.push(format!(
"dropped the '{prefix}' prefix: the provider name is the \
[providers.custom.<name>] section name, so write \"{name}\""
));
break;
}
}
let mut model_from_spec: Option<String> = None;
if let Some(idx) = name.find(['/', ':']) {
let (head, tail) = (name[..idx].trim(), name[idx + 1..].trim());
if !head.is_empty() && !tail.is_empty() && (is_custom(head) || is_declared(head)) {
notes.push(format!(
"split \"{name}\" into provider \"{head}\" and model \"{tail}\": \
{} takes a provider name only; the model goes in {}",
key.provider, key.model
));
model_from_spec = Some(tail.to_string());
name = head;
}
}
let model = match (
model_key.map(str::trim).filter(|m| !m.is_empty()),
model_from_spec,
) {
(Some(explicit), Some(found)) if explicit != found => {
notes.push(format!(
"{} = \"{explicit}\" wins over the \"{found}\" found in {}",
key.model, key.provider
));
Some(explicit.to_string())
}
(Some(explicit), _) => Some(explicit.to_string()),
(None, found) => found,
};
ProviderPair {
provider: name.to_string(),
model,
note: (!notes.is_empty()).then(|| notes.join("; ")),
}
}
pub fn normalize_in(
config: &Config,
key: ProviderKey,
spec: &str,
model_key: Option<&str>,
) -> ProviderPair {
normalize(
key,
spec,
model_key,
|name| {
config
.providers
.custom
.as_ref()
.is_some_and(|m| m.contains_key(name))
},
|name| config.providers.is_declared(name),
)
}