返回 CodeWhale
capabilities.rs
根目录 / crates / config / src / 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
8 use 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")]
13 pub 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
23 impl 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]
54 pub(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)]
92 pub 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)]
115 mod 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.5"),
148 CapabilityState::Supported
149 );
150 assert_eq!(
151 documented_server_side_web_search("openai", "gpt-5.6"),
152 CapabilityState::Supported
153 );
154 assert_eq!(
155 documented_server_side_web_search("anthropic", "claude-sonnet-4-6"),
156 CapabilityState::Supported
157 );
158
159 for (provider, model) in [
160 ("openrouter", "openai/gpt-5.6"),
161 ("custom", "gpt-5.6"),
162 ("openai", "gpt-5.6-sol"),
163 ("xai", "grok-4.5-fast"),
164 ("anthropic", "claude-haiku-4-5"),
165 ] {
166 assert_eq!(
167 documented_server_side_web_search(provider, model),
168 CapabilityState::Unknown,
169 "{provider}/{model} must not inherit a capability by similarity"
170 );
171 }
172 }
173 }
174
174 lines RUST