use std::sync::OnceLock;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModelCatalogEntry {
pub provider: String,
pub model: String,
pub display_name: String,
pub auth_modes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoginTarget {
pub provider: String,
pub auth: String,
pub label: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModelSelection {
pub provider: String,
pub model: String,
pub auth: String,
pub from_catalog: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ModelSelectionError {
#[error("unknown provider '{provider}' for model selection")]
UnknownProvider { provider: String },
#[error("model '{model}' is available from multiple providers; use /model provider/model")]
AmbiguousModel { model: String },
#[error("model selection cannot be empty")]
Empty,
}
#[derive(Deserialize)]
struct ModelCatalogFile {
openai_api_models: Vec<String>,
openai_codex_models: Vec<String>,
}
const MODEL_CATALOG_TOML: &str = include_str!("models.toml");
const IMPLEMENTED_PROVIDERS: &[&str] = &["openai", "openai-codex"];
static MODEL_CATALOG: OnceLock<Vec<ModelCatalogEntry>> = OnceLock::new();
pub fn implemented_providers() -> &'static [&'static str] {
IMPLEMENTED_PROVIDERS
}
pub fn model_catalog() -> &'static [ModelCatalogEntry] {
MODEL_CATALOG.get_or_init(|| parse_model_catalog(MODEL_CATALOG_TOML))
}
pub fn available_models_for_auths(auths: &[String]) -> Vec<ModelCatalogEntry> {
available_models_for_auths_from(model_catalog(), auths)
}
pub fn login_targets() -> Vec<LoginTarget> {
vec![
LoginTarget {
provider: "openai".into(),
auth: "api-key".into(),
label: "OpenAI API key".into(),
},
LoginTarget {
provider: "openai-codex".into(),
auth: "codex".into(),
label: "Codex OAuth".into(),
},
]
}
pub fn login_target_for_provider(provider: &str) -> Option<LoginTarget> {
login_targets()
.into_iter()
.find(|target| target.provider == provider)
}
pub fn default_model_for_provider(provider: &str) -> Option<String> {
model_catalog()
.iter()
.find(|entry| entry.provider == provider)
.map(|entry| entry.model.clone())
}
pub fn resolve_model_selection_for_auths(
input: &str,
current_provider: &str,
auth: &str,
available_auths: &[String],
) -> Result<ModelSelection, ModelSelectionError> {
resolve_model_selection_from(
model_catalog(),
input,
current_provider,
auth,
available_auths,
)
}
fn parse_model_catalog(text: &str) -> Vec<ModelCatalogEntry> {
let file: ModelCatalogFile =
toml::from_str(text).expect("embedded model catalog must be valid");
let mut entries = Vec::new();
entries.extend(model_entries("openai", "api-key", file.openai_api_models));
entries.extend(model_entries(
"openai-codex",
"codex",
file.openai_codex_models,
));
entries
}
fn model_entries(provider: &str, auth: &str, models: Vec<String>) -> Vec<ModelCatalogEntry> {
models
.into_iter()
.map(|model| ModelCatalogEntry {
provider: provider.to_string(),
display_name: model.clone(),
model,
auth_modes: vec![auth.to_string()],
})
.collect()
}
fn available_models_for_auths_from(
catalog: &[ModelCatalogEntry],
auths: &[String],
) -> Vec<ModelCatalogEntry> {
let mut models = catalog
.iter()
.filter(|entry| implemented_providers().contains(&entry.provider.as_str()))
.filter(|entry| {
entry
.auth_modes
.iter()
.any(|mode| auths.iter().any(|auth| auth == mode))
})
.cloned()
.collect::<Vec<_>>();
models.sort_by(|left, right| {
left.provider
.cmp(&right.provider)
.then_with(|| left.model.cmp(&right.model))
});
models
}
fn provider_default_auth(provider: &str) -> Option<&'static str> {
match provider {
"openai" => Some("api-key"),
"openai-codex" => Some("codex"),
_ => None,
}
}
fn selection_from_entry(entry: &ModelCatalogEntry) -> ModelSelection {
ModelSelection {
provider: entry.provider.clone(),
model: entry.model.clone(),
auth: entry
.auth_modes
.first()
.map(String::as_str)
.unwrap_or("api-key")
.to_string(),
from_catalog: true,
}
}
fn resolve_model_selection_from(
catalog: &[ModelCatalogEntry],
input: &str,
current_provider: &str,
auth: &str,
available_auths: &[String],
) -> Result<ModelSelection, ModelSelectionError> {
let input = input.trim();
if input.is_empty() {
return Err(ModelSelectionError::Empty);
}
if let Some((provider, model)) = input.split_once('/') {
let provider = provider.trim();
let model = model.trim();
if provider.is_empty() || model.is_empty() {
return Err(ModelSelectionError::Empty);
}
if !IMPLEMENTED_PROVIDERS.contains(&provider) {
return Err(ModelSelectionError::UnknownProvider {
provider: provider.to_string(),
});
}
if let Some(entry) = catalog
.iter()
.find(|entry| entry.provider == provider && entry.model == model)
{
return Ok(selection_from_entry(entry));
}
return Ok(ModelSelection {
provider: provider.to_string(),
model: model.to_string(),
auth: provider_default_auth(provider).unwrap_or(auth).to_string(),
from_catalog: false,
});
}
let auths = if available_auths.is_empty() {
vec![auth.to_string()]
} else {
available_auths.to_vec()
};
let matches = available_models_for_auths_from(catalog, &auths)
.into_iter()
.filter(|entry| entry.model == input)
.collect::<Vec<_>>();
match matches.as_slice() {
[entry] => Ok(selection_from_entry(entry)),
[] => Ok(ModelSelection {
provider: current_provider.to_string(),
model: input.to_string(),
auth: provider_default_auth(current_provider)
.unwrap_or(auth)
.to_string(),
from_catalog: false,
}),
_ => Err(ModelSelectionError::AmbiguousModel {
model: input.to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_catalog() -> Vec<ModelCatalogEntry> {
vec![
ModelCatalogEntry {
provider: "openai".into(),
model: "shared-model".into(),
display_name: "shared-model".into(),
auth_modes: vec!["api-key".into(), "codex".into()],
},
ModelCatalogEntry {
provider: "openai".into(),
model: "unique-openai".into(),
display_name: "unique-openai".into(),
auth_modes: vec!["api-key".into()],
},
ModelCatalogEntry {
provider: "openai".into(),
model: "shared-model".into(),
display_name: "shared-model duplicate".into(),
auth_modes: vec!["api-key".into()],
},
ModelCatalogEntry {
provider: "openai-codex".into(),
model: "unique-codex".into(),
display_name: "unique-codex".into(),
auth_modes: vec!["codex".into()],
},
ModelCatalogEntry {
provider: "future".into(),
model: "future-model".into(),
display_name: "future-model".into(),
auth_modes: vec!["api-key".into()],
},
]
}
#[test]
fn parses_embedded_model_catalog() {
let catalog = model_catalog();
assert!(catalog
.iter()
.any(|entry| entry.provider == "openai" && entry.model == "gpt-5.5-pro"));
assert!(catalog
.iter()
.any(|entry| entry.provider == "openai-codex" && entry.model == "gpt-5.3-codex-spark"));
}
#[test]
fn available_models_filters_to_implemented_providers() {
let models = available_models_for_auths(&["codex".into()]);
assert!(models.iter().all(|entry| entry.provider == "openai-codex"));
assert!(models
.iter()
.any(|entry| entry.provider == "openai-codex" && entry.model == "gpt-5.5"));
assert!(models
.iter()
.any(|entry| entry.provider == "openai-codex" && entry.model == "gpt-5.4-mini"));
assert!(models
.iter()
.any(|entry| entry.provider == "openai-codex" && entry.model == "gpt-5.3-codex-spark"));
assert!(models
.iter()
.all(|entry| implemented_providers().contains(&entry.provider.as_str())));
}
#[test]
fn available_models_for_auths_includes_all_authenticated_providers() {
let models = available_models_for_auths(&["api-key".into(), "codex".into()]);
assert!(models.iter().any(|entry| entry.provider == "openai"));
assert!(models.iter().any(|entry| entry.provider == "openai-codex"));
}
#[test]
fn login_targets_use_provider_names() {
let targets = login_targets();
assert_eq!(targets[0].provider, "openai");
assert_eq!(targets[1].provider, "openai-codex");
assert!(login_target_for_provider("api-key").is_none());
assert!(login_target_for_provider("codex").is_none());
}
#[test]
fn resolves_provider_model_selection() {
let selection = resolve_model_selection_for_auths(
"openai/gpt-5.5",
"openai",
"codex",
&["codex".into()],
)
.unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "openai".into(),
model: "gpt-5.5".into(),
auth: "api-key".into(),
from_catalog: true,
}
);
}
#[test]
fn resolves_bare_unique_model_to_catalog_provider() {
let catalog = test_catalog();
let selection = resolve_model_selection_from(
&catalog,
"unique-openai",
"openai",
"api-key",
&["api-key".into()],
)
.unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "openai".into(),
model: "unique-openai".into(),
auth: "api-key".into(),
from_catalog: true,
}
);
}
#[test]
fn resolves_bare_model_across_all_available_auths() {
let catalog = test_catalog();
let selection = resolve_model_selection_from(
&catalog,
"unique-codex",
"openai",
"api-key",
&["api-key".into(), "codex".into()],
)
.unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "openai-codex".into(),
model: "unique-codex".into(),
auth: "codex".into(),
from_catalog: true,
}
);
}
#[test]
fn resolves_bare_unique_codex_model() {
let selection = resolve_model_selection_for_auths(
"gpt-5.3-codex-spark",
"openai-codex",
"codex",
&["codex".into()],
)
.unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "openai-codex".into(),
model: "gpt-5.3-codex-spark".into(),
auth: "codex".into(),
from_catalog: true,
}
);
}
#[test]
fn bare_uncataloged_model_uses_current_provider() {
let selection = resolve_model_selection_for_auths(
"brand-new-model",
"openai",
"codex",
&["codex".into()],
)
.unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "openai".into(),
model: "brand-new-model".into(),
auth: "api-key".into(),
from_catalog: false,
}
);
}
#[test]
fn bare_ambiguous_model_returns_error() {
let catalog = test_catalog();
let err = resolve_model_selection_from(
&catalog,
"shared-model",
"openai",
"api-key",
&["api-key".into()],
)
.unwrap_err();
assert_eq!(
err,
ModelSelectionError::AmbiguousModel {
model: "shared-model".into()
}
);
}
#[test]
fn unknown_provider_is_rejected() {
let err = resolve_model_selection_for_auths(
"missing/gpt-5.5",
"openai",
"codex",
&["codex".into()],
)
.unwrap_err();
assert_eq!(
err,
ModelSelectionError::UnknownProvider {
provider: "missing".into()
}
);
}
}