use crate::llm::capabilities::CapabilitiesFile;
const TOOL_TASKS: [&str; 3] = ["agent", "code", "verify"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityFootgun {
pub provider: String,
pub model_match: String,
pub message: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CapabilityAuditReport {
pub footguns: Vec<CapabilityFootgun>,
}
impl CapabilityAuditReport {
pub fn is_clean(&self) -> bool {
self.footguns.is_empty()
}
pub fn render(&self) -> String {
self.footguns
.iter()
.map(|footgun| {
format!(
"provider.{} model_match=\"{}\": {}",
footgun.provider, footgun.model_match, footgun.message
)
})
.collect::<Vec<_>>()
.join("\n")
}
}
pub fn audit_capabilities(file: &CapabilitiesFile) -> CapabilityAuditReport {
let mut report = CapabilityAuditReport::default();
for (provider, rules) in &file.provider {
for rule in rules {
let reasoning_required_for_tools = rule.reasoning_required_for_tools.unwrap_or(false);
if reasoning_required_for_tools {
if let Some(overrides) = &rule.auto_reasoning_overrides {
let offending: Vec<&str> = TOOL_TASKS
.iter()
.copied()
.filter(|task| {
overrides
.get(*task)
.map(|level| level.eq_ignore_ascii_case("off"))
.unwrap_or(false)
})
.collect();
if !offending.is_empty() {
report.footguns.push(CapabilityFootgun {
provider: provider.clone(),
model_match: rule.model_match.clone(),
message: format!(
"declares reasoning_required_for_tools = true but also pins \
auto_reasoning_overrides {{ {} = \"off\" }}; this route calls \
tools inside its reasoning channel, so forcing reasoning off \
for a tool task is the billed-noncommittal failure (0 \
tool_calls). Remove the \"off\" override(s) for tool tasks.",
offending.join("/")
),
});
}
}
}
if provider == "openrouter" && reasoning_required_for_tools {
let pinned = rule
.openrouter_provider_order
.as_ref()
.map(|order| !order.is_empty())
.unwrap_or(false);
if !pinned {
report.footguns.push(CapabilityFootgun {
provider: provider.clone(),
model_match: rule.model_match.clone(),
message: "is an OpenRouter route with \
reasoning_required_for_tools = true (a Harmony-style tool route on \
the OpenRouter sub-provider lottery) but declares no \
openrouter_provider_order pin. Some OpenRouter upstreams \
mis-serialize the tool call even with reasoning ON. Pin a closed \
allowlist of known-clean upstreams, e.g. \
openrouter_provider_order = [\"Cerebras\", \"Groq\"]."
.to_string(),
});
}
}
}
}
report
}
pub fn audit_builtin() -> CapabilityAuditReport {
audit_capabilities(crate::llm::capabilities::builtin_file())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llm::capabilities::parse_capabilities_toml;
fn audit_toml(src: &str) -> CapabilityAuditReport {
audit_capabilities(&parse_capabilities_toml(src).expect("parses"))
}
#[test]
fn shipped_matrix_has_no_footguns() {
let report = audit_builtin();
assert!(
report.is_clean(),
"shipped capability matrix has footguns:\n{}",
report.render()
);
}
#[test]
fn flags_reasoning_off_for_tools_contradiction() {
let report = audit_toml(
r#"
[[provider.someprov]]
model_match = "harmony-*"
reasoning_required_for_tools = true
auto_reasoning_overrides = { agent = "off" }
"#,
);
assert_eq!(report.footguns.len(), 1, "{}", report.render());
assert_eq!(report.footguns[0].provider, "someprov");
assert!(report.footguns[0].message.contains("billed-noncommittal"));
}
#[test]
fn flags_lottery_route_without_pin() {
let report = audit_toml(
r#"
[[provider.openrouter]]
model_match = "vendor/harmony-*"
reasoning_required_for_tools = true
reasoning_effort_levels = ["low", "medium", "high"]
"#,
);
assert_eq!(report.footguns.len(), 1, "{}", report.render());
assert!(report.footguns[0]
.message
.contains("openrouter_provider_order"));
}
#[test]
fn pinned_lottery_route_is_clean() {
let report = audit_toml(
r#"
[[provider.openrouter]]
model_match = "vendor/harmony-*"
reasoning_required_for_tools = true
openrouter_provider_order = ["Cerebras", "Groq"]
"#,
);
assert!(report.is_clean(), "{}", report.render());
}
#[test]
fn empty_pin_is_treated_as_no_pin() {
let report = audit_toml(
r#"
[[provider.openrouter]]
model_match = "vendor/harmony-*"
reasoning_required_for_tools = true
openrouter_provider_order = []
"#,
);
assert_eq!(report.footguns.len(), 1, "{}", report.render());
}
#[test]
fn non_openrouter_required_route_does_not_need_a_pin() {
let report = audit_toml(
r#"
[[provider.groq]]
model_match = "*gpt-oss-*"
reasoning_required_for_tools = true
reasoning_effort_levels = ["low", "medium", "high"]
"#,
);
assert!(report.is_clean(), "{}", report.render());
}
#[test]
fn qwen_style_off_override_without_required_flag_is_clean() {
let report = audit_toml(
r#"
[[provider.ollama]]
model_match = "qwen3.6*"
auto_reasoning_overrides = { agent = "off" }
"#,
);
assert!(report.is_clean(), "{}", report.render());
}
#[test]
fn ordinary_models_are_clean() {
let report = audit_toml(
r#"
[[provider.openrouter]]
model_match = "anthropic/claude-*"
native_tools = true
[[provider.openai]]
model_match = "gpt-*"
native_tools = true
"#,
);
assert!(report.is_clean(), "{}", report.render());
}
}