use super::registry::ProviderId;
pub const BEDROCK_INFERENCE_PROFILE_PREFIXES: &[&str] = &["us.", "eu.", "ap.", "jp.", "global."];
const BEDROCK_VENDOR_SEGMENTS: &[&str] = &[
"ai21",
"amazon",
"anthropic",
"cohere",
"deepseek",
"luma",
"meta",
"mistral",
"openai",
"qwen",
"stability",
"twelvelabs",
"writer",
];
const FIREWORKS_NATIVE_PREFIX: &str = "accounts/fireworks/models/";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShapeEvidence {
Conclusive,
Probable,
}
pub fn classify_model_shape(model: &str) -> Option<(ProviderId, ShapeEvidence)> {
let id = model.trim();
if id.is_empty() {
return None;
}
if id.starts_with(FIREWORKS_NATIVE_PREFIX) {
return Some((ProviderId::Fireworks, ShapeEvidence::Conclusive));
}
if id.contains('/') {
return Some((ProviderId::OpenRouter, ShapeEvidence::Conclusive));
}
if BEDROCK_INFERENCE_PROFILE_PREFIXES
.iter()
.any(|pfx| id.starts_with(pfx))
{
return Some((ProviderId::Bedrock, ShapeEvidence::Conclusive));
}
let vendor = id.split('.').next().unwrap_or(id);
if vendor.len() < id.len() && BEDROCK_VENDOR_SEGMENTS.contains(&vendor) {
return Some((ProviderId::Bedrock, ShapeEvidence::Probable));
}
None
}
pub fn infer_provider_from_model_shape(model: &str) -> Option<ProviderId> {
classify_model_shape(model).map(|(provider, _)| provider)
}
pub fn shape_mismatch(provider: ProviderId, model: &str) -> Option<ProviderId> {
match infer_provider_from_model_shape(model) {
Some(inferred) if inferred != provider => Some(inferred),
_ => None,
}
}
pub fn conclusive_shape_mismatch(provider: ProviderId, model: &str) -> Option<ProviderId> {
match classify_model_shape(model) {
Some((inferred, ShapeEvidence::Conclusive)) if inferred != provider => Some(inferred),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn openrouter_slug_is_openrouter() {
assert_eq!(
infer_provider_from_model_shape("anthropic/claude-opus-4.8"),
Some(ProviderId::OpenRouter)
);
assert_eq!(
infer_provider_from_model_shape("openai/gpt-5.4-mini-20260317"),
Some(ProviderId::OpenRouter)
);
}
#[test]
fn us_profile_id_is_bedrock() {
for id in [
"us.anthropic.claude-sonnet-4-6",
"eu.anthropic.claude-haiku-4-5",
"global.anthropic.claude-opus-4-8",
] {
assert_eq!(
infer_provider_from_model_shape(id),
Some(ProviderId::Bedrock),
"{id} must read as Bedrock"
);
}
}
#[test]
fn dotted_vendor_id_is_bedrock() {
assert_eq!(
infer_provider_from_model_shape("anthropic.claude-sonnet-4-6"),
Some(ProviderId::Bedrock)
);
assert_eq!(
infer_provider_from_model_shape("amazon.nova-pro-v1:0"),
Some(ProviderId::Bedrock)
);
}
#[test]
fn fireworks_native_id_is_fireworks() {
assert_eq!(
infer_provider_from_model_shape("accounts/fireworks/models/llama-v3p1-70b-instruct"),
Some(ProviderId::Fireworks),
"the fireworks-native id must not be read as an OpenRouter slug"
);
}
#[test]
fn bare_ids_are_ambiguous() {
for id in ["", " ", "claude-opus-4-5-20260101", "gpt-5.4-mini"] {
assert_eq!(
infer_provider_from_model_shape(id),
None,
"{id:?} must stay ambiguous so the configured default decides"
);
}
}
#[test]
fn unknown_dotted_prefix_is_ambiguous() {
assert_eq!(infer_provider_from_model_shape("llama-3.1-70b"), None);
}
#[test]
fn mismatch_flags_openrouter_slug_on_bedrock() {
assert_eq!(
shape_mismatch(ProviderId::Bedrock, "anthropic/claude-opus-4.8"),
Some(ProviderId::OpenRouter),
"#6114: this pair must be reportable, not silently run on Bedrock"
);
assert_eq!(
shape_mismatch(ProviderId::OpenRouter, "us.anthropic.claude-sonnet-4-6"),
Some(ProviderId::Bedrock)
);
}
#[test]
fn mismatch_is_none_when_shape_agrees() {
assert_eq!(
shape_mismatch(ProviderId::Bedrock, "us.anthropic.claude-sonnet-4-6"),
None
);
assert_eq!(
shape_mismatch(
ProviderId::Fireworks,
"accounts/fireworks/models/llama-v3p1-70b-instruct"
),
None
);
}
#[test]
fn mismatch_is_none_for_ambiguous_id() {
assert_eq!(shape_mismatch(ProviderId::Bedrock, "claude-opus-4-8"), None);
assert_eq!(shape_mismatch(ProviderId::Anthropic, ""), None);
}
#[test]
fn slug_and_profile_evidence_is_conclusive() {
for id in [
"anthropic/claude-opus-4.8",
"us.anthropic.claude-sonnet-4-6",
"accounts/fireworks/models/llama-v3p1-70b-instruct",
] {
let (_, evidence) = classify_model_shape(id).expect("{id} must classify");
assert_eq!(
evidence,
ShapeEvidence::Conclusive,
"{id} belongs to exactly one catalogue"
);
}
}
#[test]
fn dotted_vendor_evidence_is_probable() {
let (provider, evidence) =
classify_model_shape("anthropic.claude-sonnet-4-6").expect("must classify");
assert_eq!(provider, ProviderId::Bedrock);
assert_eq!(
evidence,
ShapeEvidence::Probable,
"a vendor-name match is a guess, not a catalogue fact"
);
}
#[test]
fn conclusive_mismatch_ignores_a_probable_shape() {
assert_eq!(
conclusive_shape_mismatch(ProviderId::OpenRouter, "anthropic.claude-sonnet-4-6"),
None,
"an explicit prefix outranks the dotted-vendor guess"
);
assert_eq!(
conclusive_shape_mismatch(ProviderId::Anthropic, "amazon.nova-pro-v1:0"),
None
);
assert_eq!(
shape_mismatch(ProviderId::OpenRouter, "anthropic.claude-sonnet-4-6"),
Some(ProviderId::Bedrock)
);
}
#[test]
fn conclusive_mismatch_still_flags_a_slug_on_bedrock() {
assert_eq!(
conclusive_shape_mismatch(ProviderId::Bedrock, "anthropic/claude-opus-4.8"),
Some(ProviderId::OpenRouter),
"nothing makes an OpenRouter slug runnable on Bedrock"
);
assert_eq!(
conclusive_shape_mismatch(ProviderId::OpenRouter, "us.anthropic.claude-sonnet-4-6"),
Some(ProviderId::Bedrock)
);
assert_eq!(
conclusive_shape_mismatch(
ProviderId::OpenRouter,
"accounts/fireworks/models/llama-v3p1-70b-instruct"
),
Some(ProviderId::Fireworks)
);
}
}