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    OpenAiCreateImageToGemini,
43    GeminiToOpenAiCreateImage,
44    OpenAiCreateImageToOpenAiResponses,
45    OpenAiEditImageToOpenAiResponses,
46    OpenAiEditImageToGemini,
47    GeminiToOpenAiEditImage,
48    OpenAiToClaudeCompact,
49    ClaudeToOpenAiCompact,
50    OpenAiCompactToOpenAiResponses,
51    OpenAiResponsesToOpenAiCompact,
52    OpenAiCompactToGemini,
53    GeminiToOpenAiCompact,
54    OpenAiCompactToOpenAiChat,
55    OpenAiChatToOpenAiCompact,
56}
57
58/// Resolve operation keys to a concrete pair module.
59///
60/// Same-kind passthrough is intentionally not represented here; routing should
61/// bypass the transform layer when source and target wire kinds match.
62pub fn resolve(
63    source: OperationKey,
64    target: OperationKey,
65) -> Result<TransformPair, TransformError> {
66    if !source.is_consistent() {
67        return Err(TransformError::InvalidInput {
68            reason: "source operation and kind are inconsistent".to_owned(),
69        });
70    }
71    if !target.is_consistent() {
72        return Err(TransformError::InvalidInput {
73            reason: "target operation and kind are inconsistent".to_owned(),
74        });
75    }
76    if source == target {
77        return Err(TransformError::unsupported_pair(source, target));
78    }
79    // Compaction and image generation are the sanctioned cross-operation routes:
80    // OpenAI has dedicated compact/image endpoints, while the other providers
81    // service them through generate-content.
82    if let Some(pair) = resolve_compaction(source, target) {
83        return Ok(pair);
84    }
85    if let Some(pair) = resolve_image_generation(source, target) {
86        return Ok(pair);
87    }
88    if source.operation != target.operation {
89        return Err(TransformError::unsupported_pair(source, target));
90    }
91
92    match source.operation {
93        Operation::GenerateContent | Operation::StreamGenerateContent => {
94            resolve_content_generation(source, target)
95        }
96        Operation::CountTokens => resolve_provider_pair(source, target, count_tokens_pair),
97        Operation::ListModels | Operation::GetModel => {
98            resolve_provider_pair(source, target, models_pair)
99        }
100        Operation::CreateEmbedding => resolve_provider_pair(source, target, embeddings_pair),
101        // Compaction and image generation are never same-operation routes: OpenAI
102        // is the only provider with dedicated compact/image endpoints, and every
103        // such conversion is handled cross-operation above.
104        Operation::CompactContent
105        | Operation::CreateImage
106        | Operation::EditImage
107        | Operation::CreateConversation
108        | Operation::ConnectRealtime => Err(TransformError::unsupported_pair(source, target)),
109    }
110}
111
112fn resolve_content_generation(
113    source: OperationKey,
114    target: OperationKey,
115) -> Result<TransformPair, TransformError> {
116    let OperationKind::ContentGeneration(source_kind) = source.kind else {
117        return Err(TransformError::unsupported_pair(source, target));
118    };
119    let OperationKind::ContentGeneration(target_kind) = target.kind else {
120        return Err(TransformError::unsupported_pair(source, target));
121    };
122
123    use ContentGenerationKind as Kind;
124    use TransformPair as Pair;
125
126    match (source_kind, target_kind) {
127        (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
128            Ok(Pair::OpenAiResponsesToOpenAiChat)
129        }
130        (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
131            Ok(Pair::OpenAiChatToOpenAiResponses)
132        }
133        (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
134            Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
135        }
136        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
137            Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
138        }
139        (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
140            Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
141        }
142        (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
143            Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
144        }
145        (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
146            Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
147        }
148        (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
149            Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
150        }
151        (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
152            Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
153        }
154        (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
155            Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
156        }
157        (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
158        (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
159        (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
160            Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
161        }
162        (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
163            Ok(Pair::GeminiGenerateContentToOpenAiResponses)
164        }
165        (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
166        (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
167        (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
168            Ok(Pair::OpenAiChatToGeminiGenerateContent)
169        }
170        (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
171            Ok(Pair::GeminiGenerateContentToOpenAiChat)
172        }
173        (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
174            Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
175        }
176        (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
177            Ok(Pair::GeminiGenerateContentToClaudeMessages)
178        }
179        _ => Err(TransformError::unsupported_pair(source, target)),
180    }
181}
182
183fn resolve_provider_pair(
184    source: OperationKey,
185    target: OperationKey,
186    pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
187) -> Result<TransformPair, TransformError> {
188    let OperationKind::Provider(source_provider) = source.kind else {
189        return Err(TransformError::unsupported_pair(source, target));
190    };
191    let OperationKind::Provider(target_provider) = target.kind else {
192        return Err(TransformError::unsupported_pair(source, target));
193    };
194
195    pair_fn(source_provider, target_provider)
196        .ok_or_else(|| TransformError::unsupported_pair(source, target))
197}
198
199fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
200    provider_matrix(
201        source,
202        target,
203        ProviderMatrix {
204            openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
205            claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
206            openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
207            gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
208            claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
209            gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
210        },
211    )
212}
213
214fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
215    provider_matrix(
216        source,
217        target,
218        ProviderMatrix {
219            openai_to_claude: TransformPair::OpenAiToClaudeModels,
220            claude_to_openai: TransformPair::ClaudeToOpenAiModels,
221            openai_to_gemini: TransformPair::OpenAiToGeminiModels,
222            gemini_to_openai: TransformPair::GeminiToOpenAiModels,
223            claude_to_gemini: TransformPair::ClaudeToGeminiModels,
224            gemini_to_claude: TransformPair::GeminiToClaudeModels,
225        },
226    )
227}
228
229fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
230    match (source, target) {
231        (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
232        (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
233        _ => None,
234    }
235}
236
237/// Resolve cross-operation image-generation routes. OpenAI has dedicated
238/// create/edit-image endpoints; Gemini services them through generate-content
239/// (image response modality), so it has no image operation of its own.
240fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
241    use ContentGenerationKind as Kind;
242    use OperationKind as OK;
243
244    // OpenAI create/edit-image -> OpenAI Responses: codex-like channels have no
245    // images endpoint and generate/edit via the Responses `image_generation`
246    // tool. Each pair serves both directions (request: images->responses;
247    // response: responses->images), so resolve it for the reverse ordering too.
248    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
249        (source.kind, target.kind)
250        && target.operation.is_content_generation()
251    {
252        return match source.operation {
253            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
254            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
255            _ => None,
256        };
257    }
258    if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
259        (source.kind, target.kind)
260        && source.operation.is_content_generation()
261    {
262        return match target.operation {
263            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
264            Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
265            _ => None,
266        };
267    }
268
269    // OpenAI create/edit image -> Gemini generate-content.
270    if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
271        (source.kind, target.kind)
272        && target.operation.is_content_generation()
273    {
274        return match source.operation {
275            Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
276            Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
277            _ => None,
278        };
279    }
280
281    // Gemini generate-content -> OpenAI create/edit image.
282    if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
283        (source.kind, target.kind)
284        && source.operation.is_content_generation()
285    {
286        return match target.operation {
287            Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
288            Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
289            _ => None,
290        };
291    }
292
293    None
294}
295
296/// Resolve cross-operation compaction routes. Compaction is the only sanctioned
297/// cross-operation conversion: a generate-content payload folded into an OpenAI
298/// compact payload, or an OpenAI compact payload serviced by a generate-content
299/// upstream.
300fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
301    use ContentGenerationKind as Kind;
302    use OperationKind as OK;
303
304    // content-generation -> OpenAI compact.
305    if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
306        (source.kind, target.operation, target.kind)
307        && source.operation.is_content_generation()
308    {
309        return match kind {
310            Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
311            Kind::OpenAiResponsesWebSocket => None,
312            Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
313            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
314            Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
315        };
316    }
317
318    // OpenAI compact -> content-generation.
319    if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
320        (source.kind, source.operation)
321        && let OK::ContentGeneration(kind) = target.kind
322        && target.operation.is_content_generation()
323    {
324        return match kind {
325            Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
326            Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
327            Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
328            Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
329            Kind::OpenAiResponsesWebSocket => None,
330        };
331    }
332
333    None
334}
335
336#[derive(Debug, Clone, Copy)]
337struct ProviderMatrix {
338    openai_to_claude: TransformPair,
339    claude_to_openai: TransformPair,
340    openai_to_gemini: TransformPair,
341    gemini_to_openai: TransformPair,
342    claude_to_gemini: TransformPair,
343    gemini_to_claude: TransformPair,
344}
345
346fn provider_matrix(
347    source: Provider,
348    target: Provider,
349    matrix: ProviderMatrix,
350) -> Option<TransformPair> {
351    match (source, target) {
352        (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
353        (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
354        (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
355        (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
356        (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
357        (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
358        _ => None,
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn edit_image_resolves_to_responses_image_tool_pair() {
368        let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
369        let target = OperationKey::content_generation(
370            Operation::StreamGenerateContent,
371            ContentGenerationKind::OpenAiResponses,
372        );
373
374        assert_eq!(
375            resolve(source, target).unwrap(),
376            TransformPair::OpenAiEditImageToOpenAiResponses
377        );
378        assert_eq!(
379            resolve(target, source).unwrap(),
380            TransformPair::OpenAiEditImageToOpenAiResponses
381        );
382    }
383}