use crate::Settings;
use crate::session::manager::SessionManager;
use anyhow::{Context as _, Result};
use kiss_ai::{Model, Registry, ThinkingLevel};
pub struct ModelRequest<'a> {
pub pattern: Option<&'a str>,
pub provider: Option<&'a str>,
pub api_key_given: bool,
pub import_external_credentials: bool,
}
pub fn choose(
request: &ModelRequest<'_>,
settings: &Settings,
registry: &Registry,
) -> Result<Option<(Model, Option<ThinkingLevel>)>> {
if let Some(pattern) = &request.pattern {
return registry
.resolve(pattern, request.provider)
.map(Some)
.with_context(|| format!("no model matches pattern '{pattern}'"));
}
if let (Some(provider), Some(model)) = (&settings.default_provider, &settings.default_model)
&& let Some(found) = registry.resolve(model, Some(provider))
{
return Ok(Some(found));
}
if let Some(provider) = request.provider.or(settings.default_provider.as_deref())
&& let Some(model) = registry.provider_default(provider)
{
return Ok(Some((model, None)));
}
if request.api_key_given {
let provider = request.provider.unwrap_or("anthropic");
if let Some(model) = registry.provider_default(provider) {
return Ok(Some((model, None)));
}
}
if let Some(model) = registry
.all()
.iter()
.find(|model| registry.signed_in(&model.provider))
{
return Ok(Some((model.clone(), None)));
}
if !request.import_external_credentials {
return Ok(None);
}
let external = kiss_ai::auth::external::discover();
let mut checked = std::collections::HashSet::new();
for model in registry.all() {
let credential_provider = registry.credential_provider(&model.provider);
if !checked.insert(credential_provider) {
continue;
}
if kiss_ai::auth::external::auto_import_unique_from_sources(credential_provider, &external)
.ok()
.flatten()
.is_some()
&& registry.signed_in(&model.provider)
{
return Ok(Some((model.clone(), None)));
}
}
Ok(None)
}
pub fn for_session(
request: &ModelRequest<'_>,
thinking: Option<ThinkingLevel>,
chosen: (Model, Option<ThinkingLevel>),
manager: &SessionManager,
settings: &Settings,
registry: &Registry,
) -> (Model, ThinkingLevel) {
let (model, pattern_thinking) = chosen;
let (saved_model, saved_thinking) = manager.session_settings();
let model = saved_model
.filter(|_| request.pattern.is_none() && request.provider.is_none())
.and_then(|(provider, id)| registry.resolve(&id, Some(&provider)))
.map_or(model, |(model, _)| model);
let thinking = thinking
.or(pattern_thinking)
.or(saved_thinking)
.or_else(|| {
settings
.default_thinking_level
.as_deref()
.and_then(ThinkingLevel::parse)
})
.unwrap_or(ThinkingLevel::Off);
let thinking = model.clamp_thinking_level(thinking);
(model, thinking)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_resumed_session_keeps_its_model_unless_the_caller_names_one() {
let registry = Registry::from_builtin();
let settings = Settings::default();
let mut manager = SessionManager::in_memory(std::path::Path::new("/"));
let saved = registry.provider_default("anthropic").unwrap();
manager
.append_model_change(&saved.provider, &saved.id)
.unwrap();
manager
.append_thinking_level_change(ThinkingLevel::High)
.unwrap();
let chosen = registry.provider_default("openai").unwrap();
let request = |provider| ModelRequest {
pattern: None,
provider,
api_key_given: true,
import_external_credentials: false,
};
let (model, thinking) = for_session(
&request(None),
None,
(chosen.clone(), None),
&manager,
&settings,
®istry,
);
assert_eq!(
(model.provider.as_str(), thinking),
("anthropic", ThinkingLevel::High)
);
let (model, thinking) = for_session(
&request(Some("openai")),
Some(ThinkingLevel::Low),
(chosen, None),
&manager,
&settings,
®istry,
);
assert_eq!(
(model.provider.as_str(), thinking),
("openai", ThinkingLevel::Low)
);
let plain = registry
.all()
.iter()
.find(|model| !model.reasoning)
.unwrap()
.clone();
let named = ModelRequest {
pattern: Some(&plain.id),
..request(None)
};
let (_, thinking) = for_session(
&named,
None,
(plain.clone(), None),
&manager,
&settings,
®istry,
);
assert_eq!(thinking, ThinkingLevel::Off);
}
}