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