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/";
const BEDROCK_ARN_HEAD: &str = "arn:aws:bedrock:";
const BEDROCK_ARN_MODEL_RESOURCES: &[(&str, bool)] = &[
("application-inference-profile", true),
("inference-profile", true),
("foundation-model", false),
];
fn is_bedrock_model_arn(id: &str) -> bool {
let Some(rest) = id.strip_prefix(BEDROCK_ARN_HEAD) else {
return false;
};
let mut fields = rest.splitn(3, ':');
let (Some(region), Some(account), Some(resource)) =
(fields.next(), fields.next(), fields.next())
else {
return false;
};
let Some((kind, name)) = resource.split_once('/') else {
return false;
};
let Some(&(_, has_account)) = BEDROCK_ARN_MODEL_RESOURCES.iter().find(|(k, _)| *k == kind)
else {
return false;
};
let region_ok = !region.is_empty()
&& region
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
let account_ok = if has_account {
account.len() == 12 && account.bytes().all(|b| b.is_ascii_digit())
} else {
account.is_empty()
};
let name_ok = !name.is_empty() && !name.contains('/') && !name.contains(char::is_whitespace);
region_ok && account_ok && name_ok
}
#[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 is_bedrock_model_arn(id) {
return Some((ProviderId::Bedrock, 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)
);
}
const PILOT_SHAPED_ARN: &str =
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile/9iatxd8u1751";
#[test]
fn application_inference_profile_arn_is_conclusive_bedrock() {
assert_eq!(
classify_model_shape(PILOT_SHAPED_ARN),
Some((ProviderId::Bedrock, ShapeEvidence::Conclusive)),
"#9200: a Bedrock application-inference-profile ARN is Bedrock's catalogue"
);
}
#[test]
fn every_bedrock_model_arn_is_conclusive_in_both_checks() {
for arn in [
PILOT_SHAPED_ARN,
"arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-sonnet-4-6",
"arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-pro-v1:0",
" arn:aws:bedrock:eu-central-1::foundation-model/anthropic.claude-haiku-4-5 ",
] {
assert_eq!(
classify_model_shape(arn),
Some((ProviderId::Bedrock, ShapeEvidence::Conclusive)),
"{arn}"
);
assert_eq!(
conclusive_shape_mismatch(ProviderId::OpenRouter, arn),
Some(ProviderId::Bedrock),
"{arn}: no aggregator can run a Bedrock ARN"
);
assert_eq!(
conclusive_shape_mismatch(ProviderId::Bedrock, arn),
None,
"{arn}: a Bedrock ARN on Bedrock is no mismatch"
);
}
}
#[test]
fn shape_classification_table() {
use ProviderId::{Bedrock, Fireworks, OpenRouter};
use ShapeEvidence::{Conclusive, Probable};
let table: &[(&str, Option<(ProviderId, ShapeEvidence)>)] = &[
("", None),
(" ", None),
("claude-opus-4-5-20260101", None),
("claude-opus-4-8", None),
("gpt-5.4-mini", None),
("llama-3.1-70b", None),
("anthropic/claude-opus-4.8", Some((OpenRouter, Conclusive))),
(
"anthropic/claude-sonnet-4.6",
Some((OpenRouter, Conclusive)),
),
(
"openai/gpt-5.4-mini-20260317",
Some((OpenRouter, Conclusive)),
),
(
"us.anthropic.claude-sonnet-4-6",
Some((Bedrock, Conclusive)),
),
("eu.anthropic.claude-haiku-4-5", Some((Bedrock, Conclusive))),
("ap.anthropic.claude-haiku-4-5", Some((Bedrock, Conclusive))),
("jp.anthropic.claude-haiku-4-5", Some((Bedrock, Conclusive))),
(
"global.anthropic.claude-opus-4-8",
Some((Bedrock, Conclusive)),
),
("anthropic.claude-sonnet-4-6", Some((Bedrock, Probable))),
("amazon.nova-pro-v1:0", Some((Bedrock, Probable))),
(
"accounts/fireworks/models/llama-v3p1-70b-instruct",
Some((Fireworks, Conclusive)),
),
(PILOT_SHAPED_ARN, Some((Bedrock, Conclusive))),
(
"arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-sonnet-4-6",
Some((Bedrock, Conclusive)),
),
(
"arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-pro-v1:0",
Some((Bedrock, Conclusive)),
),
];
for (id, expected) in table {
assert_eq!(classify_model_shape(id), *expected, "{id:?}");
}
}
#[test]
fn non_bedrock_arns_and_look_alikes_keep_their_classification() {
let slug = Some((ProviderId::OpenRouter, ShapeEvidence::Conclusive));
for (id, expected) in [
(
"arn:aws:sagemaker:us-west-2:111122223333:endpoint/my-endpoint",
slug,
),
(
"arn:aws:bedrock-agent:us-west-2:111122223333:agent/AGENT12345",
slug,
),
(
"arn:aws:bedrock:us-west-2:111122223333:agent/AGENT12345",
slug,
),
(format!("x{PILOT_SHAPED_ARN}").as_str(), slug),
(format!("profile={PILOT_SHAPED_ARN}").as_str(), slug),
(format!("openrouter/{PILOT_SHAPED_ARN}").as_str(), slug),
(
"arn:aws-us-gov:bedrock:us-gov-west-1:111122223333:application-inference-profile/abc",
slug,
),
(
"ARN:AWS:BEDROCK:US-WEST-2:111122223333:APPLICATION-INFERENCE-PROFILE/ABC",
slug,
),
("anthropic/claude-sonnet-4.6", slug),
] {
assert_eq!(classify_model_shape(id), expected, "{id:?}");
assert_ne!(
conclusive_shape_mismatch(ProviderId::OpenRouter, id),
Some(ProviderId::Bedrock),
"{id:?} must not be claimed for Bedrock"
);
}
}
#[test]
fn malformed_bedrock_arn_keeps_its_pre_9200_classification() {
let slug = Some((ProviderId::OpenRouter, ShapeEvidence::Conclusive));
for (id, expected) in [
("arn:aws:bedrock:us-west-2:111122223333", None),
("arn:aws:bedrock:", None),
(
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile",
None,
),
(
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile/",
slug,
),
("arn:aws:bedrock:us-east-1::foundation-model/", slug),
("arn:aws:bedrock:us-west-2:111122223333:/9iatxd8u1751", slug),
(
"arn:aws:bedrock::111122223333:application-inference-profile/abc",
slug,
),
(
"arn:aws:bedrock:US_WEST:111122223333:application-inference-profile/abc",
slug,
),
(
"arn:aws:bedrock:us-west-2:12345:application-inference-profile/abc",
slug,
),
(
"arn:aws:bedrock:us-west-2::application-inference-profile/abc",
slug,
),
(
"arn:aws:bedrock:us-east-1:111122223333:foundation-model/amazon.nova-pro-v1:0",
slug,
),
(
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile/ab cd",
slug,
),
(
"arn:aws:bedrock:us-west-2:111122223333:application-inference-profile/a/b",
slug,
),
] {
assert_eq!(classify_model_shape(id), expected, "{id:?}");
}
}
}