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 if input.line.is_some() {
125 report.omitted("model.line", "Gemini has no model line field");
126 }
127 if input.capabilities.server_tools.is_some() {
128 report.omitted(
129 "model.capabilities.server_tools",
130 "Gemini has no server-tool capability fields",
131 );
132 }
133 report.omitted("model.created_at", "Gemini has no model creation timestamp");
134 report.omitted("model.type", "Gemini has no model type field");
135 let value = gemini_models::Model {
136 name,
137 base_model_id,
138 version,
139 display_name: Some(input.display_name),
140 description: None,
141 input_token_limit: Some(input.max_input_tokens),
142 output_token_limit: Some(input.max_tokens),
143 supported_generation_methods: None,
144 thinking: Some(input.capabilities.thinking.supported),
145 temperature: None,
146 max_temperature: None,
147 top_p: None,
148 top_k: None,
149 rest: Default::default(),
150 };
151 Ok(common::converted(value, report))
152}
153
154pub fn openai_to_claude(
155 input: openai_models::Model,
156 supplement: &ClaudeModelSupplement,
157) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
158 let identity = common::openai_identity(&input)?.to_owned();
159 let created_at = match input.created {
160 Some(created) => {
161 if let Some(supplied) = &supplement.created_at
162 && common::parse_iso(supplied)?.unix_timestamp_nanos()
163 != created as i128 * 1_000_000_000
164 {
165 return Err(TransformError::new(
166 crate::transform::TransformErrorKind::Conflict,
167 "model.created_at",
168 "source and supplied metadata disagree",
169 ));
170 }
171 common::epoch_to_iso(created)?
172 }
173 None => {
174 let supplied = common::require(supplement.created_at.clone(), "model.created_at")?;
175 common::parse_iso(&supplied)?;
176 supplied
177 }
178 };
179 let display_name = common::choose(
180 input.display_name.clone(),
181 supplement.display_name.clone(),
182 "model.display_name",
183 )?;
184 let source_input = input
185 .context_window
186 .or(input.max_context_window)
187 .map(i64::try_from)
188 .transpose()
189 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?;
190 let source_output = input
191 .max_output_tokens
192 .map(i64::try_from)
193 .transpose()
194 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?;
195 let max_input_tokens = common::choose(
196 source_input,
197 supplement.max_input_tokens,
198 "model.max_input_tokens",
199 )?;
200 let max_tokens = common::choose(source_output, supplement.max_tokens, "model.max_tokens")?;
201 let mut report = Report::default();
202 common::report_openai_extensions(&input, &mut report);
203 if input.description.is_some() {
204 report.omitted(
205 "model.description",
206 "Claude model objects have no description",
207 );
208 }
209 if input.generation_methods.is_some() {
210 report.omitted(
211 "model.generation_methods",
212 "Claude model objects have no generation-method list",
213 );
214 }
215 if let Some(modalities) = &input.input_modalities
216 && modalities
217 .iter()
218 .any(|modality| !matches!(modality.as_str(), "text" | "image"))
219 {
220 report.omitted(
221 "model.input_modalities",
222 "Claude capabilities only represent image input from this modality list",
223 );
224 }
225 if let Some(levels) = &input.supported_reasoning_levels
226 && levels.iter().any(|level| {
227 !level.description.is_empty()
228 || !matches!(
229 level.effort,
230 openai_models::ModelReasoningEffort::None
231 | openai_models::ModelReasoningEffort::Low
232 | openai_models::ModelReasoningEffort::Medium
233 | openai_models::ModelReasoningEffort::High
234 | openai_models::ModelReasoningEffort::Xhigh
235 | openai_models::ModelReasoningEffort::Max
236 )
237 })
238 {
239 report.omitted(
240 "model.supported_reasoning_levels",
241 "effort descriptions and some choices have no Claude capability field",
242 );
243 }
244 let supplement = common::reconcile_openai_capabilities(&input, supplement.clone())?;
245
246 common::report_openai_loss(&mut report);
247 report.omitted(
248 "model.object",
249 "Claude model objects have a distinct type field",
250 );
251 Ok(common::converted(
252 common::claude_model(
253 identity,
254 created_at,
255 display_name,
256 max_input_tokens,
257 max_tokens,
258 supplement,
259 ),
260 report,
261 ))
262}
263
264pub fn openai_to_gemini(
265 input: openai_models::Model,
266 supplement: &GeminiModelSupplement,
267) -> Result<Converted<gemini_models::Model>, TransformError> {
268 let identity = common::openai_identity(&input)?.to_owned();
269 let mut report = Report::default();
270 common::report_openai_extensions(&input, &mut report);
271 if input.input_modalities.is_some() {
272 report.omitted(
273 "model.input_modalities",
274 "Gemini model objects have no input-modality list",
275 );
276 }
277 if input.supported_reasoning_levels.is_some() {
278 report.omitted(
279 "model.supported_reasoning_levels",
280 "Gemini model objects only advertise a thinking boolean",
281 );
282 }
283 let thinking = common::openai_thinking(&input);
284
285 report.omitted("model.owned_by", "Gemini has no owner field");
286 report.omitted("model.object", "Gemini has no OpenAI object discriminator");
287 report.omitted("model.created", "Gemini has no model creation timestamp");
288 let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
289 let version = common::non_empty(&supplement.version, "model.version")?;
290 let value = gemini_models::Model {
291 name: common::gemini_name(&identity)?,
292 base_model_id,
293 version,
294 display_name: input.display_name,
295 description: input.description,
296 input_token_limit: input
297 .context_window
298 .or(input.max_context_window)
299 .map(i64::try_from)
300 .transpose()
301 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?,
302 output_token_limit: input
303 .max_output_tokens
304 .map(i64::try_from)
305 .transpose()
306 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?,
307 supported_generation_methods: input.generation_methods,
308 thinking,
309 temperature: None,
310 max_temperature: None,
311 top_p: None,
312 top_k: None,
313 rest: Default::default(),
314 };
315 Ok(common::converted(value, report))
316}
317
318pub fn gemini_to_openai(
319 input: gemini_models::Model,
320 supplement: &OpenAiModelSupplement,
321) -> Result<Converted<openai_models::Model>, TransformError> {
322 let created = common::require(supplement.created, "model.created")?;
323 if supplement.owned_by.is_empty() {
324 return Err(TransformError::missing_metadata("model.owned_by"));
325 }
326 let mut report = Report::default();
327 common::report_gemini_loss(&mut report);
328 let mut value = common::openai_model(
329 common::bare_gemini_id(&input.name)?,
330 created,
331 supplement.owned_by.clone(),
332 );
333 value.display_name = input.display_name;
334 value.description = input.description;
335 value.context_window = input
336 .input_token_limit
337 .map(u64::try_from)
338 .transpose()
339 .map_err(|e| TransformError::shape("model.input_token_limit", e.to_string()))?;
340 value.max_output_tokens = input
341 .output_token_limit
342 .map(u64::try_from)
343 .transpose()
344 .map_err(|e| TransformError::shape("model.output_token_limit", e.to_string()))?;
345 value.thinking_supported = input.thinking;
346 value.generation_methods = input.supported_generation_methods;
347 Ok(common::converted(value, report))
348}
349
350pub fn gemini_to_claude(
351 input: gemini_models::Model,
352 supplement: &ClaudeModelSupplement,
353) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
354 let created_at = common::require(supplement.created_at.clone(), "model.created_at")?;
355 common::parse_iso(&created_at)?;
356 let display_name = common::choose(
357 input.display_name.clone(),
358 supplement.display_name.clone(),
359 "model.display_name",
360 )?;
361 let max_input_tokens = common::choose(
362 input.input_token_limit,
363 supplement.max_input_tokens,
364 "model.max_input_tokens",
365 )?;
366 let max_tokens = common::choose(
367 input.output_token_limit,
368 supplement.max_tokens,
369 "model.max_tokens",
370 )?;
371 let mut report = Report::default();
372 report.omitted(
373 "model.description",
374 "Claude model objects have no description",
375 );
376 report.omitted(
377 "model.supported_generation_methods",
378 "Claude model objects have no generation-method list",
379 );
380 report.omitted(
381 "model.base_model_id",
382 "Claude model objects have no base-model id",
383 );
384 report.omitted(
385 "model.version",
386 "Claude model objects have no model version",
387 );
388 report.omitted(
389 "model.temperature",
390 "Claude model objects have no temperature field",
391 );
392 report.omitted(
393 "model.max_temperature",
394 "Claude model objects have no max_temperature field",
395 );
396 report.omitted("model.top_p", "Claude model objects have no top_p field");
397 report.omitted("model.top_k", "Claude model objects have no top_k field");
398 if input.thinking.is_some() {
399 report.changed(
400 "model.thinking",
401 "Claude thinking support was reconciled with source metadata",
402 );
403 }
404 let mut supplement = supplement.clone();
405 supplement.capabilities.thinking.supported = common::choose(
406 input.thinking,
407 Some(supplement.capabilities.thinking.supported),
408 "model.thinking",
409 )?;
410 Ok(common::converted(
411 common::claude_model(
412 common::bare_gemini_id(&input.name)?,
413 created_at,
414 display_name,
415 max_input_tokens,
416 max_tokens,
417 supplement,
418 ),
419 report,
420 ))
421}