1use crate::catalog::BuiltinModelEntry;
27use crate::catalog::models_dev::{MdCatalog, protocol_for};
28use crate::catalog::override_::{OverrideFile, apply_model_overrides, apply_provider_overrides};
29use crate::catalog::provider::BuiltinProviderEntry;
30use std::collections::BTreeMap;
31
32pub fn materialize(
39 catalog: &MdCatalog,
40 product_meta: &ProductMeta,
41 user_overrides: &OverrideFile,
42) -> (
43 Vec<BuiltinProviderEntry>,
44 BTreeMap<String, Vec<BuiltinModelEntry>>,
45) {
46 let mut providers = Vec::new();
47 let mut models: BTreeMap<String, Vec<BuiltinModelEntry>> = BTreeMap::new();
48
49 for (pid, mdprov) in &catalog.0 {
50 let (api, auth) = protocol_for(mdprov.npm.as_deref().unwrap_or(""));
51 let extra = product_meta
52 .extra_headers
53 .get(pid)
54 .cloned()
55 .unwrap_or_default();
56
57 providers.push(BuiltinProviderEntry {
58 id: pid.clone(),
59 display_name: mdprov.name.clone(),
60 aliases: vec![], api: api.to_string(),
62 env_key: mdprov.env.first().cloned().unwrap_or_default(),
63 extra_env_keys: mdprov.env[1..].to_vec(),
64 base_url: mdprov.api.clone().unwrap_or_default(),
65 auth_method: auth,
66 extra_headers: extra,
67 category: String::new(), description: String::new(), default_enabled: true,
70 });
71
72 for (mid, mdmodel) in &mdprov.models {
73 let model_prov = mdmodel.provider.as_ref();
75 let model_npm = model_prov
76 .and_then(|p| p.npm.as_deref())
77 .unwrap_or_else(|| mdprov.npm.as_deref().unwrap_or(""));
78 let (model_api, model_auth) = protocol_for(model_npm);
79 let model_base_url = model_prov.and_then(|p| p.api.clone());
80
81 models
82 .entry(pid.clone())
83 .or_default()
84 .push(BuiltinModelEntry {
85 id: mid.clone(),
86 name: mdmodel.name.clone(),
87 api: model_api.to_string(),
88 provider: pid.clone(),
89 reasoning: mdmodel.reasoning,
90 auth_method: model_auth,
91 base_url: model_base_url,
92 input: normalize_modalities(&mdmodel.modalities),
93 cost_input: mdmodel.cost.as_ref().map(|c| c.input).unwrap_or(0.0),
94 cost_output: mdmodel.cost.as_ref().map(|c| c.output).unwrap_or(0.0),
95 cost_cache_read: mdmodel
96 .cost
97 .as_ref()
98 .and_then(|c| c.cache_read)
99 .unwrap_or(0.0),
100 cost_cache_write: mdmodel
101 .cost
102 .as_ref()
103 .and_then(|c| c.cache_write)
104 .unwrap_or(0.0),
105 context_window: mdmodel.limit.context as u32,
106 max_tokens: mdmodel.limit.output as u32,
107 });
108 }
109 }
110
111 apply_provider_overrides(&mut providers, &user_overrides.provider);
113 apply_model_overrides(&mut models, &user_overrides.model);
114
115 (providers, models)
116}
117
118pub fn snapshot_gzip_bytes() -> &'static [u8] {
132 include_bytes!("../../data/catalog/_snapshot.json.gz")
133}
134
135pub fn load_snapshot_catalog() -> Option<MdCatalog> {
140 use std::io::Read;
141 let compressed: &[u8] = snapshot_gzip_bytes();
142 let mut decoder = flate2::read::GzDecoder::new(compressed);
143 let mut json = String::new();
144 decoder.read_to_string(&mut json).ok()?;
145 serde_json::from_str::<MdCatalog>(&json).ok()
146}
147
148pub fn materialize_providers() -> Vec<BuiltinProviderEntry> {
153 let Some(catalog) = load_snapshot_catalog() else {
154 return Vec::new();
155 };
156 let product_meta = ProductMeta::builtin();
157 let overrides = crate::catalog::load_overrides().unwrap_or_default();
158 let (providers, _models) = materialize(&catalog, &product_meta, &overrides);
159 providers
160}
161
162pub fn product_meta_toml() -> &'static str {
170 include_str!("../../data/catalog/product-meta.toml")
171}
172
173#[derive(Default)]
178pub struct ProductMeta {
179 pub extra_headers: std::collections::HashMap<String, Vec<(String, String)>>,
181}
182
183impl ProductMeta {
184 pub fn builtin() -> Self {
186 include_str!("../../data/catalog/product-meta.toml")
187 .parse()
188 .unwrap_or_default()
189 }
190}
191
192impl std::str::FromStr for ProductMeta {
193 type Err = String;
194 fn from_str(s: &str) -> Result<Self, Self::Err> {
195 #[derive(serde::Deserialize)]
196 struct Raw {
197 #[serde(default)]
198 provider: Vec<RawProvider>,
199 }
200 #[derive(serde::Deserialize)]
201 struct RawProvider {
202 id: String,
203 #[serde(default)]
204 extra_headers: Vec<(String, String)>,
205 }
206 let raw: Raw = toml::from_str(s).map_err(|e| e.to_string())?;
207 let mut extra_headers = std::collections::HashMap::new();
208 for p in raw.provider {
209 if !p.extra_headers.is_empty() {
210 extra_headers.insert(p.id, p.extra_headers);
211 }
212 }
213 Ok(Self { extra_headers })
214 }
215}
216
217fn normalize_modalities(md: &Option<crate::catalog::models_dev::MdModalities>) -> Vec<String> {
219 match md {
220 Some(m) => match &m.input {
221 Some(input) if !input.is_empty() => input.clone(),
222 _ => vec!["text".to_string()],
223 },
224 _ => vec!["text".to_string()],
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::Api;
232
233 #[test]
234 fn protocol_for_anthropic() {
235 let (api, auth) = protocol_for("@ai-sdk/anthropic");
236 assert_eq!(api, Api::AnthropicMessages);
237 assert_eq!(auth, crate::catalog::provider::AuthMethod::XApiKey);
238 }
239
240 #[test]
241 fn protocol_for_google() {
242 let (api, auth) = protocol_for("@ai-sdk/google");
243 assert_eq!(api, Api::GoogleGenerativeAi);
244 assert_eq!(auth, crate::catalog::provider::AuthMethod::None);
245 }
246
247 #[test]
248 fn protocol_for_openai_compatible() {
249 let (api, auth) = protocol_for("@ai-sdk/openai-compatible");
250 assert_eq!(api, Api::OpenAiCompletions);
251 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
252 }
253
254 #[test]
255 fn protocol_for_unknown() {
256 let (api, auth) = protocol_for("some-new-sdk");
257 assert_eq!(api, Api::OpenAiCompletions);
258 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
259 }
260
261 #[test]
262 fn protocol_for_empty() {
263 let (api, auth) = protocol_for("");
264 assert_eq!(api, Api::OpenAiCompletions);
265 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
266 }
267
268 #[test]
269 fn protocol_for_mistral() {
270 let (api, auth) = protocol_for("@ai-sdk/mistral");
273 assert_eq!(api, Api::OpenAiCompletions);
274 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
275 }
276
277 #[test]
278 fn protocol_for_azure() {
279 let (api, auth) = protocol_for("@ai-sdk/azure");
280 assert_eq!(api, Api::AzureOpenAiResponses);
281 assert_eq!(auth, crate::catalog::provider::AuthMethod::ApiKey);
282 }
283
284 #[test]
285 fn protocol_for_amazon_bedrock() {
286 let (api, auth) = protocol_for("@ai-sdk/amazon-bedrock");
287 assert_eq!(api, Api::BedrockConverseStream);
288 assert_eq!(auth, crate::catalog::provider::AuthMethod::None);
289 }
290
291 #[test]
292 fn product_meta_parses() {
293 let meta = ProductMeta::builtin();
294 let openrouter_headers = meta.extra_headers.get("openrouter");
296 assert!(
297 openrouter_headers.is_some(),
298 "product-meta.toml should include openrouter"
299 );
300 if let Some(headers) = openrouter_headers {
301 assert!(headers.iter().any(|(k, _)| k == "HTTP-Referer"));
302 }
303 }
304
305 #[test]
306 fn materialize_snapshot_counts() {
307 use std::io::Read;
308
309 let compressed = include_bytes!("../../data/catalog/_snapshot.json.gz");
311 let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
312 let mut json = String::new();
313 decoder.read_to_string(&mut json).unwrap();
314
315 let catalog: crate::catalog::MdCatalog = serde_json::from_str(&json).unwrap();
316 let (providers, models) =
317 super::materialize(&catalog, &ProductMeta::default(), &Default::default());
318
319 assert!(!providers.is_empty(), "providers should not be empty");
320 assert_eq!(providers.len(), 145, "expected 145 providers");
321 let model_count: usize = models.values().map(|v| v.len()).sum();
322 assert_eq!(model_count, 5277, "expected 5277 models");
323 }
324}