use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityState {
Supported,
Unsupported,
#[default]
Unknown,
}
impl CapabilityState {
#[must_use]
pub const fn from_optional_bool(value: Option<bool>) -> Self {
match value {
Some(true) => Self::Supported,
Some(false) => Self::Unsupported,
None => Self::Unknown,
}
}
#[must_use]
pub const fn is_supported(self) -> bool {
matches!(self, Self::Supported)
}
}
#[must_use]
pub(crate) fn documented_server_side_web_search(
provider_id: &str,
wire_model_id: &str,
) -> CapabilityState {
let provider_id = provider_id.trim().to_ascii_lowercase();
let wire_model_id = wire_model_id.trim().to_ascii_lowercase();
let supported = match provider_id.as_str() {
"openai" => matches!(
wire_model_id.as_str(),
"gpt-5.6" | "gpt-5.5" | "gpt-5.4" | "gpt-4.1" | "gpt-4.1-mini" | "o4-mini"
),
"anthropic" => matches!(
wire_model_id.as_str(),
"claude-fable-5"
| "claude-opus-4-8"
| "claude-mythos-5"
| "claude-mythos-preview"
| "claude-opus-4-7"
| "claude-opus-4-6"
| "claude-sonnet-5"
| "claude-sonnet-4-6"
),
"xai" => matches!(wire_model_id.as_str(), "grok-4.6" | "grok-4.5"),
_ => false,
};
if supported {
CapabilityState::Supported
} else {
CapabilityState::Unknown
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteCapabilities {
#[serde(default)]
pub attachments: CapabilityState,
#[serde(default)]
pub image_input: CapabilityState,
#[serde(default)]
pub reasoning: CapabilityState,
#[serde(default)]
pub native_tool_calls: CapabilityState,
#[serde(default)]
pub structured_output: CapabilityState,
#[serde(default)]
pub parallel_tool_calls: CapabilityState,
#[serde(default)]
pub streaming: CapabilityState,
#[serde(default)]
pub prompt_caching: CapabilityState,
#[serde(default)]
pub server_side_web_search: CapabilityState,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn optional_boolean_preserves_unknown_and_false() {
assert_eq!(
CapabilityState::from_optional_bool(None),
CapabilityState::Unknown
);
assert_eq!(
CapabilityState::from_optional_bool(Some(false)),
CapabilityState::Unsupported
);
assert_eq!(
CapabilityState::from_optional_bool(Some(true)),
CapabilityState::Supported
);
}
#[test]
fn unsourced_route_capabilities_default_to_unknown() {
let capabilities = RouteCapabilities::default();
assert_eq!(capabilities.streaming, CapabilityState::Unknown);
assert_eq!(
capabilities.server_side_web_search,
CapabilityState::Unknown
);
}
#[test]
fn documented_web_search_is_exact_and_provider_owned() {
assert_eq!(
documented_server_side_web_search("xai", "grok-4.6"),
CapabilityState::Supported
);
assert_eq!(
documented_server_side_web_search("xai", "grok-4.5"),
CapabilityState::Supported
);
assert_eq!(
documented_server_side_web_search("openai", "gpt-5.6"),
CapabilityState::Supported
);
assert_eq!(
documented_server_side_web_search("anthropic", "claude-sonnet-4-6"),
CapabilityState::Supported
);
for (provider, model) in [
("openrouter", "openai/gpt-5.6"),
("custom", "gpt-5.6"),
("openai", "gpt-5.6-sol"),
("xai", "grok-4.6-fast"),
("xai", "grok-4.6-latest"),
("xai", "grok-4.5-fast"),
("anthropic", "claude-haiku-4-5"),
] {
assert_eq!(
documented_server_side_web_search(provider, model),
CapabilityState::Unknown,
"{provider}/{model} must not inherit a capability by similarity"
);
}
}
}