use crate::api::ReasoningEffort;
#[derive(Debug, Clone, Copy)]
pub struct PremiumPricingTier {
pub input_threshold: u32,
pub price_input_per_m: f64,
pub price_output_per_m: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelInfo {
pub context_window: u32,
pub auto_compact_token_limit: Option<u32>,
pub requires_adaptive_thinking: bool,
pub supports_server_compaction: bool,
pub supported_efforts: &'static [ReasoningEffort],
pub price_input_per_m: f64,
pub price_output_per_m: f64,
pub premium_tier: Option<PremiumPricingTier>,
}
impl Default for ModelInfo {
fn default() -> Self {
Self {
context_window: 200_000,
auto_compact_token_limit: Some(170_000),
requires_adaptive_thinking: false,
supports_server_compaction: false,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
],
price_input_per_m: 3.0,
price_output_per_m: 15.0,
premium_tier: None,
}
}
}
impl ModelInfo {
pub fn auto_compact_at(&self) -> u32 {
let api_ceiling = ((self.context_window as u64).saturating_mul(9) / 10) as u32;
match self.auto_compact_token_limit {
Some(limit) => limit.min(api_ceiling),
None => api_ceiling,
}
}
pub fn effective_window(&self) -> u32 {
((self.context_window as u64).saturating_mul(95) / 100) as u32
}
pub fn supported_efforts_label(&self) -> String {
self.supported_efforts
.iter()
.map(|e| e.as_label())
.collect::<Vec<_>>()
.join(", ")
}
}
pub fn lookup(model: &str) -> ModelInfo {
let m = model.to_ascii_lowercase();
if m.starts_with("claude-opus-4-7") {
return ModelInfo {
context_window: 1_000_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: true,
supports_server_compaction: true,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
ReasoningEffort::Max,
],
price_input_per_m: 5.0,
price_output_per_m: 25.0,
premium_tier: None,
};
}
if m.starts_with("claude-opus-4-6") {
return ModelInfo {
context_window: 1_000_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: true,
supports_server_compaction: true,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::Max,
],
price_input_per_m: 5.0,
price_output_per_m: 25.0,
premium_tier: None,
};
}
if m.starts_with("claude-sonnet-4-6") {
return ModelInfo {
context_window: 1_000_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: true,
supports_server_compaction: true,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::Max,
],
price_input_per_m: 3.0,
price_output_per_m: 15.0,
premium_tier: None,
};
}
if m.starts_with("claude-haiku-4-5") {
return ModelInfo {
context_window: 200_000,
auto_compact_token_limit: Some(170_000),
requires_adaptive_thinking: false,
supports_server_compaction: false,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
],
price_input_per_m: 1.0,
price_output_per_m: 5.0,
premium_tier: None,
};
}
if m.contains("codex") {
return ModelInfo {
context_window: 400_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: false,
supports_server_compaction: false,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
],
price_input_per_m: 1.75,
price_output_per_m: 14.0,
premium_tier: None,
};
}
if m.starts_with("gpt-5.4") {
return ModelInfo {
context_window: 1_050_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: false,
supports_server_compaction: false,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
],
price_input_per_m: 2.5,
price_output_per_m: 15.0,
premium_tier: Some(PremiumPricingTier {
input_threshold: 272_000,
price_input_per_m: 5.0,
price_output_per_m: 22.5,
}),
};
}
if m.starts_with("gpt-5.5") {
return ModelInfo {
context_window: 1_050_000,
auto_compact_token_limit: Some(250_000),
requires_adaptive_thinking: false,
supports_server_compaction: false,
supported_efforts: &[
ReasoningEffort::Off,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
],
price_input_per_m: 5.0,
price_output_per_m: 30.0,
premium_tier: Some(PremiumPricingTier {
input_threshold: 272_000,
price_input_per_m: 10.0,
price_output_per_m: 45.0,
}),
};
}
ModelInfo::default()
}
pub fn effort_support_error(model: &str, effort: ReasoningEffort) -> Option<String> {
let info = lookup(model);
if info.supported_efforts.contains(&effort) {
return None;
}
Some(format!(
"Model `{}` does not accept reasoning effort `{}`. Supported levels: {}.",
model,
effort.as_label(),
info.supported_efforts_label(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opus_4_7_has_1m_context_and_server_compaction() {
let info = lookup("claude-opus-4-7");
assert_eq!(info.context_window, 1_000_000);
assert!(info.requires_adaptive_thinking);
assert!(info.supports_server_compaction);
}
#[test]
fn lookup_matches_versioned_opus_4_7_ids() {
assert!(lookup("claude-opus-4-7").requires_adaptive_thinking);
assert!(lookup("claude-opus-4-7-20260301").requires_adaptive_thinking);
assert!(lookup("Claude-Opus-4-7").requires_adaptive_thinking);
}
#[test]
fn anthropic_1m_models_all_use_adaptive_thinking() {
for slug in ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"] {
assert!(
lookup(slug).requires_adaptive_thinking,
"{slug} should use adaptive thinking"
);
}
}
#[test]
fn legacy_anthropic_models_still_use_manual_thinking() {
for slug in ["claude-sonnet-4-5", "claude-opus-4-5", "claude-haiku-4-5"] {
assert!(
!lookup(slug).requires_adaptive_thinking,
"{slug} should keep the legacy budget_tokens shape"
);
}
}
#[test]
fn unknown_model_falls_back_to_sonnet_class_pricing() {
let info = lookup("some-future-model-2099");
assert_eq!(info.price_input_per_m, 3.0);
assert_eq!(info.price_output_per_m, 15.0);
}
#[test]
fn auto_compact_at_clamps_override_against_api_ceiling() {
let info = ModelInfo {
context_window: 100_000,
auto_compact_token_limit: Some(200_000),
..ModelInfo::default()
};
assert_eq!(info.auto_compact_at(), 90_000);
}
#[test]
fn auto_compact_at_falls_back_to_90pct_when_unset() {
let info = ModelInfo {
context_window: 200_000,
auto_compact_token_limit: None,
..ModelInfo::default()
};
assert_eq!(info.auto_compact_at(), 180_000);
}
#[test]
fn effective_window_reserves_5pct_headroom() {
let info = ModelInfo {
context_window: 1_000_000,
..ModelInfo::default()
};
assert_eq!(info.effective_window(), 950_000);
}
#[test]
fn cliff_models_compact_below_272k_premium_threshold() {
for slug in ["gpt-5.5", "gpt-5.4"] {
let info = lookup(slug);
assert!(info.auto_compact_at() < 272_000);
let tier = info
.premium_tier
.expect("cliff models carry a premium tier");
assert_eq!(tier.input_threshold, 272_000);
assert!(tier.price_input_per_m > info.price_input_per_m);
}
}
#[test]
fn anthropic_1m_models_advertise_server_compaction() {
for slug in ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"] {
assert!(
lookup(slug).supports_server_compaction,
"{slug} should opt into server-side compaction"
);
}
}
#[test]
fn haiku_does_not_advertise_server_compaction() {
assert!(!lookup("claude-haiku-4-5").supports_server_compaction);
}
#[test]
fn effort_support_matches_provider_matrix() {
use ReasoningEffort::*;
let supports = |slug: &str, e: ReasoningEffort| effort_support_error(slug, e).is_none();
for slug in [
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"claude-sonnet-4-5",
"gpt-5.5",
"gpt-5.4",
"gpt-5.3-codex",
] {
for e in [Off, Low, Medium, High] {
assert!(supports(slug, e), "{slug} should accept {e:?}");
}
}
assert!(supports("claude-opus-4-7", XHigh));
assert!(supports("gpt-5.5", XHigh));
assert!(supports("gpt-5.4", XHigh));
assert!(supports("gpt-5.3-codex", XHigh));
assert!(!supports("claude-opus-4-6", XHigh));
assert!(!supports("claude-sonnet-4-6", XHigh));
assert!(!supports("claude-haiku-4-5", XHigh));
assert!(supports("claude-opus-4-7", Max));
assert!(supports("claude-opus-4-6", Max));
assert!(supports("claude-sonnet-4-6", Max));
assert!(!supports("claude-haiku-4-5", Max));
assert!(!supports("gpt-5.5", Max));
assert!(!supports("gpt-5.3-codex", Max));
}
#[test]
fn effort_support_error_lists_supported_levels_for_the_model() {
let err = effort_support_error("gpt-5.5", ReasoningEffort::Max)
.expect("max on gpt-5.5 should be rejected");
assert!(err.contains("gpt-5.5"));
assert!(err.contains("`max`"));
let listed = err
.split("Supported levels: ")
.nth(1)
.expect("error message lists supported levels");
for label in ["off", "low", "medium", "high", "xhigh"] {
assert!(listed.contains(label), "expected {label} in {listed}");
}
assert!(!listed.contains("max"));
let err = effort_support_error("claude-sonnet-4-6", ReasoningEffort::XHigh)
.expect("xhigh on sonnet-4-6 should be rejected");
assert!(err.contains("claude-sonnet-4-6"));
assert!(err.contains("`xhigh`"));
let listed = err
.split("Supported levels: ")
.nth(1)
.expect("error message lists supported levels");
assert!(listed.contains("max"));
assert!(!listed.contains("xhigh"));
assert!(effort_support_error("claude-opus-4-7", ReasoningEffort::Max).is_none());
assert!(effort_support_error("gpt-5.5", ReasoningEffort::XHigh).is_none());
assert!(effort_support_error("claude-haiku-4-5", ReasoningEffort::High).is_none());
}
}