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 _ => {
133 unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
134 }
135 }
136}
137
138fn resolve_content_generation(
139 source: OperationKey,
140 target: OperationKey,
141) -> Result<TransformPair, TransformError> {
142 let OperationKind::ContentGeneration(source_kind) = source.kind() else {
143 return Err(TransformError::unsupported_pair(source, target));
144 };
145 let OperationKind::ContentGeneration(target_kind) = target.kind() else {
146 return Err(TransformError::unsupported_pair(source, target));
147 };
148
149 use ContentGenerationKind as Kind;
150 use TransformPair as Pair;
151
152 match (source_kind, target_kind) {
153 (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
154 Ok(Pair::OpenAiResponsesToOpenAiChat)
155 }
156 (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
157 Ok(Pair::OpenAiChatToOpenAiResponses)
158 }
159 (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
160 Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
161 }
162 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
163 Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
164 }
165 (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
166 Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
167 }
168 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
169 Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
170 }
171 (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
172 Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
173 }
174 (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
175 Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
176 }
177 (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
178 Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
179 }
180 (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
181 Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
182 }
183 (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
184 (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
185 (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
186 Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
187 }
188 (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
189 Ok(Pair::GeminiGenerateContentToOpenAiResponses)
190 }
191 (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
192 (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
193 (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
194 Ok(Pair::OpenAiChatToGeminiGenerateContent)
195 }
196 (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
197 Ok(Pair::GeminiGenerateContentToOpenAiChat)
198 }
199 (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
200 Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
201 }
202 (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
203 Ok(Pair::GeminiGenerateContentToClaudeMessages)
204 }
205 _ => Err(TransformError::unsupported_pair(source, target)),
206 }
207}
208
209fn resolve_provider_pair(
210 source: OperationKey,
211 target: OperationKey,
212 pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
213) -> Result<TransformPair, TransformError> {
214 let OperationKind::Provider(source_provider) = source.kind() else {
215 return Err(TransformError::unsupported_pair(source, target));
216 };
217 let OperationKind::Provider(target_provider) = target.kind() else {
218 return Err(TransformError::unsupported_pair(source, target));
219 };
220
221 pair_fn(source_provider, target_provider)
222 .ok_or_else(|| TransformError::unsupported_pair(source, target))
223}
224
225fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
226 provider_matrix(
227 source,
228 target,
229 ProviderMatrix {
230 openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
231 claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
232 openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
233 gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
234 claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
235 gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
236 },
237 )
238}
239
240fn imagen_pair(source: Provider, target: Provider) -> Option<TransformPair> {
242 match (source, target) {
243 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiImagen),
244 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiImagenToOpenAi),
245 _ => None,
246 }
247}
248
249fn videos_pair(source: Provider, target: Provider) -> Option<TransformPair> {
251 match (source, target) {
252 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiVideos),
253 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiVideos),
254 _ => None,
255 }
256}
257
258fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
259 provider_matrix(
260 source,
261 target,
262 ProviderMatrix {
263 openai_to_claude: TransformPair::OpenAiToClaudeModels,
264 claude_to_openai: TransformPair::ClaudeToOpenAiModels,
265 openai_to_gemini: TransformPair::OpenAiToGeminiModels,
266 gemini_to_openai: TransformPair::GeminiToOpenAiModels,
267 claude_to_gemini: TransformPair::ClaudeToGeminiModels,
268 gemini_to_claude: TransformPair::GeminiToClaudeModels,
269 },
270 )
271}
272
273fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
274 match (source, target) {
275 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
276 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
277 _ => None,
278 }
279}
280
281fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
285 use ContentGenerationKind as Kind;
286 use OperationKind as OK;
287
288 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
293 (source.kind(), target.kind())
294 && target.operation().is_content_generation()
295 {
296 return match source.operation() {
297 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
298 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
299 _ => None,
300 };
301 }
302 if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
303 (source.kind(), target.kind())
304 && source.operation().is_content_generation()
305 {
306 return match target.operation() {
307 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
308 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
309 _ => None,
310 };
311 }
312
313 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
315 (source.kind(), target.kind())
316 && target.operation().is_content_generation()
317 {
318 return match source.operation() {
319 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
320 Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
321 _ => None,
322 };
323 }
324
325 if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
327 (source.kind(), target.kind())
328 && source.operation().is_content_generation()
329 {
330 return match target.operation() {
331 Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
332 Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
333 _ => None,
334 };
335 }
336
337 None
338}
339
340fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
345 use ContentGenerationKind as Kind;
346 use OperationKind as OK;
347
348 if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
350 (source.kind(), target.operation(), target.kind())
351 && source.operation().is_content_generation()
352 {
353 return match kind {
354 Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
355 Kind::OpenAiResponsesWebSocket => None,
356 Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
357 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
358 Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
359 _ => unreachable!(
360 "new non-exhaustive protocol variant requires a lockstep transform update"
361 ),
362 };
363 }
364
365 if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
367 (source.kind(), source.operation())
368 && let OK::ContentGeneration(kind) = target.kind()
369 && target.operation().is_content_generation()
370 {
371 return match kind {
372 Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
373 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
374 Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
375 Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
376 Kind::OpenAiResponsesWebSocket => None,
377 _ => unreachable!(
378 "new non-exhaustive protocol variant requires a lockstep transform update"
379 ),
380 };
381 }
382
383 None
384}
385
386#[derive(Debug, Clone, Copy)]
387struct ProviderMatrix {
388 openai_to_claude: TransformPair,
389 claude_to_openai: TransformPair,
390 openai_to_gemini: TransformPair,
391 gemini_to_openai: TransformPair,
392 claude_to_gemini: TransformPair,
393 gemini_to_claude: TransformPair,
394}
395
396fn provider_matrix(
397 source: Provider,
398 target: Provider,
399 matrix: ProviderMatrix,
400) -> Option<TransformPair> {
401 match (source, target) {
402 (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
403 (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
404 (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
405 (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
406 (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
407 (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
408 _ => None,
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn edit_image_resolves_to_responses_image_tool_pair() {
418 let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
419 let target = OperationKey::content_generation(
420 Operation::StreamGenerateContent,
421 ContentGenerationKind::OpenAiResponses,
422 );
423
424 assert_eq!(
425 resolve(source, target).unwrap(),
426 TransformPair::OpenAiEditImageToOpenAiResponses
427 );
428 assert_eq!(
429 resolve(target, source).unwrap(),
430 TransformPair::OpenAiEditImageToOpenAiResponses
431 );
432 }
433}