1#[derive(
22 Debug,
23 Clone,
24 Copy,
25 PartialEq,
26 Eq,
27 Hash,
28 PartialOrd,
29 Ord,
30 serde::Serialize,
31 serde::Deserialize,
32 strum::IntoStaticStr,
33 strum::EnumString,
34 strum::EnumIter,
35)]
36#[serde(rename_all = "snake_case")]
37#[strum(serialize_all = "snake_case")]
38#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
39pub enum Operation {
40 ListModels,
42 GetModel,
43 CountTokens,
45 GenerateContent,
47 StreamGenerateContent,
48 CreateModeration,
51 GuardianReview,
52 GuardianClassify,
53 CompactContent,
55 SummarizeMemory,
56 CreateConversation,
57 CreateEmbedding,
59 BatchCreateEmbedding,
60 Rerank,
62 WebSearch,
64 CreateImage,
66 EditImage,
67 CreateSpeech,
69 CreateTranscription,
70 CreateTranslation,
71 CreateFile,
73 ListFiles,
74 RetrieveFile,
75 RetrieveFileContent,
76 DeleteFile,
77 CreateVideo,
84 RetrieveVideo,
85 ListVideos,
86 DeleteVideo,
87 DownloadVideoContent,
91 CreateRealtimeCall,
94 ConnectRealtime,
98}
99
100impl Operation {
101 pub const fn produces_usage(self) -> bool {
105 match self {
106 Self::GenerateContent
107 | Self::StreamGenerateContent
108 | Self::CreateModeration
109 | Self::GuardianReview
110 | Self::GuardianClassify
111 | Self::CompactContent
112 | Self::SummarizeMemory
113 | Self::CreateEmbedding
114 | Self::BatchCreateEmbedding
115 | Self::Rerank
116 | Self::WebSearch
117 | Self::CreateImage
118 | Self::EditImage
119 | Self::CreateSpeech
120 | Self::CreateTranscription
121 | Self::CreateTranslation
122 | Self::CreateVideo
123 | Self::ConnectRealtime => true,
124 Self::ListModels
125 | Self::GetModel
126 | Self::CountTokens
127 | Self::CreateConversation
128 | Self::CreateFile
129 | Self::ListFiles
130 | Self::RetrieveFile
131 | Self::RetrieveFileContent
132 | Self::DeleteFile
133 | Self::RetrieveVideo
134 | Self::ListVideos
135 | Self::DeleteVideo
136 | Self::DownloadVideoContent
137 | Self::CreateRealtimeCall => false,
138 }
139 }
140
141 pub fn usage_operations() -> impl Iterator<Item = Self> {
143 <Self as strum::IntoEnumIterator>::iter().filter(|op| op.produces_usage())
144 }
145
146 pub fn id(self) -> &'static str {
148 self.into()
149 }
150
151 pub fn from_id(value: &str) -> Option<Self> {
152 value.parse().ok()
153 }
154}
155
156#[derive(
164 Debug,
165 Clone,
166 Copy,
167 PartialEq,
168 Eq,
169 Hash,
170 PartialOrd,
171 Ord,
172 serde::Serialize,
173 serde::Deserialize,
174 strum::IntoStaticStr,
175 strum::EnumString,
176 strum::EnumIter,
177)]
178#[serde(rename_all = "snake_case")]
179#[strum(serialize_all = "snake_case")]
180#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
181pub enum WireFamily {
182 #[serde(rename = "openai")]
183 #[strum(serialize = "openai")]
184 OpenAi,
185 Claude,
186 Gemini,
187}
188
189impl WireFamily {
190 pub fn id(self) -> &'static str {
191 self.into()
192 }
193
194 pub fn from_id(value: &str) -> Option<Self> {
195 value.parse().ok()
196 }
197}
198
199#[derive(
210 Debug,
211 Clone,
212 Copy,
213 PartialEq,
214 Eq,
215 Hash,
216 PartialOrd,
217 Ord,
218 serde::Serialize,
219 serde::Deserialize,
220 strum::IntoStaticStr,
221 strum::EnumString,
222 strum::EnumIter,
223)]
224#[serde(rename_all = "snake_case")]
225#[strum(serialize_all = "snake_case")]
226#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
227pub enum Dialect {
228 #[serde(rename = "openai")]
230 #[strum(serialize = "openai")]
231 OpenAi,
232 Claude,
234 Gemini,
236 #[serde(rename = "openai_chat")]
238 #[strum(serialize = "openai_chat")]
239 OpenAiChat,
240 #[serde(rename = "openai_responses_websocket")]
244 #[strum(serialize = "openai_responses_websocket")]
245 OpenAiResponsesWebSocket,
246}
247
248impl Dialect {
249 pub fn id(self) -> &'static str {
250 self.into()
251 }
252
253 pub fn from_id(value: &str) -> Option<Self> {
254 value.parse().ok()
255 }
256
257 pub const fn family(self) -> WireFamily {
259 match self {
260 Self::OpenAi | Self::OpenAiChat | Self::OpenAiResponsesWebSocket => WireFamily::OpenAi,
261 Self::Claude => WireFamily::Claude,
262 Self::Gemini => WireFamily::Gemini,
263 }
264 }
265
266 pub const fn pair_dialect(self) -> Self {
269 match self {
270 Self::OpenAiResponsesWebSocket => Self::OpenAi,
271 other => other,
272 }
273 }
274}
275
276#[derive(
281 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
282)]
283#[serde(deny_unknown_fields)]
284pub struct OperationKey {
285 pub operation: Operation,
286 pub dialect: Dialect,
287}
288
289impl OperationKey {
290 pub const fn family(self) -> WireFamily {
291 self.dialect.family()
292 }
293
294 pub const fn pair_key(self) -> Self {
297 Self {
298 operation: self.operation,
299 dialect: self.dialect.pair_dialect(),
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use strum::IntoEnumIterator;
308
309 #[test]
314 fn only_inference_and_metered_tool_operations_produce_usage() {
315 for op in [
316 Operation::ListModels,
317 Operation::GetModel,
318 Operation::CountTokens,
319 Operation::CreateConversation,
320 Operation::CreateFile,
321 Operation::ListFiles,
322 Operation::RetrieveFile,
323 Operation::RetrieveFileContent,
324 Operation::DeleteFile,
325 Operation::RetrieveVideo,
326 Operation::ListVideos,
327 Operation::DeleteVideo,
328 Operation::DownloadVideoContent,
329 Operation::CreateRealtimeCall,
330 ] {
331 assert!(!op.produces_usage(), "{}", op.id());
332 }
333 assert!(Operation::GenerateContent.produces_usage());
334 assert!(Operation::CreateEmbedding.produces_usage());
335 assert!(Operation::ConnectRealtime.produces_usage());
336 }
337
338 #[test]
339 fn operation_ids_are_pinned() {
340 let ids: Vec<&'static str> = Operation::iter().map(Operation::id).collect();
341 assert_eq!(
342 ids,
343 [
344 "list_models",
345 "get_model",
346 "count_tokens",
347 "generate_content",
348 "stream_generate_content",
349 "create_moderation",
350 "guardian_review",
351 "guardian_classify",
352 "compact_content",
353 "summarize_memory",
354 "create_conversation",
355 "create_embedding",
356 "batch_create_embedding",
357 "rerank",
358 "web_search",
359 "create_image",
360 "edit_image",
361 "create_speech",
362 "create_transcription",
363 "create_translation",
364 "create_file",
365 "list_files",
366 "retrieve_file",
367 "retrieve_file_content",
368 "delete_file",
369 "create_video",
370 "retrieve_video",
371 "list_videos",
372 "delete_video",
373 "download_video_content",
374 "create_realtime_call",
375 "connect_realtime",
376 ]
377 );
378 }
379
380 #[test]
382 fn dialect_ids_are_pinned() {
383 let ids: Vec<&'static str> = Dialect::iter().map(Dialect::id).collect();
384 assert_eq!(
385 ids,
386 [
387 "openai",
388 "claude",
389 "gemini",
390 "openai_chat",
391 "openai_responses_websocket",
392 ]
393 );
394
395 let families: Vec<&'static str> = WireFamily::iter().map(WireFamily::id).collect();
396 assert_eq!(families, ["openai", "claude", "gemini"]);
397 }
398
399 #[test]
400 fn every_id_round_trips() {
401 for operation in Operation::iter() {
402 assert_eq!(Operation::from_id(operation.id()), Some(operation));
403 }
404 for dialect in Dialect::iter() {
405 assert_eq!(Dialect::from_id(dialect.id()), Some(dialect));
406 }
407 for family in WireFamily::iter() {
408 assert_eq!(WireFamily::from_id(family.id()), Some(family));
409 }
410 }
411
412 #[test]
415 fn serde_agrees_with_strum() {
416 for operation in Operation::iter() {
417 let json = serde_json::to_string(&operation).expect("serializes");
418 assert_eq!(json, format!("\"{}\"", operation.id()));
419 let back: Operation = serde_json::from_str(&json).expect("deserializes");
420 assert_eq!(back, operation);
421 }
422 for dialect in Dialect::iter() {
423 let json = serde_json::to_string(&dialect).expect("serializes");
424 assert_eq!(json, format!("\"{}\"", dialect.id()));
425 let back: Dialect = serde_json::from_str(&json).expect("deserializes");
426 assert_eq!(back, dialect);
427 }
428 for family in WireFamily::iter() {
429 let json = serde_json::to_string(&family).expect("serializes");
430 assert_eq!(json, format!("\"{}\"", family.id()));
431 }
432 }
433
434 #[test]
435 fn unknown_ids_are_rejected() {
436 assert_eq!(Operation::from_id("not_an_operation"), None);
437 assert_eq!(Dialect::from_id("open_ai"), None);
438 assert_eq!(WireFamily::from_id("open_ai"), None);
439 }
440
441 #[test]
444 fn families_are_derivable() {
445 assert_eq!(Dialect::OpenAiChat.family(), WireFamily::OpenAi);
446 assert_eq!(
447 Dialect::OpenAiResponsesWebSocket.family(),
448 WireFamily::OpenAi
449 );
450 assert_eq!(Dialect::OpenAi.family(), WireFamily::OpenAi);
451 assert_eq!(Dialect::Claude.family(), WireFamily::Claude);
452 assert_eq!(Dialect::Gemini.family(), WireFamily::Gemini);
453 }
454
455 #[test]
456 fn only_the_websocket_envelope_borrows_another_dialects_pairs() {
457 let ws = OperationKey {
458 operation: Operation::StreamGenerateContent,
459 dialect: Dialect::OpenAiResponsesWebSocket,
460 };
461 assert_eq!(
462 ws.pair_key(),
463 OperationKey {
464 operation: Operation::StreamGenerateContent,
465 dialect: Dialect::OpenAi,
466 }
467 );
468
469 for dialect in Dialect::iter().filter(|d| *d != Dialect::OpenAiResponsesWebSocket) {
470 assert_eq!(dialect.pair_dialect(), dialect);
471 }
472 }
473}