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