#![doc = include_str!("../README.md")]
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Write;
use std::path::Path;
type ModelsDevData = HashMap<String, ProviderData>;
#[derive(Debug, Deserialize)]
struct ProviderData {
#[allow(dead_code)]
id: String,
#[allow(dead_code)]
name: String,
#[serde(default)]
#[allow(dead_code)]
env: Vec<String>,
#[serde(default)]
models: HashMap<String, ModelData>,
}
#[derive(Debug, Deserialize)]
struct ModelData {
id: String,
name: String,
#[serde(default)]
tool_call: Option<bool>,
#[serde(default)]
reasoning: Option<bool>,
#[serde(default)]
reasoning_options: Vec<ReasoningOption>,
#[serde(default)]
#[allow(dead_code)]
cost: Option<CostData>,
#[serde(default)]
limit: Option<LimitData>,
#[serde(default)]
modalities: Option<ModalitiesData>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ReasoningOption {
Effort { values: Vec<Option<String>> },
Toggle,
BudgetTokens,
}
#[derive(Debug, Deserialize, Default)]
struct ModalitiesData {
#[serde(default)]
input: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CostData {
#[serde(default)]
input: f64,
#[serde(default)]
output: f64,
#[serde(default)]
cache_read: Option<f64>,
#[serde(default)]
cache_write: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct LimitData {
#[serde(default)]
context: u32,
#[serde(default)]
#[allow(dead_code)]
output: u32,
}
impl CostData {
fn has_prompt_caching(&self) -> bool {
self.cache_read.is_some() || self.cache_write.is_some()
}
}
struct ProviderConfig {
dev_id: &'static str,
source_dev_id: Option<&'static str>,
extra_source_ids: &'static [&'static str],
explicit_models: Option<&'static [ExplicitModel]>,
enum_name: &'static str,
parser_name: &'static str,
genai_provider_name: &'static str,
display_name: &'static str,
env_var: Option<&'static str>,
oauth_provider_id: Option<&'static str>,
fallback_reasoning_levels: &'static [&'static str],
is_hybrid_dynamic: bool,
}
struct ExplicitModel {
id: &'static str,
context_window: u32,
}
impl ProviderConfig {
const fn standard(
dev_id: &'static str,
enum_name: &'static str,
parser_name: &'static str,
display_name: &'static str,
env_var: Option<&'static str>,
) -> Self {
Self {
dev_id,
source_dev_id: None,
extra_source_ids: &[],
explicit_models: None,
enum_name,
parser_name,
genai_provider_name: parser_name,
display_name,
env_var,
oauth_provider_id: None,
fallback_reasoning_levels: &["low", "medium", "high"],
is_hybrid_dynamic: false,
}
}
fn explicit_model(&self, model_id: &str) -> Option<&'static ExplicitModel> {
self.explicit_models.and_then(|models| models.iter().find(|model| model.id == model_id))
}
fn inner_enum_name(&self) -> String {
if self.is_hybrid_dynamic {
format!("{}FoundationModel", self.enum_name)
} else {
format!("{}Model", self.enum_name)
}
}
fn outer_enum_name(&self) -> String {
format!("{}Model", self.enum_name)
}
fn json_key(&self) -> &'static str {
self.source_dev_id.unwrap_or(self.dev_id)
}
}
#[allow(clippy::struct_field_names)]
struct DynamicProviderConfig {
enum_name: &'static str,
parser_name: &'static str,
genai_provider_name: &'static str,
display_name: &'static str,
}
const PROVIDERS: &[ProviderConfig] = &[
ProviderConfig::standard("anthropic", "Anthropic", "anthropic", "Anthropic", Some("ANTHROPIC_API_KEY")),
ProviderConfig {
dev_id: "codex",
source_dev_id: Some("openai"),
extra_source_ids: &[],
explicit_models: Some(CODEX_SUBSCRIPTION_MODELS),
enum_name: "Codex",
parser_name: "codex",
genai_provider_name: "openai",
display_name: "Codex",
env_var: None,
oauth_provider_id: Some("codex"),
fallback_reasoning_levels: &["low", "medium", "high", "xhigh"],
is_hybrid_dynamic: false,
},
ProviderConfig::standard("deepseek", "DeepSeek", "deepseek", "DeepSeek", Some("DEEPSEEK_API_KEY")),
ProviderConfig {
genai_provider_name: "gcp.gemini",
..ProviderConfig::standard("google", "Gemini", "gemini", "Gemini", Some("GEMINI_API_KEY"))
},
ProviderConfig {
genai_provider_name: "moonshot_ai",
..ProviderConfig::standard("moonshotai", "Moonshot", "moonshot", "Moonshot", Some("MOONSHOT_API_KEY"))
},
ProviderConfig::standard("openai", "Openai", "openai", "OpenAI", Some("OPENAI_API_KEY")),
ProviderConfig::standard("openrouter", "OpenRouter", "openrouter", "OpenRouter", Some("OPENROUTER_API_KEY")),
ProviderConfig {
extra_source_ids: &["zai-coding-plan"],
..ProviderConfig::standard("zai", "ZAi", "zai", "ZAI", Some("ZAI_API_KEY"))
},
ProviderConfig {
genai_provider_name: "aws.bedrock",
is_hybrid_dynamic: true,
..ProviderConfig::standard("amazon-bedrock", "Bedrock", "bedrock", "AWS Bedrock", None)
},
];
const DYNAMIC_PROVIDERS: &[DynamicProviderConfig] = &[
DynamicProviderConfig {
enum_name: "Ollama",
parser_name: "ollama",
genai_provider_name: "ollama",
display_name: "Ollama",
},
DynamicProviderConfig {
enum_name: "LlamaCpp",
parser_name: "llamacpp",
genai_provider_name: "llama.cpp",
display_name: "LlamaCpp",
},
];
const CODEX_SUBSCRIPTION_CONTEXT_WINDOW: u32 = 272_000;
const CODEX_SUBSCRIPTION_MODELS: &[ExplicitModel] = &[
ExplicitModel { id: "gpt-5.6-sol", context_window: 372_000 },
ExplicitModel { id: "gpt-5.6-terra", context_window: 372_000 },
ExplicitModel { id: "gpt-5.6-luna", context_window: 372_000 },
ExplicitModel { id: "gpt-5.5", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
ExplicitModel { id: "gpt-5.4", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
ExplicitModel { id: "gpt-5.4-mini", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
ExplicitModel { id: "gpt-5.2", context_window: CODEX_SUBSCRIPTION_CONTEXT_WINDOW },
];
#[derive(Debug, Clone)]
struct ModelInfo {
variant_name: String,
model_id: String,
display_name: String,
context_window: u32,
reasoning_levels: Vec<String>,
input_modalities: Vec<String>,
supports_prompt_caching: bool,
}
type ProviderModels = BTreeMap<&'static str, Vec<ModelInfo>>;
struct CodegenCtx {
provider_models: ProviderModels,
}
pub struct GeneratedOutput {
pub rust_source: String,
pub provider_docs: HashMap<String, String>,
}
#[derive(Debug, thiserror::Error)]
pub enum CodegenError {
#[error("read: {0}")]
Read(#[from] std::io::Error),
#[error("parse: {0}")]
Parse(#[from] serde_json::Error),
#[error("Provider '{0}' not found in models.dev data")]
ProviderNotFound(String),
#[error("Configured model '{model_id}' was not found in provider '{provider_id}'")]
ConfiguredModelNotFound { provider_id: String, model_id: String },
#[error("Configured model '{model_id}' is duplicated for provider '{provider_id}'")]
DuplicateConfiguredModel { provider_id: String, model_id: String },
#[error("Configured model '{model_id}' is not tool-capable in provider '{provider_id}'")]
ConfiguredModelUnavailable { provider_id: String, model_id: String },
#[error("Model '{model_id}' declares unsupported reasoning effort '{effort}'")]
UnsupportedReasoningEffort { model_id: String, effort: String },
}
pub fn generate(models_json_path: &Path) -> Result<GeneratedOutput, CodegenError> {
let json_bytes = std::fs::read_to_string(models_json_path)?;
let data: ModelsDevData = serde_json::from_str(&json_bytes)?;
let provider_models = build_provider_models(&data)?;
let ctx = CodegenCtx { provider_models };
Ok(GeneratedOutput { rust_source: emit_generated_source(&ctx), provider_docs: emit_provider_docs(&ctx) })
}
fn build_provider_models(data: &ModelsDevData) -> Result<ProviderModels, CodegenError> {
let mut provider_models = ProviderModels::new();
for cfg in PROVIDERS {
let json_key = cfg.json_key();
let provider_data = data.get(json_key).ok_or_else(|| CodegenError::ProviderNotFound(json_key.to_string()))?;
validate_provider_config(cfg, provider_data)?;
let mut models: Vec<ModelInfo> = collect_models_from(cfg, &provider_data.models)?;
for &extra_key in cfg.extra_source_ids {
if let Some(extra_data) = data.get(extra_key) {
let extra = collect_models_from(cfg, &extra_data.models)?;
let existing_ids: std::collections::HashSet<String> =
models.iter().map(|m| m.model_id.clone()).collect();
models.extend(extra.into_iter().filter(|m| !existing_ids.contains(&m.model_id)));
}
}
models.sort_by(|a, b| a.model_id.cmp(&b.model_id));
provider_models.insert(cfg.dev_id, models);
}
Ok(provider_models)
}
fn validate_provider_config(cfg: &ProviderConfig, provider: &ProviderData) -> Result<(), CodegenError> {
let Some(explicit_models) = cfg.explicit_models else {
return Ok(());
};
let mut seen = HashSet::new();
for configured in explicit_models {
if !seen.insert(configured.id) {
return Err(CodegenError::DuplicateConfiguredModel {
provider_id: cfg.dev_id.to_string(),
model_id: configured.id.to_string(),
});
}
let Some(model) = provider.models.get(configured.id) else {
return Err(CodegenError::ConfiguredModelNotFound {
provider_id: cfg.dev_id.to_string(),
model_id: configured.id.to_string(),
});
};
if model.tool_call != Some(true) {
return Err(CodegenError::ConfiguredModelUnavailable {
provider_id: cfg.dev_id.to_string(),
model_id: configured.id.to_string(),
});
}
}
Ok(())
}
fn collect_models_from(
cfg: &ProviderConfig,
models: &HashMap<String, ModelData>,
) -> Result<Vec<ModelInfo>, CodegenError> {
models
.values()
.filter(|m| m.tool_call == Some(true))
.filter(|m| !is_alias(&m.id))
.filter(|m| cfg.explicit_models.is_none() || cfg.explicit_model(&m.id).is_some())
.map(|m| {
let reasoning_levels =
if m.reasoning.unwrap_or(false) { reasoning_levels_for_model(cfg, m)? } else { Vec::new() };
let input_modalities =
m.modalities.as_ref().map_or_else(|| vec!["text".to_string()], |md| md.input.clone());
let source_context_window = m.limit.as_ref().map_or(0, |l| l.context);
let context_window =
cfg.explicit_model(&m.id).map_or(source_context_window, |explicit| explicit.context_window);
Ok(ModelInfo {
variant_name: model_id_to_variant(&m.id),
model_id: m.id.clone(),
display_name: m.name.clone(),
context_window,
reasoning_levels,
input_modalities,
supports_prompt_caching: m.cost.as_ref().is_some_and(CostData::has_prompt_caching),
})
})
.collect()
}
fn reasoning_levels_for_model(cfg: &ProviderConfig, model: &ModelData) -> Result<Vec<String>, CodegenError> {
let Some(values) = model.reasoning_options.iter().find_map(|option| match option {
ReasoningOption::Effort { values } => Some(values),
ReasoningOption::Toggle | ReasoningOption::BudgetTokens => None,
}) else {
return Ok(cfg.fallback_reasoning_levels.iter().map(|level| (*level).to_string()).collect());
};
values
.iter()
.filter_map(|value| value.as_deref())
.filter(|value| !matches!(*value, "none" | "default"))
.map(|effort| {
effort.parse::<utils::ReasoningEffort>().map(|parsed| parsed.as_str().to_string()).map_err(|_| {
CodegenError::UnsupportedReasoningEffort { model_id: model.id.clone(), effort: effort.to_string() }
})
})
.collect()
}
fn is_alias(id: &str) -> bool {
id.ends_with("-latest")
}
fn model_id_to_variant(id: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for ch in id.chars() {
if ch == '-' || ch == '.' || ch == '/' || ch == ':' {
capitalize_next = true;
} else if capitalize_next {
result.push(ch.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(ch);
}
}
if result.starts_with(|c: char| c.is_ascii_digit()) {
result.insert(0, '_');
}
result
}
fn emit_generated_source(ctx: &CodegenCtx) -> String {
let provider_enum = emit_provider_enum();
let provider_enum_impl = emit_provider_enum_impl();
let provider_enum_display = emit_provider_enum_display();
let provider_enum_fromstr = emit_provider_enum_fromstr();
let provider_enums = emit_provider_enums(&ctx.provider_models);
let provider_impls = emit_provider_impls(&ctx.provider_models);
let llm_model_enum = emit_llm_model_enum();
let from_impls = emit_from_impls();
let llm_model_impl = emit_llm_model_impl();
let display_impl = emit_display_impl();
let fromstr_impl = emit_fromstr_impl();
let file_tokens = quote! {
use std::borrow::Cow;
use std::sync::LazyLock;
use crate::ReasoningEffort;
#provider_enum
#provider_enum_impl
#provider_enum_display
#provider_enum_fromstr
#provider_enums
#provider_impls
#llm_model_enum
#from_impls
#llm_model_impl
#display_impl
#fromstr_impl
};
let file: syn::File = syn::parse2(file_tokens).expect("generated tokens parse as Rust");
let formatted = prettyplease::unparse(&file);
format!(
"// Auto-generated from models.dev — do not edit manually\n// Regenerated automatically by build.rs\n\n{formatted}"
)
}
fn emit_provider_enum() -> TokenStream {
let catalog_variants = PROVIDERS.iter().map(|cfg| format_ident!("{}", cfg.enum_name));
let dynamic_variants = DYNAMIC_PROVIDERS.iter().map(|d| format_ident!("{}", d.enum_name));
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
#(#catalog_variants,)*
#(#dynamic_variants,)*
}
}
}
fn emit_provider_enum_impl() -> TokenStream {
let parser_arms = provider_match_arms(|cfg| cfg.parser_name, |d| d.parser_name);
let genai_provider_name_arms = provider_match_arms(|cfg| cfg.genai_provider_name, |d| d.genai_provider_name);
let display_arms = provider_match_arms(|cfg| cfg.display_name, |d| d.display_name);
let env_var_some = PROVIDERS.iter().filter_map(|cfg| {
cfg.env_var.map(|var| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v => Some(#var), }
})
});
let env_var_none = provider_or_pats(|cfg| cfg.env_var.is_none(), |_| true);
let oauth_some = PROVIDERS.iter().filter_map(|cfg| {
cfg.oauth_provider_id.map(|id| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v => Some(#id), }
})
});
let oauth_none = provider_or_pats(|cfg| cfg.oauth_provider_id.is_none(), |_| true);
let is_local_true = provider_or_pats(|_| false, |_| true);
let is_local_false = provider_or_pats(|_| true, |_| false);
let all_variants = PROVIDERS
.iter()
.map(|cfg| format_ident!("{}", cfg.enum_name))
.chain(DYNAMIC_PROVIDERS.iter().map(|d| format_ident!("{}", d.enum_name)));
quote! {
impl Provider {
pub const ALL: &[Provider] = &[#(Self::#all_variants),*];
pub fn parser_name(self) -> &'static str {
match self { #parser_arms }
}
#[allow(clippy::match_same_arms)]
pub fn genai_provider_name(self) -> &'static str {
match self { #genai_provider_name_arms }
}
pub fn display_name(self) -> &'static str {
match self { #display_arms }
}
pub fn required_env_var(self) -> Option<&'static str> {
match self {
#(#env_var_some)*
#env_var_none => None,
}
}
pub fn oauth_provider_id(self) -> Option<&'static str> {
match self {
#(#oauth_some)*
#oauth_none => None,
}
}
pub fn is_local(self) -> bool {
match self {
#is_local_true => true,
#is_local_false => false,
}
}
}
}
}
fn emit_provider_enum_display() -> TokenStream {
quote! {
impl std::fmt::Display for Provider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.parser_name())
}
}
}
}
fn emit_provider_enum_fromstr() -> TokenStream {
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
let name = cfg.parser_name;
quote! { #name => Ok(Self::#v), }
});
let dynamic_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
let name = d.parser_name;
quote! { #name => Ok(Self::#v), }
});
quote! {
impl std::str::FromStr for Provider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
#(#catalog_arms)*
#(#dynamic_arms)*
other => Err(format!("Unknown provider: '{other}'")),
}
}
}
}
}
fn emit_provider_enums(provider_models: &ProviderModels) -> TokenStream {
let enums = PROVIDERS.iter().map(|cfg| {
let inner = format_ident!("{}", cfg.inner_enum_name());
let variants = provider_models[cfg.dev_id].iter().map(|m| format_ident!("{}", m.variant_name));
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum #inner {
#(#variants,)*
}
}
});
quote! { #(#enums)* }
}
fn emit_provider_impls(provider_models: &ProviderModels) -> TokenStream {
let impls = PROVIDERS.iter().map(|cfg| {
let models = &provider_models[cfg.dev_id];
let enum_ident = format_ident!("{}", cfg.inner_enum_name());
let model_id_arms = models.iter().map(|m| {
let v = format_ident!("{}", m.variant_name);
let id = &m.model_id;
quote! { Self::#v => #id, }
});
let display_name_arms = grouped_arms(
models,
|m| m.display_name.clone(),
|m| {
let s = &m.display_name;
quote! { #s }
},
);
let context_window_arms =
grouped_arms(models, |m| m.context_window, |m| num_lit_with_underscores(m.context_window));
let reasoning_levels_arms = emit_reasoning_levels_arms(models);
let prompt_caching_arms = grouped_arms(
models,
|m| m.supports_prompt_caching,
|m| {
let b = m.supports_prompt_caching;
quote! { #b }
},
);
let modality_methods = ["image", "audio"].iter().map(|modality| {
let method = format_ident!("supports_{}", modality);
let mod_owned = (*modality).to_string();
let arms = grouped_arms(models, move |m| m.input_modalities.contains(&mod_owned), {
let mod_owned = (*modality).to_string();
move |m| {
let b = m.input_modalities.contains(&mod_owned);
quote! { #b }
}
});
quote! {
#[allow(clippy::too_many_lines)]
pub fn #method(self) -> bool {
match self { #arms }
}
}
});
let all_variants = models.iter().map(|m| format_ident!("{}", m.variant_name));
let from_str_impl = emit_from_str_impl(&enum_ident, cfg.parser_name, models);
quote! {
impl #enum_ident {
#[allow(clippy::too_many_lines)]
fn model_id(self) -> &'static str {
match self { #(#model_id_arms)* }
}
#[allow(clippy::too_many_lines)]
fn display_name(self) -> &'static str {
match self { #display_name_arms }
}
#[allow(clippy::too_many_lines)]
fn context_window(self) -> u32 {
match self { #context_window_arms }
}
#[allow(clippy::too_many_lines)]
pub fn reasoning_levels(self) -> &'static [ReasoningEffort] {
match self { #reasoning_levels_arms }
}
pub fn supports_reasoning(self) -> bool {
!self.reasoning_levels().is_empty()
}
#[allow(clippy::too_many_lines)]
pub fn supports_prompt_caching(self) -> bool {
match self { #prompt_caching_arms }
}
#(#modality_methods)*
const ALL: &[#enum_ident] = &[#(Self::#all_variants),*];
}
#from_str_impl
}
});
quote! { #(#impls)* }
}
fn emit_from_str_impl(enum_ident: &proc_macro2::Ident, parser_name: &str, models: &[ModelInfo]) -> TokenStream {
let arms = models.iter().map(|m| {
let id = &m.model_id;
let v = format_ident!("{}", m.variant_name);
quote! { #id => Ok(Self::#v), }
});
let err_msg = format!("Unknown {parser_name} model: '{{s}}'");
quote! {
impl std::str::FromStr for #enum_ident {
type Err = String;
#[allow(clippy::too_many_lines)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
#(#arms)*
_ => Err(format!(#err_msg)),
}
}
}
}
}
fn grouped_arms<K, R>(
models: &[ModelInfo],
key_fn: impl Fn(&ModelInfo) -> K,
rhs_fn: impl Fn(&ModelInfo) -> R,
) -> TokenStream
where
K: Eq + Ord,
R: ToTokens,
{
let mut groups: BTreeMap<K, Vec<&ModelInfo>> = BTreeMap::new();
for m in models {
groups.entry(key_fn(m)).or_default().push(m);
}
let arms = groups.values().map(|members| {
let pats = members.iter().map(|m| {
let v = format_ident!("{}", m.variant_name);
quote! { Self::#v }
});
let rhs = rhs_fn(members[0]);
quote! { #(#pats)|* => #rhs, }
});
quote! { #(#arms)* }
}
fn emit_reasoning_levels_arms(models: &[ModelInfo]) -> TokenStream {
grouped_arms(
models,
|m| m.reasoning_levels.clone(),
|m| {
if m.reasoning_levels.is_empty() {
quote! { &[] }
} else {
let items = m.reasoning_levels.iter().map(|l| {
let variant = format_ident!("{}", level_str_to_variant(l));
quote! { ReasoningEffort::#variant }
});
quote! { &[#(#items),*] }
}
},
)
}
fn level_str_to_variant(level: &str) -> String {
let canonical =
level.parse::<utils::ReasoningEffort>().unwrap_or_else(|_| panic!("Unknown reasoning level: {level}")).as_str();
let mut variant = canonical.to_string();
variant[..1].make_ascii_uppercase();
variant
}
fn emit_llm_model_enum() -> TokenStream {
let catalog_variants = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
let inner = format_ident!("{}Model", cfg.enum_name);
quote! { #v(#inner) }
});
let dynamic_variants = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
quote! { #v(String) }
});
quote! {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LlmModel {
#(#catalog_variants,)*
#(#dynamic_variants,)*
}
}
}
fn emit_from_impls() -> TokenStream {
let impls = PROVIDERS.iter().map(|cfg| {
let outer = format_ident!("{}Model", cfg.enum_name);
let v = format_ident!("{}", cfg.enum_name);
quote! {
impl From<#outer> for LlmModel {
fn from(m: #outer) -> Self {
LlmModel::#v(m)
}
}
}
});
quote! { #(#impls)* }
}
fn emit_llm_model_impl() -> TokenStream {
let model_id = emit_llm_model_id();
let display_name = emit_llm_display_name();
let provider = emit_llm_provider();
let provider_enum = emit_llm_provider_enum();
let provider_display_name = emit_llm_provider_display_name();
let context_window = emit_llm_context_window();
let required_env_var = emit_llm_required_env_var();
let all_required_env_vars = emit_llm_all_required_env_vars();
let oauth_provider_id = emit_llm_oauth_provider_id();
let reasoning_levels = emit_llm_reasoning_levels();
let supports_reasoning = emit_llm_supports_reasoning();
let supports_prompt_caching = emit_llm_supports_prompt_caching();
let modality_methods = ["image", "audio"].iter().map(|m| emit_llm_supports_modality(m));
let all = emit_llm_all();
quote! {
impl LlmModel {
#model_id
#display_name
#provider
#provider_enum
#provider_display_name
#context_window
#required_env_var
#all_required_env_vars
#oauth_provider_id
#reasoning_levels
#supports_reasoning
#supports_prompt_caching
#(#modality_methods)*
#all
}
}
}
fn emit_llm_model_id() -> TokenStream {
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
if cfg.is_hybrid_dynamic {
quote! { Self::#v(m) => m.model_id(), }
} else {
quote! { Self::#v(m) => Cow::Borrowed(m.model_id()), }
}
});
let dyn_pats = dynamic_pattern_with_binding("s");
quote! {
pub fn model_id(&self) -> Cow<'static, str> {
match self {
#(#catalog_arms)*
#dyn_pats => Cow::Owned(s.clone()),
}
}
}
}
fn emit_llm_display_name() -> TokenStream {
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
if cfg.is_hybrid_dynamic {
quote! { Self::#v(m) => m.display_name(), }
} else {
quote! { Self::#v(m) => Cow::Borrowed(m.display_name()), }
}
});
let dyn_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
let fmt = format!("{} {{s}}", d.enum_name);
quote! { Self::#v(s) => Cow::Owned(format!(#fmt)), }
});
quote! {
pub fn display_name(&self) -> Cow<'static, str> {
match self {
#(#catalog_arms)*
#(#dyn_arms)*
}
}
}
}
fn emit_llm_provider() -> TokenStream {
let arms = llm_match_arms_ignored(|cfg| cfg.parser_name, |d| d.parser_name);
quote! {
pub fn provider(&self) -> &'static str {
match self { #arms }
}
}
}
fn emit_llm_provider_enum() -> TokenStream {
let arms = llm_match_arms_ignored(
|cfg| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Provider::#v }
},
|d| {
let v = format_ident!("{}", d.enum_name);
quote! { Provider::#v }
},
);
quote! {
pub fn provider_enum(&self) -> Provider {
match self { #arms }
}
}
}
fn emit_llm_provider_display_name() -> TokenStream {
let arms = llm_match_arms_ignored(|cfg| cfg.display_name, |d| d.display_name);
quote! {
pub fn provider_display_name(&self) -> &'static str {
match self { #arms }
}
}
}
fn emit_llm_context_window() -> TokenStream {
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
if cfg.is_hybrid_dynamic {
quote! { Self::#v(m) => m.context_window(), }
} else {
quote! { Self::#v(m) => Some(m.context_window()), }
}
});
let dyn_pats = dynamic_pattern_with_binding("_");
quote! {
pub fn context_window(&self) -> Option<u32> {
match self {
#(#catalog_arms)*
#dyn_pats => None,
}
}
}
}
fn emit_llm_required_env_var() -> TokenStream {
let some_arms = PROVIDERS.iter().filter_map(|cfg| {
cfg.env_var.map(|var| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v(_) => Some(#var), }
})
});
let none_pats = llm_or_pats(|cfg| cfg.env_var.is_none(), |_| true);
quote! {
pub fn required_env_var(&self) -> Option<&'static str> {
match self {
#(#some_arms)*
#none_pats => None,
}
}
}
}
fn emit_llm_all_required_env_vars() -> TokenStream {
let vars = PROVIDERS.iter().filter_map(|cfg| cfg.env_var);
quote! {
pub const ALL_REQUIRED_ENV_VARS: &[&str] = &[#(#vars),*];
}
}
fn emit_llm_oauth_provider_id() -> TokenStream {
let some_arms = PROVIDERS.iter().filter_map(|cfg| {
cfg.oauth_provider_id.map(|id| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v(_) => Some(#id), }
})
});
let none_pats = llm_or_pats(|cfg| cfg.oauth_provider_id.is_none(), |_| true);
quote! {
pub fn oauth_provider_id(&self) -> Option<&'static str> {
match self {
#(#some_arms)*
#none_pats => None,
}
}
}
}
fn emit_llm_reasoning_levels() -> TokenStream {
let body = llm_delegate_with_dynamic_default("reasoning_levels", "e! { &[] });
quote! {
pub fn reasoning_levels(&self) -> &'static [ReasoningEffort] {
#body
}
}
}
fn emit_llm_supports_reasoning() -> TokenStream {
quote! {
pub fn supports_reasoning(&self) -> bool {
!self.reasoning_levels().is_empty()
}
}
}
fn emit_llm_supports_prompt_caching() -> TokenStream {
let body = llm_delegate_with_dynamic_default("supports_prompt_caching", "e! { false });
quote! {
pub fn supports_prompt_caching(&self) -> bool {
#body
}
}
}
fn emit_llm_supports_modality(modality: &str) -> TokenStream {
let method = format!("supports_{modality}");
let method_ident = format_ident!("{}", method);
let doc = format!(" Whether this model supports {modality} input");
let body = llm_delegate_with_dynamic_default(&method, "e! { false });
quote! {
#[doc = #doc]
pub fn #method_ident(&self) -> bool {
#body
}
}
}
fn emit_llm_all() -> TokenStream {
let pushes = PROVIDERS.iter().map(|cfg| {
let inner = format_ident!("{}", cfg.inner_enum_name());
let outer = format_ident!("{}", cfg.outer_enum_name());
let v = format_ident!("{}", cfg.enum_name);
if cfg.is_hybrid_dynamic {
quote! {
v.extend(#inner::ALL.iter().copied().map(#outer::Foundation).map(LlmModel::#v));
}
} else {
quote! {
v.extend(#inner::ALL.iter().copied().map(LlmModel::#v));
}
}
});
quote! {
pub fn all() -> &'static [LlmModel] {
static ALL: LazyLock<Vec<LlmModel>> = LazyLock::new(|| {
let mut v = Vec::new();
#(#pushes)*
v
});
&ALL
}
}
}
fn emit_display_impl() -> TokenStream {
quote! {
impl std::fmt::Display for LlmModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.provider(), self.model_id())
}
}
}
}
fn emit_fromstr_impl() -> TokenStream {
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let name = cfg.parser_name;
let outer = format_ident!("{}Model", cfg.enum_name);
let v = format_ident!("{}", cfg.enum_name);
quote! { #name => model_str.parse::<#outer>().map(Self::#v), }
});
let dyn_arms = DYNAMIC_PROVIDERS.iter().map(|d| {
let name = d.parser_name;
let v = format_ident!("{}", d.enum_name);
quote! { #name => Ok(Self::#v(model_str.to_string())), }
});
quote! {
impl std::str::FromStr for LlmModel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (provider_str, model_str) = s.split_once(':').unwrap_or((s, ""));
match provider_str {
#(#catalog_arms)*
#(#dyn_arms)*
_ => Err(format!("Unknown provider: '{provider_str}'")),
}
}
}
}
}
fn dynamic_pattern_with_binding(binding: &str) -> TokenStream {
let binding_ident = if binding == "_" {
quote! { _ }
} else {
let b = format_ident!("{}", binding);
quote! { #b }
};
let pats = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
quote! { Self::#v(#binding_ident) }
});
quote! { #(#pats)|* }
}
fn provider_match_arms<V: ToTokens>(
catalog_value: impl Fn(&ProviderConfig) -> V,
dynamic_value: impl Fn(&DynamicProviderConfig) -> V,
) -> TokenStream {
let catalog = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
let val = catalog_value(cfg);
quote! { Self::#v => #val, }
});
let dynamic = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
let val = dynamic_value(d);
quote! { Self::#v => #val, }
});
quote! { #(#catalog)* #(#dynamic)* }
}
fn provider_or_pats(
include_catalog: impl Fn(&ProviderConfig) -> bool,
include_dynamic: impl Fn(&DynamicProviderConfig) -> bool,
) -> TokenStream {
let catalog = PROVIDERS.iter().filter(|cfg| include_catalog(cfg)).map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v }
});
let dynamic = DYNAMIC_PROVIDERS.iter().filter(|d| include_dynamic(d)).map(|d| {
let v = format_ident!("{}", d.enum_name);
quote! { Self::#v }
});
let pats = catalog.chain(dynamic);
quote! { #(#pats)|* }
}
fn llm_match_arms_ignored<V: ToTokens>(
catalog_value: impl Fn(&ProviderConfig) -> V,
dynamic_value: impl Fn(&DynamicProviderConfig) -> V,
) -> TokenStream {
let catalog = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
let val = catalog_value(cfg);
quote! { Self::#v(_) => #val, }
});
let dynamic = DYNAMIC_PROVIDERS.iter().map(|d| {
let v = format_ident!("{}", d.enum_name);
let val = dynamic_value(d);
quote! { Self::#v(_) => #val, }
});
quote! { #(#catalog)* #(#dynamic)* }
}
fn llm_or_pats(
include_catalog: impl Fn(&ProviderConfig) -> bool,
include_dynamic: impl Fn(&DynamicProviderConfig) -> bool,
) -> TokenStream {
let catalog = PROVIDERS.iter().filter(|cfg| include_catalog(cfg)).map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v(_) }
});
let dynamic = DYNAMIC_PROVIDERS.iter().filter(|d| include_dynamic(d)).map(|d| {
let v = format_ident!("{}", d.enum_name);
quote! { Self::#v(_) }
});
let pats = catalog.chain(dynamic);
quote! { #(#pats)|* }
}
fn llm_delegate_with_dynamic_default(method: &str, dynamic_value: &TokenStream) -> TokenStream {
let method_ident = format_ident!("{}", method);
let catalog_arms = PROVIDERS.iter().map(|cfg| {
let v = format_ident!("{}", cfg.enum_name);
quote! { Self::#v(m) => m.#method_ident(), }
});
let dyn_pat = dynamic_pattern_with_binding("_");
quote! {
match self {
#(#catalog_arms)*
#dyn_pat => #dynamic_value,
}
}
}
fn num_lit_with_underscores(n: u32) -> TokenStream {
format_number(n).parse().expect("formatted number parses as a token")
}
fn format_number(n: u32) -> String {
let s = n.to_string();
if s.len() <= 4 {
return s;
}
let mut result = String::with_capacity(s.len() + s.len() / 3);
for (i, ch) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
result.push('_');
}
result.push(ch);
}
result
}
fn emit_provider_docs(ctx: &CodegenCtx) -> HashMap<String, String> {
let mut docs = HashMap::new();
for cfg in PROVIDERS {
let models = &ctx.provider_models[cfg.dev_id];
let mut doc = String::new();
pushln(&mut doc, format!("`{}` LLM provider.", cfg.display_name));
blank(&mut doc);
pushln(&mut doc, "# Authentication");
blank(&mut doc);
match cfg.env_var {
Some(var) => pushln(&mut doc, format!("Set the `{var}` environment variable.")),
None if cfg.oauth_provider_id.is_some() => {
pushln(&mut doc, "This provider uses OAuth authentication.");
}
None => {
pushln(
&mut doc,
"Uses the default AWS credential chain (environment variables, config files, IAM roles).",
);
}
}
blank(&mut doc);
pushln(&mut doc, "# Supported models");
blank(&mut doc);
pushln(&mut doc, "| Model ID | Name | Context | Reasoning | Image | Audio |");
pushln(&mut doc, "|----------|------|---------|-----------|-------|-------|");
for model in models {
let ctx_str = format_context_window(model.context_window);
let reasoning = if model.reasoning_levels.is_empty() { "" } else { "yes" };
let image = if model.input_modalities.contains(&"image".to_string()) { "yes" } else { "" };
let audio = if model.input_modalities.contains(&"audio".to_string()) { "yes" } else { "" };
pushln(
&mut doc,
format!(
"| `{}` | `{}` | `{}` | {} | {} | {} |",
model.model_id, model.display_name, ctx_str, reasoning, image, audio
),
);
}
docs.insert(cfg.dev_id.to_string(), doc);
}
for dyn_cfg in DYNAMIC_PROVIDERS {
let mut doc = String::new();
pushln(&mut doc, format!("`{}` LLM provider.", dyn_cfg.display_name));
blank(&mut doc);
pushln(
&mut doc,
format!("This provider accepts any model name at runtime (e.g. `{}:my-model`).", dyn_cfg.parser_name),
);
pushln(&mut doc, "No API key is required.");
docs.insert(dyn_cfg.parser_name.to_string(), doc);
}
docs
}
fn format_context_window(tokens: u32) -> String {
if tokens == 0 {
return "unknown".to_string();
}
if tokens >= 1_000_000 && tokens.is_multiple_of(1_000_000) {
format!("{}M", tokens / 1_000_000)
} else if tokens >= 1_000 && tokens.is_multiple_of(1_000) {
format!("{}k", tokens / 1_000)
} else {
format_number(tokens)
}
}
fn pushln(out: &mut String, line: impl AsRef<str>) {
writeln!(out, "{}", line.as_ref()).expect("writing to String should not fail");
}
fn blank(out: &mut String) {
pushln(out, "");
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use serde_json::json;
use tempfile::NamedTempFile;
#[test]
fn model_id_to_variant_pascal_cases_segments() {
assert_eq!(model_id_to_variant("claude-sonnet-4-5-20250929"), "ClaudeSonnet4520250929");
assert_eq!(model_id_to_variant("gemini-2.5-flash"), "Gemini25Flash");
assert_eq!(model_id_to_variant("deepseek-chat"), "DeepseekChat");
assert_eq!(model_id_to_variant("glm-4.5"), "Glm45");
}
#[test]
fn model_id_to_variant_handles_slash_and_colon() {
assert_eq!(model_id_to_variant("anthropic/claude-opus-4.6"), "AnthropicClaudeOpus46");
assert_eq!(model_id_to_variant("openai/gpt-5.1-codex-max"), "OpenaiGpt51CodexMax");
assert_eq!(model_id_to_variant("deepseek/deepseek-r1:free"), "DeepseekDeepseekR1Free");
}
#[test]
fn is_alias_detects_latest_suffix() {
assert!(is_alias("claude-sonnet-4-5-latest"));
assert!(is_alias("claude-3-7-sonnet-latest"));
assert!(!is_alias("claude-sonnet-4-5-20250929"));
}
#[test]
fn build_uses_explicit_context_windows_for_codex_models() {
let data = minimal_models_dev_json();
let models = build_from_value(&data);
let window = |id: &str| models["codex"].iter().find(|model| model.model_id == id).unwrap().context_window;
for model_id in ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2"] {
assert_eq!(window(model_id), 272_000);
}
for model_id in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
assert_eq!(window(model_id), 372_000);
}
}
#[test]
fn format_context_window_formats_correctly() {
assert_eq!(format_context_window(1_000_000), "1M");
assert_eq!(format_context_window(200_000), "200k");
assert_eq!(format_context_window(8_000), "8k");
assert_eq!(format_context_window(0), "unknown");
}
#[test]
fn level_str_to_variant_covers_all_reasoning_efforts() {
for effort in utils::ReasoningEffort::all() {
let _ = level_str_to_variant(effort.as_str());
}
}
#[test]
fn build_sorts_models_and_filters_aliases_and_non_tool_call() {
let mut data = minimal_models_dev_json();
anthropic_models(
&mut data,
json!({
"b-model": {"id": "b-model", "name": "B Model", "tool_call": true, "limit": {"context": 2000, "output": 0}},
"a-model": {"id": "a-model", "name": "A Model", "tool_call": true, "limit": {"context": 1000, "output": 0}},
"alpha-latest": {"id": "alpha-latest", "name": "Alias", "tool_call": true, "limit": {"context": 500, "output": 0}},
"no-tools": {"id": "no-tools", "name": "No Tools", "tool_call": false, "limit": {"context": 500, "output": 0}}
}),
);
let models = build_from_value(&data);
let ids: Vec<&str> = models["anthropic"].iter().map(|m| m.model_id.as_str()).collect();
assert_eq!(ids, vec!["a-model", "b-model"]);
}
#[test]
fn build_extra_source_ids_merges_unique_models_into_provider() {
let mut data = minimal_models_dev_json();
zai_extra_models(
&mut data,
json!({
"extra-model": {"id": "extra-model", "name": "Extra Model", "tool_call": true, "limit": {"context": 4000, "output": 0}}
}),
);
let models = build_from_value(&data);
assert!(models["zai"].iter().any(|m| m.model_id == "extra-model"));
}
#[test]
fn build_extra_source_ids_does_not_duplicate_existing_models() {
let mut data = minimal_models_dev_json();
let shared = json!({
"shared-model": {"id": "shared-model", "name": "Shared Model", "tool_call": true, "limit": {"context": 1000, "output": 0}}
});
insert_models(&mut data, "zai", shared.clone());
insert_models(&mut data, "zai-coding-plan", shared);
let models = build_from_value(&data);
let count = models["zai"].iter().filter(|m| m.model_id == "shared-model").count();
assert_eq!(count, 1);
}
#[test]
fn build_derives_reasoning_levels_from_source_metadata() {
let mut data = minimal_models_dev_json();
anthropic_models(
&mut data,
json!({
"claude-test": {
"id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["low", "high", "max"]}],
"limit": {"context": 200_000, "output": 0}
}
}),
);
let models = build_from_value(&data);
let model = models["anthropic"].iter().find(|model| model.model_id == "claude-test").unwrap();
assert_eq!(model.reasoning_levels, ["low", "high", "max"]);
}
#[test]
fn build_rejects_unknown_reasoning_effort_metadata() {
let mut data = minimal_models_dev_json();
anthropic_models(
&mut data,
json!({
"claude-test": {
"id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["ultra"]}],
"limit": {"context": 200_000, "output": 0}
}
}),
);
let parsed: ModelsDevData = serde_json::from_value(data).unwrap();
let error = build_provider_models(&parsed).unwrap_err();
assert!(matches!(error, CodegenError::UnsupportedReasoningEffort { .. }));
}
#[test]
fn build_derives_prompt_caching_from_cost_fields() {
let mut data = minimal_models_dev_json();
insert_models(
&mut data,
"amazon-bedrock",
json!({
"cached": {
"id": "cached", "name": "Cached", "tool_call": true,
"limit": {"context": 200_000, "output": 0},
"cost": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75}
},
"uncached": {
"id": "uncached", "name": "Uncached", "tool_call": true,
"limit": {"context": 200_000, "output": 0},
"cost": {"input": 3.0, "output": 15.0}
}
}),
);
let models = build_from_value(&data);
let bedrock = &models["amazon-bedrock"];
let cached = bedrock.iter().find(|m| m.model_id == "cached").unwrap();
let uncached = bedrock.iter().find(|m| m.model_id == "uncached").unwrap();
assert!(cached.supports_prompt_caching);
assert!(!uncached.supports_prompt_caching);
}
#[test]
fn build_assigns_codex_model_specific_reasoning_levels() {
let mut data = minimal_models_dev_json();
insert_models(
&mut data,
"openai",
json!({
"gpt-5.6-sol": {
"id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}],
"limit": {"context": 200_000, "output": 0}
},
"gpt-5.6-luna": {
"id": "gpt-5.6-luna", "name": "GPT-5.6 Luna", "tool_call": true, "reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["none", "low", "medium", "high", "xhigh", "max"]}],
"limit": {"context": 200_000, "output": 0}
},
"gpt-5.4": {
"id": "gpt-5.4", "name": "GPT-5.4", "tool_call": true, "reasoning": true,
"limit": {"context": 200_000, "output": 0}
}
}),
);
let models = build_from_value(&data);
let levels = |id: &str| models["codex"].iter().find(|m| m.model_id == id).unwrap().reasoning_levels.clone();
assert_eq!(levels("gpt-5.6-sol"), vec!["low", "medium", "high", "xhigh", "max"]);
assert_eq!(levels("gpt-5.6-luna"), vec!["low", "medium", "high", "xhigh", "max"]);
assert_eq!(levels("gpt-5.4"), vec!["low", "medium", "high", "xhigh"]);
}
#[test]
fn build_applies_codex_subscription_context_window_override() {
let mut data = minimal_models_dev_json();
insert_models(
&mut data,
"openai",
json!({
"gpt-5.5": {
"id": "gpt-5.5", "name": "GPT-5.5", "tool_call": true, "reasoning": true,
"limit": {"context": 1_050_000, "output": 128_000}
}
}),
);
let models = build_from_value(&data);
let codex = models["codex"].iter().find(|m| m.model_id == "gpt-5.5").unwrap();
let openai = models["openai"].iter().find(|m| m.model_id == "gpt-5.5").unwrap();
assert_eq!(codex.context_window, 272_000);
assert_eq!(openai.context_window, 1_050_000);
}
#[test]
fn generate_uses_codex_subscription_model_ids() {
let mut data = minimal_models_dev_json();
insert_models(
&mut data,
"openai",
json!({
"gpt-5.1-codex": {
"id": "gpt-5.1-codex", "name": "GPT-5.1 Codex", "tool_call": true, "reasoning": true,
"limit": {"context": 400_000, "output": 128_000}
},
"gpt-5.6": {
"id": "gpt-5.6", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
"limit": {"context": 1_050_000, "output": 128_000}
},
"gpt-5.6-sol": {
"id": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "tool_call": true, "reasoning": true,
"limit": {"context": 1_050_000, "output": 128_000}
},
"gpt-5.6-terra": {
"id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", "tool_call": true, "reasoning": true,
"limit": {"context": 1_050_000, "output": 128_000}
},
"gpt-5.6-luna": {
"id": "gpt-5.6-luna", "name": "GPT-5.6 Luna", "tool_call": true, "reasoning": true,
"limit": {"context": 1_050_000, "output": 128_000}
}
}),
);
let tmp = NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), serde_json::to_string(&data).unwrap()).unwrap();
let output = generate(tmp.path()).unwrap();
let codex_doc = &output.provider_docs["codex"];
assert!(!codex_doc.contains("`gpt-5.6`"));
assert!(!codex_doc.contains("`gpt-5.1-codex`"));
assert!(codex_doc.contains("| `gpt-5.6-sol` | `GPT-5.6 Sol` | `372k` |"));
assert!(codex_doc.contains("| `gpt-5.6-terra` | `GPT-5.6 Terra` | `372k` |"));
assert!(codex_doc.contains("| `gpt-5.6-luna` | `GPT-5.6 Luna` | `372k` |"));
let openai_doc = &output.provider_docs["openai"];
assert!(openai_doc.contains("`gpt-5.6`"));
assert!(openai_doc.contains("`gpt-5.1-codex`"));
assert!(openai_doc.contains("`gpt-5.6-sol`"));
}
#[test]
fn generate_emits_provider_docs() {
let mut data = minimal_models_dev_json();
anthropic_models(
&mut data,
json!({
"claude-test": {
"id": "claude-test", "name": "Claude Test", "tool_call": true, "reasoning": true,
"limit": {"context": 200_000, "output": 0},
"modalities": {"input": ["text", "image"]}
}
}),
);
let tmp = NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), serde_json::to_string(&data).unwrap()).unwrap();
let output = generate(tmp.path()).unwrap();
let anthropic_doc = &output.provider_docs["anthropic"];
assert!(anthropic_doc.contains("`Anthropic` LLM provider."));
assert!(anthropic_doc.contains("`ANTHROPIC_API_KEY`"));
assert!(anthropic_doc.contains("| `claude-test` | `Claude Test` | `200k` | yes | yes | |"));
let ollama_doc = &output.provider_docs["ollama"];
assert!(ollama_doc.contains("`Ollama` LLM provider."));
assert!(ollama_doc.contains("any model name at runtime"));
}
fn build_from_value(data: &Value) -> ProviderModels {
let parsed: ModelsDevData = serde_json::from_value(data.clone()).expect("parse fixture");
build_provider_models(&parsed).expect("build provider models")
}
fn anthropic_models(data: &mut Value, models: Value) {
insert_models(data, "anthropic", models);
}
fn zai_extra_models(data: &mut Value, models: Value) {
insert_models(data, "zai-coding-plan", models);
}
fn insert_models(data: &mut Value, provider_key: &str, models: Value) {
let provider = data.as_object_mut().unwrap().get_mut(provider_key).unwrap().as_object_mut().unwrap();
let target = provider.get_mut("models").unwrap().as_object_mut().unwrap();
let Value::Object(models) = models else {
panic!("models fixture must be an object");
};
target.extend(models);
}
fn minimal_models_dev_json() -> Value {
let mut root = serde_json::Map::new();
for cfg in PROVIDERS {
let json_key = cfg.json_key();
root.entry(json_key.to_string())
.or_insert_with(|| json!({"id": json_key, "name": json_key, "env": [], "models": {}}));
for &extra in cfg.extra_source_ids {
root.entry(extra.to_string())
.or_insert_with(|| json!({"id": extra, "name": extra, "env": [], "models": {}}));
}
}
let openai = root.get_mut("openai").unwrap()["models"].as_object_mut().unwrap();
for model in CODEX_SUBSCRIPTION_MODELS {
openai.insert(
model.id.to_string(),
json!({
"id": model.id,
"name": model.id,
"tool_call": true,
"reasoning": true,
"reasoning_options": [{"type": "effort", "values": ["low", "medium", "high", "xhigh"]}],
"limit": {"context": 1_050_000, "output": 0}
}),
);
}
Value::Object(root)
}
}