use std::cell::RefCell;
use std::collections::BTreeMap;
use std::sync::OnceLock;
use serde::Deserialize;
use super::providers::anthropic::claude_generation;
use super::providers::openai_compat::gpt_generation;
const BUILTIN_TOML: &str = include_str!("capabilities.toml");
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CapabilitiesFile {
#[serde(default)]
pub provider: BTreeMap<String, Vec<ProviderRule>>,
#[serde(default)]
pub provider_family: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderRule {
pub model_match: String,
#[serde(default)]
pub version_min: Option<Vec<u32>>,
#[serde(default)]
pub native_tools: Option<bool>,
#[serde(default)]
pub defer_loading: Option<bool>,
#[serde(default)]
pub tool_search: Option<Vec<String>>,
#[serde(default)]
pub max_tools: Option<u32>,
#[serde(default)]
pub prompt_caching: Option<bool>,
#[serde(default)]
pub thinking: Option<bool>,
#[serde(default)]
pub preserve_thinking: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Capabilities {
pub native_tools: bool,
pub defer_loading: bool,
pub tool_search: Vec<String>,
pub max_tools: Option<u32>,
pub prompt_caching: bool,
pub thinking: bool,
pub preserve_thinking: bool,
}
thread_local! {
static USER_OVERRIDES: RefCell<Option<CapabilitiesFile>> = const { RefCell::new(None) };
}
static BUILTIN: OnceLock<CapabilitiesFile> = OnceLock::new();
fn builtin() -> &'static CapabilitiesFile {
BUILTIN.get_or_init(|| {
toml::from_str::<CapabilitiesFile>(BUILTIN_TOML)
.expect("capabilities.toml must parse at build time")
})
}
pub fn set_user_overrides(file: Option<CapabilitiesFile>) {
USER_OVERRIDES.with(|cell| *cell.borrow_mut() = file);
}
pub fn clear_user_overrides() {
set_user_overrides(None);
}
pub fn set_user_overrides_toml(src: &str) -> Result<(), String> {
let parsed: CapabilitiesFile = toml::from_str(src).map_err(|e| e.to_string())?;
set_user_overrides(Some(parsed));
Ok(())
}
pub fn set_user_overrides_from_manifest_toml(src: &str) -> Result<(), String> {
#[derive(Deserialize)]
struct Manifest {
#[serde(default)]
capabilities: Option<CapabilitiesFile>,
}
let parsed: Manifest = toml::from_str(src).map_err(|e| e.to_string())?;
set_user_overrides(parsed.capabilities);
Ok(())
}
pub fn lookup(provider: &str, model: &str) -> Capabilities {
let user = USER_OVERRIDES.with(|cell| cell.borrow().clone());
lookup_with(provider, model, builtin(), user.as_ref())
}
fn lookup_with(
provider: &str,
model: &str,
builtin: &CapabilitiesFile,
user: Option<&CapabilitiesFile>,
) -> Capabilities {
if provider == "mock" {
if let Some(caps) = try_match_layer(user, builtin, "anthropic", model, provider) {
return caps;
}
if let Some(caps) = try_match_layer(user, builtin, "openai", model, provider) {
return caps;
}
return Capabilities::default();
}
let mut current = provider.to_string();
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
while visited.insert(current.clone()) {
if let Some(caps) = try_match_layer(user, builtin, ¤t, model, provider) {
return caps;
}
let next = user
.and_then(|f| f.provider_family.get(¤t))
.or_else(|| builtin.provider_family.get(¤t))
.cloned();
match next {
Some(parent) => current = parent,
None => break,
}
}
Capabilities::default()
}
fn try_match_layer(
user: Option<&CapabilitiesFile>,
builtin: &CapabilitiesFile,
layer_provider: &str,
model: &str,
_original_provider: &str,
) -> Option<Capabilities> {
if let Some(user) = user {
if let Some(rules) = user.provider.get(layer_provider) {
for rule in rules {
if rule_matches(rule, model) {
return Some(rule_to_caps(rule));
}
}
}
}
if let Some(rules) = builtin.provider.get(layer_provider) {
for rule in rules {
if rule_matches(rule, model) {
return Some(rule_to_caps(rule));
}
}
}
None
}
fn rule_to_caps(rule: &ProviderRule) -> Capabilities {
Capabilities {
native_tools: rule.native_tools.unwrap_or(false),
defer_loading: rule.defer_loading.unwrap_or(false),
tool_search: rule.tool_search.clone().unwrap_or_default(),
max_tools: rule.max_tools,
prompt_caching: rule.prompt_caching.unwrap_or(false),
thinking: rule.thinking.unwrap_or(false),
preserve_thinking: rule.preserve_thinking.unwrap_or(false),
}
}
fn rule_matches(rule: &ProviderRule, model: &str) -> bool {
let lower = model.to_lowercase();
if !glob_match(&rule.model_match.to_lowercase(), &lower) {
return false;
}
if let Some(version_min) = &rule.version_min {
if version_min.len() != 2 {
return false;
}
let want = (version_min[0], version_min[1]);
let have = match extract_version(model) {
Some(v) => v,
None => return false,
};
if have < want {
return false;
}
}
true
}
fn extract_version(model: &str) -> Option<(u32, u32)> {
claude_generation(model).or_else(|| gpt_generation(model))
}
fn glob_match(pattern: &str, input: &str) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') {
if let Some(rest) = prefix.strip_prefix('*') {
return input.contains(rest);
}
return input.starts_with(prefix);
}
if let Some(suffix) = pattern.strip_prefix('*') {
return input.ends_with(suffix);
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
return input.starts_with(parts[0]) && input.ends_with(parts[1]);
}
return input == pattern;
}
input == pattern
}
#[cfg(test)]
mod tests {
use super::*;
fn reset() {
clear_user_overrides();
}
#[test]
fn anthropic_opus_47_gets_full_capabilities() {
reset();
let caps = lookup("anthropic", "claude-opus-4-7");
assert!(caps.native_tools);
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["bm25", "regex"]);
assert!(caps.prompt_caching);
assert!(caps.thinking);
assert_eq!(caps.max_tools, Some(10000));
}
#[test]
fn anthropic_haiku_44_has_no_tool_search() {
reset();
let caps = lookup("anthropic", "claude-haiku-4-4");
assert!(caps.native_tools);
assert!(caps.prompt_caching);
assert!(!caps.defer_loading);
assert!(caps.tool_search.is_empty());
}
#[test]
fn anthropic_haiku_45_supports_tool_search() {
reset();
let caps = lookup("anthropic", "claude-haiku-4-5");
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["bm25", "regex"]);
}
#[test]
fn old_claude_gets_catchall() {
reset();
let caps = lookup("anthropic", "claude-opus-3-5");
assert!(caps.native_tools);
assert!(caps.prompt_caching);
assert!(!caps.defer_loading);
assert!(caps.tool_search.is_empty());
}
#[test]
fn openai_gpt_54_supports_tool_search() {
reset();
let caps = lookup("openai", "gpt-5.4");
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["hosted", "client"]);
}
#[test]
fn openai_gpt_53_has_native_tools_only() {
reset();
let caps = lookup("openai", "gpt-5.3");
assert!(caps.native_tools);
assert!(!caps.defer_loading);
assert!(caps.tool_search.is_empty());
}
#[test]
fn openrouter_inherits_openai() {
reset();
let caps = lookup("openrouter", "gpt-5.4");
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["hosted", "client"]);
}
#[test]
fn groq_inherits_openai_family_only() {
reset();
let caps = lookup("groq", "gpt-5.5-preview");
assert!(caps.defer_loading);
}
#[test]
fn mock_with_claude_model_routes_to_anthropic() {
reset();
let caps = lookup("mock", "claude-sonnet-4-7");
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["bm25", "regex"]);
}
#[test]
fn mock_with_gpt_model_routes_to_openai() {
reset();
let caps = lookup("mock", "gpt-5.4-preview");
assert!(caps.defer_loading);
assert_eq!(caps.tool_search, vec!["hosted", "client"]);
}
#[test]
fn qwen36_ollama_preserves_thinking() {
reset();
let caps = lookup("ollama", "qwen3.6:35b-a3b-coding-nvfp4");
assert!(caps.native_tools);
assert!(caps.thinking);
assert!(
caps.preserve_thinking,
"Qwen3.6 should enable preserve_thinking by default for long-horizon loops"
);
}
#[test]
fn qwen35_ollama_does_not_preserve_thinking() {
reset();
let caps = lookup("ollama", "qwen3.5:35b-a3b-coding-nvfp4");
assert!(caps.native_tools);
assert!(caps.thinking);
assert!(
!caps.preserve_thinking,
"Qwen3.5 lacks the preserve_thinking kwarg — rely on the chat template's rolling checkpoint instead"
);
}
#[test]
fn qwen36_routed_providers_all_preserve_thinking() {
reset();
for (provider, model) in [
("openrouter", "qwen/qwen3.6-plus"),
("together", "Qwen/Qwen3.6-35B-A3B"),
("huggingface", "Qwen/Qwen3.6-35B-A3B"),
("fireworks", "accounts/fireworks/models/qwen3p6-plus"),
("dashscope", "qwen3.6-plus"),
("llamacpp", "unsloth/Qwen3.6-35B-A3B-GGUF"),
("local", "Qwen3.6-35B-A3B"),
("mlx", "unsloth/Qwen3.6-27B-UD-MLX-4bit"),
("mlx", "Qwen/Qwen3.6-27B"),
] {
let caps = lookup(provider, model);
assert!(caps.thinking, "{provider}/{model}: thinking");
assert!(
caps.preserve_thinking,
"{provider}/{model}: preserve_thinking must be on for Qwen3.6"
);
assert!(caps.native_tools, "{provider}/{model}: native_tools");
}
}
#[test]
fn dashscope_and_llamacpp_resolve_capabilities() {
reset();
let caps = lookup("dashscope", "gpt-5.4-preview");
assert!(caps.defer_loading);
let caps = lookup("llamacpp", "gpt-5.4-preview");
assert!(caps.defer_loading);
}
#[test]
fn unknown_provider_has_no_capabilities() {
reset();
let caps = lookup("my-custom-proxy", "foo-bar-1");
assert!(!caps.native_tools);
assert!(!caps.defer_loading);
assert!(caps.tool_search.is_empty());
}
#[test]
fn user_override_adds_new_provider() {
reset();
let toml_src = r#"
[[provider.my-proxy]]
model_match = "*"
native_tools = true
tool_search = ["hosted"]
"#;
set_user_overrides_toml(toml_src).unwrap();
let caps = lookup("my-proxy", "anything");
assert!(caps.native_tools);
assert_eq!(caps.tool_search, vec!["hosted"]);
clear_user_overrides();
}
#[test]
fn user_override_takes_precedence_over_builtin() {
reset();
let toml_src = r#"
[[provider.anthropic]]
model_match = "claude-opus-*"
native_tools = true
defer_loading = false
tool_search = []
"#;
set_user_overrides_toml(toml_src).unwrap();
let caps = lookup("anthropic", "claude-opus-4-7");
assert!(caps.native_tools);
assert!(!caps.defer_loading);
assert!(caps.tool_search.is_empty());
clear_user_overrides();
}
#[test]
fn user_override_from_manifest_toml() {
reset();
let manifest = r#"
[package]
name = "demo"
[[capabilities.provider.my-proxy]]
model_match = "*"
native_tools = true
tool_search = ["hosted"]
"#;
set_user_overrides_from_manifest_toml(manifest).unwrap();
let caps = lookup("my-proxy", "foo");
assert!(caps.native_tools);
assert_eq!(caps.tool_search, vec!["hosted"]);
clear_user_overrides();
}
#[test]
fn version_min_requires_parseable_model() {
reset();
let toml_src = r#"
[[provider.custom]]
model_match = "*"
version_min = [5, 4]
native_tools = true
"#;
set_user_overrides_toml(toml_src).unwrap();
let caps = lookup("custom", "mystery-model");
assert!(!caps.native_tools);
clear_user_overrides();
}
#[test]
fn glob_match_substring() {
assert!(glob_match("*gpt*", "openai/gpt-5.4"));
assert!(glob_match("*claude*", "anthropic/claude-opus-4-7"));
assert!(!glob_match("*xyz*", "openai/gpt-5.4"));
}
#[test]
fn openrouter_namespaced_anthropic_model() {
reset();
let caps = lookup("anthropic", "anthropic/claude-opus-4-7");
assert!(caps.defer_loading);
}
}