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)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub enum Operation {
41 ListModels,
43 GetModel,
44 CountTokens,
46 GenerateContent,
48 StreamGenerateContent,
49 CreateModeration,
52 CreateDecision,
54 CompactContent,
56 SummarizeMemory,
57 CreateConversation,
58 CreateEmbedding,
60 BatchCreateEmbedding,
61 Rerank,
63 WebSearch,
65 CreateImage,
67 EditImage,
68 CreateSpeech,
70 CreateTranscription,
71 CreateTranslation,
72 CreateFile,
74 ListFiles,
75 RetrieveFile,
76 RetrieveFileContent,
77 DeleteFile,
78 CreateVideo,
85 RetrieveVideo,
86 ListVideos,
87 DeleteVideo,
88 DownloadVideoContent,
92 CreateVoice,
94 CreateRealtimeClientSecret,
96 HangupRealtimeCall,
98 CreateLiveSession,
100 HangupLiveSession,
102 ConnectLive,
104 CreateRealtimeCall,
107 ConnectRealtime,
111}
112
113impl Operation {
114 pub const fn produces_usage(self) -> bool {
118 match self {
119 Self::GenerateContent
120 | Self::StreamGenerateContent
121 | Self::CreateModeration
122 | Self::CreateDecision
123 | Self::CompactContent
124 | Self::SummarizeMemory
125 | Self::CreateEmbedding
126 | Self::BatchCreateEmbedding
127 | Self::Rerank
128 | Self::WebSearch
129 | Self::CreateImage
130 | Self::EditImage
131 | Self::CreateSpeech
132 | Self::CreateTranscription
133 | Self::CreateTranslation
134 | Self::CreateVideo
135 | Self::ConnectRealtime
136 | Self::ConnectLive => true,
137 Self::ListModels
138 | Self::GetModel
139 | Self::CountTokens
140 | Self::CreateConversation
141 | Self::CreateFile
142 | Self::ListFiles
143 | Self::RetrieveFile
144 | Self::RetrieveFileContent
145 | Self::DeleteFile
146 | Self::RetrieveVideo
147 | Self::ListVideos
148 | Self::DeleteVideo
149 | Self::DownloadVideoContent
150 | Self::CreateRealtimeCall
151 | Self::CreateVoice
152 | Self::CreateRealtimeClientSecret
153 | Self::HangupRealtimeCall
154 | Self::CreateLiveSession
155 | Self::HangupLiveSession => false,
156 }
157 }
158
159 pub fn usage_operations() -> impl Iterator<Item = Self> {
161 <Self as strum::IntoEnumIterator>::iter().filter(|op| op.produces_usage())
162 }
163
164 pub fn id(self) -> &'static str {
166 self.into()
167 }
168
169 pub fn from_id(value: &str) -> Option<Self> {
170 value.parse().ok()
171 }
172}
173
174#[derive(
182 Debug,
183 Clone,
184 Copy,
185 PartialEq,
186 Eq,
187 Hash,
188 PartialOrd,
189 Ord,
190 serde::Serialize,
191 serde::Deserialize,
192 strum::IntoStaticStr,
193 strum::EnumString,
194 strum::EnumIter,
195)]
196#[serde(rename_all = "snake_case")]
197#[strum(serialize_all = "snake_case")]
198#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
199#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
200pub enum WireFamily {
201 #[serde(rename = "openai")]
202 #[strum(serialize = "openai")]
203 OpenAi,
204 Claude,
205 Gemini,
206}
207
208impl WireFamily {
209 pub fn id(self) -> &'static str {
210 self.into()
211 }
212
213 pub fn from_id(value: &str) -> Option<Self> {
214 value.parse().ok()
215 }
216}
217
218#[derive(
229 Debug,
230 Clone,
231 Copy,
232 PartialEq,
233 Eq,
234 Hash,
235 PartialOrd,
236 Ord,
237 serde::Serialize,
238 serde::Deserialize,
239 strum::IntoStaticStr,
240 strum::EnumString,
241 strum::EnumIter,
242)]
243#[serde(rename_all = "snake_case")]
244#[strum(serialize_all = "snake_case")]
245#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
246#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
247pub enum Dialect {
248 #[serde(rename = "openai")]
250 #[strum(serialize = "openai")]
251 OpenAi,
252 Claude,
254 Gemini,
256 #[serde(rename = "openai_chat")]
258 #[strum(serialize = "openai_chat")]
259 OpenAiChat,
260 #[serde(rename = "openai_responses_websocket")]
264 #[strum(serialize = "openai_responses_websocket")]
265 OpenAiResponsesWebSocket,
266}
267
268impl Dialect {
269 pub fn id(self) -> &'static str {
270 self.into()
271 }
272
273 pub fn from_id(value: &str) -> Option<Self> {
274 value.parse().ok()
275 }
276
277 pub const fn family(self) -> WireFamily {
279 match self {
280 Self::OpenAi | Self::OpenAiChat | Self::OpenAiResponsesWebSocket => WireFamily::OpenAi,
281 Self::Claude => WireFamily::Claude,
282 Self::Gemini => WireFamily::Gemini,
283 }
284 }
285
286 pub const fn pair_dialect(self) -> Self {
289 match self {
290 Self::OpenAiResponsesWebSocket => Self::OpenAi,
291 other => other,
292 }
293 }
294}
295
296#[derive(
301 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
302)]
303#[serde(deny_unknown_fields)]
304#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
305pub struct OperationKey {
306 pub operation: Operation,
307 pub dialect: Dialect,
308}
309
310impl OperationKey {
311 pub const fn family(self) -> WireFamily {
312 self.dialect.family()
313 }
314
315 pub const fn pair_key(self) -> Self {
318 Self {
319 operation: self.operation,
320 dialect: self.dialect.pair_dialect(),
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use strum::IntoEnumIterator;
329
330 #[test]
335 fn only_inference_and_metered_tool_operations_produce_usage() {
336 for op in [
337 Operation::ListModels,
338 Operation::GetModel,
339 Operation::CountTokens,
340 Operation::CreateConversation,
341 Operation::CreateFile,
342 Operation::ListFiles,
343 Operation::RetrieveFile,
344 Operation::RetrieveFileContent,
345 Operation::DeleteFile,
346 Operation::RetrieveVideo,
347 Operation::ListVideos,
348 Operation::DeleteVideo,
349 Operation::DownloadVideoContent,
350 Operation::CreateRealtimeCall,
351 ] {
352 assert!(!op.produces_usage(), "{}", op.id());
353 }
354 assert!(Operation::GenerateContent.produces_usage());
355 assert!(Operation::CreateEmbedding.produces_usage());
356 assert!(Operation::ConnectRealtime.produces_usage());
357 }
358
359 #[test]
360 fn operation_ids_are_pinned() {
361 let ids: Vec<&'static str> = Operation::iter().map(Operation::id).collect();
362 assert_eq!(
363 ids,
364 [
365 "list_models",
366 "get_model",
367 "count_tokens",
368 "generate_content",
369 "stream_generate_content",
370 "create_moderation",
371 "create_decision",
372 "compact_content",
373 "summarize_memory",
374 "create_conversation",
375 "create_embedding",
376 "batch_create_embedding",
377 "rerank",
378 "web_search",
379 "create_image",
380 "edit_image",
381 "create_speech",
382 "create_transcription",
383 "create_translation",
384 "create_file",
385 "list_files",
386 "retrieve_file",
387 "retrieve_file_content",
388 "delete_file",
389 "create_video",
390 "retrieve_video",
391 "list_videos",
392 "delete_video",
393 "download_video_content",
394 "create_voice",
395 "create_realtime_client_secret",
396 "hangup_realtime_call",
397 "create_live_session",
398 "hangup_live_session",
399 "connect_live",
400 "create_realtime_call",
401 "connect_realtime",
402 ]
403 );
404 }
405
406 #[test]
408 fn dialect_ids_are_pinned() {
409 let ids: Vec<&'static str> = Dialect::iter().map(Dialect::id).collect();
410 assert_eq!(
411 ids,
412 [
413 "openai",
414 "claude",
415 "gemini",
416 "openai_chat",
417 "openai_responses_websocket",
418 ]
419 );
420
421 let families: Vec<&'static str> = WireFamily::iter().map(WireFamily::id).collect();
422 assert_eq!(families, ["openai", "claude", "gemini"]);
423 }
424
425 #[test]
426 fn every_id_round_trips() {
427 for operation in Operation::iter() {
428 assert_eq!(Operation::from_id(operation.id()), Some(operation));
429 }
430 for dialect in Dialect::iter() {
431 assert_eq!(Dialect::from_id(dialect.id()), Some(dialect));
432 }
433 for family in WireFamily::iter() {
434 assert_eq!(WireFamily::from_id(family.id()), Some(family));
435 }
436 }
437
438 #[test]
441 fn serde_agrees_with_strum() {
442 for operation in Operation::iter() {
443 let json = serde_json::to_string(&operation).expect("serializes");
444 assert_eq!(json, format!("\"{}\"", operation.id()));
445 let back: Operation = serde_json::from_str(&json).expect("deserializes");
446 assert_eq!(back, operation);
447 }
448 for dialect in Dialect::iter() {
449 let json = serde_json::to_string(&dialect).expect("serializes");
450 assert_eq!(json, format!("\"{}\"", dialect.id()));
451 let back: Dialect = serde_json::from_str(&json).expect("deserializes");
452 assert_eq!(back, dialect);
453 }
454 for family in WireFamily::iter() {
455 let json = serde_json::to_string(&family).expect("serializes");
456 assert_eq!(json, format!("\"{}\"", family.id()));
457 }
458 }
459
460 #[test]
461 fn unknown_ids_are_rejected() {
462 assert_eq!(Operation::from_id("not_an_operation"), None);
463 assert_eq!(Dialect::from_id("open_ai"), None);
464 assert_eq!(WireFamily::from_id("open_ai"), None);
465 }
466
467 #[test]
470 fn families_are_derivable() {
471 assert_eq!(Dialect::OpenAiChat.family(), WireFamily::OpenAi);
472 assert_eq!(
473 Dialect::OpenAiResponsesWebSocket.family(),
474 WireFamily::OpenAi
475 );
476 assert_eq!(Dialect::OpenAi.family(), WireFamily::OpenAi);
477 assert_eq!(Dialect::Claude.family(), WireFamily::Claude);
478 assert_eq!(Dialect::Gemini.family(), WireFamily::Gemini);
479 }
480
481 #[test]
482 fn only_the_websocket_envelope_borrows_another_dialects_pairs() {
483 let ws = OperationKey {
484 operation: Operation::StreamGenerateContent,
485 dialect: Dialect::OpenAiResponsesWebSocket,
486 };
487 assert_eq!(
488 ws.pair_key(),
489 OperationKey {
490 operation: Operation::StreamGenerateContent,
491 dialect: Dialect::OpenAi,
492 }
493 );
494
495 for dialect in Dialect::iter().filter(|d| *d != Dialect::OpenAiResponsesWebSocket) {
496 assert_eq!(dialect.pair_dialect(), dialect);
497 }
498 }
499}