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