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