#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchRule {
Prefix(&'static str),
Substring(&'static str),
}
impl MatchRule {
fn matches(&self, full: &str, bare: &str) -> bool {
match self {
MatchRule::Prefix(p) => bare.starts_with(p),
MatchRule::Substring(s) => full.contains(s),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThinkingShape {
ProviderDefault,
AnthropicAdaptive,
AnthropicBudget,
GeminiLevel,
GeminiBudget {
min: i32,
can_disable: bool,
},
OllamaEffortString,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EffortCeiling {
None,
High,
Max,
XHigh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelCapEntry {
pub rule: MatchRule,
pub thinking: ThinkingShape,
pub supports_temperature: bool,
pub effort_ceiling: EffortCeiling,
pub vision: bool,
pub context_window: Option<usize>,
}
const fn vision_marker(marker: &'static str) -> ModelCapEntry {
ModelCapEntry {
rule: MatchRule::Substring(marker),
vision: true,
..UNKNOWN_MODEL
}
}
const fn claude(
rule: MatchRule,
thinking: ThinkingShape,
supports_temperature: bool,
effort_ceiling: EffortCeiling,
) -> ModelCapEntry {
ModelCapEntry {
rule,
thinking,
supports_temperature,
effort_ceiling,
vision: true,
context_window: None,
}
}
pub const UNKNOWN_MODEL: ModelCapEntry = ModelCapEntry {
rule: MatchRule::Substring(""),
thinking: ThinkingShape::ProviderDefault,
supports_temperature: true,
effort_ceiling: EffortCeiling::None,
vision: false,
context_window: None,
};
use EffortCeiling as E;
use MatchRule::{Prefix, Substring};
use ThinkingShape as T;
pub const CATALOG: &[ModelCapEntry] = &[
claude(
Prefix("claude-opus-4-7"),
T::AnthropicAdaptive,
false,
E::XHigh,
),
claude(
Prefix("claude-opus-4-8"),
T::AnthropicAdaptive,
false,
E::XHigh,
),
claude(
Prefix("claude-fable-5"),
T::AnthropicAdaptive,
false,
E::XHigh,
),
claude(
Prefix("claude-mythos"),
T::AnthropicAdaptive,
false,
E::XHigh,
),
claude(
Prefix("claude-opus-4-6"),
T::AnthropicAdaptive,
true,
E::Max,
),
claude(
Prefix("claude-sonnet-4-6"),
T::AnthropicAdaptive,
true,
E::Max,
),
claude(Prefix("claude-opus-4-5"), T::AnthropicBudget, true, E::High),
claude(
Prefix("claude-sonnet-4-5"),
T::AnthropicBudget,
true,
E::None,
),
claude(
Prefix("claude-haiku-4-5"),
T::AnthropicBudget,
true,
E::None,
),
claude(Prefix("claude-opus-4-1"), T::AnthropicBudget, true, E::None),
claude(Prefix("claude-opus-4-2"), T::AnthropicBudget, true, E::None),
claude(Prefix("claude-3-5"), T::AnthropicBudget, true, E::None),
claude(
Substring("claude-opus-4"),
T::AnthropicBudget,
true,
E::None,
),
claude(
Substring("claude-sonnet-4"),
T::AnthropicBudget,
true,
E::None,
),
claude(
Substring("claude-haiku-4"),
T::AnthropicBudget,
true,
E::None,
),
claude(Substring("claude-3"), T::AnthropicBudget, true, E::None),
claude(Substring("claude-4"), T::AnthropicBudget, true, E::None),
ModelCapEntry {
rule: Prefix("claude-"),
thinking: T::AnthropicBudget,
supports_temperature: true,
effort_ceiling: E::None,
vision: false,
context_window: None,
},
ModelCapEntry {
rule: Prefix("o1"),
..OPENAI_REASONING
},
ModelCapEntry {
rule: Prefix("o3"),
..OPENAI_REASONING
},
ModelCapEntry {
rule: Prefix("o4"),
..OPENAI_REASONING
},
ModelCapEntry {
rule: Prefix("muse-spark"),
vision: true,
context_window: Some(crate::constants::META_MUSE_SPARK_CONTEXT_WINDOW),
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Prefix("gpt-5.6"),
supports_temperature: false,
vision: true,
context_window: Some(1_500_000),
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Substring("gpt-5.6"),
vision: true,
context_window: Some(1_500_000),
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Prefix("gpt-5"),
supports_temperature: false,
vision: true,
context_window: Some(400_000),
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Substring("gpt-5"),
vision: true,
context_window: Some(400_000),
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Substring("gpt-4.1"),
vision: true,
context_window: Some(400_000),
..UNKNOWN_MODEL
},
vision_marker("gpt-4o"),
vision_marker("chatgpt-4o"),
vision_marker("gpt-4-turbo"),
vision_marker("gpt-4-vision"),
ModelCapEntry {
rule: Prefix("gemini-3"),
thinking: T::GeminiLevel,
vision: true,
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Prefix("gemini-2.5-pro"),
thinking: T::GeminiBudget {
min: 128,
can_disable: false,
},
vision: true,
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Prefix("gemini-2.5-flash-lite"),
thinking: T::GeminiBudget {
min: 512,
can_disable: true,
},
vision: true,
..UNKNOWN_MODEL
},
ModelCapEntry {
rule: Prefix("gemini-2.5-flash"),
thinking: T::GeminiBudget {
min: 0,
can_disable: true,
},
vision: true,
..UNKNOWN_MODEL
},
vision_marker("gemini"),
ModelCapEntry {
rule: Prefix("gpt-oss"),
thinking: T::OllamaEffortString,
..UNKNOWN_MODEL
},
vision_marker("-vision"),
vision_marker("-vl"),
vision_marker("vl-"),
vision_marker("llava"),
vision_marker("pixtral"),
vision_marker("internvl"),
vision_marker("molmo"),
vision_marker("llama-3.2-11b"),
vision_marker("llama-3.2-90b"),
vision_marker("llama-4"),
vision_marker("phi-3.5-vision"),
vision_marker("phi-4-multimodal"),
vision_marker("mistral-small-3"),
vision_marker("gemma-3"),
];
const OPENAI_REASONING: ModelCapEntry = ModelCapEntry {
rule: Substring(""),
thinking: T::ProviderDefault,
supports_temperature: false,
effort_ceiling: E::None,
vision: false,
context_window: None,
};
pub fn lookup(model_id: &str) -> &'static ModelCapEntry {
let full = model_id.to_ascii_lowercase();
let bare = full.rsplit('/').next().unwrap_or(full.as_str());
CATALOG
.iter()
.find(|entry| entry.rule.matches(&full, bare))
.unwrap_or(&UNKNOWN_MODEL)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_model_gets_default_row() {
let entry = lookup("totally-unknown-model");
assert_eq!(entry.thinking, T::ProviderDefault);
assert!(entry.supports_temperature);
assert_eq!(entry.effort_ceiling, E::None);
assert!(!entry.vision);
assert_eq!(entry.context_window, None);
}
#[test]
fn ordering_flash_lite_before_flash() {
assert_eq!(
lookup("gemini-2.5-flash-lite").thinking,
T::GeminiBudget {
min: 512,
can_disable: true
}
);
assert_eq!(
lookup("gemini-2.5-flash-lite-preview").thinking,
T::GeminiBudget {
min: 512,
can_disable: true
}
);
assert_eq!(
lookup("gemini-2.5-flash").thinking,
T::GeminiBudget {
min: 0,
can_disable: true
}
);
}
#[test]
fn ordering_gpt5_prefix_before_substring() {
assert!(!lookup("openai/gpt-5").supports_temperature);
assert!(!lookup("gpt-5-mini").supports_temperature);
let mid = lookup("some-gpt-5-variant");
assert!(mid.supports_temperature);
assert!(mid.vision);
assert_eq!(mid.context_window, Some(400_000));
}
#[test]
fn ordering_specific_claude_before_catch_all() {
assert_eq!(lookup("claude-opus-4-7").effort_ceiling, E::XHigh);
assert_eq!(
lookup("claude-3-5-sonnet-20241022").thinking,
T::AnthropicBudget
);
assert!(lookup("claude-opus-4-20250514").vision);
let catch_all = lookup("claude-2.1");
assert!(!catch_all.vision);
assert_eq!(catch_all.context_window, None);
}
#[test]
fn ordering_gpt56_before_gpt5() {
let bare = lookup("gpt-5.6");
assert!(!bare.supports_temperature);
assert!(bare.vision);
assert_eq!(bare.context_window, Some(1_500_000));
let gateway = lookup("openai/some-gpt-5.6-variant");
assert!(gateway.supports_temperature);
assert_eq!(gateway.context_window, Some(1_500_000));
assert_eq!(lookup("gpt-5-mini").context_window, Some(400_000));
}
#[test]
fn prefix_matches_bare_id_after_provider_strip() {
assert_eq!(
lookup("anthropic/claude-opus-4-7").thinking,
T::AnthropicAdaptive
);
assert!(!lookup("openai/gpt-5").supports_temperature);
assert_eq!(lookup("ollama/gpt-oss:20b").thinking, T::OllamaEffortString);
assert_eq!(lookup("myprov/some-model").thinking, T::ProviderDefault);
}
#[test]
fn substring_matches_full_id() {
assert!(lookup("qwen/qwen2.5-vl-7b-instruct").vision);
assert!(lookup("openrouter/anthropic/claude-sonnet-4.5").vision);
}
#[test]
fn matching_is_case_insensitive() {
assert_eq!(
lookup("Claude-Opus-4-7-Special").thinking,
T::AnthropicAdaptive
);
assert_eq!(lookup("GPT-OSS:20b").thinking, T::OllamaEffortString);
assert!(!lookup("GPT-5").supports_temperature);
}
#[test]
fn effort_ceiling_ordering_encodes_the_old_trio() {
assert!(E::XHigh > E::Max && E::Max > E::High && E::High > E::None);
for entry in CATALOG {
if entry.effort_ceiling == E::XHigh {
assert!(entry.effort_ceiling >= E::Max);
}
}
}
}