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        _ => {
133            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
134        }
135    }
136}
137
138fn resolve_content_generation(
139    source: OperationKey,
140    target: OperationKey,
141) -> Result<TransformPair, TransformError> {
142    let OperationKind::ContentGeneration(source_kind) = source.kind() else {
143        return Err(TransformError::unsupported_pair(source, target));
144    };
145    let OperationKind::ContentGeneration(target_kind) = target.kind() else {
146        return Err(TransformError::unsupported_pair(source, target));
147    };
148
149    use ContentGenerationKind as Kind;
150    use TransformPair as Pair;
151
152    match (source_kind, target_kind) {
153        (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
154            Ok(Pair::OpenAiResponsesToOpenAiChat)
155        }
156        (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
157            Ok(Pair::OpenAiChatToOpenAiResponses)
158        }
159        (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
160            Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
161        }
162        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
163            Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
164        }
165        (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
166            Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
167        }
168        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
169            Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
170        }
171        (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
172            Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
173        }
174        (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
175            Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
176        }
177        (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
178            Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
179        }
180        (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
181            Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
182        }
183        (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
184        (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
185        (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
186            Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
187        }
188        (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
189            Ok(Pair::GeminiGenerateContentToOpenAiResponses)
190        }
191        (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
192        (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
193        (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
194            Ok(Pair::OpenAiChatToGeminiGenerateContent)
195        }
196        (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
197            Ok(Pair::GeminiGenerateContentToOpenAiChat)
198        }
199        (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
200            Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
201        }
202        (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
203            Ok(Pair::GeminiGenerateContentToClaudeMessages)
204        }
205        _ => Err(TransformError::unsupported_pair(source, target)),
206    }
207}
208
209fn resolve_provider_pair(
210    source: OperationKey,
211    target: OperationKey,
212    pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
213) -> Result<TransformPair, TransformError> {
214    let OperationKind::Provider(source_provider) = source.kind() else {
215        return Err(TransformError::unsupported_pair(source, target));
216    };
217    let OperationKind::Provider(target_provider) = target.kind() else {
218        return Err(TransformError::unsupported_pair(source, target));
219    };
220
221    pair_fn(source_provider, target_provider)
222        .ok_or_else(|| TransformError::unsupported_pair(source, target))
223}
224
225fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
226    provider_matrix(
227        source,
228        target,
229        ProviderMatrix {
230            openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
231            claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
232            openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
233            gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
234            claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
235            gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
236        },
237    )
238}
239
240/// Native image generation converts only between OpenAI and Imagen predict.
241fn imagen_pair(source: Provider, target: Provider) -> Option<TransformPair> {
242    match (source, target) {
243        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiImagen),
244        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiImagenToOpenAi),
245        _ => None,
246    }
247}
248
249/// Videos convert only between OpenAI jobs and Gemini Veo operations.
250fn videos_pair(source: Provider, target: Provider) -> Option<TransformPair> {
251    match (source, target) {
252        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiVideos),
253        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiVideos),
254        _ => None,
255    }
256}
257
258fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
259    provider_matrix(
260        source,
261        target,
262        ProviderMatrix {
263            openai_to_claude: TransformPair::OpenAiToClaudeModels,
264            claude_to_openai: TransformPair::ClaudeToOpenAiModels,
265            openai_to_gemini: TransformPair::OpenAiToGeminiModels,
266            gemini_to_openai: TransformPair::GeminiToOpenAiModels,
267            claude_to_gemini: TransformPair::ClaudeToGeminiModels,
268            gemini_to_claude: TransformPair::GeminiToClaudeModels,
269        },
270    )
271}
272
273fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
274    match (source, target) {
275        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
276        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
277        _ => None,
278    }
279}
280
281/// Resolve cross-operation image-generation routes. OpenAI has dedicated
282/// create/edit-image endpoints; Gemini services them through generate-content
283/// (image response modality), so it has no image operation of its own.
284fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
285    use ContentGenerationKind as Kind;
286    use OperationKind as OK;
287
288    // OpenAI create/edit-image -> OpenAI Responses: codex-like channels have no
289    // images endpoint and generate/edit via the Responses `image_generation`
290    // tool. Each pair serves both directions (request: images->responses;
291    // response: responses->images), so resolve it for the reverse ordering too.
292    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
293        (source.kind(), target.kind())
294        && target.operation().is_content_generation()
295    {
296        return match source.operation() {
297            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
298            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
299            _ => None,
300        };
301    }
302    if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
303        (source.kind(), target.kind())
304        && source.operation().is_content_generation()
305    {
306        return match target.operation() {
307            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
308            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
309            _ => None,
310        };
311    }
312
313    // OpenAI create/edit image -> Gemini generate-content.
314    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
315        (source.kind(), target.kind())
316        && target.operation().is_content_generation()
317    {
318        return match source.operation() {
319            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
320            Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
321            _ => None,
322        };
323    }
324
325    // Gemini generate-content -> OpenAI create/edit image.
326    if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
327        (source.kind(), target.kind())
328        && source.operation().is_content_generation()
329    {
330        return match target.operation() {
331            Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
332            Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
333            _ => None,
334        };
335    }
336
337    None
338}
339
340/// Resolve cross-operation compaction routes. Compaction is the only sanctioned
341/// cross-operation conversion: a generate-content payload folded into an OpenAI
342/// compact payload, or an OpenAI compact payload serviced by a generate-content
343/// upstream.
344fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
345    use ContentGenerationKind as Kind;
346    use OperationKind as OK;
347
348    // content-generation -> OpenAI compact.
349    if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
350        (source.kind(), target.operation(), target.kind())
351        && source.operation().is_content_generation()
352    {
353        return match kind {
354            Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
355            Kind::OpenAiResponsesWebSocket => None,
356            Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
357            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
358            Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
359            _ => unreachable!(
360                "new non-exhaustive protocol variant requires a lockstep transform update"
361            ),
362        };
363    }
364
365    // OpenAI compact -> content-generation.
366    if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
367        (source.kind(), source.operation())
368        && let OK::ContentGeneration(kind) = target.kind()
369        && target.operation().is_content_generation()
370    {
371        return match kind {
372            Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
373            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
374            Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
375            Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
376            Kind::OpenAiResponsesWebSocket => None,
377            _ => unreachable!(
378                "new non-exhaustive protocol variant requires a lockstep transform update"
379            ),
380        };
381    }
382
383    None
384}
385
386#[derive(Debug, Clone, Copy)]
387struct ProviderMatrix {
388    openai_to_claude: TransformPair,
389    claude_to_openai: TransformPair,
390    openai_to_gemini: TransformPair,
391    gemini_to_openai: TransformPair,
392    claude_to_gemini: TransformPair,
393    gemini_to_claude: TransformPair,
394}
395
396fn provider_matrix(
397    source: Provider,
398    target: Provider,
399    matrix: ProviderMatrix,
400) -> Option<TransformPair> {
401    match (source, target) {
402        (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
403        (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
404        (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
405        (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
406        (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
407        (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
408        _ => None,
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn edit_image_resolves_to_responses_image_tool_pair() {
418        let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
419        let target = OperationKey::content_generation(
420            Operation::StreamGenerateContent,
421            ContentGenerationKind::OpenAiResponses,
422        );
423
424        assert_eq!(
425            resolve(source, target).unwrap(),
426            TransformPair::OpenAiEditImageToOpenAiResponses
427        );
428        assert_eq!(
429            resolve(target, source).unwrap(),
430            TransformPair::OpenAiEditImageToOpenAiResponses
431        );
432    }
433}