codewhale_config/route/
capabilities.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum CapabilityState {
14 Supported,
16 Unsupported,
18 #[default]
20 Unknown,
21}
22
23impl CapabilityState {
24 #[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 #[must_use]
36 pub const fn is_supported(self) -> bool {
37 matches!(self, Self::Supported)
38 }
39}
40
41#[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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
92pub struct RouteCapabilities {
93 #[serde(default)]
94 pub attachments: CapabilityState,
95 #[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}