#![allow(dead_code)]
use std::collections::BTreeMap;
use std::sync::OnceLock;
use crate::models::{
context_window_for_model, max_output_tokens_for_model, model_supports_reasoning,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelProvider {
DeepSeek,
Anthropic,
OpenAi,
OpenAiCodex,
Moonshot,
Zai,
Minimax,
Qwen,
Arcee,
XiaomiMimo,
Other,
}
#[must_use]
pub fn serving_provider(provider: ModelProvider) -> crate::config::ApiProvider {
match provider {
ModelProvider::DeepSeek => crate::config::ApiProvider::Deepseek,
ModelProvider::Anthropic => crate::config::ApiProvider::Anthropic,
ModelProvider::OpenAi => crate::config::ApiProvider::Openai,
ModelProvider::OpenAiCodex => crate::config::ApiProvider::OpenaiCodex,
ModelProvider::Moonshot => crate::config::ApiProvider::Moonshot,
ModelProvider::Zai => crate::config::ApiProvider::Zai,
ModelProvider::Minimax => crate::config::ApiProvider::Minimax,
ModelProvider::Qwen => crate::config::ApiProvider::Openrouter,
ModelProvider::Arcee => crate::config::ApiProvider::Arcee,
ModelProvider::XiaomiMimo => crate::config::ApiProvider::XiaomiMimo,
ModelProvider::Other => crate::config::ApiProvider::Openrouter,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelMetadata {
pub id: &'static str,
pub provider: ModelProvider,
pub context_window: Option<u32>,
pub max_output: Option<u32>,
pub supports_reasoning: bool,
}
impl ModelMetadata {
fn seed(id: &'static str, provider: ModelProvider) -> Self {
Self {
id,
provider,
context_window: context_window_for_model(id),
max_output: max_output_tokens_for_model(id),
supports_reasoning: model_supports_reasoning(id),
}
}
}
const SEED_MODEL_IDS: &[(&str, ModelProvider)] = &[
("deepseek-v4-pro", ModelProvider::DeepSeek),
("deepseek-v4-flash", ModelProvider::DeepSeek),
("deepseek-ai/deepseek-v4-pro", ModelProvider::DeepSeek),
("deepseek-ai/deepseek-v4-flash", ModelProvider::DeepSeek),
("deepseek/deepseek-v4-pro", ModelProvider::DeepSeek),
("deepseek/deepseek-v4-flash", ModelProvider::DeepSeek),
("deepseek-reasoner", ModelProvider::DeepSeek),
("deepseek-coder:1.3b", ModelProvider::DeepSeek),
("claude-opus-4-8", ModelProvider::Anthropic),
("claude-sonnet-4-6", ModelProvider::Anthropic),
("claude-haiku-4-5", ModelProvider::Anthropic),
("gpt-5.5", ModelProvider::OpenAi),
("gpt-5.5-pro", ModelProvider::OpenAi),
("gpt-5-codex", ModelProvider::OpenAiCodex),
("gpt-5.3-codex", ModelProvider::OpenAiCodex),
("kimi-k2.7-code", ModelProvider::Moonshot),
("kimi-k2.6", ModelProvider::Moonshot),
("kimi-for-coding", ModelProvider::Moonshot),
("moonshotai/kimi-k2.7-code", ModelProvider::Moonshot),
("moonshotai/kimi-k2.6", ModelProvider::Moonshot),
("z-ai/glm-5.1", ModelProvider::Zai),
("z-ai/glm-5.2", ModelProvider::Zai),
("glm-5.1", ModelProvider::Zai),
("glm-5.2", ModelProvider::Zai),
("minimax/minimax-m3", ModelProvider::Minimax),
("minimax-m3", ModelProvider::Minimax),
("minimax/minimax-2.7", ModelProvider::Minimax),
("minimax-m2.7", ModelProvider::Minimax),
("qwen/qwen3.6-flash", ModelProvider::Qwen),
("qwen/qwen3.6-plus", ModelProvider::Qwen),
("qwen/qwen3.6-35b-a3b", ModelProvider::Qwen),
("trinity-large-thinking", ModelProvider::Arcee),
("arcee-ai/trinity-large-thinking", ModelProvider::Arcee),
("trinity-mini", ModelProvider::Arcee),
("mimo-v2.5-pro", ModelProvider::XiaomiMimo),
("mimo-v2.5", ModelProvider::XiaomiMimo),
];
fn registry() -> &'static BTreeMap<&'static str, ModelMetadata> {
static REGISTRY: OnceLock<BTreeMap<&'static str, ModelMetadata>> = OnceLock::new();
REGISTRY.get_or_init(|| {
SEED_MODEL_IDS
.iter()
.map(|&(id, provider)| (id, ModelMetadata::seed(id, provider)))
.collect()
})
}
#[must_use]
pub fn lookup(model: &str) -> Option<ModelMetadata> {
if let Some(meta) = registry().get(model) {
return Some(meta.clone());
}
let lowered = model.to_lowercase();
if lowered != model
&& let Some(meta) = registry().get(lowered.as_str())
{
return Some(meta.clone());
}
let context_window = context_window_for_model(model);
let max_output = max_output_tokens_for_model(model);
let supports_reasoning = model_supports_reasoning(model);
if context_window.is_none() && max_output.is_none() && !supports_reasoning {
return None;
}
Some(ModelMetadata {
id: "",
provider: ModelProvider::Other,
context_window,
max_output,
supports_reasoning,
})
}
#[must_use]
pub fn seeded_model_ids() -> Vec<&'static str> {
registry().keys().copied().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_context_window_matches_models_rs() {
let sample = [
("deepseek-v4-pro", Some(1_000_000)),
("deepseek-v4-flash", Some(1_000_000)),
("deepseek-coder:1.3b", Some(128_000)),
("claude-opus-4-8", Some(1_000_000)),
("claude-sonnet-4-6", Some(1_000_000)),
("claude-haiku-4-5", Some(200_000)),
("gpt-5.5", Some(1_050_000)),
("gpt-5-codex", Some(400_000)),
("kimi-k2.7-code", Some(262_144)),
("kimi-k2.6", Some(262_144)),
("z-ai/glm-5.1", Some(202_752)),
("z-ai/glm-5.2", Some(1_000_000)),
("minimax/minimax-m3", Some(1_000_000)),
("minimax-m2.7", Some(204_800)),
("qwen/qwen3.6-flash", Some(1_000_000)),
("qwen/qwen3.6-35b-a3b", Some(262_144)),
("trinity-large-thinking", Some(262_144)),
("trinity-mini", Some(128_000)),
("mimo-v2.5-pro", Some(1_000_000)),
];
for (model, expected) in sample {
let meta = lookup(model)
.unwrap_or_else(|| panic!("seeded model {model} should be in the registry"));
assert_eq!(
meta.context_window, expected,
"registry context window for {model} drifted from expected"
);
assert_eq!(
meta.context_window,
context_window_for_model(model),
"registry context window for {model} drifted from models.rs"
);
}
}
#[test]
fn registry_max_output_and_reasoning_match_models_rs() {
for &(id, _) in SEED_MODEL_IDS {
let meta = lookup(id).unwrap_or_else(|| panic!("{id} should be seeded"));
assert_eq!(
meta.max_output,
max_output_tokens_for_model(id),
"registry max_output for {id} drifted from models.rs"
);
assert_eq!(
meta.supports_reasoning,
model_supports_reasoning(id),
"registry supports_reasoning for {id} drifted from models.rs"
);
}
}
#[test]
fn serving_provider_maps_each_family() {
use crate::config::ApiProvider;
assert_eq!(
serving_provider(ModelProvider::DeepSeek),
ApiProvider::Deepseek
);
assert_eq!(
serving_provider(ModelProvider::Anthropic),
ApiProvider::Anthropic
);
assert_eq!(serving_provider(ModelProvider::OpenAi), ApiProvider::Openai);
assert_eq!(
serving_provider(ModelProvider::OpenAiCodex),
ApiProvider::OpenaiCodex
);
assert_eq!(
serving_provider(ModelProvider::Moonshot),
ApiProvider::Moonshot
);
assert_eq!(serving_provider(ModelProvider::Zai), ApiProvider::Zai);
assert_eq!(
serving_provider(ModelProvider::Minimax),
ApiProvider::Minimax
);
assert_eq!(
serving_provider(ModelProvider::Qwen),
ApiProvider::Openrouter
);
assert_eq!(serving_provider(ModelProvider::Arcee), ApiProvider::Arcee);
assert_eq!(
serving_provider(ModelProvider::XiaomiMimo),
ApiProvider::XiaomiMimo
);
assert_eq!(
serving_provider(ModelProvider::Other),
ApiProvider::Openrouter
);
}
#[test]
fn deepseek_models_are_classified_as_deepseek() {
for id in [
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-ai/deepseek-v4-pro",
] {
let meta = lookup(id).expect("DeepSeek default should be seeded");
assert_eq!(meta.provider, ModelProvider::DeepSeek);
assert_eq!(meta.context_window, Some(1_000_000));
}
}
#[test]
fn lookup_is_case_insensitive_for_seeded_ids() {
let lower = lookup("deepseek-v4-pro").expect("seeded");
let upper = lookup("DeepSeek-V4-Pro").expect("case-insensitive seed match");
assert_eq!(upper.id, "deepseek-v4-pro");
assert_eq!(upper.context_window, lower.context_window);
assert_eq!(upper.provider, ModelProvider::DeepSeek);
}
#[test]
fn lookup_falls_back_to_models_rs_for_unseeded_known_ids() {
let meta = lookup("deepseek-v3.2-256k-preview").expect("known via models.rs heuristics");
assert_eq!(meta.context_window, Some(256_000));
assert_eq!(
meta.context_window,
context_window_for_model("deepseek-v3.2-256k-preview")
);
assert_eq!(meta.provider, ModelProvider::Other);
}
#[test]
fn lookup_returns_none_for_completely_unknown_model() {
assert!(lookup("totally-made-up-model-xyz").is_none());
}
#[test]
fn seeded_model_ids_are_non_empty_and_unique() {
let ids = seeded_model_ids();
assert!(!ids.is_empty());
let mut sorted = ids.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), ids.len(), "seed ids must be unique");
}
}