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 Audio,
33 Video,
34 Files,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum Operation {
46 ListModels,
47 GetModel,
48 CountTokens,
49 GenerateContent,
50 StreamGenerateContent,
51 CreateImage,
52 EditImage,
53 WebSearch,
54 Rerank,
55 CreateEmbedding,
56 CreateSpeech,
57 CreateTranscription,
58 CreateTranslation,
59 CreateVideo,
60 RetrieveVideo,
61 ListVideos,
62 DeleteVideo,
63 DownloadVideoContent,
64 RemixVideo,
65 CreateVideoCharacter,
66 GetVideoCharacter,
67 EditVideo,
68 ExtendVideo,
69 CreateFile,
70 ListFiles,
71 RetrieveFile,
72 DeleteFile,
73 DownloadFileContent,
74 CompactContent,
75 CreateConversation,
76 CreateRealtimeCall,
77 ConnectRealtime,
78}
79
80impl Operation {
81 pub const fn group(self) -> OperationGroup {
83 match self {
84 Self::ListModels | Self::GetModel => OperationGroup::Models,
85 Self::CountTokens => OperationGroup::CountTokens,
86 Self::GenerateContent | Self::StreamGenerateContent => OperationGroup::GenerateContent,
87 Self::CreateImage | Self::EditImage => OperationGroup::Images,
88 Self::WebSearch | Self::Rerank => OperationGroup::Search,
89 Self::CreateEmbedding => OperationGroup::Embeddings,
90 Self::CreateSpeech | Self::CreateTranscription | Self::CreateTranslation => {
91 OperationGroup::Audio
92 }
93 Self::CreateVideo
94 | Self::RetrieveVideo
95 | Self::ListVideos
96 | Self::DeleteVideo
97 | Self::DownloadVideoContent
98 | Self::RemixVideo
99 | Self::CreateVideoCharacter
100 | Self::GetVideoCharacter
101 | Self::EditVideo
102 | Self::ExtendVideo => OperationGroup::Video,
103 Self::CreateFile
104 | Self::ListFiles
105 | Self::RetrieveFile
106 | Self::DeleteFile
107 | Self::DownloadFileContent => OperationGroup::Files,
108 Self::CompactContent => OperationGroup::Compact,
109 Self::CreateConversation => OperationGroup::Conversation,
110 Self::CreateRealtimeCall | Self::ConnectRealtime => OperationGroup::Realtime,
111 }
112 }
113
114 pub const fn has_request_body(self) -> bool {
116 !matches!(
117 self,
118 Self::ListModels
119 | Self::GetModel
120 | Self::RetrieveVideo
121 | Self::ListVideos
122 | Self::DeleteVideo
123 | Self::DownloadVideoContent
124 | Self::GetVideoCharacter
125 | Self::ConnectRealtime
126 | Self::ListFiles
127 | Self::RetrieveFile
128 | Self::DeleteFile
129 | Self::DownloadFileContent
130 )
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
140#[serde(untagged)]
141#[non_exhaustive]
142pub enum OperationKind {
143 ContentGeneration(ContentGenerationKind),
144 Provider(Provider),
145}
146
147impl OperationKind {
148 pub const fn provider(self) -> Provider {
149 match self {
150 Self::ContentGeneration(kind) => kind.provider(),
151 Self::Provider(provider) => provider,
152 }
153 }
154
155 pub const fn is_content_generation(self) -> bool {
156 matches!(self, Self::ContentGeneration(_))
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163#[non_exhaustive]
164pub enum ContentGenerationKind {
165 OpenAiResponses,
166 #[serde(rename = "open_ai_responses_websocket")]
167 OpenAiResponsesWebSocket,
168 OpenAiChatCompletions,
169 ClaudeMessages,
170 GeminiGenerateContent,
171}
172
173impl ContentGenerationKind {
174 pub const fn provider(self) -> Provider {
175 match self {
176 Self::OpenAiResponses
177 | Self::OpenAiResponsesWebSocket
178 | Self::OpenAiChatCompletions => Provider::OpenAi,
179 Self::ClaudeMessages => Provider::Claude,
180 Self::GeminiGenerateContent => Provider::Gemini,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
187#[non_exhaustive]
188pub struct OperationKey {
189 operation: Operation,
190 kind: OperationKind,
191}
192
193impl OperationKey {
194 pub fn content_generation(operation: Operation, kind: ContentGenerationKind) -> Self {
195 assert!(
196 operation.is_content_generation(),
197 "content-generation kind used with non-content operation"
198 );
199 Self {
200 operation,
201 kind: OperationKind::ContentGeneration(kind),
202 }
203 }
204
205 pub fn provider(operation: Operation, provider: Provider) -> Self {
206 assert!(
207 !operation.is_content_generation(),
208 "provider kind used with content-generation operation"
209 );
210 Self {
211 operation,
212 kind: OperationKind::Provider(provider),
213 }
214 }
215
216 pub const fn group(self) -> OperationGroup {
217 self.operation.group()
218 }
219
220 pub const fn operation(self) -> Operation {
222 self.operation
223 }
224
225 pub const fn kind(self) -> OperationKind {
227 self.kind
228 }
229
230 pub const fn provider_family(self) -> Provider {
231 self.kind.provider()
232 }
233
234 pub const fn is_consistent(self) -> bool {
235 self.operation.is_content_generation() == self.kind.is_content_generation()
236 }
237
238 pub const fn try_new(
239 operation: Operation,
240 kind: OperationKind,
241 ) -> Result<Self, OperationKeyError> {
242 let key = Self { operation, kind };
243 if key.is_consistent() {
244 Ok(key)
245 } else {
246 Err(OperationKeyError { operation, kind })
247 }
248 }
249
250 #[cfg(test)]
251 pub(crate) const fn new_unchecked(operation: Operation, kind: OperationKind) -> Self {
252 Self { operation, kind }
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, gproxy_protocol_macros::WireBuilder)]
257#[non_exhaustive]
258pub struct OperationKeyError {
259 pub operation: Operation,
260 pub kind: OperationKind,
261}
262
263impl std::fmt::Display for OperationKeyError {
264 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 write!(
266 formatter,
267 "operation {:?} is inconsistent with kind {:?}",
268 self.operation, self.kind
269 )
270 }
271}
272
273impl std::error::Error for OperationKeyError {}
274
275impl<'de> Deserialize<'de> for OperationKey {
276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
277 where
278 D: serde::Deserializer<'de>,
279 {
280 #[derive(Deserialize)]
281 struct WireOperationKey {
282 operation: Operation,
283 kind: OperationKind,
284 }
285
286 let wire = WireOperationKey::deserialize(deserializer)?;
287 Self::try_new(wire.operation, wire.kind).map_err(serde::de::Error::custom)
288 }
289}
290
291impl Operation {
292 pub const fn is_content_generation(self) -> bool {
293 matches!(self, Self::GenerateContent | Self::StreamGenerateContent)
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[serde(rename_all = "UPPERCASE")]
300#[non_exhaustive]
301pub enum HttpMethod {
302 Get,
303 Post,
304 Put,
305 Patch,
306 Delete,
307}
308
309impl From<HttpMethod> for http::Method {
310 fn from(m: HttpMethod) -> Self {
311 match m {
312 HttpMethod::Get => http::Method::GET,
313 HttpMethod::Post => http::Method::POST,
314 HttpMethod::Put => http::Method::PUT,
315 HttpMethod::Patch => http::Method::PATCH,
316 HttpMethod::Delete => http::Method::DELETE,
317 }
318 }
319}
320
321#[derive(
323 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder,
324)]
325#[non_exhaustive]
326pub struct Endpoint {
327 pub operation_key: OperationKey,
328 pub method: HttpMethod,
329 pub path: String,
331}
332
333impl Endpoint {
334 pub fn new(operation_key: OperationKey, method: HttpMethod, path: impl Into<String>) -> Self {
335 Self {
336 operation_key,
337 method,
338 path: path.into(),
339 }
340 }
341
342 pub fn content_generation(
343 operation: Operation,
344 kind: ContentGenerationKind,
345 method: HttpMethod,
346 path: impl Into<String>,
347 ) -> Self {
348 Self::new(
349 OperationKey::content_generation(operation, kind),
350 method,
351 path,
352 )
353 }
354
355 pub fn provider(
356 operation: Operation,
357 provider: Provider,
358 method: HttpMethod,
359 path: impl Into<String>,
360 ) -> Self {
361 Self::new(OperationKey::provider(operation, provider), method, path)
362 }
363
364 pub const fn provider_family(&self) -> Provider {
365 self.operation_key.provider_family()
366 }
367
368 pub const fn group(&self) -> OperationGroup {
369 self.operation_key.group()
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn deserialization_rejects_inconsistent_operation_key() {
379 let value = serde_json::json!({
380 "operation": "generate_content",
381 "kind": "open_ai"
382 });
383 assert!(serde_json::from_value::<OperationKey>(value).is_err());
384 }
385
386 #[test]
387 fn try_new_checks_the_invariant() {
388 assert!(
389 OperationKey::try_new(
390 Operation::GenerateContent,
391 OperationKind::Provider(Provider::OpenAi),
392 )
393 .is_err()
394 );
395 }
396
397 #[test]
398 fn rerank_is_a_provider_shaped_search_operation() {
399 assert_eq!(Operation::Rerank.group(), OperationGroup::Search);
400 assert!(Operation::Rerank.has_request_body());
401 assert!(OperationKey::provider(Operation::Rerank, Provider::OpenAi).is_consistent());
402 }
403
404 #[test]
405 fn video_operations_have_expected_group_and_body_semantics() {
406 for operation in [
407 Operation::CreateVideo,
408 Operation::RetrieveVideo,
409 Operation::ListVideos,
410 Operation::DeleteVideo,
411 Operation::DownloadVideoContent,
412 Operation::RemixVideo,
413 Operation::CreateVideoCharacter,
414 Operation::GetVideoCharacter,
415 Operation::EditVideo,
416 Operation::ExtendVideo,
417 ] {
418 assert_eq!(operation.group(), OperationGroup::Video);
419 assert!(OperationKey::provider(operation, Provider::OpenAi).is_consistent());
420 }
421
422 assert!(Operation::CreateVideo.has_request_body());
423 assert!(Operation::RemixVideo.has_request_body());
424 assert!(!Operation::RetrieveVideo.has_request_body());
425 assert!(!Operation::ListVideos.has_request_body());
426 assert!(!Operation::DeleteVideo.has_request_body());
427 assert!(!Operation::DownloadVideoContent.has_request_body());
428 assert!(!Operation::GetVideoCharacter.has_request_body());
429 }
430}