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