Skip to main content

gproxy_transform/transform/
registry.rs

1use crate::protocol::{ContentGenerationKind, Operation, OperationKey, OperationKind, Provider};
2
3use super::TransformError;
4
5/// Stable identifier for a supported transform implementation.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TransformPair {
8    OpenAiResponsesToOpenAiChat,
9    OpenAiChatToOpenAiResponses,
10    OpenAiResponsesToOpenAiResponsesWebSocket,
11    OpenAiResponsesWebSocketToOpenAiResponses,
12    OpenAiChatToOpenAiResponsesWebSocket,
13    OpenAiResponsesWebSocketToOpenAiChat,
14    ClaudeMessagesToOpenAiResponsesWebSocket,
15    OpenAiResponsesWebSocketToClaudeMessages,
16    GeminiGenerateContentToOpenAiResponsesWebSocket,
17    OpenAiResponsesWebSocketToGeminiGenerateContent,
18    OpenAiResponsesToClaudeMessages,
19    ClaudeMessagesToOpenAiResponses,
20    OpenAiResponsesToGeminiGenerateContent,
21    GeminiGenerateContentToOpenAiResponses,
22    OpenAiChatToClaudeMessages,
23    ClaudeMessagesToOpenAiChat,
24    OpenAiChatToGeminiGenerateContent,
25    GeminiGenerateContentToOpenAiChat,
26    ClaudeMessagesToGeminiGenerateContent,
27    GeminiGenerateContentToClaudeMessages,
28    OpenAiToClaudeCountTokens,
29    ClaudeToOpenAiCountTokens,
30    OpenAiToGeminiCountTokens,
31    GeminiToOpenAiCountTokens,
32    ClaudeToGeminiCountTokens,
33    GeminiToClaudeCountTokens,
34    OpenAiToClaudeModels,
35    ClaudeToOpenAiModels,
36    OpenAiToGeminiModels,
37    GeminiToOpenAiModels,
38    ClaudeToGeminiModels,
39    GeminiToClaudeModels,
40    OpenAiToGeminiEmbeddings,
41    GeminiToOpenAiEmbeddings,
42    OpenAiToGeminiVideos,
43    GeminiToOpenAiVideos,
44    OpenAiToGeminiImagen,
45    GeminiImagenToOpenAi,
46    OpenAiCreateImageToGemini,
47    GeminiToOpenAiCreateImage,
48    OpenAiCreateImageToOpenAiResponses,
49    OpenAiEditImageToOpenAiResponses,
50    OpenAiEditImageToGemini,
51    GeminiToOpenAiEditImage,
52    OpenAiToClaudeCompact,
53    ClaudeToOpenAiCompact,
54    OpenAiCompactToOpenAiResponses,
55    OpenAiResponsesToOpenAiCompact,
56    OpenAiCompactToGemini,
57    GeminiToOpenAiCompact,
58    OpenAiCompactToOpenAiChat,
59    OpenAiChatToOpenAiCompact,
60}
61
62/// Resolve operation keys to a concrete pair module.
63///
64/// Same-kind passthrough is intentionally not represented here; routing should
65/// bypass the transform layer when source and target wire kinds match.
66pub fn resolve(
67    source: OperationKey,
68    target: OperationKey,
69) -> Result<TransformPair, TransformError> {
70    if !source.is_consistent() {
71        return Err(TransformError::InvalidInput {
72            reason: "source operation and kind are inconsistent".to_owned(),
73        });
74    }
75    if !target.is_consistent() {
76        return Err(TransformError::InvalidInput {
77            reason: "target operation and kind are inconsistent".to_owned(),
78        });
79    }
80    if source == target {
81        return Err(TransformError::unsupported_pair(source, target));
82    }
83    // Compaction and image generation are the sanctioned cross-operation routes:
84    // OpenAI has dedicated compact/image endpoints, while the other providers
85    // service them through generate-content.
86    if let Some(pair) = resolve_compaction(source, target) {
87        return Ok(pair);
88    }
89    if let Some(pair) = resolve_image_generation(source, target) {
90        return Ok(pair);
91    }
92    if source.operation() != target.operation() {
93        return Err(TransformError::unsupported_pair(source, target));
94    }
95
96    match source.operation() {
97        Operation::GenerateContent | Operation::StreamGenerateContent => {
98            resolve_content_generation(source, target)
99        }
100        Operation::CountTokens => resolve_provider_pair(source, target, count_tokens_pair),
101        Operation::ListModels | Operation::GetModel => {
102            resolve_provider_pair(source, target, models_pair)
103        }
104        Operation::CreateEmbedding => resolve_provider_pair(source, target, embeddings_pair),
105        // Compaction and image generation are never same-operation routes: OpenAI
106        // is the only provider with dedicated compact/image endpoints, and every
107        // such conversion is handled cross-operation above.
108        Operation::CreateVideo | Operation::RetrieveVideo => {
109            resolve_provider_pair(source, target, videos_pair)
110        }
111        // Imagen models expose a native predict endpoint; gemini-*-image models
112        // are served by the cross-operation generate-content route above.
113        Operation::CreateImage => resolve_provider_pair(source, target, imagen_pair),
114        Operation::CompactContent
115        | Operation::EditImage
116        | Operation::Rerank
117        | Operation::CreateSpeech
118        | Operation::CreateTranscription
119        | Operation::CreateTranslation
120        | Operation::CreateConversation
121        | Operation::CreateRealtimeCall
122        | Operation::ConnectRealtime
123        | Operation::WebSearch
124        | Operation::ListVideos
125        | Operation::DeleteVideo
126        | Operation::DownloadVideoContent
127        | Operation::RemixVideo
128        | Operation::CreateVideoCharacter
129        | Operation::GetVideoCharacter
130        | Operation::EditVideo
131        | Operation::ExtendVideo => Err(TransformError::unsupported_pair(source, target)),
132        Operation::CreateFile
133        | Operation::ListFiles
134        | Operation::RetrieveFile
135        | Operation::DeleteFile
136        | Operation::DownloadFileContent => Err(TransformError::unsupported_pair(source, target)),
137        _ => {
138            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
139        }
140    }
141}
142
143fn resolve_content_generation(
144    source: OperationKey,
145    target: OperationKey,
146) -> Result<TransformPair, TransformError> {
147    let OperationKind::ContentGeneration(source_kind) = source.kind() else {
148        return Err(TransformError::unsupported_pair(source, target));
149    };
150    let OperationKind::ContentGeneration(target_kind) = target.kind() else {
151        return Err(TransformError::unsupported_pair(source, target));
152    };
153
154    use ContentGenerationKind as Kind;
155    use TransformPair as Pair;
156
157    match (source_kind, target_kind) {
158        (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
159            Ok(Pair::OpenAiResponsesToOpenAiChat)
160        }
161        (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
162            Ok(Pair::OpenAiChatToOpenAiResponses)
163        }
164        (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
165            Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
166        }
167        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
168            Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
169        }
170        (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
171            Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
172        }
173        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
174            Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
175        }
176        (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
177            Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
178        }
179        (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
180            Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
181        }
182        (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
183            Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
184        }
185        (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
186            Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
187        }
188        (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
189        (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
190        (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
191            Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
192        }
193        (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
194            Ok(Pair::GeminiGenerateContentToOpenAiResponses)
195        }
196        (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
197        (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
198        (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
199            Ok(Pair::OpenAiChatToGeminiGenerateContent)
200        }
201        (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
202            Ok(Pair::GeminiGenerateContentToOpenAiChat)
203        }
204        (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
205            Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
206        }
207        (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
208            Ok(Pair::GeminiGenerateContentToClaudeMessages)
209        }
210        _ => Err(TransformError::unsupported_pair(source, target)),
211    }
212}
213
214fn resolve_provider_pair(
215    source: OperationKey,
216    target: OperationKey,
217    pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
218) -> Result<TransformPair, TransformError> {
219    let OperationKind::Provider(source_provider) = source.kind() else {
220        return Err(TransformError::unsupported_pair(source, target));
221    };
222    let OperationKind::Provider(target_provider) = target.kind() else {
223        return Err(TransformError::unsupported_pair(source, target));
224    };
225
226    pair_fn(source_provider, target_provider)
227        .ok_or_else(|| TransformError::unsupported_pair(source, target))
228}
229
230fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
231    provider_matrix(
232        source,
233        target,
234        ProviderMatrix {
235            openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
236            claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
237            openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
238            gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
239            claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
240            gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
241        },
242    )
243}
244
245/// Native image generation converts only between OpenAI and Imagen predict.
246fn imagen_pair(source: Provider, target: Provider) -> Option<TransformPair> {
247    match (source, target) {
248        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiImagen),
249        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiImagenToOpenAi),
250        _ => None,
251    }
252}
253
254/// Videos convert only between OpenAI jobs and Gemini Veo operations.
255fn videos_pair(source: Provider, target: Provider) -> Option<TransformPair> {
256    match (source, target) {
257        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiVideos),
258        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiVideos),
259        _ => None,
260    }
261}
262
263fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
264    provider_matrix(
265        source,
266        target,
267        ProviderMatrix {
268            openai_to_claude: TransformPair::OpenAiToClaudeModels,
269            claude_to_openai: TransformPair::ClaudeToOpenAiModels,
270            openai_to_gemini: TransformPair::OpenAiToGeminiModels,
271            gemini_to_openai: TransformPair::GeminiToOpenAiModels,
272            claude_to_gemini: TransformPair::ClaudeToGeminiModels,
273            gemini_to_claude: TransformPair::GeminiToClaudeModels,
274        },
275    )
276}
277
278fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
279    match (source, target) {
280        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
281        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
282        _ => None,
283    }
284}
285
286/// Resolve cross-operation image-generation routes. OpenAI has dedicated
287/// create/edit-image endpoints; Gemini services them through generate-content
288/// (image response modality), so it has no image operation of its own.
289fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
290    use ContentGenerationKind as Kind;
291    use OperationKind as OK;
292
293    // OpenAI create/edit-image -> OpenAI Responses: codex-like channels have no
294    // images endpoint and generate/edit via the Responses `image_generation`
295    // tool. Each pair serves both directions (request: images->responses;
296    // response: responses->images), so resolve it for the reverse ordering too.
297    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
298        (source.kind(), target.kind())
299        && target.operation().is_content_generation()
300    {
301        return match source.operation() {
302            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
303            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
304            _ => None,
305        };
306    }
307    if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
308        (source.kind(), target.kind())
309        && source.operation().is_content_generation()
310    {
311        return match target.operation() {
312            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
313            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
314            _ => None,
315        };
316    }
317
318    // OpenAI create/edit image -> Gemini generate-content.
319    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
320        (source.kind(), target.kind())
321        && target.operation().is_content_generation()
322    {
323        return match source.operation() {
324            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
325            Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
326            _ => None,
327        };
328    }
329
330    // Gemini generate-content -> OpenAI create/edit image.
331    if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
332        (source.kind(), target.kind())
333        && source.operation().is_content_generation()
334    {
335        return match target.operation() {
336            Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
337            Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
338            _ => None,
339        };
340    }
341
342    None
343}
344
345/// Resolve cross-operation compaction routes. Compaction is the only sanctioned
346/// cross-operation conversion: a generate-content payload folded into an OpenAI
347/// compact payload, or an OpenAI compact payload serviced by a generate-content
348/// upstream.
349fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
350    use ContentGenerationKind as Kind;
351    use OperationKind as OK;
352
353    // content-generation -> OpenAI compact.
354    if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
355        (source.kind(), target.operation(), target.kind())
356        && source.operation().is_content_generation()
357    {
358        return match kind {
359            Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
360            Kind::OpenAiResponsesWebSocket => None,
361            Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
362            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
363            Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
364            _ => unreachable!(
365                "new non-exhaustive protocol variant requires a lockstep transform update"
366            ),
367        };
368    }
369
370    // OpenAI compact -> content-generation.
371    if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
372        (source.kind(), source.operation())
373        && let OK::ContentGeneration(kind) = target.kind()
374        && target.operation().is_content_generation()
375    {
376        return match kind {
377            Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
378            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
379            Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
380            Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
381            Kind::OpenAiResponsesWebSocket => None,
382            _ => unreachable!(
383                "new non-exhaustive protocol variant requires a lockstep transform update"
384            ),
385        };
386    }
387
388    None
389}
390
391#[derive(Debug, Clone, Copy)]
392struct ProviderMatrix {
393    openai_to_claude: TransformPair,
394    claude_to_openai: TransformPair,
395    openai_to_gemini: TransformPair,
396    gemini_to_openai: TransformPair,
397    claude_to_gemini: TransformPair,
398    gemini_to_claude: TransformPair,
399}
400
401fn provider_matrix(
402    source: Provider,
403    target: Provider,
404    matrix: ProviderMatrix,
405) -> Option<TransformPair> {
406    match (source, target) {
407        (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
408        (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
409        (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
410        (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
411        (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
412        (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
413        _ => None,
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn edit_image_resolves_to_responses_image_tool_pair() {
423        let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
424        let target = OperationKey::content_generation(
425            Operation::StreamGenerateContent,
426            ContentGenerationKind::OpenAiResponses,
427        );
428
429        assert_eq!(
430            resolve(source, target).unwrap(),
431            TransformPair::OpenAiEditImageToOpenAiResponses
432        );
433        assert_eq!(
434            resolve(target, source).unwrap(),
435            TransformPair::OpenAiEditImageToOpenAiResponses
436        );
437    }
438}