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