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
162#[derive(Default)]
167pub struct ProductMeta {
168 pub extra_headers: std::collections::HashMap<String, Vec<(String, String)>>,
170}
171
172impl ProductMeta {
173 pub fn builtin() -> Self {
175 include_str!("../../data/catalog/product-meta.toml")
176 .parse()
177 .unwrap_or_default()
178 }
179}
180
181impl std::str::FromStr for ProductMeta {
182 type Err = String;
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 #[derive(serde::Deserialize)]
185 struct Raw {
186 #[serde(default)]
187 provider: Vec<RawProvider>,
188 }
189 #[derive(serde::Deserialize)]
190 struct RawProvider {
191 id: String,
192 #[serde(default)]
193 extra_headers: Vec<(String, String)>,
194 }
195 let raw: Raw = toml::from_str(s).map_err(|e| e.to_string())?;
196 let mut extra_headers = std::collections::HashMap::new();
197 for p in raw.provider {
198 if !p.extra_headers.is_empty() {
199 extra_headers.insert(p.id, p.extra_headers);
200 }
201 }
202 Ok(Self { extra_headers })
203 }
204}
205
206fn normalize_modalities(md: &Option<crate::catalog::models_dev::MdModalities>) -> Vec<String> {
208 match md {
209 Some(m) => match &m.input {
210 Some(input) if !input.is_empty() => input.clone(),
211 _ => vec!["text".to_string()],
212 },
213 _ => vec!["text".to_string()],
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::Api;
221
222 #[test]
223 fn protocol_for_anthropic() {
224 let (api, auth) = protocol_for("@ai-sdk/anthropic");
225 assert_eq!(api, Api::AnthropicMessages);
226 assert_eq!(auth, crate::catalog::provider::AuthMethod::XApiKey);
227 }
228
229 #[test]
230 fn protocol_for_google() {
231 let (api, auth) = protocol_for("@ai-sdk/google");
232 assert_eq!(api, Api::GoogleGenerativeAi);
233 assert_eq!(auth, crate::catalog::provider::AuthMethod::None);
234 }
235
236 #[test]
237 fn protocol_for_openai_compatible() {
238 let (api, auth) = protocol_for("@ai-sdk/openai-compatible");
239 assert_eq!(api, Api::OpenAiCompletions);
240 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
241 }
242
243 #[test]
244 fn protocol_for_unknown() {
245 let (api, auth) = protocol_for("some-new-sdk");
246 assert_eq!(api, Api::OpenAiCompletions);
247 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
248 }
249
250 #[test]
251 fn protocol_for_empty() {
252 let (api, auth) = protocol_for("");
253 assert_eq!(api, Api::OpenAiCompletions);
254 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
255 }
256
257 #[test]
258 fn protocol_for_mistral() {
259 let (api, auth) = protocol_for("@ai-sdk/mistral");
262 assert_eq!(api, Api::OpenAiCompletions);
263 assert_eq!(auth, crate::catalog::provider::AuthMethod::Bearer);
264 }
265
266 #[test]
267 fn protocol_for_azure() {
268 let (api, auth) = protocol_for("@ai-sdk/azure");
269 assert_eq!(api, Api::AzureOpenAiResponses);
270 assert_eq!(auth, crate::catalog::provider::AuthMethod::ApiKey);
271 }
272
273 #[test]
274 fn protocol_for_amazon_bedrock() {
275 let (api, auth) = protocol_for("@ai-sdk/amazon-bedrock");
276 assert_eq!(api, Api::BedrockConverseStream);
277 assert_eq!(auth, crate::catalog::provider::AuthMethod::None);
278 }
279
280 #[test]
281 fn product_meta_parses() {
282 let meta = ProductMeta::builtin();
283 let openrouter_headers = meta.extra_headers.get("openrouter");
285 assert!(
286 openrouter_headers.is_some(),
287 "product-meta.toml should include openrouter"
288 );
289 if let Some(headers) = openrouter_headers {
290 assert!(headers.iter().any(|(k, _)| k == "HTTP-Referer"));
291 }
292 }
293
294 #[test]
295 fn materialize_snapshot_counts() {
296 use std::io::Read;
297
298 let compressed = include_bytes!("../../data/catalog/_snapshot.json.gz");
300 let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
301 let mut json = String::new();
302 decoder.read_to_string(&mut json).unwrap();
303
304 let catalog: crate::catalog::MdCatalog = serde_json::from_str(&json).unwrap();
305 let (providers, models) =
306 super::materialize(&catalog, &ProductMeta::default(), &Default::default());
307
308 assert!(!providers.is_empty(), "providers should not be empty");
309 assert_eq!(providers.len(), 145, "expected 145 providers");
310 let model_count: usize = models.values().map(|v| v.len()).sum();
311 assert_eq!(model_count, 5277, "expected 5277 models");
312 }
313}