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