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, PartialEq, Eq)]
pub enum Provider {
Anthropic,
OpenAI,
}
impl Provider {
pub fn label(self) -> &'static str {
match self {
Provider::Anthropic => "Anthropic",
Provider::OpenAI => "OpenAI",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Model {
pub name: &'static str,
pub description: &'static str,
pub provider: Provider,
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 Model {
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(", ")
}
}
impl Default for Model {
fn default() -> Self {
SUPPORTED_MODELS[DEFAULT_MODEL_INDEX]
}
}
const DEFAULT_MODEL_INDEX: usize = 2;
pub const DEFAULT_MODEL_NAME: &str = SUPPORTED_MODELS[DEFAULT_MODEL_INDEX].name;
const _: () = assert!(DEFAULT_MODEL_INDEX < SUPPORTED_MODELS.len());
const OPENAI_PREMIUM_INPUT_THRESHOLD: u32 = 272_000;
pub const CLAUDE_FABLE: &str = "claude-fable-5";
pub const CLAUDE_OPUS: &str = "claude-opus-4-8";
pub const CLAUDE_SONNET: &str = "claude-sonnet-4-6";
pub const CLAUDE_HAIKU: &str = "claude-haiku-4-5";
pub const GPT_FLAGSHIP: &str = "gpt-5.5";
pub const GPT_MID_TIER: &str = "gpt-5.4";
pub const GPT_MINI: &str = "gpt-5.4-mini";
pub const GPT_CODEX: &str = "gpt-5.3-codex";
pub const SUPPORTED_MODELS: &[Model] = &[
Model {
name: CLAUDE_FABLE,
description: "Anthropic's most capable model - demanding reasoning, 1M context",
provider: Provider::Anthropic,
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: 10.0,
price_output_per_m: 50.0,
premium_tier: None,
},
Model {
name: CLAUDE_OPUS,
description: "Powerful Anthropic reasoning model, 1M context",
provider: Provider::Anthropic,
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,
},
Model {
name: CLAUDE_SONNET,
description: "Balanced Anthropic model - default for day-to-day coding",
provider: Provider::Anthropic,
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,
},
Model {
name: CLAUDE_HAIKU,
description: "Fastest, cheapest Anthropic model - 200k context",
provider: Provider::Anthropic,
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,
},
Model {
name: GPT_FLAGSHIP,
description: "OpenAI flagship - strongest GPT for code and long context",
provider: Provider::OpenAI,
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: OPENAI_PREMIUM_INPUT_THRESHOLD,
price_input_per_m: 10.0,
price_output_per_m: 45.0,
}),
},
Model {
name: GPT_MID_TIER,
description: "Mid-tier OpenAI reasoning model - cheaper than the flagship",
provider: Provider::OpenAI,
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: OPENAI_PREMIUM_INPUT_THRESHOLD,
price_input_per_m: 5.0,
price_output_per_m: 22.5,
}),
},
Model {
name: GPT_MINI,
description: "Compact OpenAI model - best price for coding and tool use",
provider: Provider::OpenAI,
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: 0.75,
price_output_per_m: 4.5,
premium_tier: None,
},
Model {
name: GPT_CODEX,
description: "Code-specialised OpenAI model for software engineering",
provider: Provider::OpenAI,
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,
},
];
pub fn supported_models_label() -> String {
SUPPORTED_MODELS
.iter()
.map(|m| m.name)
.collect::<Vec<_>>()
.join(", ")
}
pub fn canonical_model(name: &str) -> Option<&'static Model> {
SUPPORTED_MODELS
.iter()
.find(|m| m.name.eq_ignore_ascii_case(name))
}
pub fn model_support_error(name: &str) -> Option<String> {
if canonical_model(name).is_some() {
return None;
}
Some(format!(
"Model `{}` is not supported. Available models: {}.",
name,
supported_models_label()
))
}
pub fn provider_for(name: &str) -> Provider {
lookup(name).provider
}
pub fn lookup(name: &str) -> &'static Model {
canonical_model(name).unwrap_or(&SUPPORTED_MODELS[DEFAULT_MODEL_INDEX])
}
pub fn effort_support_error(name: &str, effort: ReasoningEffort) -> Option<String> {
let info = lookup(name);
if info.supported_efforts.contains(&effort) {
return None;
}
Some(format!(
"Model `{}` does not accept reasoning effort `{}`. Supported levels: {}.",
name,
effort.as_label(),
info.supported_efforts_label(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_routes_supported_models_correctly() {
assert_eq!(provider_for(CLAUDE_FABLE), Provider::Anthropic);
assert_eq!(provider_for(CLAUDE_OPUS), Provider::Anthropic);
assert_eq!(provider_for(CLAUDE_SONNET), Provider::Anthropic);
assert_eq!(provider_for(CLAUDE_HAIKU), Provider::Anthropic);
assert_eq!(provider_for(GPT_FLAGSHIP), Provider::OpenAI);
assert_eq!(provider_for(GPT_MID_TIER), Provider::OpenAI);
assert_eq!(provider_for(GPT_MINI), Provider::OpenAI);
assert_eq!(provider_for(GPT_CODEX), Provider::OpenAI);
assert_eq!(
provider_for(&CLAUDE_OPUS.to_uppercase()),
Provider::Anthropic
);
assert_eq!(provider_for("unknown-model"), Provider::Anthropic);
}
#[test]
fn provider_label_is_human_readable() {
assert_eq!(Provider::OpenAI.label(), "OpenAI");
assert_eq!(Provider::Anthropic.label(), "Anthropic");
}
#[test]
fn supported_models_contains_every_whitelisted_id_in_order() {
let names: Vec<&str> = SUPPORTED_MODELS.iter().map(|m| m.name).collect();
assert_eq!(
names,
vec![
CLAUDE_FABLE,
CLAUDE_OPUS,
CLAUDE_SONNET,
CLAUDE_HAIKU,
GPT_FLAGSHIP,
GPT_MID_TIER,
GPT_MINI,
GPT_CODEX,
]
);
}
#[test]
fn default_model_is_the_cli_default() {
assert_eq!(Model::default().name, CLAUDE_SONNET);
assert_eq!(SUPPORTED_MODELS[DEFAULT_MODEL_INDEX].name, CLAUDE_SONNET);
}
#[test]
fn canonical_model_normalises_case() {
let m = canonical_model(&CLAUDE_SONNET.to_uppercase()).expect("matches whitelist");
assert_eq!(m.name, CLAUDE_SONNET);
}
#[test]
fn model_support_error_accepts_whitelist_and_rejects_others() {
for m in SUPPORTED_MODELS {
assert!(
model_support_error(m.name).is_none(),
"{} should be accepted",
m.name
);
}
let err = model_support_error("gpt-9.9-imaginary").expect("imaginary model is rejected");
assert!(err.contains("gpt-9.9-imaginary"));
for m in SUPPORTED_MODELS {
assert!(
err.contains(m.name),
"supported list must mention {}",
m.name
);
}
}
#[test]
fn flagship_has_1m_context_and_server_compaction() {
let info = lookup(CLAUDE_FABLE);
assert_eq!(info.context_window, 1_000_000);
assert!(info.requires_adaptive_thinking);
assert!(info.supports_server_compaction);
}
#[test]
fn anthropic_adaptive_models_match_their_lookup_flag() {
for slug in [CLAUDE_FABLE, CLAUDE_OPUS, CLAUDE_SONNET] {
assert!(
lookup(slug).requires_adaptive_thinking,
"{slug} should use adaptive thinking"
);
}
assert!(!lookup(CLAUDE_HAIKU).requires_adaptive_thinking);
}
#[test]
fn unknown_model_resolves_to_the_default_model() {
let info = lookup("some-future-model-2099");
assert_eq!(info.name, Model::default().name);
assert_eq!(info.price_input_per_m, Model::default().price_input_per_m);
}
#[test]
fn gpt_mini_uses_its_own_pricing_not_full_size() {
let mini = lookup(GPT_MINI);
let full = lookup(GPT_MID_TIER);
assert!(
mini.price_input_per_m < full.price_input_per_m,
"mini should be cheaper than full"
);
assert!(mini.premium_tier.is_none());
assert!(full.premium_tier.is_some());
}
#[test]
fn auto_compact_at_clamps_override_against_api_ceiling() {
let info = Model {
context_window: 100_000,
auto_compact_token_limit: Some(200_000),
..Model::default()
};
assert_eq!(info.auto_compact_at(), 90_000);
}
#[test]
fn auto_compact_at_falls_back_to_90pct_when_unset() {
let info = Model {
context_window: 200_000,
auto_compact_token_limit: None,
..Model::default()
};
assert_eq!(info.auto_compact_at(), 180_000);
}
#[test]
fn effective_window_reserves_5pct_headroom() {
let info = Model {
context_window: 1_000_000,
..Model::default()
};
assert_eq!(info.effective_window(), 950_000);
}
#[test]
fn cliff_models_compact_below_premium_threshold() {
for slug in [GPT_FLAGSHIP, GPT_MID_TIER] {
let info = lookup(slug);
assert!(info.auto_compact_at() < OPENAI_PREMIUM_INPUT_THRESHOLD);
let tier = info
.premium_tier
.expect("cliff models carry a premium tier");
assert_eq!(tier.input_threshold, OPENAI_PREMIUM_INPUT_THRESHOLD);
assert!(tier.price_input_per_m > info.price_input_per_m);
}
}
#[test]
fn anthropic_adaptive_models_advertise_server_compaction() {
for slug in [CLAUDE_FABLE, CLAUDE_OPUS, CLAUDE_SONNET] {
assert!(
lookup(slug).supports_server_compaction,
"{slug} should opt into server-side compaction"
);
}
}
#[test]
fn fastest_model_does_not_advertise_server_compaction() {
assert!(!lookup(CLAUDE_HAIKU).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 m in SUPPORTED_MODELS {
for e in [Off, Low, Medium, High] {
assert!(supports(m.name, e), "{} should accept {e:?}", m.name);
}
}
assert!(supports(CLAUDE_FABLE, XHigh));
assert!(supports(CLAUDE_OPUS, XHigh));
assert!(supports(GPT_FLAGSHIP, XHigh));
assert!(supports(GPT_MID_TIER, XHigh));
assert!(supports(GPT_MINI, XHigh));
assert!(supports(GPT_CODEX, XHigh));
assert!(!supports(CLAUDE_SONNET, XHigh));
assert!(!supports(CLAUDE_HAIKU, XHigh));
assert!(supports(CLAUDE_FABLE, Max));
assert!(supports(CLAUDE_OPUS, Max));
assert!(supports(CLAUDE_SONNET, Max));
assert!(!supports(CLAUDE_HAIKU, Max));
assert!(!supports(GPT_FLAGSHIP, Max));
assert!(!supports(GPT_MINI, Max));
assert!(!supports(GPT_CODEX, Max));
}
#[test]
fn effort_support_error_lists_supported_levels_for_the_model() {
let err = effort_support_error(GPT_FLAGSHIP, ReasoningEffort::Max)
.expect("max on an OpenAI model should be rejected");
assert!(err.contains(GPT_FLAGSHIP));
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, ReasoningEffort::XHigh)
.expect("xhigh on the default Anthropic model should be rejected");
assert!(err.contains(CLAUDE_SONNET));
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, ReasoningEffort::Max).is_none());
assert!(effort_support_error(GPT_FLAGSHIP, ReasoningEffort::XHigh).is_none());
assert!(effort_support_error(CLAUDE_HAIKU, ReasoningEffort::High).is_none());
}
}