use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderFeature {
ExplicitCacheBreakpoints,
AutomaticPrefixCaching,
RetentionTiers,
PriorityScheduling,
ModelRouting,
DeferredToolLoading,
FileReferences,
StructuredOutput,
PrefixAffinityHints,
StreamingTokenCounts,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CacheEconomics {
pub write_short_multiplier: f64,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub write_long_multiplier: Option<f64>,
pub read_multiplier: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelFamilyCapabilities {
pub model_family: String,
pub supported_features: HashSet<ProviderFeature>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_cache_breakpoints: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub min_cacheable_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cache_economics: Option<CacheEconomics>,
}
impl ModelFamilyCapabilities {
pub fn supports(&self, feature: ProviderFeature) -> bool {
self.supported_features.contains(&feature)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BackendCapabilities {
pub backend_id: String,
pub supported_features: HashSet<ProviderFeature>,
pub model_families: HashMap<String, ModelFamilyCapabilities>,
}
impl BackendCapabilities {
pub fn none(backend_id: &str) -> Self {
Self {
backend_id: backend_id.to_string(),
supported_features: HashSet::new(),
model_families: HashMap::new(),
}
}
pub fn supports(&self, feature: ProviderFeature) -> bool {
self.supported_features.contains(&feature)
}
pub fn model_supports(&self, model_family: &str, feature: ProviderFeature) -> bool {
if let Some(family_caps) = self.model_families.get(model_family) {
family_caps.supports(feature)
} else {
self.supports(feature)
}
}
pub fn add_model_family(&mut self, caps: ModelFamilyCapabilities) {
self.model_families.insert(caps.model_family.clone(), caps);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilityRegistry {
backends: HashMap<String, BackendCapabilities>,
}
impl CapabilityRegistry {
pub fn new() -> Self {
Self {
backends: HashMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut registry = Self::new();
let anthropic_features: HashSet<ProviderFeature> = [
ProviderFeature::ExplicitCacheBreakpoints,
ProviderFeature::RetentionTiers,
ProviderFeature::StreamingTokenCounts,
]
.into_iter()
.collect();
let mut anthropic = BackendCapabilities {
backend_id: "anthropic".to_string(),
supported_features: anthropic_features.clone(),
model_families: HashMap::new(),
};
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-opus-4.6".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(4096),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-opus-4.5".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(4096),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-opus-4.1".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-opus-4".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-sonnet-4.6".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(2048),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-sonnet-4.5".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-sonnet-4".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-haiku-4.5".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(4096),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-haiku-3.5".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(2048),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-3.5-sonnet".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-3-opus".to_string(),
supported_features: anthropic_features.clone(),
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(2048),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
anthropic.add_model_family(ModelFamilyCapabilities {
model_family: "claude-3-haiku".to_string(),
supported_features: anthropic_features,
max_cache_breakpoints: Some(4),
min_cacheable_tokens: Some(1024),
cache_economics: Some(CacheEconomics {
write_short_multiplier: 1.25,
write_long_multiplier: Some(2.0),
read_multiplier: 0.1,
}),
});
registry.register_backend(anthropic);
let openai_features: HashSet<ProviderFeature> = [
ProviderFeature::AutomaticPrefixCaching,
ProviderFeature::StreamingTokenCounts,
ProviderFeature::StructuredOutput,
]
.into_iter()
.collect();
let mut openai = BackendCapabilities {
backend_id: "openai".to_string(),
supported_features: openai_features.clone(),
model_families: HashMap::new(),
};
openai.add_model_family(ModelFamilyCapabilities {
model_family: "gpt-4o".to_string(),
supported_features: openai_features.clone(),
max_cache_breakpoints: None,
min_cacheable_tokens: None,
cache_economics: None,
});
openai.add_model_family(ModelFamilyCapabilities {
model_family: "gpt-4o-mini".to_string(),
supported_features: openai_features,
max_cache_breakpoints: None,
min_cacheable_tokens: None,
cache_economics: None,
});
let o1_features: HashSet<ProviderFeature> = [ProviderFeature::StreamingTokenCounts]
.into_iter()
.collect();
openai.add_model_family(ModelFamilyCapabilities {
model_family: "o1".to_string(),
supported_features: o1_features,
max_cache_breakpoints: None,
min_cacheable_tokens: None,
cache_economics: None,
});
registry.register_backend(openai);
registry
}
pub fn register_backend(&mut self, caps: BackendCapabilities) {
self.backends.insert(caps.backend_id.clone(), caps);
}
pub fn get_backend(&self, backend_id: &str) -> Option<&BackendCapabilities> {
self.backends.get(backend_id)
}
pub fn supports_feature(&self, backend_id: &str, feature: ProviderFeature) -> bool {
self.backends
.get(backend_id)
.is_some_and(|b| b.supports(feature))
}
pub fn model_supports_feature(
&self,
backend_id: &str,
model_family: &str,
feature: ProviderFeature,
) -> bool {
self.backends
.get(backend_id)
.is_some_and(|b| b.model_supports(model_family, feature))
}
pub fn list_backend_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.backends.keys().cloned().collect();
ids.sort();
ids
}
}
#[cfg(test)]
#[path = "../../tests/unit/acg/capability_tests.rs"]
mod tests;