use super::*;
use crate::llm::capabilities::{self, DecisionProtocol, StructuredOutputStrategy};
use llm_config::ModelOperation;
fn decision_rows() -> Vec<(String, String)> {
llm_config::embedded_config(None)
.models
.iter()
.filter(|(id, entry)| {
entry.supports_operation(ModelOperation::Decision)
|| decision_contract_for_route(&entry.provider, id).is_some()
})
.map(|(id, entry)| (id.clone(), entry.provider.clone()))
.collect()
}
#[test]
fn native_schema_gateway_derives_decision_but_explicit_unsupported_does_not() {
let id = "vercel/openai/gpt-5.4-nano";
let entry = llm_config::model_catalog_entry(id).expect("gateway route exists");
let mut caps = capabilities::lookup(&entry.provider, id);
let contract = decision_contract::resolved_decision_contract(id, &entry, &caps)
.expect("native schema supports structured decisions");
assert_eq!(contract.protocol, DecisionProtocol::StructuredLlm);
assert!(decision_contract::resolved_operations(id, &entry, &caps)
.contains(&ModelOperation::Decision));
caps.structured_output = Some("none".into());
caps.json_schema = Some("native".into());
caps.structured_output_strategy = StructuredOutputStrategy::Unsupported;
assert!(decision_contract::resolved_decision_contract(id, &entry, &caps).is_none());
assert!(!decision_contract::resolved_operations(id, &entry, &caps)
.contains(&ModelOperation::Decision));
}
#[test]
fn real_tool_schema_and_prompt_validation_routes_remain_decision_capable() {
for (provider, model, expected) in [
(
"openrouter",
"anthropic/claude-fable-5",
StructuredOutputStrategy::ToolSchema,
),
(
"minimax",
"MiniMax-M2.5-highspeed",
StructuredOutputStrategy::PromptValidation,
),
(
"fireworks",
"accounts/fireworks/models/gpt-oss-120b",
StructuredOutputStrategy::PromptValidation,
),
] {
let contract = decision_contract_for_route(provider, model)
.expect("real chat route remains supported");
assert_eq!(
contract.structured_output_strategy,
Some(expected),
"{provider}/{model}"
);
}
}
#[test]
fn every_decision_route_resolves_a_complete_contract() {
let rows = decision_rows();
assert!(
rows.len() >= 8,
"measured only {} decision rows; a near-empty census cannot falsify anything",
rows.len()
);
let incomplete: Vec<_> = rows
.iter()
.filter(|(id, provider)| decision_contract_for_route(provider, id).is_none())
.map(|(id, _)| id.clone())
.collect();
assert!(
incomplete.is_empty(),
"{} of {} decision rows resolve no contract: {:?}",
incomplete.len(),
rows.len(),
incomplete
);
}
#[test]
fn a_native_decision_route_publishes_limits_and_claims_no_chat() {
let mut native = 0;
for (id, provider) in decision_rows() {
let contract = decision_contract_for_route(&provider, &id).expect("contract resolves");
if !contract.protocol.is_native() {
continue;
}
native += 1;
assert!(
contract.limits.is_some(),
"native decision route {id} publishes no request ceilings"
);
let entry = llm_config::model_catalog_entry(&id).expect("row exists");
assert!(
!entry.supports_operation(ModelOperation::TextGeneration),
"native decision route {id} also claims text generation; a decision \
model must not inherit a generic chat transport"
);
assert!(
!contract.question_kinds.is_empty(),
"native decision route {id} accepts no question kind"
);
}
assert!(
native >= 4,
"measured only {native} native decision routes; expected the four Jev rows"
);
}
#[test]
fn a_structured_llm_route_can_actually_honour_a_schema() {
let mut structured = 0;
for (id, provider) in decision_rows() {
let contract = decision_contract_for_route(&provider, &id).expect("contract resolves");
if contract.protocol != DecisionProtocol::StructuredLlm {
continue;
}
structured += 1;
let entry = llm_config::model_catalog_entry(&id).expect("row exists");
assert!(
entry.supports_operation(ModelOperation::TextGeneration),
"structured_llm route {id} dials the chat endpoint but declares no \
text_generation operation"
);
let caps = capabilities::lookup(&provider, &id);
assert_ne!(
caps.structured_output_strategy,
StructuredOutputStrategy::Unsupported,
"structured_llm route {id} must have a supported validation strategy"
);
assert_eq!(
contract.structured_output_strategy,
Some(caps.structured_output_strategy)
);
}
assert!(
structured >= 7,
"measured only {structured} structured_llm routes; the curated chat set is larger"
);
}
#[test]
fn gateway_chat_decisions_are_explicitly_admitted_and_resolve_the_same_protocol() {
for id in [
"openai/gpt-5.4-nano",
"openai/gpt-5.4-mini",
"vercel/openai/gpt-5.4-nano",
] {
let entry = llm_config::model_catalog_entry(id).expect("curated gateway row exists");
assert!(
entry.supports_operation(ModelOperation::TextGeneration),
"{id}"
);
assert!(entry.supports_operation(ModelOperation::Decision), "{id}");
let contract = decision_contract_for_route(&entry.provider, id)
.expect("gateway decision contract resolves");
assert_eq!(contract.protocol, DecisionProtocol::StructuredLlm, "{id}");
}
}
#[test]
fn nano_shutdown_leaves_mini_as_the_live_openai_decision_route() {
for id in ["gpt-5.4-nano", "openai/gpt-5.4-nano"] {
let entry = llm_config::model_catalog_entry(id).expect("nano route is shipped");
assert!(entry.deprecated, "{id}");
assert_eq!(entry.sunset_date.as_deref(), Some("2027-04-01"), "{id}");
}
for (id, keeps_temperature) in [("gpt-5.4-mini", true), ("openai/gpt-5.4-mini", false)] {
let entry = llm_config::model_catalog_entry(id).expect("mini route is shipped");
assert!(!entry.deprecated, "{id}");
assert!(entry.sunset_date.is_none(), "{id}");
assert!(entry.supports_operation(ModelOperation::Decision), "{id}");
let contract = decision_contract_for_route(&entry.provider, id)
.expect("mini decision contract resolves");
assert_eq!(contract.protocol, DecisionProtocol::StructuredLlm, "{id}");
assert_eq!(
capabilities::lookup(&entry.provider, id).temperature_supported,
keeps_temperature,
"{id}"
);
}
for id in ["gpt-6-luna", "openai/gpt-6-luna"] {
let entry = llm_config::model_catalog_entry(id).expect("luna route is shipped");
assert!(!entry.deprecated, "{id}");
let contract = decision_contract_for_route(&entry.provider, id)
.expect("luna derives a structured contract");
assert_eq!(contract.protocol, DecisionProtocol::StructuredLlm, "{id}");
assert!(
!capabilities::lookup(&entry.provider, id).temperature_supported,
"{id}: the temperature-0 profile refuses this route until harn#9157"
);
}
}
#[test]
fn no_route_names_a_decision_protocol_without_declaring_the_operation() {
let config = llm_config::embedded_config(None);
let stray: Vec<_> = config
.models
.iter()
.filter(|(_, entry)| !entry.supports_operation(ModelOperation::Decision))
.filter(|(id, entry)| {
capabilities::lookup(&entry.provider, id)
.decision_protocol
.is_some()
})
.map(|(id, _)| id.clone())
.collect();
assert!(
stray.is_empty(),
"{} rows carry a decision protocol without the operation that admits it: {:?}",
stray.len(),
stray
);
}
#[test]
fn jev_declares_no_text_generation_operation() {
let entry = llm_config::model_catalog_entry("vercel/typesafe-ai/jev")
.expect("the Jev route is shipped");
assert!(!entry.supports_operation(ModelOperation::TextGeneration));
assert!(entry.supports_operation(ModelOperation::Decision));
let sibling = llm_config::model_catalog_entry("gpt-5.4-nano").expect("curated row is shipped");
assert!(sibling.supports_operation(ModelOperation::TextGeneration));
assert!(sibling.supports_operation(ModelOperation::Decision));
}
#[test]
fn no_alias_resolves_to_a_decision_only_row() {
let config = llm_config::embedded_config(None);
assert!(
config.aliases.len() > 20,
"measured only {} aliases; a near-empty census cannot falsify anything",
config.aliases.len()
);
let offending: Vec<_> = config
.aliases
.iter()
.filter(|(_, alias)| {
config.models.get(&alias.id).is_some_and(|entry| {
entry.supports_operation(ModelOperation::Decision)
&& !entry.supports_operation(ModelOperation::TextGeneration)
})
})
.map(|(name, _)| name.clone())
.collect();
assert!(
offending.is_empty(),
"{} aliases point at a decision-only row, which would make it selectable \
as a tier or ladder rung: {:?}",
offending.len(),
offending
);
}
#[test]
fn an_unserved_jev_sibling_is_not_in_the_catalog_at_all() {
assert!(
llm_config::model_catalog_id_for_route("vercel_ai_gateway", "vercel/typesafe-ai/jev-2")
.is_none(),
"an unserved sibling must not resolve to a catalog row"
);
assert!(
decision_contract_for_route("vercel_ai_gateway", "vercel/typesafe-ai/jev-2").is_none(),
"an unserved sibling must resolve no decision contract"
);
}
#[test]
fn a_predicate_site_naming_each_native_jev_route_is_admitted() {
let routes = [
("typesafe", "typesafe/jev-1.13.0"),
("typesafe", "typesafe/jev-latest"),
("vercel_ai_gateway", "vercel/typesafe-ai/jev"),
("openrouter", "openrouter/typesafe/jev-1.13"),
];
for (provider, model) in routes {
let diagnostics = validate_predicate_models(&[predicate_site(provider, model)]);
assert!(
diagnostics.is_empty(),
"{provider}/{model} was refused: {:?}",
diagnostics
.iter()
.map(|diagnostic| diagnostic.message.clone())
.collect::<Vec<_>>()
);
}
let refused = validate_predicate_models(&[predicate_site("openai", "text-embedding-3-small")]);
assert_eq!(refused.len(), 1);
assert!(
refused[0].message.contains("decision"),
"{:?}",
refused[0].message
);
}
#[test]
fn a_decision_row_without_a_protocol_is_refused_by_name() {
let overlay = llm_config::parse_config_toml(
r#"
[models.synthetic-protocolless-decision]
name = "Synthetic protocolless decision"
provider = "openrouter"
context_window = 8192
operations = ["decision"]
"#,
)
.expect("overlay parses");
llm_config::set_user_overrides(Some(overlay));
let diagnostics = validate_predicate_models(&[predicate_site(
"openrouter",
"synthetic-protocolless-decision",
)]);
llm_config::set_user_overrides(None);
assert_eq!(
diagnostics.len(),
1,
"{:?}",
diagnostics
.iter()
.map(|diagnostic| diagnostic.message.clone())
.collect::<Vec<_>>()
);
assert!(
diagnostics[0].message.contains("decision protocol"),
"{:?}",
diagnostics[0].message
);
}
fn predicate_site(provider: &str, model: &str) -> harn_parser::PredicateSite {
harn_parser::PredicateSite {
id: "site".to_string(),
kind: harn_parser::PredicateSiteKind::Predicate,
questions: vec![harn_parser::PredicateQuestionSpec {
id: "site".to_string(),
kind: harn_parser::PredicateQuestionKind::Boolean,
instructions: "is this ready".to_string(),
labels: Vec::new(),
}],
input_type: harn_parser::TypeExpr::Named("dict".to_string()),
line: 1,
column: 1,
start: 0,
end: 1,
model_route: Some(harn_parser::PredicateModelRoute {
provider: provider.to_string(),
model: model.to_string(),
}),
}
}