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" => wire_model_id == "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)]
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}