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::Rerank
108 | Operation::CreateConversation
109 | Operation::ConnectRealtime => Err(TransformError::unsupported_pair(source, target)),
110 _ => {
111 unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
112 }
113 }
114}
115
116fn resolve_content_generation(
117 source: OperationKey,
118 target: OperationKey,
119) -> Result<TransformPair, TransformError> {
120 let OperationKind::ContentGeneration(source_kind) = source.kind() else {
121 return Err(TransformError::unsupported_pair(source, target));
122 };
123 let OperationKind::ContentGeneration(target_kind) = target.kind() else {
124 return Err(TransformError::unsupported_pair(source, target));
125 };
126
127 use ContentGenerationKind as Kind;
128 use TransformPair as Pair;
129
130 match (source_kind, target_kind) {
131 (Kind::OpenAiResponses, Kind::OpenAiChatCompletions) => {
132 Ok(Pair::OpenAiResponsesToOpenAiChat)
133 }
134 (Kind::OpenAiChatCompletions, Kind::OpenAiResponses) => {
135 Ok(Pair::OpenAiChatToOpenAiResponses)
136 }
137 (Kind::OpenAiResponses, Kind::OpenAiResponsesWebSocket) => {
138 Ok(Pair::OpenAiResponsesToOpenAiResponsesWebSocket)
139 }
140 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiResponses) => {
141 Ok(Pair::OpenAiResponsesWebSocketToOpenAiResponses)
142 }
143 (Kind::OpenAiChatCompletions, Kind::OpenAiResponsesWebSocket) => {
144 Ok(Pair::OpenAiChatToOpenAiResponsesWebSocket)
145 }
146 (Kind::OpenAiResponsesWebSocket, Kind::OpenAiChatCompletions) => {
147 Ok(Pair::OpenAiResponsesWebSocketToOpenAiChat)
148 }
149 (Kind::ClaudeMessages, Kind::OpenAiResponsesWebSocket) => {
150 Ok(Pair::ClaudeMessagesToOpenAiResponsesWebSocket)
151 }
152 (Kind::OpenAiResponsesWebSocket, Kind::ClaudeMessages) => {
153 Ok(Pair::OpenAiResponsesWebSocketToClaudeMessages)
154 }
155 (Kind::GeminiGenerateContent, Kind::OpenAiResponsesWebSocket) => {
156 Ok(Pair::GeminiGenerateContentToOpenAiResponsesWebSocket)
157 }
158 (Kind::OpenAiResponsesWebSocket, Kind::GeminiGenerateContent) => {
159 Ok(Pair::OpenAiResponsesWebSocketToGeminiGenerateContent)
160 }
161 (Kind::OpenAiResponses, Kind::ClaudeMessages) => Ok(Pair::OpenAiResponsesToClaudeMessages),
162 (Kind::ClaudeMessages, Kind::OpenAiResponses) => Ok(Pair::ClaudeMessagesToOpenAiResponses),
163 (Kind::OpenAiResponses, Kind::GeminiGenerateContent) => {
164 Ok(Pair::OpenAiResponsesToGeminiGenerateContent)
165 }
166 (Kind::GeminiGenerateContent, Kind::OpenAiResponses) => {
167 Ok(Pair::GeminiGenerateContentToOpenAiResponses)
168 }
169 (Kind::OpenAiChatCompletions, Kind::ClaudeMessages) => Ok(Pair::OpenAiChatToClaudeMessages),
170 (Kind::ClaudeMessages, Kind::OpenAiChatCompletions) => Ok(Pair::ClaudeMessagesToOpenAiChat),
171 (Kind::OpenAiChatCompletions, Kind::GeminiGenerateContent) => {
172 Ok(Pair::OpenAiChatToGeminiGenerateContent)
173 }
174 (Kind::GeminiGenerateContent, Kind::OpenAiChatCompletions) => {
175 Ok(Pair::GeminiGenerateContentToOpenAiChat)
176 }
177 (Kind::ClaudeMessages, Kind::GeminiGenerateContent) => {
178 Ok(Pair::ClaudeMessagesToGeminiGenerateContent)
179 }
180 (Kind::GeminiGenerateContent, Kind::ClaudeMessages) => {
181 Ok(Pair::GeminiGenerateContentToClaudeMessages)
182 }
183 _ => Err(TransformError::unsupported_pair(source, target)),
184 }
185}
186
187fn resolve_provider_pair(
188 source: OperationKey,
189 target: OperationKey,
190 pair_fn: fn(Provider, Provider) -> Option<TransformPair>,
191) -> Result<TransformPair, TransformError> {
192 let OperationKind::Provider(source_provider) = source.kind() else {
193 return Err(TransformError::unsupported_pair(source, target));
194 };
195 let OperationKind::Provider(target_provider) = target.kind() else {
196 return Err(TransformError::unsupported_pair(source, target));
197 };
198
199 pair_fn(source_provider, target_provider)
200 .ok_or_else(|| TransformError::unsupported_pair(source, target))
201}
202
203fn count_tokens_pair(source: Provider, target: Provider) -> Option<TransformPair> {
204 provider_matrix(
205 source,
206 target,
207 ProviderMatrix {
208 openai_to_claude: TransformPair::OpenAiToClaudeCountTokens,
209 claude_to_openai: TransformPair::ClaudeToOpenAiCountTokens,
210 openai_to_gemini: TransformPair::OpenAiToGeminiCountTokens,
211 gemini_to_openai: TransformPair::GeminiToOpenAiCountTokens,
212 claude_to_gemini: TransformPair::ClaudeToGeminiCountTokens,
213 gemini_to_claude: TransformPair::GeminiToClaudeCountTokens,
214 },
215 )
216}
217
218fn models_pair(source: Provider, target: Provider) -> Option<TransformPair> {
219 provider_matrix(
220 source,
221 target,
222 ProviderMatrix {
223 openai_to_claude: TransformPair::OpenAiToClaudeModels,
224 claude_to_openai: TransformPair::ClaudeToOpenAiModels,
225 openai_to_gemini: TransformPair::OpenAiToGeminiModels,
226 gemini_to_openai: TransformPair::GeminiToOpenAiModels,
227 claude_to_gemini: TransformPair::ClaudeToGeminiModels,
228 gemini_to_claude: TransformPair::GeminiToClaudeModels,
229 },
230 )
231}
232
233fn embeddings_pair(source: Provider, target: Provider) -> Option<TransformPair> {
234 match (source, target) {
235 (Provider::OpenAi, Provider::Gemini) => Some(TransformPair::OpenAiToGeminiEmbeddings),
236 (Provider::Gemini, Provider::OpenAi) => Some(TransformPair::GeminiToOpenAiEmbeddings),
237 _ => None,
238 }
239}
240
241fn resolve_image_generation(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
245 use ContentGenerationKind as Kind;
246 use OperationKind as OK;
247
248 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::OpenAiResponses)) =
253 (source.kind(), target.kind())
254 && target.operation().is_content_generation()
255 {
256 return match source.operation() {
257 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
258 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
259 _ => None,
260 };
261 }
262 if let (OK::ContentGeneration(Kind::OpenAiResponses), OK::Provider(Provider::OpenAi)) =
263 (source.kind(), target.kind())
264 && source.operation().is_content_generation()
265 {
266 return match target.operation() {
267 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToOpenAiResponses),
268 Operation::EditImage => Some(TransformPair::OpenAiEditImageToOpenAiResponses),
269 _ => None,
270 };
271 }
272
273 if let (OK::Provider(Provider::OpenAi), OK::ContentGeneration(Kind::GeminiGenerateContent)) =
275 (source.kind(), target.kind())
276 && target.operation().is_content_generation()
277 {
278 return match source.operation() {
279 Operation::CreateImage => Some(TransformPair::OpenAiCreateImageToGemini),
280 Operation::EditImage => Some(TransformPair::OpenAiEditImageToGemini),
281 _ => None,
282 };
283 }
284
285 if let (OK::ContentGeneration(Kind::GeminiGenerateContent), OK::Provider(Provider::OpenAi)) =
287 (source.kind(), target.kind())
288 && source.operation().is_content_generation()
289 {
290 return match target.operation() {
291 Operation::CreateImage => Some(TransformPair::GeminiToOpenAiCreateImage),
292 Operation::EditImage => Some(TransformPair::GeminiToOpenAiEditImage),
293 _ => None,
294 };
295 }
296
297 None
298}
299
300fn resolve_compaction(source: OperationKey, target: OperationKey) -> Option<TransformPair> {
305 use ContentGenerationKind as Kind;
306 use OperationKind as OK;
307
308 if let (OK::ContentGeneration(kind), Operation::CompactContent, OK::Provider(Provider::OpenAi)) =
310 (source.kind(), target.operation(), target.kind())
311 && source.operation().is_content_generation()
312 {
313 return match kind {
314 Kind::OpenAiResponses => Some(TransformPair::OpenAiResponsesToOpenAiCompact),
315 Kind::OpenAiResponsesWebSocket => None,
316 Kind::GeminiGenerateContent => Some(TransformPair::GeminiToOpenAiCompact),
317 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiChatToOpenAiCompact),
318 Kind::ClaudeMessages => Some(TransformPair::ClaudeToOpenAiCompact),
319 _ => unreachable!(
320 "new non-exhaustive protocol variant requires a lockstep transform update"
321 ),
322 };
323 }
324
325 if let (OK::Provider(Provider::OpenAi), Operation::CompactContent) =
327 (source.kind(), source.operation())
328 && let OK::ContentGeneration(kind) = target.kind()
329 && target.operation().is_content_generation()
330 {
331 return match kind {
332 Kind::GeminiGenerateContent => Some(TransformPair::OpenAiCompactToGemini),
333 Kind::OpenAiChatCompletions => Some(TransformPair::OpenAiCompactToOpenAiChat),
334 Kind::ClaudeMessages => Some(TransformPair::OpenAiToClaudeCompact),
335 Kind::OpenAiResponses => Some(TransformPair::OpenAiCompactToOpenAiResponses),
336 Kind::OpenAiResponsesWebSocket => None,
337 _ => unreachable!(
338 "new non-exhaustive protocol variant requires a lockstep transform update"
339 ),
340 };
341 }
342
343 None
344}
345
346#[derive(Debug, Clone, Copy)]
347struct ProviderMatrix {
348 openai_to_claude: TransformPair,
349 claude_to_openai: TransformPair,
350 openai_to_gemini: TransformPair,
351 gemini_to_openai: TransformPair,
352 claude_to_gemini: TransformPair,
353 gemini_to_claude: TransformPair,
354}
355
356fn provider_matrix(
357 source: Provider,
358 target: Provider,
359 matrix: ProviderMatrix,
360) -> Option<TransformPair> {
361 match (source, target) {
362 (Provider::OpenAi, Provider::Claude) => Some(matrix.openai_to_claude),
363 (Provider::Claude, Provider::OpenAi) => Some(matrix.claude_to_openai),
364 (Provider::OpenAi, Provider::Gemini) => Some(matrix.openai_to_gemini),
365 (Provider::Gemini, Provider::OpenAi) => Some(matrix.gemini_to_openai),
366 (Provider::Claude, Provider::Gemini) => Some(matrix.claude_to_gemini),
367 (Provider::Gemini, Provider::Claude) => Some(matrix.gemini_to_claude),
368 _ => None,
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn edit_image_resolves_to_responses_image_tool_pair() {
378 let source = OperationKey::provider(Operation::EditImage, Provider::OpenAi);
379 let target = OperationKey::content_generation(
380 Operation::StreamGenerateContent,
381 ContentGenerationKind::OpenAiResponses,
382 );
383
384 assert_eq!(
385 resolve(source, target).unwrap(),
386 TransformPair::OpenAiEditImageToOpenAiResponses
387 );
388 assert_eq!(
389 resolve(target, source).unwrap(),
390 TransformPair::OpenAiEditImageToOpenAiResponses
391 );
392 }
393}