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