Skip to main content

codewhale_config/route/
capabilities.rs

1//! Route-scoped capability facts.
2//!
3//! Capability state is deliberately three-valued: an absent catalog fact is
4//! unknown, not unsupported, and must never be promoted to supported by a
5//! transport/protocol heuristic. These values travel with the exact provider
6//! offering selected by [`super::resolver::RouteResolver`].
7
8use serde::{Deserialize, Serialize};
9
10/// Whether a resolved provider/model offering supports one capability.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum CapabilityState {
14    /// The selected offering explicitly reports support.
15    Supported,
16    /// The selected offering explicitly reports no support.
17    Unsupported,
18    /// The selected offering did not state the fact.
19    #[default]
20    Unknown,
21}
22
23impl CapabilityState {
24    /// Preserve a sourced optional boolean as a three-state fact.
25    #[must_use]
26    pub const fn from_optional_bool(value: Option<bool>) -> Self {
27        match value {
28            Some(true) => Self::Supported,
29            Some(false) => Self::Unsupported,
30            None => Self::Unknown,
31        }
32    }
33
34    /// Whether the source explicitly reports support.
35    #[must_use]
36    pub const fn is_supported(self) -> bool {
37        matches!(self, Self::Supported)
38    }
39}
40
41/// Return the documented server-side web-search fact for one exact direct
42/// provider/model offering.
43///
44/// This is intentionally a small sourced table, not a protocol or model-family
45/// heuristic. Aggregators, custom endpoints, aliases, snapshots, and nearby
46/// model names remain [`CapabilityState::Unknown`] until a provider-owned fact
47/// exists for that exact offering.
48///
49/// Sources:
50/// - OpenAI Responses web search: <https://developers.openai.com/api/docs/guides/tools-web-search>
51/// - Anthropic web search tool: <https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool>
52/// - xAI web search tool: <https://docs.x.ai/developers/tools/web-search>
53#[must_use]
54pub(crate) fn documented_server_side_web_search(
55    provider_id: &str,
56    wire_model_id: &str,
57) -> CapabilityState {
58    let provider_id = provider_id.trim().to_ascii_lowercase();
59    let wire_model_id = wire_model_id.trim().to_ascii_lowercase();
60    let supported = match provider_id.as_str() {
61        "openai" => matches!(
62            wire_model_id.as_str(),
63            "gpt-5.6" | "gpt-5.5" | "gpt-5.4" | "gpt-4.1" | "gpt-4.1-mini" | "o4-mini"
64        ),
65        "anthropic" => matches!(
66            wire_model_id.as_str(),
67            "claude-fable-5"
68                | "claude-opus-4-8"
69                | "claude-mythos-5"
70                | "claude-mythos-preview"
71                | "claude-opus-4-7"
72                | "claude-opus-4-6"
73                | "claude-sonnet-5"
74                | "claude-sonnet-4-6"
75        ),
76        "xai" => wire_model_id == "grok-4.5",
77        _ => false,
78    };
79    if supported {
80        CapabilityState::Supported
81    } else {
82        CapabilityState::Unknown
83    }
84}
85
86/// Capability facts owned by one provider/model route offering.
87///
88/// Fields without a current authoritative catalog source remain `Unknown`.
89/// They are present now so live/provider-native facts can be added without
90/// changing the candidate contract or guessing from request protocol.
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
92pub struct RouteCapabilities {
93    #[serde(default)]
94    pub attachments: CapabilityState,
95    #[serde(default)]
96    pub reasoning: CapabilityState,
97    #[serde(default)]
98    pub native_tool_calls: CapabilityState,
99    #[serde(default)]
100    pub structured_output: CapabilityState,
101    #[serde(default)]
102    pub parallel_tool_calls: CapabilityState,
103    #[serde(default)]
104    pub streaming: CapabilityState,
105    #[serde(default)]
106    pub prompt_caching: CapabilityState,
107    #[serde(default)]
108    pub server_side_web_search: CapabilityState,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn optional_boolean_preserves_unknown_and_false() {
117        assert_eq!(
118            CapabilityState::from_optional_bool(None),
119            CapabilityState::Unknown
120        );
121        assert_eq!(
122            CapabilityState::from_optional_bool(Some(false)),
123            CapabilityState::Unsupported
124        );
125        assert_eq!(
126            CapabilityState::from_optional_bool(Some(true)),
127            CapabilityState::Supported
128        );
129    }
130
131    #[test]
132    fn unsourced_route_capabilities_default_to_unknown() {
133        let capabilities = RouteCapabilities::default();
134        assert_eq!(capabilities.streaming, CapabilityState::Unknown);
135        assert_eq!(
136            capabilities.server_side_web_search,
137            CapabilityState::Unknown
138        );
139    }
140
141    #[test]
142    fn documented_web_search_is_exact_and_provider_owned() {
143        assert_eq!(
144            documented_server_side_web_search("xai", "grok-4.5"),
145            CapabilityState::Supported
146        );
147        assert_eq!(
148            documented_server_side_web_search("openai", "gpt-5.6"),
149            CapabilityState::Supported
150        );
151        assert_eq!(
152            documented_server_side_web_search("anthropic", "claude-sonnet-4-6"),
153            CapabilityState::Supported
154        );
155
156        for (provider, model) in [
157            ("openrouter", "openai/gpt-5.6"),
158            ("custom", "gpt-5.6"),
159            ("openai", "gpt-5.6-sol"),
160            ("xai", "grok-4.5-fast"),
161            ("anthropic", "claude-haiku-4-5"),
162        ] {
163            assert_eq!(
164                documented_server_side_web_search(provider, model),
165                CapabilityState::Unknown,
166                "{provider}/{model} must not inherit a capability by similarity"
167            );
168        }
169    }
170}