use super::*;
use crate::core::sm::config::SmInferenceConfig;
fn empty_cfg() -> SmInferenceConfig {
SmInferenceConfig {
provider: "auto".to_string(),
sm_model: String::new(),
summary_model: String::new(),
model: String::new(),
fallback: Vec::new(),
temperature: 0.3,
context_token_budget: 24_000,
compaction_model: String::new(),
compressed_context_max_tokens: 4_000,
}
}
fn routed(model: &str, default_provider: ProviderKind) -> (ProviderKind, String) {
resolve_provider_and_model(model, default_provider)
.unwrap_or_else(|e| panic!("{model:?} must route: {e}"))
}
#[test]
fn route_anthropic_prefix() {
let (kind, model) = routed("anthropic/claude-sonnet-4-6", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::Anthropic);
assert_eq!(model, "claude-sonnet-4-6");
}
#[test]
fn route_bedrock_prefix() {
let (kind, model) = routed(
"bedrock/us.anthropic.claude-sonnet-4-6",
ProviderKind::Anthropic,
);
assert_eq!(kind, ProviderKind::Bedrock);
assert_eq!(model, "us.anthropic.claude-sonnet-4-6");
}
#[test]
fn route_openrouter_prefix() {
let (kind, model) = routed("openrouter/anthropic/claude-haiku", ProviderKind::Bedrock);
assert_eq!(kind, ProviderKind::OpenRouter);
assert_eq!(model, "anthropic/claude-haiku");
}
#[test]
fn route_bare_uses_default() {
let (kind, model) = routed("claude-sonnet-4-6", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::OpenRouter);
assert_eq!(model, "claude-sonnet-4-6");
let (kind, _) = routed("claude-haiku", ProviderKind::Auto);
assert_eq!(kind, ProviderKind::Auto);
}
#[test]
fn route_bare_openrouter_slug_beats_the_default() {
let (kind, model) = routed("google/gemini-3-pro", ProviderKind::Bedrock);
assert_eq!(kind, ProviderKind::OpenRouter);
assert_eq!(model, "google/gemini-3-pro");
let (kind, _) = routed("openai/gpt-5.4-mini-20260317", ProviderKind::Auto);
assert_eq!(
kind,
ProviderKind::OpenRouter,
"an unambiguous shape outranks the credential precedence chain (#6114)"
);
}
#[test]
fn route_bare_bedrock_profile_beats_the_default() {
let (kind, model) = routed("us.anthropic.claude-sonnet-4-6", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::Bedrock);
assert_eq!(model, "us.anthropic.claude-sonnet-4-6");
}
#[test]
fn route_prefix_contradicting_the_shape_is_an_error() {
let err = resolve_provider_and_model(
"bedrock/anthropic/claude-opus-4.8",
ProviderKind::OpenRouter,
)
.expect_err("a bedrock/ prefix on an OpenRouter slug must not route");
let msg = err.to_string();
assert!(
msg.contains("anthropic/claude-opus-4.8"),
"the error must name the requested id: {msg}"
);
assert!(
msg.contains("bedrock") && msg.contains("openrouter"),
"the error must name both providers: {msg}"
);
resolve_provider_and_model(
"anthropic/us.anthropic.claude-sonnet-4-6",
ProviderKind::Auto,
)
.expect_err("a bedrock profile id must not be sent to api.anthropic.com");
}
#[test]
fn route_explicit_prefix_survives_a_probable_bedrock_shape() {
let cases = [
("openrouter/anthropic.claude-x", ProviderKind::OpenRouter),
("anthropic/anthropic.claude-x", ProviderKind::Anthropic),
("bedrock/anthropic.claude-x", ProviderKind::Bedrock),
];
for (requested, expected) in cases {
let (kind, model) = routed(requested, ProviderKind::Bedrock);
assert_eq!(kind, expected, "the prefix on {requested:?} must win");
assert_eq!(
model, "anthropic.claude-x",
"only the routing prefix is stripped"
);
}
let (kind, _) = routed("anthropic.claude-x", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::Bedrock);
}
#[test]
fn shape_error_only_suggests_parseable_provider_values() {
const REFUSAL: &str = "than the one requested. ";
let unbuildable = resolve_provider_and_model(
"accounts/fireworks/models/llama-v3p1-70b-instruct",
ProviderKind::OpenRouter,
)
.expect_err("a fireworks-native id must not run on OpenRouter")
.to_string();
let buildable = resolve_provider_and_model(
"bedrock/anthropic/claude-opus-4.8",
ProviderKind::OpenRouter,
)
.expect_err("an OpenRouter slug must not run on Bedrock")
.to_string();
for msg in [&unbuildable, &buildable] {
let (_, remedy) = msg
.split_once(REFUSAL)
.unwrap_or_else(|| panic!("the message must carry a remedy: {msg}"));
for token in remedy.split('"').skip(1).step_by(2) {
let is_prefix = ProviderKind::ALL
.iter()
.filter_map(|k| k.model_prefix())
.any(|p| token.starts_with(p));
assert!(
is_prefix || ProviderKind::parse(token).is_ok(),
"the remedy offers {token:?}, which is neither a parseable provider \
value nor a routing prefix: {msg}"
);
}
}
assert!(
unbuildable.contains("has no client for fireworks")
&& unbuildable.contains(&ProviderKind::accepted_values()),
"the unbuildable remedy must name the accepted set: {unbuildable}"
);
assert!(
!unbuildable.contains("provider to \"fireworks\""),
"must never suggest a provider value parse() rejects: {unbuildable}"
);
}
#[test]
fn route_shape_the_sm_cannot_build_is_an_error() {
let err = resolve_provider_and_model(
"accounts/fireworks/models/llama-v3p1-70b-instruct",
ProviderKind::OpenRouter,
)
.expect_err("a fireworks-native id must not run on OpenRouter");
let msg = err.to_string();
assert!(
msg.contains("accounts/fireworks/models/llama-v3p1-70b-instruct")
&& msg.contains("fireworks"),
"the error must name the id and the provider it belongs to: {msg}"
);
}
#[test]
fn tier_orchestration_uses_sm_model() {
let mut cfg = empty_cfg();
cfg.sm_model = "anthropic/claude-sonnet-4-6".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
assert_eq!(m, "anthropic/claude-sonnet-4-6");
}
#[test]
fn tier_orchestration_alias_fallback() {
let mut cfg = empty_cfg();
cfg.sm_model = String::new();
cfg.model = "legacy/sonnet".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
assert_eq!(m, "legacy/sonnet");
}
#[test]
fn tier_defaults_are_sonnet_and_haiku() {
let cfg = SmInferenceConfig::default();
let orch = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
let summ = resolve_tier_model(&cfg, SmModelTier::Summary).unwrap();
assert!(
orch.contains("sonnet"),
"orchestration default is Sonnet: {orch}"
);
assert!(summ.contains("haiku"), "summary default is Haiku: {summ}");
}
#[test]
fn tier_summary_default() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Summary).unwrap();
assert_eq!(m, "anthropic/claude-haiku");
}
#[test]
fn tier_compaction_override() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
cfg.compaction_model = "bedrock/us.anthropic.claude-haiku".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Compaction).unwrap();
assert_eq!(m, "bedrock/us.anthropic.claude-haiku");
}
#[test]
fn tier_compaction_falls_back_to_summary() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
cfg.compaction_model = String::new();
let m = resolve_tier_model(&cfg, SmModelTier::Compaction).unwrap();
assert_eq!(m, "anthropic/claude-haiku");
}
#[test]
fn tier_empty_is_validation_error() {
let cfg = empty_cfg();
let err = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap_err();
assert!(matches!(err, SmLlmError::Validation(_)));
}
#[tokio::test]
async fn registry_rejects_unknown_provider() {
let cfg = SmInferenceConfig {
provider: "totally-bogus".to_string(),
..SmInferenceConfig::default()
};
let registry = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: None,
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("unknown provider rejected");
assert!(matches!(err, SmLlmError::Validation(_)));
assert!(err.is_alarm());
}
#[tokio::test]
async fn registry_routes_explicit_prefix() {
let mut cfg = empty_cfg();
cfg.provider = "openrouter".to_string();
cfg.sm_model = "anthropic/claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("anthropic prefix builds");
assert_eq!(resolved.kind, ProviderKind::Anthropic);
assert_eq!(resolved.model, "claude-sonnet-4-6");
assert_eq!(resolved.provider.name(), "anthropic");
}
#[tokio::test]
async fn registry_auto_precedence() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let or_only = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = or_only
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("openrouter selected");
assert_eq!(resolved.kind, ProviderKind::OpenRouter);
assert_eq!(resolved.provider.name(), "openrouter");
let both = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = both
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("anthropic wins");
assert_eq!(resolved.kind, ProviderKind::Anthropic);
}
#[tokio::test]
async fn registry_degraded_without_creds() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let registry = ProviderRegistry::default(); let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("no creds → degraded");
assert!(err.is_degraded(), "expected degraded, got: {err}");
assert!(!err.is_alarm());
assert!(!err.is_retryable());
}
#[tokio::test]
async fn registry_explicit_anthropic_without_key_degrades() {
let mut cfg = empty_cfg();
cfg.provider = "anthropic".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("anthropic without key degrades");
assert!(err.is_degraded(), "expected degraded, got: {err}");
}
#[cfg(not(feature = "bedrock"))]
#[tokio::test]
async fn registry_bedrock_prefix_without_feature() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "bedrock/us.anthropic.claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: true,
openrouter_api_key: None,
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("bedrock without feature errors");
match err {
SmLlmError::Validation(msg) => {
assert!(msg.contains("bedrock"), "msg should mention bedrock: {msg}");
assert!(msg.contains("feature"), "msg should mention feature: {msg}");
}
other => panic!("expected Validation, got {other:?}"),
}
}
#[cfg(feature = "bedrock")]
#[test]
fn registry_auto_prefers_bedrock_when_available() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "bedrock/us.anthropic.claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: true,
openrouter_api_key: Some("sk-or".to_string()),
};
let (kind, model) = registry
.resolve_kind_and_model(&cfg, SmModelTier::Orchestration)
.expect("auto precedence resolves to bedrock");
assert_eq!(kind, ProviderKind::Bedrock);
assert_eq!(model, "us.anthropic.claude-sonnet-4-6");
}