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