mod anthropic;
mod gemini;
mod openai;
use crate::Provider;
use crate::model_profile::catalog::ModelTier;
#[derive(Debug, Clone, Copy)]
pub struct ModelCapabilities {
pub id: &'static str,
pub provider: Provider,
pub display_name: &'static str,
pub tier: ModelTier,
pub model_family: &'static str,
pub context_window: u32,
pub max_output_tokens: u32,
pub context_window_beta: Option<BetaValue<u32>>,
pub max_output_tokens_beta: Option<BetaValue<u32>>,
pub vision: bool,
pub image_tool_results: bool,
pub inline_video: bool,
pub realtime: bool,
pub supports_temperature: bool,
pub supports_top_p: bool,
pub supports_top_k: bool,
pub thinking: ThinkingSupport,
pub supports_reasoning: bool,
pub effort_levels: &'static [&'static str],
pub supports_web_search: bool,
pub supports_inference_geo: bool,
pub supports_compaction: bool,
pub supports_structured_output: bool,
pub supports_legacy_penalties: bool,
pub supports_thinking_budget_legacy: bool,
pub beta_headers: &'static [BetaHeader],
pub call_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Copy)]
pub struct BetaValue<T: 'static> {
pub header: &'static str,
pub value: T,
}
#[derive(Debug, Clone, Copy)]
pub struct BetaHeader {
pub feature: &'static str,
pub header_name: &'static str,
pub header_value: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThinkingSupport {
None,
AnthropicEnabledOnly,
AnthropicAdaptiveOnly,
AnthropicAdaptiveAndEnabled,
GeminiThinkingLevel,
}
pub fn capabilities_for(provider: Provider, model_id: &str) -> Option<&'static ModelCapabilities> {
let table: &'static [ModelCapabilities] = match provider {
Provider::Anthropic => anthropic::CAPABILITIES,
Provider::OpenAI => openai::CAPABILITIES,
Provider::Gemini => gemini::CAPABILITIES,
_ => return None,
};
table.iter().find(|c| c.id == model_id)
}
pub(crate) fn all_capabilities() -> impl Iterator<Item = &'static ModelCapabilities> {
anthropic::CAPABILITIES
.iter()
.chain(openai::CAPABILITIES.iter())
.chain(gemini::CAPABILITIES.iter())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn every_capability_matches_a_catalog_entry() {
for caps in all_capabilities() {
let entry = crate::model_profile::catalog::entry_for(caps.provider.as_str(), caps.id);
assert!(
entry.is_some(),
"capability row '{}' (provider '{}') has no catalog entry",
caps.id,
caps.provider.as_str(),
);
}
}
#[test]
fn no_duplicate_capability_ids_within_provider() {
for provider_name in crate::model_profile::catalog::provider_names() {
let provider = Provider::parse_strict(provider_name)
.unwrap_or_else(|| panic!("catalog provider '{provider_name}' must parse"));
let ids: Vec<&str> = all_capabilities()
.filter(|c| c.provider == provider)
.map(|c| c.id)
.collect();
let mut unique: Vec<&str> = ids.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(ids.len(), unique.len(), "duplicate ids in {provider_name}");
}
}
#[test]
fn every_catalog_entry_has_capabilities() {
for entry in crate::model_profile::catalog::catalog() {
let provider = Provider::parse_strict(entry.provider)
.unwrap_or_else(|| panic!("catalog provider '{}' must parse", entry.provider));
let caps = capabilities_for(provider, entry.id);
assert!(
caps.is_some(),
"catalog model '{}' (provider '{}') has no capability row",
entry.id,
entry.provider,
);
}
}
#[test]
fn tier_matches_catalog_entry() {
for caps in all_capabilities() {
let entry = crate::model_profile::catalog::entry_for(caps.provider.as_str(), caps.id)
.unwrap_or_else(|| panic!("missing catalog entry for {}", caps.id));
assert_eq!(caps.tier, entry.tier, "tier mismatch for {}", caps.id);
}
}
#[test]
fn claude_haiku_45_is_cataloged_with_official_limits() {
for model in ["claude-haiku-4-5-20251001", "claude-haiku-4-5"] {
let caps = capabilities_for(Provider::Anthropic, model)
.unwrap_or_else(|| panic!("{model} must be in the Anthropic catalog"));
assert_eq!(caps.model_family, "claude-haiku-4");
assert_eq!(caps.context_window, 200_000);
assert_eq!(caps.max_output_tokens, 64_000);
assert_eq!(caps.thinking, ThinkingSupport::AnthropicEnabledOnly);
assert!(!caps.supports_compaction);
}
}
#[test]
fn typed_provider_mismatch_fails_closed() {
assert!(capabilities_for(Provider::Anthropic, "gpt-5.4").is_none());
assert!(capabilities_for(Provider::OpenAI, "gemini-3-flash-preview").is_none());
assert!(capabilities_for(Provider::Other, "gpt-5.4").is_none());
}
#[test]
fn display_provider_string_cannot_be_promoted_to_capability_owner() {
let display_provider = Provider::parse_strict("Gemini").unwrap_or(Provider::Other);
assert_eq!(display_provider, Provider::Other);
assert!(
capabilities_for(display_provider, "gemini-3-flash-preview").is_none(),
"display provider strings must fail closed at the typed capability boundary"
);
}
}