1#![doc = include_str!("../README.md")]
2
3use proc_macro2::TokenStream;
4use quote::{ToTokens, format_ident, quote};
5use serde::Deserialize;
6use std::collections::{BTreeMap, HashMap, HashSet};
7use std::fmt::Write;
8use std::path::Path;
9
10type ModelsDevData = HashMap<String, ProviderData>;
11
12#[derive(Debug, Deserialize)]
13struct ProviderData {
14 #[allow(dead_code)]
15 id: String,
16 #[allow(dead_code)]
17 name: String,
18 #[serde(default)]
19 #[allow(dead_code)]
20 env: Vec<String>,
21 #[serde(default)]
22 models: HashMap<String, ModelData>,
23}
24
25#[derive(Debug, Deserialize)]
26struct ModelData {
27 id: String,
28 name: String,
29 #[serde(default)]
30 tool_call: Option<bool>,
31 #[serde(default)]
32 reasoning: Option<bool>,
33 #[serde(default)]
34 reasoning_options: Vec<ReasoningOption>,
35 #[serde(default)]
36 #[allow(dead_code)]
37 cost: Option<CostData>,
38 #[serde(default)]
39 limit: Option<LimitData>,
40 #[serde(default)]
41 modalities: Option<ModalitiesData>,
42 #[serde(default)]
43 provider: Option<ModelProviderData>,
44}
45
46#[derive(Debug, Deserialize)]
48struct ModelProviderData {
49 #[serde(default)]
50 api: Option<String>,
51 #[serde(default)]
52 shape: Option<String>,
53}
54
55#[derive(Debug, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57enum ReasoningOption {
58 Effort { values: Vec<Option<String>> },
59 Toggle,
60 BudgetTokens,
61}
62
63#[derive(Debug, Deserialize, Default)]
64struct ModalitiesData {
65 #[serde(default)]
66 input: Vec<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Deserialize)]
70struct CostData {
71 #[serde(default)]
72 input: f64,
73 #[serde(default)]
74 output: f64,
75 #[serde(default)]
76 cache_read: Option<f64>,
77 #[serde(default)]
78 cache_write: Option<f64>,
79}
80
81#[derive(Debug, Deserialize)]
82struct LimitData {
83 #[serde(default)]
84 context: u32,
85 #[serde(default)]
86 #[allow(dead_code)]
87 output: u32,
88}
89
90impl CostData {
91 fn has_prompt_caching(&self) -> bool {
92 self.cache_read.is_some() || self.cache_write.is_some()
93 }
94}
95
96struct ProviderConfig {
98 dev_id: &'static str,
100 source_dev_id: Option<&'static str>,
102 extra_source_ids: &'static [&'static str],
104 explicit_models: Option<&'static [ExplicitModel]>,
109 enum_name: &'static str,
111 parser_name: &'static str,
113 genai_provider_name: &'static str,
115 display_name: &'static str,
117 env_var: Option<&'static str>,
119 oauth_provider_id: Option<&'static str>,
121 fallback_reasoning_levels: &'static [&'static str],
123 use_model_transport: bool,
127 uses_openai_compatible_api: bool,
129 is_hybrid_dynamic: bool,
135}
136
137struct ExplicitModel {
139 id: &'static str,
140 context_window: u32,
141}
142
143impl ProviderConfig {
144 const fn standard(
146 dev_id: &'static str,
147 enum_name: &'static str,
148 parser_name: &'static str,
149 display_name: &'static str,
150 env_var: Option<&'static str>,
151 ) -> Self {
152 Self {
153 dev_id,
154 source_dev_id: None,
155 extra_source_ids: &[],
156 explicit_models: None,
157 enum_name,
158 parser_name,
159 genai_provider_name: parser_name,
160 display_name,
161 env_var,
162 oauth_provider_id: None,
163 fallback_reasoning_levels: &["low", "medium", "high"],
164 use_model_transport: false,
165 uses_openai_compatible_api: false,
166 is_hybrid_dynamic: false,
167 }
168 }
169
170 const fn openai_compatible(
171 dev_id: &'static str,
172 enum_name: &'static str,
173 parser_name: &'static str,
174 display_name: &'static str,
175 env_var: &'static str,
176 ) -> Self {
177 let mut config = Self::standard(dev_id, enum_name, parser_name, display_name, Some(env_var));
178 config.uses_openai_compatible_api = true;
179 config
180 }
181
182 fn explicit_model(&self, model_id: &str) -> Option<&'static ExplicitModel> {
183 self.explicit_models.and_then(|models| models.iter().find(|model| model.id == model_id))
184 }
185
186 fn inner_enum_name(&self) -> String {
189 if self.is_hybrid_dynamic {
190 format!("{}FoundationModel", self.enum_name)
191 } else {
192 format!("{}Model", self.enum_name)
193 }
194 }
195
196 fn outer_enum_name(&self) -> String {
198 format!("{}Model", self.enum_name)
199 }
200
201 fn json_key(&self) -> &'static str {
203 self.source_dev_id.unwrap_or(self.dev_id)
204 }
205}
206
207#[allow(clippy::struct_field_names)]
209struct DynamicProviderConfig {
210 enum_name: &'static str,
212 parser_name: &'static str,
214 genai_provider_name: &'static str,
216 display_name: &'static str,
218}
219
220const PROVIDERS: &[ProviderConfig] = &[
221 ProviderConfig::standard("anthropic", "Anthropic", "anthropic", "Anthropic", Some("ANTHROPIC_API_KEY")),
222 ProviderConfig {
223 source_dev_id: Some("azure"),
224 genai_provider_name: "azure.ai.openai",
225 ..ProviderConfig::openai_compatible(
226 "azure-foundry",
227 "AzureFoundry",
228 "azure-foundry",
229 "Microsoft Foundry",
230 "AZURE_OPENAI_API_KEY",
231 )
232 },
233 ProviderConfig {
234 dev_id: "codex",
235 source_dev_id: Some("openai"),
236 extra_source_ids: &[],
237 explicit_models: Some(CODEX_SUBSCRIPTION_MODELS),
238 enum_name: "Codex",
239 parser_name: "codex",
240 genai_provider_name: "openai",
241 display_name: "Codex",
242 env_var: None,
243 oauth_provider_id: Some("codex"),
244 fallback_reasoning_levels: &["low", "medium", "high", "xhigh"],
245 use_model_transport: false,
246 uses_openai_compatible_api: false,
247 is_hybrid_dynamic: false,
248 },
249 ProviderConfig::openai_compatible("deepseek", "DeepSeek", "deepseek", "DeepSeek", "DEEPSEEK_API_KEY"),
250 ProviderConfig {
251 source_dev_id: Some("fireworks-ai"),
252 ..ProviderConfig::openai_compatible("fireworks", "Fireworks", "fireworks", "Fireworks AI", "FIREWORKS_API_KEY")
253 },
254 ProviderConfig {
255 genai_provider_name: "gcp.gemini",
256 ..ProviderConfig::standard("google", "Gemini", "gemini", "Gemini", Some("GEMINI_API_KEY"))
257 },
258 ProviderConfig {
259 genai_provider_name: "moonshot_ai",
260 ..ProviderConfig::openai_compatible("moonshotai", "Moonshot", "moonshot", "Moonshot", "MOONSHOT_API_KEY")
261 },
262 ProviderConfig::standard("openai", "Openai", "openai", "OpenAI", Some("OPENAI_API_KEY")),
263 ProviderConfig::standard("openrouter", "OpenRouter", "openrouter", "OpenRouter", Some("OPENROUTER_API_KEY")),
264 ProviderConfig {
265 extra_source_ids: &["zai-coding-plan"],
266 ..ProviderConfig::openai_compatible("zai", "ZAi", "zai", "ZAI", "ZAI_API_KEY")
267 },
268 ProviderConfig {
269 genai_provider_name: "aws.bedrock",
270 use_model_transport: true,
271 is_hybrid_dynamic: true,
272 ..ProviderConfig::standard("amazon-bedrock", "Bedrock", "bedrock", "AWS Bedrock", None)
273 },
274];
275
276const DYNAMIC_PROVIDERS: &[DynamicProviderConfig] = &[
277 DynamicProviderConfig {
278 enum_name: "Ollama",
279 parser_name: "ollama",
280 genai_provider_name: "ollama",
281 display_name: "Ollama",
282 },
283 DynamicProviderConfig {
284 enum_name: "LlamaCpp",
285 parser_name: "llamacpp",
286 genai_provider_name: "llama.cpp",
287 display_name: "LlamaCpp",
288 },
289];
290
291const CODEX_SUBSCRIPTION_CONTEXT_WINDOW: u32 = 272_000;
292
293const CODEX_SUBSCRIPTION_MODELS: &[ExplicitModel] = &[
294 ExplicitModel { id: "gpt-6-astra", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
295 ExplicitModel { id: "gpt-5.6-sol", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
296 ExplicitModel { id: "gpt-5.6-terra", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
297 ExplicitModel { id: "gpt-5.6-luna", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
298 ExplicitModel { id: "gpt-5.5", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
299 ExplicitModel { id: "gpt-5.4", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
300 ExplicitModel { id: "gpt-5.4-mini", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
301 ExplicitModel { id: "gpt-5.2", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
302];
303
304#[derive(Debug, Clone)]
305struct ModelInfo {
306 variant_name: String,
307 model_id: String,
308 display_name: String,
309 context_window: u32,
310 reasoning_levels: Vec<String>,
311 input_modalities: Vec<String>,
312 pricing: Option<CostData>,
313 supports_prompt_caching: bool,
314 transport: Option<TransportInfo>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
318enum TransportInfo {
319 OpenAiResponses { base_url_template: String },
320}
321
322type ProviderModels = BTreeMap<&'static str, Vec<ModelInfo>>;
323
324struct CodegenCtx {
325 provider_models: ProviderModels,
326}
327
328pub struct GeneratedOutput {
330 pub rust_source: String,
332 pub openai_compatible_provider_ids: Vec<&'static str>,
334 pub provider_docs: HashMap<String, String>,
339}
340
341#[derive(Debug, thiserror::Error)]
342pub enum CodegenError {
343 #[error("read: {0}")]
344 Read(#[from] std::io::Error),
345 #[error("parse: {0}")]
346 Parse(#[from] serde_json::Error),
347 #[error("Provider '{0}' not found in models.dev data")]
348 ProviderNotFound(String),
349 #[error("Configured model '{model_id}' was not found in provider '{provider_id}'")]
350 ConfiguredModelNotFound { provider_id: String, model_id: String },
351 #[error("Configured model '{model_id}' is duplicated for provider '{provider_id}'")]
352 DuplicateConfiguredModel { provider_id: String, model_id: String },
353 #[error("Configured model '{model_id}' is not tool-capable in provider '{provider_id}'")]
354 ConfiguredModelUnavailable { provider_id: String, model_id: String },
355 #[error("Model '{model_id}' declares unsupported reasoning effort '{effort}'")]
356 UnsupportedReasoningEffort { model_id: String, effort: String },
357 #[error("Model '{model_id}' declares unsupported wire shape '{shape}'")]
358 UnsupportedWireShape { model_id: String, shape: String },
359 #[error("Model '{model_id}' must declare both an endpoint and wire shape")]
360 IncompleteTransport { model_id: String },
361}
362
363pub fn generate(models_json_path: &Path) -> Result<GeneratedOutput, CodegenError> {
365 let json_bytes = std::fs::read_to_string(models_json_path)?;
366 let data: ModelsDevData = serde_json::from_str(&json_bytes)?;
367
368 let provider_models = build_provider_models(&data)?;
369 let ctx = CodegenCtx { provider_models };
370 let openai_compatible_provider_ids =
371 PROVIDERS.iter().filter(|config| config.uses_openai_compatible_api).map(|config| config.dev_id).collect();
372 Ok(GeneratedOutput {
373 rust_source: emit_generated_source(&ctx),
374 openai_compatible_provider_ids,
375 provider_docs: emit_provider_docs(&ctx),
376 })
377}
378
379fn build_provider_models(data: &ModelsDevData) -> Result<ProviderModels, CodegenError> {
380 let mut provider_models = ProviderModels::new();
381
382 for cfg in PROVIDERS {
383 let json_key = cfg.json_key();
384 let provider_data = data.get(json_key).ok_or_else(|| CodegenError::ProviderNotFound(json_key.to_string()))?;
385
386 validate_provider_config(cfg, provider_data)?;
387 let mut models: Vec<ModelInfo> = collect_models_from(cfg, &provider_data.models)?;
388
389 for &extra_key in cfg.extra_source_ids {
390 if let Some(extra_data) = data.get(extra_key) {
391 let extra = collect_models_from(cfg, &extra_data.models)?;
392 let existing_ids: std::collections::HashSet<String> =
393 models.iter().map(|m| m.model_id.clone()).collect();
394 models.extend(extra.into_iter().filter(|m| !existing_ids.contains(&m.model_id)));
395 }
396 }
397
398 models.sort_by(|a, b| a.model_id.cmp(&b.model_id));
399 provider_models.insert(cfg.dev_id, models);
400 }
401
402 Ok(provider_models)
403}
404
405fn validate_provider_config(cfg: &ProviderConfig, provider: &ProviderData) -> Result<(), CodegenError> {
406 let Some(explicit_models) = cfg.explicit_models else {
407 return Ok(());
408 };
409 let mut seen = HashSet::new();
410 for configured in explicit_models {
411 if !seen.insert(configured.id) {
412 return Err(CodegenError::DuplicateConfiguredModel {
413 provider_id: cfg.dev_id.to_string(),
414 model_id: configured.id.to_string(),
415 });
416 }
417 let Some(model) = provider.models.get(configured.id) else {
418 return Err(CodegenError::ConfiguredModelNotFound {
419 provider_id: cfg.dev_id.to_string(),
420 model_id: configured.id.to_string(),
421 });
422 };
423 if model.tool_call != Some(true) {
424 return Err(CodegenError::ConfiguredModelUnavailable {
425 provider_id: cfg.dev_id.to_string(),
426 model_id: configured.id.to_string(),
427 });
428 }
429 }
430 Ok(())
431}
432
433fn collect_models_from(
434 cfg: &ProviderConfig,
435 models: &HashMap<String, ModelData>,
436) -> Result<Vec<ModelInfo>, CodegenError> {
437 models
438 .values()
439 .filter(|m| m.tool_call == Some(true))
440 .filter(|m| !is_alias(&m.id))
441 .filter(|m| cfg.explicit_models.is_none() || cfg.explicit_model(&m.id).is_some())
442 .map(|m| {
443 let reasoning_levels =
444 if m.reasoning.unwrap_or(false) { reasoning_levels_for_model(cfg, m)? } else { Vec::new() };
445 let input_modalities =
446 m.modalities.as_ref().map_or_else(|| vec!["text".to_string()], |md| md.input.clone());
447 let source_context_window = m.limit.as_ref().map_or(0, |l| l.context);
448 let context_window =
449 cfg.explicit_model(&m.id).map_or(source_context_window, |explicit| explicit.context_window);
450 let pricing = if cfg.dev_id == "codex" { None } else { m.cost.clone() };
451 Ok(ModelInfo {
452 variant_name: model_id_to_variant(&m.id),
453 model_id: m.id.clone(),
454 display_name: m.name.clone(),
455 context_window,
456 reasoning_levels,
457 input_modalities,
458 supports_prompt_caching: m.cost.as_ref().is_some_and(CostData::has_prompt_caching),
459 pricing,
460 transport: transport_for_model(cfg, m)?,
461 })
462 })
463 .collect()
464}
465
466fn transport_for_model(cfg: &ProviderConfig, model: &ModelData) -> Result<Option<TransportInfo>, CodegenError> {
467 if !cfg.use_model_transport {
468 return Ok(None);
469 }
470 let Some(provider) = &model.provider else {
471 return Ok(None);
472 };
473
474 match (&provider.api, provider.shape.as_deref()) {
475 (None, None) => Ok(None),
476 (Some(base_url_template), Some("responses")) => {
477 Ok(Some(TransportInfo::OpenAiResponses { base_url_template: base_url_template.clone() }))
478 }
479 (_, Some(shape)) if shape != "responses" => {
480 Err(CodegenError::UnsupportedWireShape { model_id: model.id.clone(), shape: shape.to_string() })
481 }
482 _ => Err(CodegenError::IncompleteTransport { model_id: model.id.clone() }),
483 }
484}
485
486fn reasoning_levels_for_model(cfg: &ProviderConfig, model: &ModelData) -> Result<Vec<String>, CodegenError> {
487 let Some(values) = model.reasoning_options.iter().find_map(|option| match option {
488 ReasoningOption::Effort { values } => Some(values),
489 ReasoningOption::Toggle | ReasoningOption::BudgetTokens => None,
490 }) else {
491 return Ok(cfg.fallback_reasoning_levels.iter().map(|level| (*level).to_string()).collect());
492 };
493
494 values
495 .iter()
496 .filter_map(|value| value.as_deref())
497 .filter(|value| !matches!(*value, "none" | "default"))
498 .map(|effort| {
499 effort.parse::<utils::ReasoningEffort>().map(|parsed| parsed.as_str().to_string()).map_err(|_| {
500 CodegenError::UnsupportedReasoningEffort { model_id: model.id.clone(), effort: effort.to_string() }
501 })
502 })
503 .collect()
504}
505
506fn is_alias(id: &str) -> bool {
508 id.ends_with("-latest")
509}
510
511fn model_id_to_variant(id: &str) -> String {
514 let mut result = String::new();
515 let mut capitalize_next = true;
516
517 for ch in id.chars() {
518 if ch == '-' || ch == '.' || ch == '/' || ch == ':' {
519 capitalize_next = true;
520 } else if capitalize_next {
521 result.push(ch.to_ascii_uppercase());
522 capitalize_next = false;
523 } else {
524 result.push(ch);
525 }
526 }
527
528 if result.starts_with(|c: char| c.is_ascii_digit()) {
529 result.insert(0, '_');
530 }
531
532 result
533}
534
535fn emit_generated_source(ctx: &CodegenCtx) -> String {
536 let provider_enum = emit_provider_enum();
537 let provider_enum_impl = emit_provider_enum_impl();
538 let provider_enum_display = emit_provider_enum_display();
539 let provider_enum_fromstr = emit_provider_enum_fromstr();
540 let provider_enums = emit_provider_enums(&ctx.provider_models);
541 let provider_impls = emit_provider_impls(&ctx.provider_models);
542 let llm_model_enum = emit_llm_model_enum();
543 let from_impls = emit_from_impls();
544 let llm_model_impl = emit_llm_model_impl();
545 let display_impl = emit_display_impl();
546 let fromstr_impl = emit_fromstr_impl();
547
548 let file_tokens = quote! {
549 use std::borrow::Cow;
550 use std::sync::LazyLock;
551 use crate::ReasoningEffort;
552
553 #provider_enum
554 #provider_enum_impl
555 #provider_enum_display
556 #provider_enum_fromstr
557 #provider_enums
558 #provider_impls
559 #llm_model_enum
560 #from_impls
561 #llm_model_impl
562 #display_impl
563 #fromstr_impl
564 };
565
566 let file: syn::File = syn::parse2(file_tokens).expect("generated tokens parse as Rust");
567 let formatted = prettyplease::unparse(&file);
568 format!(
569 "// Auto-generated from models.dev — do not edit manually\n// Regenerated automatically by build.rs\n\n{formatted}"
570 )
571}
572
573fn emit_provider_enum() -> TokenStream {
574 let catalog_variants = PROVIDERS.iter().map(|cfg| format_ident!("{}", cfg.enum_name));
575 let dynamic_variants = DYNAMIC_PROVIDERS.iter().map(|d| format_ident!("{}", d.enum_name));
576 quote! {
577 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
581 pub enum Provider {
582 #(#catalog_variants,)*
583 #(#dynamic_variants,)*
584 }
585 }
586}
587
588fn emit_provider_enum_impl() -> TokenStream {
589 let parser_arms = provider_match_arms(|cfg| cfg.parser_name, |d| d.parser_name);
590 let genai_provider_name_arms = provider_match_arms(|cfg| cfg.genai_provider_name, |d| d.genai_provider_name);
591 let display_arms = provider_match_arms(|cfg| cfg.display_name, |d| d.display_name);
592
593 let env_var_some = PROVIDERS.iter().filter_map(|cfg| {
594 cfg.env_var.map(|var| {
595 let v = format_ident!("{}", cfg.enum_name);
596 quote! { Self::#v => Some(#var), }
597 })
598 });
599
600 let env_var_none = provider_or_pats(|cfg| cfg.env_var.is_none(), |_| true);
601 let oauth_some = PROVIDERS.iter().filter_map(|cfg| {
602 cfg.oauth_provider_id.map(|id| {
603 let v = format_ident!("{}", cfg.enum_name);
604 quote! { Self::#v => Some(#id), }
605 })
606 });
607 let oauth_none = provider_or_pats(|cfg| cfg.oauth_provider_id.is_none(), |_| true);
608
609 let is_local_true = provider_or_pats(|_| false, |_| true);
610 let is_local_false = provider_or_pats(|_| true, |_| false);
611 let all_variants = PROVIDERS
612 .iter()
613 .map(|cfg| format_ident!("{}", cfg.enum_name))
614 .chain(DYNAMIC_PROVIDERS.iter().map(|d| format_ident!("{}", d.enum_name)));
615
616 quote! {
617 impl Provider {
618 pub const ALL: &[Provider] = &[#(Self::#all_variants),*];
620
621 pub fn parser_name(self) -> &'static str {
623 match self { #parser_arms }
624 }
625
626 #[allow(clippy::match_same_arms)]
628 pub fn genai_provider_name(self) -> &'static str {
629 match self { #genai_provider_name_arms }
630 }
631
632 pub fn display_name(self) -> &'static str {
634 match self { #display_arms }
635 }
636
637 pub fn required_env_var(self) -> Option<&'static str> {
639 match self {
640 #(#env_var_some)*
641 #env_var_none => None,
642 }
643 }
644
645 pub fn oauth_provider_id(self) -> Option<&'static str> {
647 match self {
648 #(#oauth_some)*
649 #oauth_none => None,
650 }
651 }
652
653 pub fn is_local(self) -> bool {
656 match self {
657 #is_local_true => true,
658 #is_local_false => false,
659 }
660 }
661 }
662 }
663}
664
665fn emit_provider_enum_display() -> TokenStream {
666 quote! {
667 impl std::fmt::Display for Provider {
668 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669 f.write_str(self.parser_name())
670 }
671 }
672 }
673}
674
675fn emit_provider_enum_fromstr() -> TokenStream {
676 let catalog_arms = PROVIDERS.iter().map(|cfg| {
677 let v = format_ident!("{}", cfg.enum_name);
678 let name = cfg.parser_name;
679 quote! { #name => Ok(Self::#v), }
680 });
681
682 let dynamic_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
683 let v = format_ident!("{}", d.enum_name);
684 let name = d.parser_name;
685 quote! { #name => Ok(Self::#v), }
686 });
687
688 quote! {
689 impl std::str::FromStr for Provider {
690 type Err = String;
691 fn from_str(s: &str) -> Result<Self, Self::Err> {
692 match s {
693 #(#catalog_arms)*
694 #(#dynamic_arms)*
695 other => Err(format!("Unknown provider: '{other}'")),
696 }
697 }
698 }
699 }
700}
701
702fn emit_provider_enums(provider_models: &ProviderModels) -> TokenStream {
703 let enums = PROVIDERS.iter().map(|cfg| {
704 let inner = format_ident!("{}", cfg.inner_enum_name());
705 let variants = provider_models[cfg.dev_id].iter().map(|m| format_ident!("{}", m.variant_name));
706 quote! {
707 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
708 pub enum #inner {
709 #(#variants,)*
710 }
711 }
712 });
713 quote! { #(#enums)* }
714}
715
716fn emit_provider_impls(provider_models: &ProviderModels) -> TokenStream {
717 let impls = PROVIDERS.iter().map(|cfg| {
718 let models = &provider_models[cfg.dev_id];
719 let enum_ident = format_ident!("{}", cfg.inner_enum_name());
720
721 let model_id_arms = models.iter().map(|m| {
722 let v = format_ident!("{}", m.variant_name);
723 let id = &m.model_id;
724 quote! { Self::#v => #id, }
725 });
726
727 let display_name_arms = grouped_arms(
728 models,
729 |m| m.display_name.clone(),
730 |m| {
731 let s = &m.display_name;
732 quote! { #s }
733 },
734 );
735
736 let context_window_arms =
737 grouped_arms(models, |m| m.context_window, |m| num_lit_with_underscores(m.context_window));
738
739 let reasoning_levels_arms = emit_reasoning_levels_arms(models);
740
741 let prompt_caching_arms = grouped_arms(
742 models,
743 |m| m.supports_prompt_caching,
744 |m| {
745 let b = m.supports_prompt_caching;
746 quote! { #b }
747 },
748 );
749
750 let pricing_arms = emit_pricing_arms(models);
751
752 let modality_methods = ["image", "audio"].iter().map(|modality| {
753 let method = format_ident!("supports_{}", modality);
754 let mod_owned = (*modality).to_string();
755 let arms = grouped_arms(models, move |m| m.input_modalities.contains(&mod_owned), {
756 let mod_owned = (*modality).to_string();
757 move |m| {
758 let b = m.input_modalities.contains(&mod_owned);
759 quote! { #b }
760 }
761 });
762 quote! {
763 #[allow(clippy::too_many_lines)]
764 pub fn #method(self) -> bool {
765 match self { #arms }
766 }
767 }
768 });
769
770 let transport_arms = emit_transport_arms(models);
771
772 let all_variants = models.iter().map(|m| format_ident!("{}", m.variant_name));
773
774 let from_str_impl = emit_from_str_impl(&enum_ident, cfg.parser_name, models);
775
776 quote! {
777 impl #enum_ident {
778 #[allow(clippy::too_many_lines)]
779 fn model_id(self) -> &'static str {
780 match self { #(#model_id_arms)* }
781 }
782
783 #[allow(clippy::too_many_lines)]
784 fn display_name(self) -> &'static str {
785 match self { #display_name_arms }
786 }
787
788 #[allow(clippy::too_many_lines)]
789 fn context_window(self) -> u32 {
790 match self { #context_window_arms }
791 }
792
793 #[allow(clippy::too_many_lines)]
794 pub fn reasoning_levels(self) -> &'static [ReasoningEffort] {
795 match self { #reasoning_levels_arms }
796 }
797
798 pub fn supports_reasoning(self) -> bool {
799 !self.reasoning_levels().is_empty()
800 }
801
802 #[allow(clippy::too_many_lines)]
803 pub fn supports_prompt_caching(self) -> bool {
804 match self { #prompt_caching_arms }
805 }
806
807 #[allow(clippy::too_many_lines, clippy::match_same_arms, clippy::unreadable_literal)]
808 pub fn pricing(self) -> Option<ModelPricing> {
809 match self { #pricing_arms }
810 }
811
812 #(#modality_methods)*
813
814 #[allow(clippy::too_many_lines)]
815 pub fn transport(self) -> Option<ModelTransport> {
816 match self { #transport_arms }
817 }
818
819 const ALL: &[#enum_ident] = &[#(Self::#all_variants),*];
820 }
821
822 #from_str_impl
823 }
824 });
825 quote! { #(#impls)* }
826}
827
828fn emit_pricing_arms(models: &[ModelInfo]) -> TokenStream {
829 let arms = models.iter().map(|model| {
830 let variant = format_ident!("{}", model.variant_name);
831 let Some(pricing) = &model.pricing else {
832 return quote! { Self::#variant => None, };
833 };
834 let input = pricing.input;
835 let output = pricing.output;
836 let cache_read = pricing.cache_read.map_or_else(|| quote! { None }, |value| quote! { Some(#value) });
837 let cache_write = pricing.cache_write.map_or_else(|| quote! { None }, |value| quote! { Some(#value) });
838 quote! {
839 Self::#variant => Some(ModelPricing {
840 input_per_million: #input,
841 output_per_million: #output,
842 cache_read_per_million: #cache_read,
843 cache_write_per_million: #cache_write,
844 }),
845 }
846 });
847 quote! { #(#arms)* }
848}
849
850fn emit_from_str_impl(enum_ident: &proc_macro2::Ident, parser_name: &str, models: &[ModelInfo]) -> TokenStream {
851 let arms = models.iter().map(|m| {
852 let id = &m.model_id;
853 let v = format_ident!("{}", m.variant_name);
854 quote! { #id => Ok(Self::#v), }
855 });
856 let err_msg = format!("Unknown {parser_name} model: '{{s}}'");
857 quote! {
858 impl std::str::FromStr for #enum_ident {
859 type Err = String;
860
861 #[allow(clippy::too_many_lines)]
862 fn from_str(s: &str) -> Result<Self, Self::Err> {
863 match s {
864 #(#arms)*
865 _ => Err(format!(#err_msg)),
866 }
867 }
868 }
869 }
870}
871
872fn grouped_arms<K, R>(
874 models: &[ModelInfo],
875 key_fn: impl Fn(&ModelInfo) -> K,
876 rhs_fn: impl Fn(&ModelInfo) -> R,
877) -> TokenStream
878where
879 K: Eq + Ord,
880 R: ToTokens,
881{
882 let mut groups: BTreeMap<K, Vec<&ModelInfo>> = BTreeMap::new();
883 for m in models {
884 groups.entry(key_fn(m)).or_default().push(m);
885 }
886 let arms = groups.values().map(|members| {
887 let pats = members.iter().map(|m| {
888 let v = format_ident!("{}", m.variant_name);
889 quote! { Self::#v }
890 });
891 let rhs = rhs_fn(members[0]);
892 quote! { #(#pats)|* => #rhs, }
893 });
894 quote! { #(#arms)* }
895}
896
897fn emit_reasoning_levels_arms(models: &[ModelInfo]) -> TokenStream {
898 grouped_arms(
899 models,
900 |m| m.reasoning_levels.clone(),
901 |m| {
902 if m.reasoning_levels.is_empty() {
903 quote! { &[] }
904 } else {
905 let items = m.reasoning_levels.iter().map(|l| {
906 let variant = format_ident!("{}", level_str_to_variant(l));
907 quote! { ReasoningEffort::#variant }
908 });
909 quote! { &[#(#items),*] }
910 }
911 },
912 )
913}
914
915fn emit_transport_arms(models: &[ModelInfo]) -> TokenStream {
916 grouped_arms(
917 models,
918 |m| m.transport.clone(),
919 |m| match m.transport.as_ref() {
920 Some(TransportInfo::OpenAiResponses { base_url_template }) => {
921 quote! { Some(ModelTransport::OpenAiResponses { base_url_template: #base_url_template }) }
922 }
923 None => quote! { None },
924 },
925 )
926}
927
928fn level_str_to_variant(level: &str) -> String {
931 let canonical =
932 level.parse::<utils::ReasoningEffort>().unwrap_or_else(|_| panic!("Unknown reasoning level: {level}")).as_str();
933 let mut variant = canonical.to_string();
934 variant[..1].make_ascii_uppercase();
935 variant
936}
937
938fn emit_llm_model_enum() -> TokenStream {
939 let catalog_variants = PROVIDERS.iter().map(|cfg| {
940 let v = format_ident!("{}", cfg.enum_name);
941 let inner = format_ident!("{}Model", cfg.enum_name);
942 quote! { #v(#inner) }
943 });
944 let dynamic_variants = DYNAMIC_PROVIDERS.iter().map(|d| {
945 let v = format_ident!("{}", d.enum_name);
946 quote! { #v(String) }
947 });
948 quote! {
949 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
951 pub enum LlmModel {
952 #(#catalog_variants,)*
953 #(#dynamic_variants,)*
954 }
955 }
956}
957
958fn emit_from_impls() -> TokenStream {
959 let impls = PROVIDERS.iter().map(|cfg| {
960 let outer = format_ident!("{}Model", cfg.enum_name);
961 let v = format_ident!("{}", cfg.enum_name);
962 quote! {
963 impl From<#outer> for LlmModel {
964 fn from(m: #outer) -> Self {
965 LlmModel::#v(m)
966 }
967 }
968 }
969 });
970 quote! { #(#impls)* }
971}
972
973fn emit_llm_model_impl() -> TokenStream {
974 let model_id = emit_llm_model_id();
975 let display_name = emit_llm_display_name();
976 let provider = emit_llm_provider();
977 let provider_enum = emit_llm_provider_enum();
978 let provider_display_name = emit_llm_provider_display_name();
979 let context_window = emit_llm_context_window();
980 let required_env_var = emit_llm_required_env_var();
981 let all_required_env_vars = emit_llm_all_required_env_vars();
982 let oauth_provider_id = emit_llm_oauth_provider_id();
983 let reasoning_levels = emit_llm_reasoning_levels();
984 let supports_reasoning = emit_llm_supports_reasoning();
985 let supports_prompt_caching = emit_llm_supports_prompt_caching();
986 let pricing = emit_llm_pricing();
987 let modality_methods = ["image", "audio"].iter().map(|m| emit_llm_supports_modality(m));
988 let transport = emit_llm_transport();
989 let all = emit_llm_all();
990
991 quote! {
992 impl LlmModel {
993 #model_id
994 #display_name
995 #provider
996 #provider_enum
997 #provider_display_name
998 #context_window
999 #required_env_var
1000 #all_required_env_vars
1001 #oauth_provider_id
1002 #reasoning_levels
1003 #supports_reasoning
1004 #supports_prompt_caching
1005 #pricing
1006 #(#modality_methods)*
1007 #transport
1008 #all
1009 }
1010 }
1011}
1012
1013fn emit_llm_model_id() -> TokenStream {
1014 let catalog_arms = PROVIDERS.iter().map(|cfg| {
1015 let v = format_ident!("{}", cfg.enum_name);
1016 if cfg.is_hybrid_dynamic {
1017 quote! { Self::#v(m) => m.model_id(), }
1018 } else {
1019 quote! { Self::#v(m) => Cow::Borrowed(m.model_id()), }
1020 }
1021 });
1022 let dyn_pats = dynamic_pattern_with_binding("s");
1023 quote! {
1024 pub fn model_id(&self) -> Cow<'static, str> {
1026 match self {
1027 #(#catalog_arms)*
1028 #dyn_pats => Cow::Owned(s.clone()),
1029 }
1030 }
1031 }
1032}
1033
1034fn emit_llm_display_name() -> TokenStream {
1035 let catalog_arms = PROVIDERS.iter().map(|cfg| {
1036 let v = format_ident!("{}", cfg.enum_name);
1037 if cfg.is_hybrid_dynamic {
1038 quote! { Self::#v(m) => m.display_name(), }
1039 } else {
1040 quote! { Self::#v(m) => Cow::Borrowed(m.display_name()), }
1041 }
1042 });
1043 let dyn_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
1044 let v = format_ident!("{}", d.enum_name);
1045 let fmt = format!("{} {{s}}", d.enum_name);
1046 quote! { Self::#v(s) => Cow::Owned(format!(#fmt)), }
1047 });
1048 quote! {
1049 pub fn display_name(&self) -> Cow<'static, str> {
1051 match self {
1052 #(#catalog_arms)*
1053 #(#dyn_arms)*
1054 }
1055 }
1056 }
1057}
1058
1059fn emit_llm_provider() -> TokenStream {
1060 let arms = llm_match_arms_ignored(|cfg| cfg.parser_name, |d| d.parser_name);
1061 quote! {
1062 pub fn provider(&self) -> &'static str {
1064 match self { #arms }
1065 }
1066 }
1067}
1068
1069fn emit_llm_provider_enum() -> TokenStream {
1070 let arms = llm_match_arms_ignored(
1071 |cfg| {
1072 let v = format_ident!("{}", cfg.enum_name);
1073 quote! { Provider::#v }
1074 },
1075 |d| {
1076 let v = format_ident!("{}", d.enum_name);
1077 quote! { Provider::#v }
1078 },
1079 );
1080 quote! {
1081 pub fn provider_enum(&self) -> Provider {
1083 match self { #arms }
1084 }
1085 }
1086}
1087
1088fn emit_llm_provider_display_name() -> TokenStream {
1089 let arms = llm_match_arms_ignored(|cfg| cfg.display_name, |d| d.display_name);
1090 quote! {
1091 pub fn provider_display_name(&self) -> &'static str {
1093 match self { #arms }
1094 }
1095 }
1096}
1097
1098fn emit_llm_context_window() -> TokenStream {
1099 let catalog_arms = PROVIDERS.iter().map(|cfg| {
1100 let v = format_ident!("{}", cfg.enum_name);
1101 if cfg.is_hybrid_dynamic {
1102 quote! { Self::#v(m) => m.context_window(), }
1103 } else {
1104 quote! { Self::#v(m) => Some(m.context_window()), }
1105 }
1106 });
1107 let dyn_pats = dynamic_pattern_with_binding("_");
1108 quote! {
1109 pub fn context_window(&self) -> Option<u32> {
1111 match self {
1112 #(#catalog_arms)*
1113 #dyn_pats => None,
1114 }
1115 }
1116 }
1117}
1118
1119fn emit_llm_required_env_var() -> TokenStream {
1120 let some_arms = PROVIDERS.iter().filter_map(|cfg| {
1121 cfg.env_var.map(|var| {
1122 let v = format_ident!("{}", cfg.enum_name);
1123 quote! { Self::#v(_) => Some(#var), }
1124 })
1125 });
1126 let none_pats = llm_or_pats(|cfg| cfg.env_var.is_none(), |_| true);
1127 quote! {
1128 pub fn required_env_var(&self) -> Option<&'static str> {
1130 match self {
1131 #(#some_arms)*
1132 #none_pats => None,
1133 }
1134 }
1135 }
1136}
1137
1138fn emit_llm_all_required_env_vars() -> TokenStream {
1139 let vars = PROVIDERS.iter().filter_map(|cfg| cfg.env_var);
1140 quote! {
1141 pub const ALL_REQUIRED_ENV_VARS: &[&str] = &[#(#vars),*];
1143 }
1144}
1145
1146fn emit_llm_oauth_provider_id() -> TokenStream {
1147 let some_arms = PROVIDERS.iter().filter_map(|cfg| {
1148 cfg.oauth_provider_id.map(|id| {
1149 let v = format_ident!("{}", cfg.enum_name);
1150 quote! { Self::#v(_) => Some(#id), }
1151 })
1152 });
1153 let none_pats = llm_or_pats(|cfg| cfg.oauth_provider_id.is_none(), |_| true);
1154 quote! {
1155 pub fn oauth_provider_id(&self) -> Option<&'static str> {
1157 match self {
1158 #(#some_arms)*
1159 #none_pats => None,
1160 }
1161 }
1162 }
1163}
1164
1165fn emit_llm_reasoning_levels() -> TokenStream {
1166 let body = llm_delegate_with_dynamic_default("reasoning_levels", "e! { &[] });
1167 quote! {
1168 pub fn reasoning_levels(&self) -> &'static [ReasoningEffort] {
1170 #body
1171 }
1172 }
1173}
1174
1175fn emit_llm_supports_reasoning() -> TokenStream {
1176 quote! {
1177 pub fn supports_reasoning(&self) -> bool {
1179 !self.reasoning_levels().is_empty()
1180 }
1181 }
1182}
1183
1184fn emit_llm_supports_prompt_caching() -> TokenStream {
1185 let body = llm_delegate_with_dynamic_default("supports_prompt_caching", "e! { false });
1186 quote! {
1187 pub fn supports_prompt_caching(&self) -> bool {
1189 #body
1190 }
1191 }
1192}
1193
1194fn emit_llm_pricing() -> TokenStream {
1195 let body = llm_delegate_with_dynamic_default("pricing", "e! { None });
1196 quote! {
1197 pub fn pricing(&self) -> Option<ModelPricing> {
1198 #body
1199 }
1200 }
1201}
1202
1203fn emit_llm_transport() -> TokenStream {
1204 let body = llm_delegate_with_dynamic_default("transport", "e! { None });
1205 quote! {
1206 pub fn transport(&self) -> Option<ModelTransport> {
1209 #body
1210 }
1211 }
1212}
1213
1214fn emit_llm_supports_modality(modality: &str) -> TokenStream {
1215 let method = format!("supports_{modality}");
1216 let method_ident = format_ident!("{}", method);
1217 let doc = format!(" Whether this model supports {modality} input");
1218 let body = llm_delegate_with_dynamic_default(&method, "e! { false });
1219 quote! {
1220 #[doc = #doc]
1221 pub fn #method_ident(&self) -> bool {
1222 #body
1223 }
1224 }
1225}
1226
1227fn emit_llm_all() -> TokenStream {
1228 let pushes = PROVIDERS.iter().map(|cfg| {
1229 let inner = format_ident!("{}", cfg.inner_enum_name());
1230 let outer = format_ident!("{}", cfg.outer_enum_name());
1231 let v = format_ident!("{}", cfg.enum_name);
1232 if cfg.is_hybrid_dynamic {
1233 quote! {
1234 v.extend(#inner::ALL.iter().copied().map(#outer::Foundation).map(LlmModel::#v));
1235 }
1236 } else {
1237 quote! {
1238 v.extend(#inner::ALL.iter().copied().map(LlmModel::#v));
1239 }
1240 }
1241 });
1242 quote! {
1243 pub fn all() -> &'static [LlmModel] {
1245 static ALL: LazyLock<Vec<LlmModel>> = LazyLock::new(|| {
1246 let mut v = Vec::new();
1247 #(#pushes)*
1248 v
1249 });
1250 &ALL
1251 }
1252 }
1253}
1254
1255fn emit_display_impl() -> TokenStream {
1256 quote! {
1257 impl std::fmt::Display for LlmModel {
1258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1259 write!(f, "{}:{}", self.provider(), self.model_id())
1260 }
1261 }
1262 }
1263}
1264
1265fn emit_fromstr_impl() -> TokenStream {
1266 let catalog_arms = PROVIDERS.iter().map(|cfg| {
1267 let name = cfg.parser_name;
1268 let outer = format_ident!("{}Model", cfg.enum_name);
1269 let v = format_ident!("{}", cfg.enum_name);
1270 quote! { #name => model_str.parse::<#outer>().map(Self::#v), }
1271 });
1272 let dyn_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
1273 let name = d.parser_name;
1274 let v = format_ident!("{}", d.enum_name);
1275 quote! { #name => Ok(Self::#v(model_str.to_string())), }
1276 });
1277 quote! {
1278 impl std::str::FromStr for LlmModel {
1279 type Err = String;
1280
1281 fn from_str(s: &str) -> Result<Self, Self::Err> {
1283 let (provider_str, model_str) = s.split_once(':').unwrap_or((s, ""));
1284 match provider_str {
1285 #(#catalog_arms)*
1286 #(#dyn_arms)*
1287 _ => Err(format!("Unknown provider: '{provider_str}'")),
1288 }
1289 }
1290 }
1291 }
1292}
1293
1294fn dynamic_pattern_with_binding(binding: &str) -> TokenStream {
1296 let binding_ident = if binding == "_" {
1297 quote! { _ }
1298 } else {
1299 let b = format_ident!("{}", binding);
1300 quote! { #b }
1301 };
1302 let pats = DYNAMIC_PROVIDERS.iter().map(|d| {
1303 let v = format_ident!("{}", d.enum_name);
1304 quote! { Self::#v(#binding_ident) }
1305 });
1306 quote! { #(#pats)|* }
1307}
1308
1309fn provider_match_arms<V: ToTokens>(
1313 catalog_value: impl Fn(&ProviderConfig) -> V,
1314 dynamic_value: impl Fn(&DynamicProviderConfig) -> V,
1315) -> TokenStream {
1316 let catalog = PROVIDERS.iter().map(|cfg| {
1317 let v = format_ident!("{}", cfg.enum_name);
1318 let val = catalog_value(cfg);
1319 quote! { Self::#v => #val, }
1320 });
1321 let dynamic = DYNAMIC_PROVIDERS.iter().map(|d| {
1322 let v = format_ident!("{}", d.enum_name);
1323 let val = dynamic_value(d);
1324 quote! { Self::#v => #val, }
1325 });
1326 quote! { #(#catalog)* #(#dynamic)* }
1327}
1328
1329fn provider_or_pats(
1332 include_catalog: impl Fn(&ProviderConfig) -> bool,
1333 include_dynamic: impl Fn(&DynamicProviderConfig) -> bool,
1334) -> TokenStream {
1335 let catalog = PROVIDERS.iter().filter(|cfg| include_catalog(cfg)).map(|cfg| {
1336 let v = format_ident!("{}", cfg.enum_name);
1337 quote! { Self::#v }
1338 });
1339 let dynamic = DYNAMIC_PROVIDERS.iter().filter(|d| include_dynamic(d)).map(|d| {
1340 let v = format_ident!("{}", d.enum_name);
1341 quote! { Self::#v }
1342 });
1343 let pats = catalog.chain(dynamic);
1344 quote! { #(#pats)|* }
1345}
1346
1347fn llm_match_arms_ignored<V: ToTokens>(
1350 catalog_value: impl Fn(&ProviderConfig) -> V,
1351 dynamic_value: impl Fn(&DynamicProviderConfig) -> V,
1352) -> TokenStream {
1353 let catalog = PROVIDERS.iter().map(|cfg| {
1354 let v = format_ident!("{}", cfg.enum_name);
1355 let val = catalog_value(cfg);
1356 quote! { Self::#v(_) => #val, }
1357 });
1358 let dynamic = DYNAMIC_PROVIDERS.iter().map(|d| {
1359 let v = format_ident!("{}", d.enum_name);
1360 let val = dynamic_value(d);
1361 quote! { Self::#v(_) => #val, }
1362 });
1363 quote! { #(#catalog)* #(#dynamic)* }
1364}
1365
1366fn llm_or_pats(
1369 include_catalog: impl Fn(&ProviderConfig) -> bool,
1370 include_dynamic: impl Fn(&DynamicProviderConfig) -> bool,
1371) -> TokenStream {
1372 let catalog = PROVIDERS.iter().filter(|cfg| include_catalog(cfg)).map(|cfg| {
1373 let v = format_ident!("{}", cfg.enum_name);
1374 quote! { Self::#v(_) }
1375 });
1376 let dynamic = DYNAMIC_PROVIDERS.iter().filter(|d| include_dynamic(d)).map(|d| {
1377 let v = format_ident!("{}", d.enum_name);
1378 quote! { Self::#v(_) }
1379 });
1380 let pats = catalog.chain(dynamic);
1381 quote! { #(#pats)|* }
1382}
1383
1384fn llm_delegate_with_dynamic_default(method: &str, dynamic_value: &TokenStream) -> TokenStream {
1388 let method_ident = format_ident!("{}", method);
1389 let catalog_arms = PROVIDERS.iter().map(|cfg| {
1390 let v = format_ident!("{}", cfg.enum_name);
1391 quote! { Self::#v(m) => m.#method_ident(), }
1392 });
1393 let dyn_pat = dynamic_pattern_with_binding("_");
1394 quote! {
1395 match self {
1396 #(#catalog_arms)*
1397 #dyn_pat => #dynamic_value,
1398 }
1399 }
1400}
1401
1402fn num_lit_with_underscores(n: u32) -> TokenStream {
1404 format_number(n).parse().expect("formatted number parses as a token")
1405}
1406
1407fn format_number(n: u32) -> String {
1409 let s = n.to_string();
1410 if s.len() <= 4 {
1411 return s;
1412 }
1413 let mut result = String::with_capacity(s.len() + s.len() / 3);
1414 for (i, ch) in s.chars().enumerate() {
1415 if i > 0 && (s.len() - i).is_multiple_of(3) {
1416 result.push('_');
1417 }
1418 result.push(ch);
1419 }
1420 result
1421}
1422
1423fn emit_provider_docs(ctx: &CodegenCtx) -> HashMap<String, String> {
1424 let mut docs = HashMap::new();
1425
1426 for cfg in PROVIDERS {
1427 let models = &ctx.provider_models[cfg.dev_id];
1428 let mut doc = String::new();
1429
1430 pushln(&mut doc, format!("`{}` LLM provider.", cfg.display_name));
1431 blank(&mut doc);
1432
1433 pushln(&mut doc, "# Authentication");
1434 blank(&mut doc);
1435 match cfg.env_var {
1436 Some(var) => pushln(&mut doc, format!("Set the `{var}` environment variable.")),
1437 None if cfg.oauth_provider_id.is_some() => {
1438 pushln(&mut doc, "This provider uses OAuth authentication.");
1439 }
1440 None => {
1441 pushln(
1442 &mut doc,
1443 "Uses the default AWS credential chain (environment variables, config files, IAM roles).",
1444 );
1445 pushln(
1446 &mut doc,
1447 "Models served from a dedicated endpoint also accept a Bedrock API key in `AWS_BEARER_TOKEN_BEDROCK`.",
1448 );
1449 }
1450 }
1451 blank(&mut doc);
1452
1453 pushln(&mut doc, "# Supported models");
1454 blank(&mut doc);
1455 pushln(&mut doc, "| Model ID | Name | Context | Reasoning | Image | Audio |");
1456 pushln(&mut doc, "|----------|------|---------|-----------|-------|-------|");
1457 for model in models {
1458 let ctx_str = format_context_window(model.context_window);
1459 let reasoning = if model.reasoning_levels.is_empty() { "" } else { "yes" };
1460 let image = if model.input_modalities.contains(&"image".to_string()) { "yes" } else { "" };
1461 let audio = if model.input_modalities.contains(&"audio".to_string()) { "yes" } else { "" };
1462 pushln(
1463 &mut doc,
1464 format!(
1465 "| `{}` | `{}` | `{}` | {} | {} | {} |",
1466 model.model_id, model.display_name, ctx_str, reasoning, image, audio
1467 ),
1468 );
1469 }
1470
1471 push_transport_section(&mut doc, models);
1472
1473 docs.insert(cfg.dev_id.to_string(), doc);
1474 }
1475
1476 for dyn_cfg in DYNAMIC_PROVIDERS {
1477 let mut doc = String::new();
1478 pushln(&mut doc, format!("`{}` LLM provider.", dyn_cfg.display_name));
1479 blank(&mut doc);
1480 pushln(
1481 &mut doc,
1482 format!("This provider accepts any model name at runtime (e.g. `{}:my-model`).", dyn_cfg.parser_name),
1483 );
1484 pushln(&mut doc, "No API key is required.");
1485 docs.insert(dyn_cfg.parser_name.to_string(), doc);
1486 }
1487
1488 docs
1489}
1490
1491fn push_transport_section(doc: &mut String, models: &[ModelInfo]) {
1493 let overridden: Vec<&ModelInfo> = models.iter().filter(|m| m.transport.is_some()).collect();
1494 if overridden.is_empty() {
1495 return;
1496 }
1497
1498 blank(doc);
1499 pushln(doc, "# Models with a dedicated endpoint");
1500 blank(doc);
1501 pushln(doc, "These models are served from their own endpoint and wire protocol");
1502 pushln(doc, "rather than the provider's default. `${VAR}` placeholders are resolved");
1503 pushln(doc, "at request time.");
1504 blank(doc);
1505 pushln(doc, "| Model ID | Endpoint | Wire shape |");
1506 pushln(doc, "|----------|----------|------------|");
1507 for model in overridden {
1508 let transport = model.transport.as_ref().expect("filtered to models with a transport");
1509 let (api, shape) = match transport {
1510 TransportInfo::OpenAiResponses { base_url_template } => (base_url_template.as_str(), "responses"),
1511 };
1512 pushln(doc, format!("| `{}` | `{api}` | `{shape}` |", model.model_id));
1513 }
1514}
1515
1516fn format_context_window(tokens: u32) -> String {
1518 if tokens == 0 {
1519 return "unknown".to_string();
1520 }
1521 if tokens >= 1_000_000 && tokens.is_multiple_of(1_000_000) {
1522 format!("{}M", tokens / 1_000_000)
1523 } else if tokens >= 1_000 && tokens.is_multiple_of(1_000) {
1524 format!("{}k", tokens / 1_000)
1525 } else {
1526 format_number(tokens)
1527 }
1528}
1529
1530fn pushln(out: &mut String, line: impl AsRef<str>) {
1531 writeln!(out, "{}", line.as_ref()).expect("writing to String should not fail");
1532}
1533
1534fn blank(out: &mut String) {
1535 pushln(out, "");
1536}
1537
1538#[cfg(test)]
1539mod tests {
1540 use super::*;
1541 use serde_json::Value;
1542 use serde_json::json;
1543 use tempfile::NamedTempFile;
1544
1545 #[test]
1546 fn model_id_to_variant_pascal_cases_segments() {
1547 assert_eq!(model_id_to_variant("claude-sonnet-4-5-20250929"), "ClaudeSonnet4520250929");
1548 assert_eq!(model_id_to_variant("gemini-2.5-flash"), "Gemini25Flash");
1549 assert_eq!(model_id_to_variant("deepseek-chat"), "DeepseekChat");
1550 assert_eq!(model_id_to_variant("glm-4.5"), "Glm45");
1551 }
1552
1553 #[test]
1554 fn model_id_to_variant_handles_slash_and_colon() {
1555 assert_eq!(model_id_to_variant("anthropic/claude-opus-4.6"), "AnthropicClaudeOpus46");
1556 assert_eq!(model_id_to_variant("openai/gpt-5.1-codex-max"), "OpenaiGpt51CodexMax");
1557 assert_eq!(model_id_to_variant("deepseek/deepseek-r1:free"), "DeepseekDeepseekR1Free");
1558 }
1559
1560 #[test]
1561 fn is_alias_detects_latest_suffix() {
1562 assert!(is_alias("claude-sonnet-4-5-latest"));
1563 assert!(is_alias("claude-3-7-sonnet-latest"));
1564 assert!(!is_alias("claude-sonnet-4-5-20250929"));
1565 }
1566
1567 #[test]
1568 fn build_uses_explicit_context_windows_for_codex_models() {
1569 let data = minimal_models_dev_json();
1570
1571 let models = build_from_value(&data);
1572 let window = |id: &str| models["codex"].iter().find(|model| model.model_id == id).unwrap().context_window;
1573 for model_id in [
1574 "gpt-6-astra",
1575 "gpt-5.6-sol",
1576 "gpt-5.6-terra",
1577 "gpt-5.6-luna",
1578 "gpt-5.5",
1579 "gpt-5.4",
1580 "gpt-5.4-mini",
1581 "gpt-5.2",
1582 ] {
1583 assert_eq!(window(model_id), 272_000);
1584 }
1585 }
1586
1587 #[test]
1588 fn transport_override_is_preserved_from_model_metadata() {
1589 let mut data = minimal_models_dev_json();
1590 insert_models(
1591 &mut data,
1592 "amazon-bedrock",
1593 json!({
1594 "with-transport": {
1595 "id": "with-transport", "name": "With Transport", "tool_call": true,
1596 "limit": {"context": 1000, "output": 0},
1597 "provider": {
1598 "npm": "@ai-sdk/amazon-bedrock/mantle",
1599 "api": "https://example.${AWS_REGION}.api.aws/openai/v1",
1600 "shape": "responses"
1601 }
1602 },
1603 "without-transport": {
1604 "id": "without-transport", "name": "Without Transport", "tool_call": true,
1605 "limit": {"context": 1000, "output": 0}
1606 }
1607 }),
1608 );
1609
1610 let models = build_from_value(&data);
1611 let transport =
1612 |id: &str| models["amazon-bedrock"].iter().find(|m| m.model_id == id).unwrap().transport.clone();
1613
1614 assert_eq!(
1615 transport("with-transport"),
1616 Some(TransportInfo::OpenAiResponses {
1617 base_url_template: "https://example.${AWS_REGION}.api.aws/openai/v1".to_string(),
1618 })
1619 );
1620 assert_eq!(transport("without-transport"), None);
1621 }
1622
1623 #[test]
1624 fn transport_override_with_only_an_npm_package_is_ignored() {
1625 let mut data = minimal_models_dev_json();
1626 anthropic_models(
1627 &mut data,
1628 json!({
1629 "npm-only": {
1630 "id": "npm-only", "name": "Npm Only", "tool_call": true,
1631 "limit": {"context": 1000, "output": 0},
1632 "provider": {"npm": "@ai-sdk/anthropic"}
1633 }
1634 }),
1635 );
1636
1637 let models = build_from_value(&data);
1638
1639 assert_eq!(models["anthropic"].iter().find(|m| m.model_id == "npm-only").unwrap().transport, None);
1640 }
1641
1642 #[test]
1643 fn unknown_wire_shape_is_rejected() {
1644 let mut data = minimal_models_dev_json();
1645 insert_models(
1646 &mut data,
1647 "amazon-bedrock",
1648 json!({
1649 "weird": {
1650 "id": "weird", "name": "Weird", "tool_call": true,
1651 "limit": {"context": 1000, "output": 0},
1652 "provider": {"api": "https://example.com/v1", "shape": "telepathy"}
1653 }
1654 }),
1655 );
1656 let parsed: ModelsDevData = serde_json::from_value(data).expect("parse fixture");
1657
1658 let error = build_provider_models(&parsed).unwrap_err();
1659
1660 assert!(
1661 matches!(error, CodegenError::UnsupportedWireShape { ref model_id, ref shape }
1662 if model_id == "weird" && shape == "telepathy"),
1663 "unexpected error: {error}"
1664 );
1665 }
1666
1667 #[test]
1668 fn incomplete_bedrock_transport_is_rejected() {
1669 let mut data = minimal_models_dev_json();
1670 insert_models(
1671 &mut data,
1672 "amazon-bedrock",
1673 json!({
1674 "incomplete": {
1675 "id": "incomplete", "name": "Incomplete", "tool_call": true,
1676 "limit": {"context": 1000, "output": 0},
1677 "provider": {"api": "https://example.com/v1"}
1678 }
1679 }),
1680 );
1681 let parsed: ModelsDevData = serde_json::from_value(data).expect("parse fixture");
1682
1683 let error = build_provider_models(&parsed).unwrap_err();
1684
1685 assert!(matches!(error, CodegenError::IncompleteTransport { ref model_id } if model_id == "incomplete"));
1686 }
1687
1688 #[test]
1689 fn format_context_window_formats_correctly() {
1690 assert_eq!(format_context_window(1_000_000), "1M");
1691 assert_eq!(format_context_window(200_000), "200k");
1692 assert_eq!(format_context_window(8_000), "8k");
1693 assert_eq!(format_context_window(0), "unknown");
1694 }
1695
1696 #[test]
1697 fn level_str_to_variant_covers_all_reasoning_efforts() {
1698 for effort in utils::ReasoningEffort::all() {
1699 let _ = level_str_to_variant(effort.as_str());
1700 }
1701 }
1702
1703 #[test]
1704 fn build_sorts_models_and_filters_aliases_and_non_tool_call() {
1705 let mut data = minimal_models_dev_json();
1706 anthropic_models(
1707 &mut data,
1708 json!({
1709 "b-model": {"id": "b-model", "name": "B Model", "tool_call": true, "limit": {"context": 2000, "output": 0}},
1710 "a-model": {"id": "a-model", "name": "A Model", "tool_call": true, "limit": {"context": 1000, "output": 0}},
1711 "alpha-latest": {"id": "alpha-latest", "name": "Alias", "tool_call": true, "limit": {"context": 500, "output": 0}},
1712 "no-tools": {"id": "no-tools", "name": "No Tools", "tool_call": false, "limit": {"context": 500, "output": 0}}
1713 }),
1714 );
1715
1716 let models = build_from_value(&data);
1717 let ids: Vec<&str> = models["anthropic"].iter().map(|m| m.model_id.as_str()).collect();
1718 assert_eq!(ids, vec!["a-model", "b-model"]);
1719 }
1720
1721 #[test]
1722 fn build_extra_source_ids_merges_unique_models_into_provider() {
1723 let mut data = minimal_models_dev_json();
1724 zai_extra_models(
1725 &mut data,
1726 json!({
1727 "extra-model": {"id": "extra-model", "name": "Extra Model", "tool_call": true, "limit": {"context": 4000, "output": 0}}
1728 }),
1729 );
1730
1731 let models = build_from_value(&data);
1732 assert!(models["zai"].iter().any(|m| m.model_id == "extra-model"));
1733 }
1734
1735 #[test]
1736 fn build_extra_source_ids_does_not_duplicate_existing_models() {
1737 let mut data = minimal_models_dev_json();
1738 let shared = json!({
1739 "shared-model": {"id": "shared-model", "name": "Shared Model", "tool_call": true, "limit": {"context": 1000, "output": 0}}
1740 });
1741 insert_models(&mut data, "zai", shared.clone());
1742 insert_models(&mut data, "zai-coding-plan", shared);
1743
1744 let models = build_from_value(&data);
1745 let count = models["zai"].iter().filter(|m| m.model_id == "shared-model").count();
1746 assert_eq!(count, 1);
1747 }
1748
1749 #[test]
1750 fn build_derives_reasoning_levels_from_source_metadata() {
1751 let mut data = minimal_models_dev_json();
1752 anthropic_models(
1753 &mut data,
1754 json!({
1755 "claude-test": {
1756 "id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
1757 "reasoning_options": [{"type": "effort", "values": ["low", "high", "max"]}],
1758 "limit": {"context": 200_000, "output": 0}
1759 }
1760 }),
1761 );
1762
1763 let models = build_from_value(&data);
1764 let model = models["anthropic"].iter().find(|model| model.model_id == "claude-test").unwrap();
1765 assert_eq!(model.reasoning_levels, ["low", "high", "max"]);
1766 }
1767
1768 #[test]
1769 fn build_rejects_unknown_reasoning_effort_metadata() {
1770 let mut data = minimal_models_dev_json();
1771 anthropic_models(
1772 &mut data,
1773 json!({
1774 "claude-test": {
1775 "id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
1776 "reasoning_options": [{"type": "effort", "values": ["ultra"]}],
1777 "limit": {"context": 200_000, "output": 0}
1778 }
1779 }),
1780 );
1781 let parsed: ModelsDevData = serde_json::from_value(data).unwrap();
1782
1783 let error = build_provider_models(&parsed).unwrap_err();
1784
1785 assert!(matches!(error, CodegenError::UnsupportedReasoningEffort { .. }));
1786 }
1787
1788 #[test]
1789 fn build_preserves_model_pricing_and_omits_codex_subscription_pricing() {
1790 let mut data = minimal_models_dev_json();
1791 anthropic_models(
1792 &mut data,
1793 json!({
1794 "priced": {
1795 "id": "priced", "name": "Priced", "tool_call": true,
1796 "limit": {"context": 200_000, "output": 0},
1797 "cost": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75}
1798 }
1799 }),
1800 );
1801 insert_models(
1802 &mut data,
1803 "openai",
1804 json!({
1805 "gpt-5.5": {
1806 "id": "gpt-5.5", "name": "GPT-5.5", "tool_call": true,
1807 "limit": {"context": 1_050_000, "output": 128_000},
1808 "cost": {"input": 1.25, "output": 10.0, "cache_read": 0.125}
1809 }
1810 }),
1811 );
1812
1813 let models = build_from_value(&data);
1814 let priced = models["anthropic"].iter().find(|model| model.model_id == "priced").unwrap();
1815 assert_eq!(priced.pricing.as_ref().map(|pricing| pricing.input), Some(3.0));
1816 assert_eq!(priced.pricing.as_ref().map(|pricing| pricing.output), Some(15.0));
1817 assert_eq!(priced.pricing.as_ref().and_then(|pricing| pricing.cache_read), Some(0.3));
1818 assert_eq!(priced.pricing.as_ref().and_then(|pricing| pricing.cache_write), Some(3.75));
1819
1820 let codex = models["codex"].iter().find(|model| model.model_id == "gpt-5.5").unwrap();
1821 assert_eq!(codex.pricing, None);
1822 assert!(codex.supports_prompt_caching);
1823 }
1824
1825 #[test]
1826 fn build_derives_prompt_caching_from_cost_fields() {
1827 let mut data = minimal_models_dev_json();
1828 insert_models(
1829 &mut data,
1830 "amazon-bedrock",
1831 json!({
1832 "cached": {
1833 "id": "cached", "name": "Cached", "tool_call": true,
1834 "limit": {"context": 200_000, "output": 0},
1835 "cost": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75}
1836 },
1837 "uncached": {
1838 "id": "uncached", "name": "Uncached", "tool_call": true,
1839 "limit": {"context": 200_000, "output": 0},
1840 "cost": {"input": 3.0, "output": 15.0}
1841 }
1842 }),
1843 );
1844
1845 let models = build_from_value(&data);
1846 let bedrock = &models["amazon-bedrock"];
1847 let cached = bedrock.iter().find(|m| m.model_id == "cached").unwrap();
1848 let uncached = bedrock.iter().find(|m| m.model_id == "uncached").unwrap();
1849 assert!(cached.supports_prompt_caching);
1850 assert!(!uncached.supports_prompt_caching);
1851 }
1852
1853 #[test]
1854 fn build_assigns_codex_model_specific_reasoning_levels() {
1855 let mut data = minimal_models_dev_json();
1856 insert_models(
1857 &mut data,
1858 "openai",
1859 json!({
1860 "gpt-5.6-sol": {
1861 "id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
1862 "reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}],
1863 "limit": {"context": 200_000, "output": 0}
1864 },
1865 "gpt-5.6-luna": {
1866 "id": "gpt-5.6-luna", "name": "GPT-5.6 Luna", "tool_call": true, "reasoning": true,
1867 "reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}],
1868 "limit": {"context": 200_000, "output": 0}
1869 },
1870 "gpt-5.4": {
1871 "id": "gpt-5.4", "name": "GPT-5.4", "tool_call": true, "reasoning": true,
1872 "limit": {"context": 200_000, "output": 0}
1873 }
1874 }),
1875 );
1876
1877 let models = build_from_value(&data);
1878 let levels = |id: &str| models["codex"].iter().find(|m| m.model_id == id).unwrap().reasoning_levels.clone();
1879 assert_eq!(levels("gpt-5.6-sol"), vec!["low", "medium", "high", "xhigh", "max"]);
1880 assert_eq!(levels("gpt-5.6-luna"), vec!["low", "medium", "high", "xhigh", "max"]);
1881 assert_eq!(levels("gpt-5.4"), vec!["low", "medium", "high", "xhigh"]);
1882 }
1883
1884 #[test]
1885 fn build_applies_codex_subscription_context_window_override() {
1886 let mut data = minimal_models_dev_json();
1887 insert_models(
1888 &mut data,
1889 "openai",
1890 json!({
1891 "gpt-5.5": {
1892 "id": "gpt-5.5", "name": "GPT-5.5", "tool_call": true, "reasoning": true,
1893 "limit": {"context": 1_050_000, "output": 128_000}
1894 }
1895 }),
1896 );
1897
1898 let models = build_from_value(&data);
1899 let codex = models["codex"].iter().find(|m| m.model_id == "gpt-5.5").unwrap();
1900 let openai = models["openai"].iter().find(|m| m.model_id == "gpt-5.5").unwrap();
1901 assert_eq!(codex.context_window, 272_000);
1902 assert_eq!(openai.context_window, 1_050_000);
1903 }
1904
1905 #[test]
1906 fn generate_uses_codex_subscription_model_ids() {
1907 let mut data = minimal_models_dev_json();
1908 insert_models(
1909 &mut data,
1910 "openai",
1911 json!({
1912 "gpt-5.1-codex": {
1913 "id": "gpt-5.1-codex", "name": "GPT-5.1 Codex", "tool_call": true, "reasoning": true,
1914 "limit": {"context": 400_000, "output": 128_000}
1915 },
1916 "gpt-5.6": {
1917 "id": "gpt-5.6", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
1918 "limit": {"context": 1_050_000, "output": 128_000}
1919 },
1920 "gpt-5.6-sol": {
1921 "id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
1922 "limit": {"context": 1_050_000, "output": 128_000}
1923 },
1924 "gpt-5.6-terra": {
1925 "id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", "tool_call": true, "reasoning": true,
1926 "limit": {"context": 1_050_000, "output": 128_000}
1927 },
1928 "gpt-5.6-luna": {
1929 "id": "gpt-5.6-luna", "name": "GPT-5.6 Luna", "tool_call": true, "reasoning": true,
1930 "limit": {"context": 1_050_000, "output": 128_000}
1931 }
1932 }),
1933 );
1934
1935 let tmp = NamedTempFile::new().unwrap();
1936 std::fs::write(tmp.path(), serde_json::to_string(&data).unwrap()).unwrap();
1937 let output = generate(tmp.path()).unwrap();
1938
1939 let codex_doc = &output.provider_docs["codex"];
1940 assert!(!codex_doc.contains("`gpt-5.6`"));
1941 assert!(!codex_doc.contains("`gpt-5.1-codex`"));
1942 assert!(codex_doc.contains("| `gpt-5.6-sol` | `GPT-5.6 Sol` | `272k` |"));
1943 assert!(codex_doc.contains("| `gpt-5.6-terra` | `GPT-5.6 Terra` | `272k` |"));
1944 assert!(codex_doc.contains("| `gpt-5.6-luna` | `GPT-5.6 Luna` | `272k` |"));
1945
1946 let openai_doc = &output.provider_docs["openai"];
1947 assert!(openai_doc.contains("`gpt-5.6`"));
1948 assert!(openai_doc.contains("`gpt-5.1-codex`"));
1949 assert!(openai_doc.contains("`gpt-5.6-sol`"));
1950 }
1951
1952 #[test]
1953 fn generate_emits_provider_docs() {
1954 let mut data = minimal_models_dev_json();
1955 anthropic_models(
1956 &mut data,
1957 json!({
1958 "claude-test": {
1959 "id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
1960 "limit": {"context": 200_000, "output": 0},
1961 "modalities": {"input": ["text", "image"]}
1962 }
1963 }),
1964 );
1965
1966 let tmp = NamedTempFile::new().unwrap();
1967 std::fs::write(tmp.path(), serde_json::to_string(&data).unwrap()).unwrap();
1968 let output = generate(tmp.path()).unwrap();
1969
1970 let anthropic_doc = &output.provider_docs["anthropic"];
1971 assert!(anthropic_doc.contains("`Anthropic` LLM provider."));
1972 assert!(anthropic_doc.contains("`ANTHROPIC_API_KEY`"));
1973 assert!(anthropic_doc.contains("| `claude-test` | `Claude Test` | `200k` | yes | yes | |"));
1974
1975 let ollama_doc = &output.provider_docs["ollama"];
1976 assert!(ollama_doc.contains("`Ollama` LLM provider."));
1977 assert!(ollama_doc.contains("any model name at runtime"));
1978 }
1979
1980 fn build_from_value(data: &Value) -> ProviderModels {
1981 let parsed: ModelsDevData = serde_json::from_value(data.clone()).expect("parse fixture");
1982 build_provider_models(&parsed).expect("build provider models")
1983 }
1984
1985 fn anthropic_models(data: &mut Value, models: Value) {
1986 insert_models(data, "anthropic", models);
1987 }
1988
1989 fn zai_extra_models(data: &mut Value, models: Value) {
1990 insert_models(data, "zai-coding-plan", models);
1991 }
1992
1993 fn insert_models(data: &mut Value, provider_key: &str, models: Value) {
1994 let provider = data.as_object_mut().unwrap().get_mut(provider_key).unwrap().as_object_mut().unwrap();
1995 let target = provider.get_mut("models").unwrap().as_object_mut().unwrap();
1996 let Value::Object(models) = models else {
1997 panic!("models fixture must be an object");
1998 };
1999 target.extend(models);
2000 }
2001
2002 fn minimal_models_dev_json() -> Value {
2003 let mut root = serde_json::Map::new();
2004 for cfg in PROVIDERS {
2005 let json_key = cfg.json_key();
2006 root.entry(json_key.to_string())
2007 .or_insert_with(|| json!({"id": json_key, "name": json_key, "env": [], "models": {}}));
2008 for &extra in cfg.extra_source_ids {
2009 root.entry(extra.to_string())
2010 .or_insert_with(|| json!({"id": extra, "name": extra, "env": [], "models": {}}));
2011 }
2012 }
2013 let openai = root.get_mut("openai").unwrap()["models"].as_object_mut().unwrap();
2014 for model in CODEX_SUBSCRIPTION_MODELS {
2015 openai.insert(
2016 model.id.to_string(),
2017 json!({
2018 "id": model.id,
2019 "name": model.id,
2020 "tool_call": true,
2021 "reasoning": true,
2022 "reasoning_options": [{"type": "effort", "values": ["low", "medium", "high", "xhigh"]}],
2023 "limit": {"context": 1_050_000, "output": 0}
2024 }),
2025 );
2026 }
2027 Value::Object(root)
2028 }
2029}