#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub use live::{LIVE_SNAPSHOT_SCHEMA_VERSION, LiveCatalogSnapshot, LiveModel, LiveModelMetadata};
mod evidence;
mod live;
pub use evidence::{
EVIDENCE_SCHEMA_VERSION, EvidenceCapability, EvidenceDocument, EvidenceOutcome,
EvidenceResolution, EvidenceScope, ModelSupportEvidence,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogDiagnostics {
pub embedded_revision: String,
pub remote_revision: Option<String>,
pub remote_enabled: bool,
pub cache_state: String,
pub cache_age_seconds: Option<u64>,
pub refresh_in_progress: bool,
pub last_refresh_attempt_ms: Option<u64>,
pub last_refresh_success_ms: Option<u64>,
pub last_refresh_error: Option<String>,
}
pub const SCHEMA_VERSION: &str = "3.0.0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogDocument {
pub schema_version: String,
pub catalog_revision: String,
pub generated_at: String,
#[serde(default)]
pub providers: BTreeMap<String, ProviderCatalog>,
}
impl CatalogDocument {
#[must_use]
pub fn empty(catalog_revision: impl Into<String>, generated_at: impl Into<String>) -> Self {
Self {
schema_version: SCHEMA_VERSION.to_string(),
catalog_revision: catalog_revision.into(),
generated_at: generated_at.into(),
providers: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCatalog {
pub provider_id: String,
pub display_name: String,
pub kind: CatalogProviderKind,
#[serde(default)]
pub website_url: Option<String>,
#[serde(default)]
pub default_model_id: Option<String>,
#[serde(default)]
pub default_codex_model_id: Option<String>,
#[serde(default)]
pub fallback_model_ids: Vec<String>,
#[serde(default)]
pub defaults: Option<ModelCatalogDefaults>,
#[serde(default)]
pub error_handling: ProviderErrorHandlingMetadata,
#[serde(default)]
pub models: BTreeMap<String, ModelCatalogEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderErrorHandlingMetadata {
#[serde(default)]
pub recoverable_error_patterns: Vec<RecoverableErrorPattern>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPattern {
pub id: String,
#[serde(default)]
pub enabled_by_default: bool,
#[serde(default)]
pub scope: RecoverableErrorPatternScope,
#[serde(default)]
pub r#match: RecoverableErrorPatternMatch,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPatternScope {
#[serde(default)]
pub provider_plugin_id: Option<String>,
#[serde(default)]
pub provider_plugin_id_contains: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub model_id_contains: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPatternMatch {
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub message_equals: Option<String>,
#[serde(default)]
pub message_contains: Option<String>,
#[serde(default)]
pub provider_message_equals: Option<String>,
#[serde(default)]
pub provider_message_contains: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogDefaults {
#[serde(default)]
pub context_window: Option<u32>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub max_image_input_base64_bytes: Option<u64>,
#[serde(default)]
pub capabilities: CatalogCapabilities,
#[serde(default)]
pub reasoning: Option<CatalogReasoning>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogProviderKind {
OpenAiCompatible,
Bedrock,
Anthropic,
Google,
OpenRouter,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogEntry {
pub model_id: String,
pub display_name: String,
#[serde(default)]
pub aliases: BTreeSet<String>,
pub status: CatalogModelStatus,
#[serde(default)]
pub bcode_support: BcodeSupportStatus,
#[serde(default)]
pub context_window: Option<u32>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub max_image_input_base64_bytes: Option<u64>,
#[serde(default)]
pub family: Option<String>,
#[serde(default)]
pub provider_model_kind: Option<String>,
#[serde(default)]
pub replaced_by: Option<String>,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub documentation_url: Option<String>,
#[serde(default)]
pub pricing: Option<CatalogPricing>,
#[serde(default)]
pub capabilities: CatalogCapabilities,
#[serde(default)]
pub reasoning: Option<CatalogReasoning>,
#[serde(default)]
pub api_surface: Option<CatalogApiSurface>,
#[serde(default)]
pub thinking_mode: Option<CatalogThinkingMode>,
#[serde(default)]
pub supported_by: BTreeSet<ModelSupportTarget>,
#[serde(default)]
pub unsupported_by: BTreeSet<ModelSupportTarget>,
#[serde(default)]
pub deployments: Vec<ModelDeployment>,
#[serde(default)]
pub live: Option<LiveModelMetadata>,
#[serde(default)]
pub source: CatalogSourceMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelDeployment {
#[serde(flatten)]
pub target: ModelSupportTarget,
#[serde(default)]
pub context_window: Option<u32>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub capabilities: CatalogCapabilities,
#[serde(default)]
pub reasoning: Option<CatalogReasoning>,
#[serde(default)]
pub pricing: Option<CatalogPricing>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ModelSupportTarget {
pub provider: String,
pub auth_mode: String,
pub api_surface: String,
#[serde(default)]
pub integration: Option<String>,
}
impl ModelSupportTarget {
#[must_use]
pub fn new(
provider: impl Into<String>,
auth_mode: impl Into<String>,
api_surface: impl Into<String>,
integration: Option<impl Into<String>>,
) -> Self {
Self {
provider: provider.into(),
auth_mode: auth_mode.into(),
api_surface: api_surface.into(),
integration: integration.map(Into::into),
}
}
#[must_use]
pub fn matches(&self, target: &Self) -> bool {
self.provider == target.provider
&& self.auth_mode == target.auth_mode
&& self.api_surface == target.api_surface
&& (self.integration == target.integration
|| self.integration.is_none()
|| self.integration.as_deref() == Some("provider_native"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogModelStatus {
Stable,
Preview,
Deprecated,
Unavailable,
Unknown,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BcodeSupportStatus {
Supported,
PartiallySupported,
Unsupported,
#[default]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogPricing {
pub currency: String,
pub unit: CatalogPricingUnit,
#[serde(default)]
pub input_micros: Option<u64>,
#[serde(default)]
pub cached_input_micros: Option<u64>,
#[serde(default)]
pub cache_write_input_micros: Option<u64>,
#[serde(default)]
pub output_micros: Option<u64>,
#[serde(default)]
pub context_threshold_tokens: Option<u64>,
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub rules: Vec<CatalogPricingRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CatalogPricingRule {
pub bucket: CatalogPricingBucket,
#[serde(default)]
pub modality: Option<CatalogTokenModality>,
#[serde(default)]
pub service_tier: Option<String>,
#[serde(default)]
pub invocation_class: Option<CatalogInvocationClass>,
#[serde(default)]
pub cache_ttl_seconds: Option<u64>,
#[serde(default)]
pub min_request_input_tokens: Option<u64>,
#[serde(default)]
pub max_request_input_tokens: Option<u64>,
#[serde(default)]
pub billing_scope: Option<String>,
pub price_micros: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogPricingBucket {
Input,
CacheReadInput,
CacheWriteInput,
Output,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogTokenModality {
Text,
Image,
Audio,
Video,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogInvocationClass {
OnDemand,
Batch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogPricingUnit {
PerMillionTokens,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct CatalogCapabilities {
#[serde(default)]
pub text_input: bool,
#[serde(default)]
pub image_input: bool,
#[serde(default)]
pub text_output: bool,
#[serde(default)]
pub tool_use: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required_tool_choice: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub named_tool_choice: Option<bool>,
#[serde(default)]
pub structured_outputs: bool,
#[serde(default)]
pub reasoning: bool,
#[serde(default)]
pub prompt_cache: bool,
#[serde(default)]
pub explicit_prompt_cache: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub prompt_cache_ttl_seconds: BTreeSet<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_cache_min_prefix_tokens: Option<u64>,
#[serde(default)]
pub native_web_search: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogApiSurface {
Converse,
InvokeModel,
Messages,
Responses,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogThinkingMode {
Budget,
Adaptive,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogReasoning {
#[serde(default)]
pub effort_values: BTreeSet<String>,
#[serde(default)]
pub default_effort: Option<String>,
#[serde(default)]
pub summary_values: BTreeSet<String>,
#[serde(default)]
pub default_summary: Option<String>,
#[serde(default)]
pub raw_reasoning_supported: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogSourceMetadata {
#[serde(default)]
pub pricing: Option<String>,
#[serde(default)]
pub metadata: Option<String>,
#[serde(default)]
pub last_verified_at: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deployment_round_trip_preserves_target_specific_limits() {
let deployment = ModelDeployment {
target: ModelSupportTarget::new(
"openai",
"chatgpt_subscription",
"chatgpt_codex",
Some("bcode"),
),
context_window: Some(372_000),
max_output_tokens: Some(128_000),
capabilities: CatalogCapabilities::default(),
reasoning: None,
pricing: None,
};
let value = serde_json::to_value(&deployment).expect("serialize deployment");
let decoded: ModelDeployment =
serde_json::from_value(value).expect("deserialize deployment");
assert_eq!(decoded, deployment);
}
#[test]
fn generic_integration_target_matches_specific_runtime_integration() {
let generic = ModelSupportTarget::new("openai", "api_key", "responses_api", None::<String>);
let runtime = ModelSupportTarget::new("openai", "api_key", "responses_api", Some("bcode"));
assert!(generic.matches(&runtime));
}
#[test]
fn api_surface_round_trips_every_known_wire_value() {
for (surface, wire) in [
(CatalogApiSurface::Converse, "converse"),
(CatalogApiSurface::InvokeModel, "invoke_model"),
(CatalogApiSurface::Messages, "messages"),
(CatalogApiSurface::Responses, "responses"),
] {
let encoded = serde_json::to_value(surface).expect("serialize api surface");
assert_eq!(encoded, serde_json::Value::String(wire.to_string()));
let decoded: CatalogApiSurface =
serde_json::from_value(encoded).expect("deserialize api surface");
assert_eq!(decoded, surface);
}
}
#[test]
fn unknown_api_surface_is_rejected_rather_than_coerced() {
let error = serde_json::from_value::<CatalogApiSurface>(serde_json::Value::String(
"some_future_surface".to_string(),
))
.expect_err("unknown api surface must be rejected");
assert!(
error.to_string().contains("unknown variant"),
"unexpected error: {error}"
);
}
#[test]
fn different_api_surfaces_do_not_match() {
let responses =
ModelSupportTarget::new("openai", "api_key", "responses_api", Some("bcode"));
let codex = ModelSupportTarget::new(
"openai",
"chatgpt_subscription",
"chatgpt_codex",
Some("bcode"),
);
assert!(!responses.matches(&codex));
}
}