Skip to main content

gproxy_protocol/
operation.rs

1//! Operation taxonomy: what a request *is*, independent of any provider.
2//!
3//! v2's model, kept: an [`Operation`] names the action, an [`OperationKind`]
4//! names the wire shape it arrives in, and the pair ([`OperationKey`]) is
5//! what routing rules and transforms key on. Content generation has several
6//! kinds because OpenAI Responses and Chat Completions are genuinely
7//! different wire shapes, not labels.
8//!
9//! Every enum here is `exhaustive`-feature gated: workspace builds match
10//! exhaustively (adding a variant is a compile-error checklist), external
11//! consumers see `#[non_exhaustive]`. The starter variant set covers the
12//! first porting wave; it grows port by port, compiler-enforced.
13
14/// Broad capability a client asks for.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
17pub enum OperationGroup {
18    Models,
19    CountTokens,
20    Memories,
21    GenerateContent,
22    Compact,
23    Conversation,
24    Embeddings,
25    Images,
26    Audio,
27    Video,
28    Files,
29    Search,
30    Rerank,
31    Realtime,
32}
33
34/// Concrete action.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
37pub enum Operation {
38    ListModels,
39    GetModel,
40    CountTokens,
41    SummarizeMemory,
42    GenerateContent,
43    StreamGenerateContent,
44    GuardianReview,
45    GuardianClassify,
46    CompactContent,
47    CreateConversation,
48    CreateEmbedding,
49    BatchCreateEmbedding,
50    Rerank,
51    WebSearch,
52    CreateImage,
53    EditImage,
54    CreateSpeech,
55    CreateTranscription,
56    CreateTranslation,
57    CreateFile,
58    ListFiles,
59    RetrieveFile,
60    RetrieveFileContent,
61    DeleteFile,
62    CreateVideo,
63    RetrieveVideo,
64    ListVideos,
65    DeleteVideo,
66    DownloadVideoContent,
67    RemixVideo,
68    CreateVideoCharacter,
69    GetVideoCharacter,
70    EditVideo,
71    ExtendVideo,
72    /// SDP handshake creating a WebRTC realtime call (`/v1/realtime/calls`).
73    /// The session's WS/observer operations arrive with the round-3
74    /// websocket-ingress design.
75    CreateRealtimeCall,
76    /// Direct websocket connection to `/v1/realtime`.
77    ConnectRealtime,
78}
79
80/// Wire family for provider-shaped (non-content-generation) operations.
81/// v2 called this `Provider`; renamed — it names a wire dialect, not a
82/// configured backend.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
85pub enum WireFamily {
86    OpenAi,
87    Claude,
88    Gemini,
89}
90
91impl WireFamily {
92    pub const fn id(self) -> &'static str {
93        match self {
94            Self::OpenAi => "openai",
95            Self::Claude => "claude",
96            Self::Gemini => "gemini",
97        }
98    }
99
100    pub fn from_id(value: &str) -> Option<Self> {
101        Some(match value {
102            "openai" => Self::OpenAi,
103            "claude" => Self::Claude,
104            "gemini" => Self::Gemini,
105            _ => return None,
106        })
107    }
108
109    /// Request headers that identify a client as speaking this family.
110    /// Several families share ingress paths (`/v1/files` is both OpenAI and
111    /// Claude), so classification disambiguates by the dialect the caller
112    /// authenticates with. Exhaustive: a new family declares its markers
113    /// here rather than growing a private table inside the engine.
114    pub const fn client_markers(self) -> &'static [&'static str] {
115        match self {
116            Self::Claude => &["x-api-key", "anthropic-version", "anthropic-beta"],
117            Self::OpenAi | Self::Gemini => &[],
118        }
119    }
120}
121
122impl ContentGenerationKind {
123    pub const fn id(self) -> &'static str {
124        match self {
125            Self::OpenAiChat => "openai_chat",
126            Self::OpenAiResponses => "openai_responses",
127            Self::OpenAiResponsesWebSocket => "openai_responses_websocket",
128            Self::ClaudeMessages => "claude_messages",
129            Self::GeminiGenerateContent => "gemini_generate_content",
130        }
131    }
132
133    pub fn from_id(value: &str) -> Option<Self> {
134        Some(match value {
135            "openai_chat" => Self::OpenAiChat,
136            "openai_responses" => Self::OpenAiResponses,
137            "openai_responses_websocket" => Self::OpenAiResponsesWebSocket,
138            "claude_messages" => Self::ClaudeMessages,
139            "gemini_generate_content" => Self::GeminiGenerateContent,
140            _ => return None,
141        })
142    }
143}
144
145impl OperationKind {
146    pub const fn id(self) -> &'static str {
147        match self {
148            Self::ContentGeneration(kind) => kind.id(),
149            Self::Family(family) => family.id(),
150        }
151    }
152
153    pub fn from_id(value: &str) -> Option<Self> {
154        ContentGenerationKind::from_id(value)
155            .map(Self::ContentGeneration)
156            .or_else(|| WireFamily::from_id(value).map(Self::Family))
157    }
158}
159
160/// The distinct content-generation wire shapes.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
163pub enum ContentGenerationKind {
164    OpenAiChat,
165    OpenAiResponses,
166    /// Envelope variant of `OpenAiResponses`: same semantics over a
167    /// websocket transport. Transforms compose it onto the Responses
168    /// pairs; it never gets pair families of its own.
169    OpenAiResponsesWebSocket,
170    ClaudeMessages,
171    GeminiGenerateContent,
172}
173
174/// Wire shape of an operation.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
176#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
177pub enum OperationKind {
178    ContentGeneration(ContentGenerationKind),
179    Family(WireFamily),
180}
181
182/// What routing rules, transforms, and channel support tables key on.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
184pub struct OperationKey {
185    operation: Operation,
186    kind: OperationKind,
187}
188
189impl OperationKey {
190    pub const fn content(operation: Operation, kind: ContentGenerationKind) -> Self {
191        assert!(
192            operation.is_content_generation(),
193            "content kind used with non-content operation"
194        );
195        Self {
196            operation,
197            kind: OperationKind::ContentGeneration(kind),
198        }
199    }
200
201    pub const fn family(operation: Operation, family: WireFamily) -> Self {
202        assert!(
203            !operation.is_content_generation(),
204            "wire family used with content operation"
205        );
206        Self {
207            operation,
208            kind: OperationKind::Family(family),
209        }
210    }
211
212    pub const fn try_new(
213        operation: Operation,
214        kind: OperationKind,
215    ) -> Result<Self, OperationKeyError> {
216        let consistent = matches!(kind, OperationKind::ContentGeneration(_))
217            == operation.is_content_generation();
218        if consistent {
219            Ok(Self { operation, kind })
220        } else {
221            Err(OperationKeyError { operation, kind })
222        }
223    }
224
225    pub const fn operation(self) -> Operation {
226        self.operation
227    }
228
229    pub const fn kind(self) -> OperationKind {
230        self.kind
231    }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235#[non_exhaustive]
236pub struct OperationKeyError {
237    pub operation: Operation,
238    pub kind: OperationKind,
239}
240
241impl std::fmt::Display for OperationKeyError {
242    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        write!(
244            formatter,
245            "operation {:?} is inconsistent with kind {:?}",
246            self.operation, self.kind
247        )
248    }
249}
250
251impl std::error::Error for OperationKeyError {}
252
253impl OperationGroup {
254    /// Stable permission and persistence id.
255    pub const fn id(self) -> &'static str {
256        use OperationGroup::*;
257        match self {
258            Models => "models",
259            CountTokens => "count_tokens",
260            Memories => "memories",
261            GenerateContent => "generate_content",
262            Compact => "compact",
263            Conversation => "conversation",
264            Embeddings => "embeddings",
265            Images => "images",
266            Audio => "audio",
267            Video => "video",
268            Files => "files",
269            Search => "search",
270            Rerank => "rerank",
271            Realtime => "realtime",
272        }
273    }
274}
275
276impl Operation {
277    pub const fn is_content_generation(self) -> bool {
278        matches!(
279            self,
280            Self::GenerateContent
281                | Self::StreamGenerateContent
282                | Self::GuardianReview
283                | Self::GuardianClassify
284        )
285    }
286
287    /// Stable persistence id. Exhaustive so adding an operation cannot silently
288    /// collapse into a debug-string or catch-all representation.
289    pub const fn id(self) -> &'static str {
290        use Operation::*;
291        match self {
292            ListModels => "list_models",
293            GetModel => "get_model",
294            CountTokens => "count_tokens",
295            SummarizeMemory => "summarize_memory",
296            GenerateContent => "generate_content",
297            StreamGenerateContent => "stream_generate_content",
298            GuardianReview => "guardian_review",
299            GuardianClassify => "guardian_classify",
300            CompactContent => "compact_content",
301            CreateConversation => "create_conversation",
302            CreateEmbedding => "create_embedding",
303            BatchCreateEmbedding => "batch_create_embedding",
304            Rerank => "rerank",
305            WebSearch => "web_search",
306            CreateImage => "create_image",
307            EditImage => "edit_image",
308            CreateSpeech => "create_speech",
309            CreateTranscription => "create_transcription",
310            CreateTranslation => "create_translation",
311            CreateFile => "create_file",
312            ListFiles => "list_files",
313            RetrieveFile => "retrieve_file",
314            RetrieveFileContent => "retrieve_file_content",
315            DeleteFile => "delete_file",
316            CreateVideo => "create_video",
317            RetrieveVideo => "retrieve_video",
318            ListVideos => "list_videos",
319            DeleteVideo => "delete_video",
320            DownloadVideoContent => "download_video_content",
321            RemixVideo => "remix_video",
322            CreateVideoCharacter => "create_video_character",
323            GetVideoCharacter => "get_video_character",
324            EditVideo => "edit_video",
325            ExtendVideo => "extend_video",
326            CreateRealtimeCall => "create_realtime_call",
327            ConnectRealtime => "connect_realtime",
328        }
329    }
330
331    pub fn from_id(value: &str) -> Option<Self> {
332        use Operation::*;
333        Some(match value {
334            "list_models" => ListModels,
335            "get_model" => GetModel,
336            "count_tokens" => CountTokens,
337            "summarize_memory" => SummarizeMemory,
338            "generate_content" => GenerateContent,
339            "stream_generate_content" => StreamGenerateContent,
340            "guardian_review" => GuardianReview,
341            "guardian_classify" => GuardianClassify,
342            "compact_content" => CompactContent,
343            "create_conversation" => CreateConversation,
344            "create_embedding" => CreateEmbedding,
345            "batch_create_embedding" => BatchCreateEmbedding,
346            "rerank" => Rerank,
347            "web_search" => WebSearch,
348            "create_image" => CreateImage,
349            "edit_image" => EditImage,
350            "create_speech" => CreateSpeech,
351            "create_transcription" => CreateTranscription,
352            "create_translation" => CreateTranslation,
353            "create_file" => CreateFile,
354            "list_files" => ListFiles,
355            "retrieve_file" => RetrieveFile,
356            "retrieve_file_content" => RetrieveFileContent,
357            "delete_file" => DeleteFile,
358            "create_video" => CreateVideo,
359            "retrieve_video" => RetrieveVideo,
360            "list_videos" => ListVideos,
361            "delete_video" => DeleteVideo,
362            "download_video_content" => DownloadVideoContent,
363            "remix_video" => RemixVideo,
364            "create_video_character" => CreateVideoCharacter,
365            "get_video_character" => GetVideoCharacter,
366            "edit_video" => EditVideo,
367            "extend_video" => ExtendVideo,
368            "create_realtime_call" => CreateRealtimeCall,
369            "connect_realtime" => ConnectRealtime,
370            _ => return None,
371        })
372    }
373
374    /// Exhaustive by design: a new operation fails to compile until its
375    /// group — and its [`spec`](crate::spec::OperationSpec) — exist.
376    pub const fn group(self) -> OperationGroup {
377        use Operation::*;
378        match self {
379            ListModels | GetModel => OperationGroup::Models,
380            CountTokens => OperationGroup::CountTokens,
381            SummarizeMemory => OperationGroup::Memories,
382            GenerateContent | StreamGenerateContent | GuardianReview | GuardianClassify => {
383                OperationGroup::GenerateContent
384            }
385            CompactContent => OperationGroup::Compact,
386            CreateConversation => OperationGroup::Conversation,
387            CreateEmbedding | BatchCreateEmbedding => OperationGroup::Embeddings,
388            Rerank => OperationGroup::Rerank,
389            WebSearch => OperationGroup::Search,
390            CreateImage | EditImage => OperationGroup::Images,
391            CreateSpeech | CreateTranscription | CreateTranslation => OperationGroup::Audio,
392            CreateFile | ListFiles | RetrieveFile | RetrieveFileContent | DeleteFile => {
393                OperationGroup::Files
394            }
395            CreateVideo | RetrieveVideo | ListVideos | DeleteVideo | DownloadVideoContent
396            | RemixVideo | CreateVideoCharacter | GetVideoCharacter | EditVideo | ExtendVideo => {
397                OperationGroup::Video
398            }
399            CreateRealtimeCall | ConnectRealtime => OperationGroup::Realtime,
400        }
401    }
402}