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