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