1use crate::protocol::{ContentGenerationKind, Operation, OperationKey, OperationKind, Provider};
2
3use super::TransformError;
4
5#[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
58pub 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 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 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
237fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
241 use ContentGenerationKind as Kind;
242 use OperationKind as OK;
243
244 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 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 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
296fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
301 use ContentGenerationKind as Kind;
302 use OperationKind as OK;
303
304 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 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}