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