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 OpenAiToGeminiVideos,
43 GeminiToOpenAiVideos,
44 OpenAiToGeminiImagen,
45 GeminiImagenToOpenAi,
46 OpenAiCreateImageToGemini,
47 GeminiToOpenAiCreateImage,
48 OpenAiCreateImageToOpenAiResponses,
49 OpenAiEditImageToOpenAiResponses,
50 OpenAiEditImageToGemini,
51 GeminiToOpenAiEditImage,
52 OpenAiToClaudeCompact,
53 ClaudeToOpenAiCompact,
54 OpenAiCompactToOpenAiResponses,
55 OpenAiResponsesToOpenAiCompact,
56 OpenAiCompactToGemini,
57 GeminiToOpenAiCompact,
58 OpenAiCompactToOpenAiChat,
59 OpenAiChatToOpenAiCompact,
60}
61
62pub fn resolve(
67 source: OperationKey,
68 target: OperationKey,
69) -> Result<TransformPair, TransformError> {
70 if !source.is_consistent() {
71 return Err(TransformError::InvalidInput {
72 reason: "source operation and kind are inconsistent".to_owned(),
73 });
74 }
75 if !target.is_consistent() {
76 return Err(TransformError::InvalidInput {
77 reason: "target operation and kind are inconsistent".to_owned(),
78 });
79 }
80 if source == target {
81 return Err(TransformError::unsupported_pair(source, target));
82 }
83 if let Some(pair) = resolve_compaction(source, target) {
87 return Ok(pair);
88 }
89 if let Some(pair) = resolve_image_generation(source, target) {
90 return Ok(pair);
91 }
92 if source.operation() != target.operation() {
93 return Err(TransformError::unsupported_pair(source, target));
94 }
95
96 match source.operation() {
97 Operation::GenerateContent | Operation::StreamGenerateContent => {
98 resolve_content_generation(source, target)
99 }
100 Operation::CountTokens => resolve_provider_pair(source, target, count_tokens_pair),
101 Operation::ListModels | Operation::GetModel => {
102 resolve_provider_pair(source, target, models_pair)
103 }
104 Operation::CreateEmbedding => resolve_provider_pair(source, target, embeddings_pair),
105 Operation::CreateVideo | Operation::RetrieveVideo => {
109 resolve_provider_pair(source, target, videos_pair)
110 }
111 Operation::CreateImage => resolve_provider_pair(source, target, imagen_pair),
114 Operation::CompactContent
115 | Operation::EditImage
116 | Operation::Rerank
117 | Operation::CreateSpeech
118 | Operation::CreateTranscription
119 | Operation::CreateTranslation
120 | Operation::CreateConversation
121 | Operation::CreateRealtimeCall
122 | Operation::ConnectRealtime
123 | Operation::WebSearch
124 | Operation::ListVideos
125 | Operation::DeleteVideo
126 | Operation::DownloadVideoContent
127 | Operation::RemixVideo
128 | Operation::CreateVideoCharacter
129 | Operation::GetVideoCharacter
130 | Operation::EditVideo
131 | Operation::ExtendVideo => Err(TransformError::unsupported_pair(source, target)),
132 Operation::CreateFile
133 | Operation::ListFiles
134 | Operation::RetrieveFile
135 | Operation::DeleteFile
136 | Operation::DownloadFileContent => Err(TransformError::unsupported_pair(source, target)),
137 _ => {
138 unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
139 }
140 }
141}
142
143fn resolve_content_generation(
144 source: OperationKey,
145 target: OperationKey,
146) -> Result<TransformPair, TransformError> {
147 let OperationKind::ContentGeneration(source_kind) = source.kind() else {
148 return Err(TransformError::unsupported_pair(source, target));
149 };
150 let OperationKind::ContentGeneration(target_kind) = target.kind() else {
151 return Err(TransformError::unsupported_pair(source, target));
152 };
153
154 use ContentGenerationKind as Kind;
155 use TransformPair as Pair;
156
157 match (source_kind, target_kind) {
158 (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
159 Ok(Pair::OpenAiResponsesToOpenAiChat)
160 }
161 (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
162 Ok(Pair::OpenAiChatToOpenAiResponses)
163 }
164 (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
165 Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
166 }
167 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
168 Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
169 }
170 (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
171 Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
172 }
173 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
174 Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
175 }
176 (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
177 Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
178 }
179 (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
180 Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
181 }
182 (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
183 Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
184 }
185 (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
186 Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
187 }
188 (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
189 (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
190 (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
191 Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
192 }
193 (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
194 Ok(Pair::GeminiGenerateContentToOpenAiResponses)
195 }
196 (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
197 (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
198 (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
199 Ok(Pair::OpenAiChatToGeminiGenerateContent)
200 }
201 (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
202 Ok(Pair::GeminiGenerateContentToOpenAiChat)
203 }
204 (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
205 Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
206 }
207 (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
208 Ok(Pair::GeminiGenerateContentToClaudeMessages)
209 }
210 _ => Err(TransformError::unsupported_pair(source, target)),
211 }
212}
213
214fn resolve_provider_pair(
215 source: OperationKey,
216 target: OperationKey,
217 pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
218) -> Result<TransformPair, TransformError> {
219 let OperationKind::Provider(source_provider) = source.kind() else {
220 return Err(TransformError::unsupported_pair(source, target));
221 };
222 let OperationKind::Provider(target_provider) = target.kind() else {
223 return Err(TransformError::unsupported_pair(source, target));
224 };
225
226 pair_fn(source_provider, target_provider)
227 .ok_or_else(|| TransformError::unsupported_pair(source, target))
228}
229
230fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
231 provider_matrix(
232 source,
233 target,
234 ProviderMatrix {
235 openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
236 claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
237 openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
238 gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
239 claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
240 gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
241 },
242 )
243}
244
245fn imagen_pair(source: Provider, target: Provider) -> Option<TransformPair> {
247 match (source, target) {
248 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiImagen),
249 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiImagenToOpenAi),
250 _ => None,
251 }
252}
253
254fn videos_pair(source: Provider, target: Provider) -> Option<TransformPair> {
256 match (source, target) {
257 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiVideos),
258 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiVideos),
259 _ => None,
260 }
261}
262
263fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
264 provider_matrix(
265 source,
266 target,
267 ProviderMatrix {
268 openai_to_claude: TransformPair::OpenAiToClaudeModels,
269 claude_to_openai: TransformPair::ClaudeToOpenAiModels,
270 openai_to_gemini: TransformPair::OpenAiToGeminiModels,
271 gemini_to_openai: TransformPair::GeminiToOpenAiModels,
272 claude_to_gemini: TransformPair::ClaudeToGeminiModels,
273 gemini_to_claude: TransformPair::GeminiToClaudeModels,
274 },
275 )
276}
277
278fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
279 match (source, target) {
280 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
281 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
282 _ => None,
283 }
284}
285
286fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
290 use ContentGenerationKind as Kind;
291 use OperationKind as OK;
292
293 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
298 (source.kind(), target.kind())
299 && target.operation().is_content_generation()
300 {
301 return match source.operation() {
302 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
303 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
304 _ => None,
305 };
306 }
307 if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
308 (source.kind(), target.kind())
309 && source.operation().is_content_generation()
310 {
311 return match target.operation() {
312 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
313 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
314 _ => None,
315 };
316 }
317
318 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
320 (source.kind(), target.kind())
321 && target.operation().is_content_generation()
322 {
323 return match source.operation() {
324 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
325 Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
326 _ => None,
327 };
328 }
329
330 if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
332 (source.kind(), target.kind())
333 && source.operation().is_content_generation()
334 {
335 return match target.operation() {
336 Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
337 Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
338 _ => None,
339 };
340 }
341
342 None
343}
344
345fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
350 use ContentGenerationKind as Kind;
351 use OperationKind as OK;
352
353 if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
355 (source.kind(), target.operation(), target.kind())
356 && source.operation().is_content_generation()
357 {
358 return match kind {
359 Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
360 Kind::OpenAiResponsesWebSocket => None,
361 Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
362 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
363 Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
364 _ => unreachable!(
365 "new non-exhaustive protocol variant requires a lockstep transform update"
366 ),
367 };
368 }
369
370 if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
372 (source.kind(), source.operation())
373 && let OK::ContentGeneration(kind) = target.kind()
374 && target.operation().is_content_generation()
375 {
376 return match kind {
377 Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
378 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
379 Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
380 Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
381 Kind::OpenAiResponsesWebSocket => None,
382 _ => unreachable!(
383 "new non-exhaustive protocol variant requires a lockstep transform update"
384 ),
385 };
386 }
387
388 None
389}
390
391#[derive(Debug, Clone, Copy)]
392struct ProviderMatrix {
393 openai_to_claude: TransformPair,
394 claude_to_openai: TransformPair,
395 openai_to_gemini: TransformPair,
396 gemini_to_openai: TransformPair,
397 claude_to_gemini: TransformPair,
398 gemini_to_claude: TransformPair,
399}
400
401fn provider_matrix(
402 source: Provider,
403 target: Provider,
404 matrix: ProviderMatrix,
405) -> Option<TransformPair> {
406 match (source, target) {
407 (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
408 (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
409 (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
410 (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
411 (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
412 (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
413 _ => None,
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn edit_image_resolves_to_responses_image_tool_pair() {
423 let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
424 let target = OperationKey::content_generation(
425 Operation::StreamGenerateContent,
426 ContentGenerationKind::OpenAiResponses,
427 );
428
429 assert_eq!(
430 resolve(source, target).unwrap(),
431 TransformPair::OpenAiEditImageToOpenAiResponses
432 );
433 assert_eq!(
434 resolve(target, source).unwrap(),
435 TransformPair::OpenAiEditImageToOpenAiResponses
436 );
437 }
438}