1use super::{
2 common,
3 supplement::{ClaudeModelSupplement, Converted, GeminiModelSupplement, OpenAiModelSupplement},
4};
5use crate::transform::{Report, TransformError};
6use crate::wire::{
7 claude::models as claude_models, gemini::models as gemini_models,
8 openai::models as openai_models,
9};
10
11pub fn claude_to_openai(
12 input: claude_models::ModelInfo,
13 supplement: &OpenAiModelSupplement,
14) -> Result<Converted<openai_models::Model>, TransformError> {
15 let (source_created, fractional) = common::iso_to_epoch_with_loss(&input.created_at)?;
16 if fractional && supplement.created.is_some() {
17 return Err(TransformError::new(
18 crate::transform::TransformErrorKind::Conflict,
19 "model.created",
20 "fractional source timestamp cannot equal integer supplied metadata",
21 ));
22 }
23 let created = common::choose(Some(source_created), supplement.created, "model.created")?;
24 if supplement.owned_by.is_empty() {
25 return Err(TransformError::missing_metadata("model.owned_by"));
26 }
27 let mut report = Report::default();
28 if fractional {
29 report.changed(
30 "model.created_at",
31 "OpenAI model timestamps have whole-second precision",
32 );
33 }
34 common::report_claude_loss(&mut report);
35 common::report_claude_lifecycle(&input, &mut report);
36 let mut value = common::openai_model(
37 common::openai_model_id(&input.id)?,
38 created,
39 supplement.owned_by.clone(),
40 );
41 value.display_name = Some(input.display_name);
42 value.context_window = input
43 .max_input_tokens
44 .map(u64::try_from)
45 .transpose()
46 .map_err(|_| TransformError::shape("model.max_input_tokens", "negative limit"))?;
47 value.max_output_tokens = input
48 .max_tokens
49 .map(u64::try_from)
50 .transpose()
51 .map_err(|_| TransformError::shape("model.max_tokens", "negative limit"))?;
52 if let Some(capabilities) = input.capabilities {
53 value.thinking_supported = Some(capabilities.thinking.supported);
54 let mut modalities = vec!["text".to_owned()];
55 if capabilities.image_input.supported {
56 modalities.push("image".to_owned());
57 }
58 value.input_modalities = Some(modalities);
59 let effort = &capabilities.effort;
60 use openai_models::ModelReasoningEffort as Effort;
61 value.supported_reasoning_levels = Some(
62 [
63 (Effort::Low, effort.low.supported),
64 (Effort::Medium, effort.medium.supported),
65 (Effort::High, effort.high.supported),
66 (
67 Effort::Xhigh,
68 effort.xhigh.as_ref().is_some_and(|value| value.supported),
69 ),
70 (Effort::Max, effort.max.supported),
71 ]
72 .into_iter()
73 .filter(|(_, supported)| effort.supported && *supported)
74 .map(|(effort, _)| openai_models::ModelReasoningLevel {
75 effort,
76 description: String::new(),
77 })
78 .collect(),
79 );
80 }
81 Ok(common::converted(value, report))
82}
83
84pub fn claude_to_gemini(
85 input: claude_models::ModelInfo,
86 supplement: &GeminiModelSupplement,
87) -> Result<Converted<gemini_models::Model>, TransformError> {
88 let name = common::gemini_name(&input.id)?;
89 let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
90 let version = common::non_empty(&supplement.version, "model.version")?;
91 let mut report = Report::default();
92 report.omitted(
93 "model.capabilities.batch",
94 "Gemini has no Claude batch capability",
95 );
96 report.omitted(
97 "model.capabilities.citations",
98 "Gemini has no Claude citations capability",
99 );
100 report.omitted(
101 "model.capabilities.code_execution",
102 "Gemini has no Claude code-execution capability",
103 );
104 report.omitted(
105 "model.capabilities.context_management",
106 "Gemini has no Claude context-management capability",
107 );
108 report.omitted(
109 "model.capabilities.effort",
110 "Gemini has no Claude effort capability",
111 );
112 report.omitted(
113 "model.capabilities.image_input",
114 "Gemini has no Claude image-input capability",
115 );
116 report.omitted(
117 "model.capabilities.pdf_input",
118 "Gemini has no Claude PDF-input capability",
119 );
120 report.omitted(
121 "model.capabilities.structured_outputs",
122 "Gemini has no Claude structured-output capability",
123 );
124 report.omitted(
125 "model.capabilities.thinking.types",
126 "Gemini represents thinking support but not Claude thinking modes",
127 );
128 report.omitted(
129 "model.allowed_fallback_models",
130 "Gemini has no fallback-model list",
131 );
132 if input.line.is_some() {
133 report.omitted("model.line", "Gemini has no model line field");
134 }
135 if input
136 .capabilities
137 .as_ref()
138 .is_some_and(|caps| caps.server_tools.is_some())
139 {
140 report.omitted(
141 "model.capabilities.server_tools",
142 "Gemini has no server-tool capability fields",
143 );
144 }
145 if input
146 .capabilities
147 .as_ref()
148 .is_some_and(|caps| caps.compaction.is_some())
149 {
150 report.omitted(
151 "model.capabilities.compaction",
152 "Gemini has no Claude compaction capability",
153 );
154 }
155 common::report_claude_lifecycle(&input, &mut report);
156 report.omitted("model.created_at", "Gemini has no model creation timestamp");
157 report.omitted("model.type", "Gemini has no model type field");
158 let value = gemini_models::Model {
159 name,
160 base_model_id,
161 version,
162 display_name: Some(input.display_name),
163 description: None,
164 input_token_limit: input.max_input_tokens,
165 output_token_limit: input.max_tokens,
166 supported_generation_methods: None,
167 thinking: input.capabilities.map(|caps| caps.thinking.supported),
168 temperature: None,
169 max_temperature: None,
170 top_p: None,
171 top_k: None,
172 rest: Default::default(),
173 };
174 Ok(common::converted(value, report))
175}
176
177pub fn openai_to_claude(
178 input: openai_models::Model,
179 supplement: &ClaudeModelSupplement,
180) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
181 let identity = common::openai_identity(&input)?.to_owned();
182 let created_at = match input.created {
183 Some(created) => {
184 if let Some(supplied) = &supplement.created_at
185 && common::parse_iso(supplied)?.unix_timestamp_nanos()
186 != created as i128 * 1_000_000_000
187 {
188 return Err(TransformError::new(
189 crate::transform::TransformErrorKind::Conflict,
190 "model.created_at",
191 "source and supplied metadata disagree",
192 ));
193 }
194 common::epoch_to_iso(created)?
195 }
196 None => {
197 let supplied = common::require(supplement.created_at.clone(), "model.created_at")?;
198 common::parse_iso(&supplied)?;
199 supplied
200 }
201 };
202 let display_name = common::choose(
203 input.display_name.clone(),
204 supplement.display_name.clone(),
205 "model.display_name",
206 )?;
207 let source_input = input
208 .context_window
209 .or(input.max_context_window)
210 .map(i64::try_from)
211 .transpose()
212 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?;
213 let source_output = input
214 .max_output_tokens
215 .map(i64::try_from)
216 .transpose()
217 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?;
218 let max_input_tokens = common::choose_optional(
219 source_input,
220 supplement.max_input_tokens,
221 "model.max_input_tokens",
222 )?;
223 let max_tokens =
224 common::choose_optional(source_output, supplement.max_tokens, "model.max_tokens")?;
225 let mut report = Report::default();
226 common::report_openai_extensions(&input, &mut report);
227 if input.description.is_some() {
228 report.omitted(
229 "model.description",
230 "Claude model objects have no description",
231 );
232 }
233 if input.generation_methods.is_some() {
234 report.omitted(
235 "model.generation_methods",
236 "Claude model objects have no generation-method list",
237 );
238 }
239 if let Some(modalities) = &input.input_modalities
240 && modalities
241 .iter()
242 .any(|modality| !matches!(modality.as_str(), "text" | "image"))
243 {
244 report.omitted(
245 "model.input_modalities",
246 "Claude capabilities only represent image input from this modality list",
247 );
248 }
249 if let Some(levels) = &input.supported_reasoning_levels
250 && levels.iter().any(|level| {
251 !level.description.is_empty()
252 || !matches!(
253 level.effort,
254 openai_models::ModelReasoningEffort::None
255 | openai_models::ModelReasoningEffort::Low
256 | openai_models::ModelReasoningEffort::Medium
257 | openai_models::ModelReasoningEffort::High
258 | openai_models::ModelReasoningEffort::Xhigh
259 | openai_models::ModelReasoningEffort::Max
260 )
261 })
262 {
263 report.omitted(
264 "model.supported_reasoning_levels",
265 "effort descriptions and some choices have no Claude capability field",
266 );
267 }
268 let supplement = common::reconcile_openai_capabilities(&input, supplement.clone())?;
269
270 common::report_openai_loss(&mut report);
271 report.omitted(
272 "model.object",
273 "Claude model objects have a distinct type field",
274 );
275 Ok(common::converted(
276 common::claude_model(
277 identity,
278 created_at,
279 display_name,
280 max_input_tokens,
281 max_tokens,
282 supplement,
283 ),
284 report,
285 ))
286}
287
288pub fn openai_to_gemini(
289 input: openai_models::Model,
290 supplement: &GeminiModelSupplement,
291) -> Result<Converted<gemini_models::Model>, TransformError> {
292 let identity = common::openai_identity(&input)?.to_owned();
293 let mut report = Report::default();
294 common::report_openai_extensions(&input, &mut report);
295 if input.input_modalities.is_some() {
296 report.omitted(
297 "model.input_modalities",
298 "Gemini model objects have no input-modality list",
299 );
300 }
301 if input.supported_reasoning_levels.is_some() {
302 report.omitted(
303 "model.supported_reasoning_levels",
304 "Gemini model objects only advertise a thinking boolean",
305 );
306 }
307 let thinking = common::openai_thinking(&input);
308
309 report.omitted("model.owned_by", "Gemini has no owner field");
310 report.omitted("model.object", "Gemini has no OpenAI object discriminator");
311 report.omitted("model.created", "Gemini has no model creation timestamp");
312 let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
313 let version = common::non_empty(&supplement.version, "model.version")?;
314 let value = gemini_models::Model {
315 name: common::gemini_name(&identity)?,
316 base_model_id,
317 version,
318 display_name: input.display_name,
319 description: input.description,
320 input_token_limit: input
321 .context_window
322 .or(input.max_context_window)
323 .map(i64::try_from)
324 .transpose()
325 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?,
326 output_token_limit: input
327 .max_output_tokens
328 .map(i64::try_from)
329 .transpose()
330 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?,
331 supported_generation_methods: input.generation_methods,
332 thinking,
333 temperature: None,
334 max_temperature: None,
335 top_p: None,
336 top_k: None,
337 rest: Default::default(),
338 };
339 Ok(common::converted(value, report))
340}
341
342pub fn gemini_to_openai(
343 input: gemini_models::Model,
344 supplement: &OpenAiModelSupplement,
345) -> Result<Converted<openai_models::Model>, TransformError> {
346 let created = common::require(supplement.created, "model.created")?;
347 if supplement.owned_by.is_empty() {
348 return Err(TransformError::missing_metadata("model.owned_by"));
349 }
350 let mut report = Report::default();
351 common::report_gemini_loss(&mut report);
352 let mut value = common::openai_model(
353 common::bare_gemini_id(&input.name)?,
354 created,
355 supplement.owned_by.clone(),
356 );
357 value.display_name = input.display_name;
358 value.description = input.description;
359 value.context_window = input
360 .input_token_limit
361 .map(u64::try_from)
362 .transpose()
363 .map_err(|e| TransformError::shape("model.input_token_limit", e.to_string()))?;
364 value.max_output_tokens = input
365 .output_token_limit
366 .map(u64::try_from)
367 .transpose()
368 .map_err(|e| TransformError::shape("model.output_token_limit", e.to_string()))?;
369 value.thinking_supported = input.thinking;
370 value.generation_methods = input.supported_generation_methods;
371 Ok(common::converted(value, report))
372}
373
374pub fn gemini_to_claude(
375 input: gemini_models::Model,
376 supplement: &ClaudeModelSupplement,
377) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
378 let created_at = common::require(supplement.created_at.clone(), "model.created_at")?;
379 common::parse_iso(&created_at)?;
380 let display_name = common::choose(
381 input.display_name.clone(),
382 supplement.display_name.clone(),
383 "model.display_name",
384 )?;
385 let max_input_tokens = common::choose_optional(
386 input.input_token_limit,
387 supplement.max_input_tokens,
388 "model.max_input_tokens",
389 )?;
390 let max_tokens = common::choose_optional(
391 input.output_token_limit,
392 supplement.max_tokens,
393 "model.max_tokens",
394 )?;
395 let mut report = Report::default();
396 report.omitted(
397 "model.description",
398 "Claude model objects have no description",
399 );
400 report.omitted(
401 "model.supported_generation_methods",
402 "Claude model objects have no generation-method list",
403 );
404 report.omitted(
405 "model.base_model_id",
406 "Claude model objects have no base-model id",
407 );
408 report.omitted(
409 "model.version",
410 "Claude model objects have no model version",
411 );
412 report.omitted(
413 "model.temperature",
414 "Claude model objects have no temperature field",
415 );
416 report.omitted(
417 "model.max_temperature",
418 "Claude model objects have no max_temperature field",
419 );
420 report.omitted("model.top_p", "Claude model objects have no top_p field");
421 report.omitted("model.top_k", "Claude model objects have no top_k field");
422 if input.thinking.is_some() {
423 report.changed(
424 "model.thinking",
425 "Claude thinking support was reconciled with source metadata",
426 );
427 }
428 let mut supplement = supplement.clone();
429 supplement.capabilities.thinking.supported = common::choose(
430 input.thinking,
431 Some(supplement.capabilities.thinking.supported),
432 "model.thinking",
433 )?;
434 Ok(common::converted(
435 common::claude_model(
436 common::bare_gemini_id(&input.name)?,
437 created_at,
438 display_name,
439 max_input_tokens,
440 max_tokens,
441 supplement,
442 ),
443 report,
444 ))
445}