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