use super::registry::ProviderId;
const OPENROUTER_OPUS_4_8: &str = "anthropic/claude-opus-4.8";
const OPENROUTER_SONNET_4_6: &str = "anthropic/claude-sonnet-4.6";
const OPENROUTER_HAIKU_4_5: &str = "anthropic/claude-haiku-4.5";
const ANTHROPIC_OPUS_4_8: &str = "claude-opus-4-8";
const ANTHROPIC_SONNET_4_6: &str = "claude-sonnet-4-6";
const ANTHROPIC_HAIKU_4_5: &str = "claude-haiku-4-5-20251001";
const BEDROCK_SONNET_4_6: &str = "us.anthropic.claude-sonnet-4-6";
const BEDROCK_HAIKU_4_5: &str = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ModelTier {
Analysis,
Interaction,
Classification,
}
impl ModelTier {
pub fn resolve(self, provider: ProviderId) -> Option<&'static str> {
match provider {
ProviderId::OpenRouter => Some(match self {
Self::Analysis => OPENROUTER_OPUS_4_8,
Self::Interaction => OPENROUTER_SONNET_4_6,
Self::Classification => OPENROUTER_HAIKU_4_5,
}),
ProviderId::Anthropic => Some(match self {
Self::Analysis => ANTHROPIC_OPUS_4_8,
Self::Interaction => ANTHROPIC_SONNET_4_6,
Self::Classification => ANTHROPIC_HAIKU_4_5,
}),
ProviderId::Bedrock => match self {
Self::Analysis => None,
Self::Interaction => Some(BEDROCK_SONNET_4_6),
Self::Classification => Some(BEDROCK_HAIKU_4_5),
},
ProviderId::OpenAI
| ProviderId::Fireworks
| ProviderId::Together
| ProviderId::AtlasCloud
| ProviderId::Local => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_TIERS: &[ModelTier] = &[
ModelTier::Analysis,
ModelTier::Interaction,
ModelTier::Classification,
];
#[test]
fn every_tier_resolves_on_openrouter() {
assert_eq!(
ModelTier::Analysis.resolve(ProviderId::OpenRouter),
Some("anthropic/claude-opus-4.8")
);
assert_eq!(
ModelTier::Interaction.resolve(ProviderId::OpenRouter),
Some("anthropic/claude-sonnet-4.6")
);
assert_eq!(
ModelTier::Classification.resolve(ProviderId::OpenRouter),
Some("anthropic/claude-haiku-4.5")
);
}
#[test]
fn every_tier_resolves_on_anthropic() {
assert_eq!(
ModelTier::Analysis.resolve(ProviderId::Anthropic),
Some("claude-opus-4-8")
);
assert_eq!(
ModelTier::Interaction.resolve(ProviderId::Anthropic),
Some("claude-sonnet-4-6"),
"the first-party id carries no date suffix, and is not derived from \
the OpenRouter spelling"
);
assert_eq!(
ModelTier::Classification.resolve(ProviderId::Anthropic),
Some("claude-haiku-4-5-20251001")
);
}
#[test]
fn the_three_tiers_resolve_three_distinct_models() {
for provider in [ProviderId::OpenRouter, ProviderId::Anthropic] {
let analysis = ModelTier::Analysis.resolve(provider);
let interaction = ModelTier::Interaction.resolve(provider);
let classification = ModelTier::Classification.resolve(provider);
assert_ne!(analysis, interaction, "{provider:?}");
assert_ne!(interaction, classification, "{provider:?}");
assert_ne!(analysis, classification, "{provider:?}");
}
}
#[test]
fn bedrock_analysis_tier_is_unmapped() {
assert_eq!(ModelTier::Analysis.resolve(ProviderId::Bedrock), None);
}
#[test]
fn bedrock_interaction_resolves_sonnet_4_6() {
assert_eq!(
ModelTier::Interaction.resolve(ProviderId::Bedrock),
Some("us.anthropic.claude-sonnet-4-6"),
"the bare profile is the form Bedrock accepts for Sonnet 4.6"
);
}
#[test]
fn bedrock_classification_resolves() {
assert_eq!(
ModelTier::Classification.resolve(ProviderId::Bedrock),
Some("us.anthropic.claude-haiku-4-5-20251001-v1:0"),
"the date-stamped, version-suffixed profile is the only form Bedrock accepts"
);
}
#[test]
fn non_claude_providers_resolve_nothing() {
for provider in [
ProviderId::OpenAI,
ProviderId::Fireworks,
ProviderId::Together,
ProviderId::AtlasCloud,
ProviderId::Local,
] {
for tier in ALL_TIERS {
assert_eq!(
tier.resolve(provider),
None,
"{provider:?} hosts no Claude model, so {tier:?} must not resolve"
);
}
}
}
#[test]
fn analysis_and_interaction_are_distinct_variants() {
assert_ne!(ModelTier::Analysis, ModelTier::Interaction);
assert_ne!(
ModelTier::Analysis.resolve(ProviderId::OpenRouter),
ModelTier::Interaction.resolve(ProviderId::OpenRouter),
"analysis is Opus 4.8 and interaction is Sonnet 4.6 (owner ruling 2026-08-18)"
);
}
}