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" => matches!(wire_model_id.as_str(), "grok-4.6" | "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    /// Whether the exact offering explicitly accepts image input.
96    #[serde(default)]
97    pub image_input: CapabilityState,
98    #[serde(default)]
99    pub reasoning: CapabilityState,
100    #[serde(default)]
101    pub native_tool_calls: CapabilityState,
102    #[serde(default)]
103    pub structured_output: CapabilityState,
104    #[serde(default)]
105    pub parallel_tool_calls: CapabilityState,
106    #[serde(default)]
107    pub streaming: CapabilityState,
108    #[serde(default)]
109    pub prompt_caching: CapabilityState,
110    #[serde(default)]
111    pub server_side_web_search: CapabilityState,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn optional_boolean_preserves_unknown_and_false() {
120        assert_eq!(
121            CapabilityState::from_optional_bool(None),
122            CapabilityState::Unknown
123        );
124        assert_eq!(
125            CapabilityState::from_optional_bool(Some(false)),
126            CapabilityState::Unsupported
127        );
128        assert_eq!(
129            CapabilityState::from_optional_bool(Some(true)),
130            CapabilityState::Supported
131        );
132    }
133
134    #[test]
135    fn unsourced_route_capabilities_default_to_unknown() {
136        let capabilities = RouteCapabilities::default();
137        assert_eq!(capabilities.streaming, CapabilityState::Unknown);
138        assert_eq!(
139            capabilities.server_side_web_search,
140            CapabilityState::Unknown
141        );
142    }
143
144    #[test]
145    fn documented_web_search_is_exact_and_provider_owned() {
146        assert_eq!(
147            documented_server_side_web_search("xai", "grok-4.6"),
148            CapabilityState::Supported
149        );
150        assert_eq!(
151            documented_server_side_web_search("xai", "grok-4.5"),
152            CapabilityState::Supported
153        );
154        assert_eq!(
155            documented_server_side_web_search("openai", "gpt-5.6"),
156            CapabilityState::Supported
157        );
158        assert_eq!(
159            documented_server_side_web_search("anthropic", "claude-sonnet-4-6"),
160            CapabilityState::Supported
161        );
162
163        for (provider, model) in [
164            ("openrouter", "openai/gpt-5.6"),
165            ("custom", "gpt-5.6"),
166            ("openai", "gpt-5.6-sol"),
167            ("xai", "grok-4.6-fast"),
168            ("xai", "grok-4.6-latest"),
169            ("xai", "grok-4.5-fast"),
170            ("anthropic", "claude-haiku-4-5"),
171        ] {
172            assert_eq!(
173                documented_server_side_web_search(provider, model),
174                CapabilityState::Unknown,
175                "{provider}/{model} must not inherit a capability by similarity"
176            );
177        }
178    }
179}