1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum Provider {
13 OpenAi,
14 Claude,
15 Gemini,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21#[non_exhaustive]
22pub enum OperationGroup {
23 Models,
24 CountTokens,
25 GenerateContent,
26 Images,
27 Search,
28 Embeddings,
29 Compact,
30 Conversation,
31 Realtime,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41#[non_exhaustive]
42pub enum Operation {
43 ListModels,
44 GetModel,
45 CountTokens,
46 GenerateContent,
47 StreamGenerateContent,
48 CreateImage,
49 EditImage,
50 WebSearch,
51 Rerank,
52 CreateEmbedding,
53 CompactContent,
54 CreateConversation,
55 CreateRealtimeCall,
56 ConnectRealtime,
57}
58
59impl Operation {
60 pub const fn group(self) -> OperationGroup {
62 match self {
63 Self::ListModels | Self::GetModel => OperationGroup::Models,
64 Self::CountTokens => OperationGroup::CountTokens,
65 Self::GenerateContent | Self::StreamGenerateContent => OperationGroup::GenerateContent,
66 Self::CreateImage | Self::EditImage => OperationGroup::Images,
67 Self::WebSearch | Self::Rerank => OperationGroup::Search,
68 Self::CreateEmbedding => OperationGroup::Embeddings,
69 Self::CompactContent => OperationGroup::Compact,
70 Self::CreateConversation => OperationGroup::Conversation,
71 Self::CreateRealtimeCall | Self::ConnectRealtime => OperationGroup::Realtime,
72 }
73 }
74
75 pub const fn has_request_body(self) -> bool {
77 !matches!(
78 self,
79 Self::ListModels | Self::GetModel | Self::ConnectRealtime
80 )
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(untagged)]
91#[non_exhaustive]
92pub enum OperationKind {
93 ContentGeneration(ContentGenerationKind),
94 Provider(Provider),
95}
96
97impl OperationKind {
98 pub const fn provider(self) -> Provider {
99 match self {
100 Self::ContentGeneration(kind) => kind.provider(),
101 Self::Provider(provider) => provider,
102 }
103 }
104
105 pub const fn is_content_generation(self) -> bool {
106 matches!(self, Self::ContentGeneration(_))
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[non_exhaustive]
114pub enum ContentGenerationKind {
115 OpenAiResponses,
116 #[serde(rename = "open_ai_responses_websocket")]
117 OpenAiResponsesWebSocket,
118 OpenAiChatCompletions,
119 ClaudeMessages,
120 GeminiGenerateContent,
121}
122
123impl ContentGenerationKind {
124 pub const fn provider(self) -> Provider {
125 match self {
126 Self::OpenAiResponses
127 | Self::OpenAiResponsesWebSocket
128 | Self::OpenAiChatCompletions => Provider::OpenAi,
129 Self::ClaudeMessages => Provider::Claude,
130 Self::GeminiGenerateContent => Provider::Gemini,
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
137#[non_exhaustive]
138pub struct OperationKey {
139 operation: Operation,
140 kind: OperationKind,
141}
142
143impl OperationKey {
144 pub fn content_generation(operation: Operation, kind: ContentGenerationKind) -> Self {
145 assert!(
146 operation.is_content_generation(),
147 "content-generation kind used with non-content operation"
148 );
149 Self {
150 operation,
151 kind: OperationKind::ContentGeneration(kind),
152 }
153 }
154
155 pub fn provider(operation: Operation, provider: Provider) -> Self {
156 assert!(
157 !operation.is_content_generation(),
158 "provider kind used with content-generation operation"
159 );
160 Self {
161 operation,
162 kind: OperationKind::Provider(provider),
163 }
164 }
165
166 pub const fn group(self) -> OperationGroup {
167 self.operation.group()
168 }
169
170 pub const fn operation(self) -> Operation {
172 self.operation
173 }
174
175 pub const fn kind(self) -> OperationKind {
177 self.kind
178 }
179
180 pub const fn provider_family(self) -> Provider {
181 self.kind.provider()
182 }
183
184 pub const fn is_consistent(self) -> bool {
185 self.operation.is_content_generation() == self.kind.is_content_generation()
186 }
187
188 pub const fn try_new(
189 operation: Operation,
190 kind: OperationKind,
191 ) -> Result<Self, OperationKeyError> {
192 let key = Self { operation, kind };
193 if key.is_consistent() {
194 Ok(key)
195 } else {
196 Err(OperationKeyError { operation, kind })
197 }
198 }
199
200 #[cfg(test)]
201 pub(crate) const fn new_unchecked(operation: Operation, kind: OperationKind) -> Self {
202 Self { operation, kind }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, gproxy_protocol_macros::WireBuilder)]
207#[non_exhaustive]
208pub struct OperationKeyError {
209 pub operation: Operation,
210 pub kind: OperationKind,
211}
212
213impl std::fmt::Display for OperationKeyError {
214 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 write!(
216 formatter,
217 "operation {:?} is inconsistent with kind {:?}",
218 self.operation, self.kind
219 )
220 }
221}
222
223impl std::error::Error for OperationKeyError {}
224
225impl<'de> Deserialize<'de> for OperationKey {
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: serde::Deserializer<'de>,
229 {
230 #[derive(Deserialize)]
231 struct WireOperationKey {
232 operation: Operation,
233 kind: OperationKind,
234 }
235
236 let wire = WireOperationKey::deserialize(deserializer)?;
237 Self::try_new(wire.operation, wire.kind).map_err(serde::de::Error::custom)
238 }
239}
240
241impl Operation {
242 pub const fn is_content_generation(self) -> bool {
243 matches!(self, Self::GenerateContent | Self::StreamGenerateContent)
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
249#[serde(rename_all = "UPPERCASE")]
250#[non_exhaustive]
251pub enum HttpMethod {
252 Get,
253 Post,
254 Put,
255 Patch,
256 Delete,
257}
258
259impl From<HttpMethod> for http::Method {
260 fn from(m: HttpMethod) -> Self {
261 match m {
262 HttpMethod::Get => http::Method::GET,
263 HttpMethod::Post => http::Method::POST,
264 HttpMethod::Put => http::Method::PUT,
265 HttpMethod::Patch => http::Method::PATCH,
266 HttpMethod::Delete => http::Method::DELETE,
267 }
268 }
269}
270
271#[derive(
273 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder,
274)]
275#[non_exhaustive]
276pub struct Endpoint {
277 pub operation_key: OperationKey,
278 pub method: HttpMethod,
279 pub path: String,
281}
282
283impl Endpoint {
284 pub fn new(operation_key: OperationKey, method: HttpMethod, path: impl Into<String>) -> Self {
285 Self {
286 operation_key,
287 method,
288 path: path.into(),
289 }
290 }
291
292 pub fn content_generation(
293 operation: Operation,
294 kind: ContentGenerationKind,
295 method: HttpMethod,
296 path: impl Into<String>,
297 ) -> Self {
298 Self::new(
299 OperationKey::content_generation(operation, kind),
300 method,
301 path,
302 )
303 }
304
305 pub fn provider(
306 operation: Operation,
307 provider: Provider,
308 method: HttpMethod,
309 path: impl Into<String>,
310 ) -> Self {
311 Self::new(OperationKey::provider(operation, provider), method, path)
312 }
313
314 pub const fn provider_family(&self) -> Provider {
315 self.operation_key.provider_family()
316 }
317
318 pub const fn group(&self) -> OperationGroup {
319 self.operation_key.group()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn deserialization_rejects_inconsistent_operation_key() {
329 let value = serde_json::json!({
330 "operation": "generate_content",
331 "kind": "open_ai"
332 });
333 assert!(serde_json::from_value::<OperationKey>(value).is_err());
334 }
335
336 #[test]
337 fn try_new_checks_the_invariant() {
338 assert!(
339 OperationKey::try_new(
340 Operation::GenerateContent,
341 OperationKind::Provider(Provider::OpenAi),
342 )
343 .is_err()
344 );
345 }
346
347 #[test]
348 fn rerank_is_a_provider_shaped_search_operation() {
349 assert_eq!(Operation::Rerank.group(), OperationGroup::Search);
350 assert!(Operation::Rerank.has_request_body());
351 assert!(OperationKey::provider(Operation::Rerank, Provider::OpenAi).is_consistent());
352 }
353}