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 => 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
236fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
240 use ContentGenerationKind as Kind;
241 use OperationKind as OK;
242
243 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 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 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
295fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
300 use ContentGenerationKind as Kind;
301 use OperationKind as OK;
302
303 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 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}