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