1use anyhow::Result;
14
15#[must_use]
20pub fn catalog_source_for(family: &str) -> Option<(Option<&'static str>, Option<&'static str>)> {
21 let pair: (Option<&'static str>, Option<&'static str>) = match family {
22 "openai" => (Some("openai"), Some("openai")),
24 "anthropic" => (Some("anthropic"), Some("anthropic")),
25 "azure" => (Some("azure"), None),
26 "bedrock" => (Some("amazon-bedrock"), None),
27 "gemini" => (Some("google"), Some("google")),
28 "gemini_cli" => (Some("google"), Some("google")),
29 "openrouter" => (Some("openrouter"), Some("openrouter")),
30 "copilot" => (Some("github-copilot"), None),
31 "minimax" => (Some("minimax"), Some("minimax")),
32 "lmstudio" => (Some("lmstudio"), None),
33 "kilocli" => (Some("kilo"), None),
34 "ovh" => (Some("ovhcloud"), None),
35 "moonshot" => (Some("moonshotai"), Some("moonshotai")),
37 "qwen" => (Some("alibaba"), Some("qwen")),
38 "glm" => (Some("zhipuai"), None),
39 "zai" => (Some("zai"), Some("z-ai")),
40 "doubao" => (None, Some("bytedance")),
41 "hunyuan" => (None, Some("tencent")),
42 "qianfan" => (None, Some("baidu")),
43 "groq" => (Some("groq"), None),
44 "mistral" => (Some("mistral"), Some("mistralai")),
45 "deepseek" => (Some("deepseek"), Some("deepseek")),
46 "together" => (Some("togetherai"), None),
47 "fireworks" => (Some("fireworks-ai"), None),
48 "cohere" => (Some("cohere"), Some("cohere")),
49 "perplexity" => (Some("perplexity"), Some("perplexity")),
50 "xai" => (Some("xai"), Some("x-ai")),
51 "cerebras" => (Some("cerebras"), None),
52 "deepinfra" => (Some("deepinfra"), None),
53 "huggingface" => (Some("huggingface"), None),
54 "ai21" => (None, Some("ai21")),
55 "reka" => (None, Some("rekaai")),
56 "baseten" => (Some("baseten"), None),
57 "nebius" => (Some("nebius"), None),
58 "friendli" => (Some("friendli"), None),
59 "stepfun" => (Some("stepfun"), Some("stepfun")),
60 "aihubmix" => (Some("aihubmix"), None),
61 "siliconflow" => (Some("siliconflow"), None),
62 "venice" => (Some("venice"), None),
63 "novita" => (Some("novita-ai"), None),
64 "nvidia" => (Some("nvidia"), Some("nvidia")),
65 "vercel" => (Some("vercel"), None),
66 "cloudflare" => (Some("cloudflare-ai-gateway"), None),
67 "synthetic" => (Some("synthetic"), None),
68 "opencode" => (Some("opencode"), None),
69 "atomic_chat" => (Some("atomic-chat"), None),
70 "telnyx" => (None, None),
71 "sambanova" | "hyperbolic" | "anyscale" | "nscale" | "lepton" | "yi" | "baichuan"
76 | "avian" | "deepmyst" | "astrai" | "sglang" | "vllm" | "osaurus" | "litellm"
77 | "llamacpp" | "ollama" | "custom" => (None, None),
78 _ => return None,
79 };
80 Some(pair)
81}
82
83pub async fn list_models_for_family(family: &str) -> Result<Vec<String>> {
87 let Some((md_key, or_prefix)) = catalog_source_for(family) else {
88 anyhow::bail!("unknown provider family {family:?}");
89 };
90 if let Some(k) = md_key
91 && let Ok(ms) = crate::models_dev::list_models_for(k).await
92 && !ms.is_empty()
93 {
94 return Ok(ms);
95 }
96 if let Some(p) = or_prefix {
97 return crate::openrouter_catalog::list_models_for_vendor(p).await;
98 }
99 anyhow::bail!("no public catalog for family {family:?}")
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
112 fn every_canonical_family_has_a_catalog_table_entry() {
113 macro_rules! collect_family_names {
114 ($(($field:ident, $type_str:literal, $cfg_ty:ty)),+ $(,)?) => {
115 vec![$($type_str),+]
116 };
117 }
118 let families: Vec<&str> =
119 zeroclaw_config::for_each_model_provider_slot!(collect_family_names);
120 let mut missing: Vec<&str> = Vec::new();
121 for family in &families {
122 if catalog_source_for(family).is_none() {
123 missing.push(family);
124 }
125 }
126 assert!(
127 missing.is_empty(),
128 "catalog_source_for is missing entries for: {missing:?}"
129 );
130 }
131
132 #[test]
133 fn unknown_family_returns_none() {
134 assert!(catalog_source_for("not_a_real_provider").is_none());
135 }
136
137 #[test]
138 fn known_family_with_dual_sources_returns_both() {
139 let (md, or) = catalog_source_for("xai").expect("xai is canonical");
140 assert_eq!(md, Some("xai"));
141 assert_eq!(or, Some("x-ai"));
142 }
143
144 #[test]
145 fn local_only_family_returns_no_sources() {
146 let (md, or) = catalog_source_for("llamacpp").expect("llamacpp is canonical");
147 assert_eq!(md, None);
148 assert_eq!(or, None);
149 }
150
151 #[test]
152 fn bespoke_family_with_only_models_dev() {
153 let (md, or) = catalog_source_for("azure").expect("azure is canonical");
154 assert_eq!(md, Some("azure"));
155 assert_eq!(or, None);
156 }
157
158 #[test]
159 fn bespoke_family_with_only_openrouter() {
160 let (md, or) = catalog_source_for("ai21").expect("ai21 is canonical");
161 assert_eq!(md, None);
162 assert_eq!(or, Some("ai21"));
163 }
164}