use oino_ai::{Model, ThinkingLevel};
use super::defaults::default_model_for_provider;
use super::reference::{
ModelReferenceRequest, ModelReferenceResult, ModelReferenceTarget, ThinkingSuffix,
resolve_model_reference,
};
use crate::collate::locale_compare;
#[derive(Clone, Copy, Debug, Default)]
pub struct CliModelRequest<'a> {
pub provider: Option<&'a str>,
pub model: Option<&'a str>,
pub thinking: Option<ThinkingLevel>,
pub inline_api_key: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CliModel {
pub model: Model,
pub thinking_level: Option<ThinkingLevel>,
pub warning: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub struct CliModelError(pub String);
fn build_fallback_model(provider_models: &[&Model], model_id: &str) -> Option<Model> {
let first = *provider_models.first()?;
let base = default_model_for_provider(&first.provider)
.and_then(|id| provider_models.iter().find(|m| m.id == id).copied())
.unwrap_or(first);
let mut model = base.clone();
model_id.clone_into(&mut model.id);
model_id.clone_into(&mut model.name);
Some(model)
}
pub fn resolve_cli_model(
request: &CliModelRequest<'_>,
models: &[Model],
has_auth: &dyn Fn(&str) -> bool,
) -> Result<Option<CliModel>, CliModelError> {
let Some(cli_model) = request.model else {
return Ok(None);
};
if models.is_empty() {
return Err(CliModelError(
"No models available. Check your installation or add models to models.json.".to_owned(),
));
}
let provider = match request.provider {
Some(cli_provider) => Some(
models
.iter()
.find(|m| m.provider.to_lowercase() == cli_provider.to_lowercase())
.map(|m| m.provider.clone())
.ok_or_else(|| {
CliModelError(format!(
"Unknown provider \"{cli_provider}\". Use --list-models to see available providers/models."
))
})?,
),
None => None,
};
let never = |_: &str| false;
let usable: &dyn Fn(&str) -> bool = if request.inline_api_key {
&never
} else {
has_auth
};
let result = resolve_model_reference(
&ModelReferenceRequest {
text: cli_model,
provider: provider.as_deref(),
thinking_suffix: ThinkingSuffix::Strict,
fuzzy: true,
},
models,
usable,
);
match result {
ModelReferenceResult::Resolved {
model,
thinking_level,
warning,
} => Ok(Some(CliModel {
model: model.clone(),
thinking_level,
warning,
})),
ModelReferenceResult::Ambiguous {
candidates,
usable_count,
..
} => Err(ambiguous_error(
cli_model,
&candidates,
usable_count,
request.inline_api_key,
)),
ModelReferenceResult::NotFound { target: None } => Err(CliModelError(format!(
"Model \"{cli_model}\" not found. Use --list-models to see available models."
))),
ModelReferenceResult::NotFound {
target: Some(target),
} => custom_model(cli_model, request.thinking, &target).map(Some),
}
}
fn ambiguous_error(
cli_model: &str,
candidates: &[&Model],
usable_count: usize,
inline_api_key: bool,
) -> CliModelError {
let mut matches: Vec<String> = candidates
.iter()
.map(|m| format!("{}/{}", m.provider, m.id))
.collect();
matches.sort_by(|a, b| locale_compare(a, b));
let hint = if inline_api_key {
"--api-key does not choose a provider."
} else if usable_count == 0 {
"No matching provider is authenticated."
} else {
"More than one matching provider is authenticated."
};
CliModelError(format!(
"Model \"{cli_model}\" is ambiguous across providers: {}. {hint} Use --provider or provider/model.",
matches.join(", ")
))
}
fn custom_model(
cli_model: &str,
thinking: Option<ThinkingLevel>,
target: &ModelReferenceTarget<'_>,
) -> Result<CliModel, CliModelError> {
let model_id = if thinking.is_some() {
&target.model_text
} else {
&target.model_id
};
let fallback_thinking = if thinking.is_some() {
None
} else {
target.thinking_level
};
let mut model = build_fallback_model(&target.provider_models, model_id).ok_or_else(|| {
CliModelError(format!(
"Model \"{cli_model}\" not found. Use --list-models to see available models."
))
})?;
if thinking
.or(fallback_thinking)
.is_some_and(|level| level != ThinkingLevel::Off)
{
model.reasoning = true;
}
Ok(CliModel {
model,
thinking_level: fallback_thinking,
warning: Some(format!(
"Model \"{model_id}\" not found for provider \"{}\". Using custom model id.",
target.provider
)),
})
}