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 Ok(common::converted(value, report))
51}
52
53pub fn claude_to_gemini(
54 input: claude_models::ModelInfo,
55 supplement: &GeminiModelSupplement,
56) -> Result<Converted<gemini_models::Model>, TransformError> {
57 let name = common::gemini_name(&input.id)?;
58 let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
59 let version = common::non_empty(&supplement.version, "model.version")?;
60 let mut report = Report::default();
61 report.omitted(
62 "model.capabilities.batch",
63 "Gemini has no Claude batch capability",
64 );
65 report.omitted(
66 "model.capabilities.citations",
67 "Gemini has no Claude citations capability",
68 );
69 report.omitted(
70 "model.capabilities.code_execution",
71 "Gemini has no Claude code-execution capability",
72 );
73 report.omitted(
74 "model.capabilities.context_management",
75 "Gemini has no Claude context-management capability",
76 );
77 report.omitted(
78 "model.capabilities.effort",
79 "Gemini has no Claude effort capability",
80 );
81 report.omitted(
82 "model.capabilities.image_input",
83 "Gemini has no Claude image-input capability",
84 );
85 report.omitted(
86 "model.capabilities.pdf_input",
87 "Gemini has no Claude PDF-input capability",
88 );
89 report.omitted(
90 "model.capabilities.structured_outputs",
91 "Gemini has no Claude structured-output capability",
92 );
93 report.omitted(
94 "model.capabilities.thinking.types",
95 "Gemini represents thinking support but not Claude thinking modes",
96 );
97 report.omitted(
98 "model.allowed_fallback_models",
99 "Gemini has no fallback-model list",
100 );
101 report.omitted("model.created_at", "Gemini has no model creation timestamp");
102 report.omitted("model.type", "Gemini has no model type field");
103 let value = gemini_models::Model {
104 name,
105 base_model_id,
106 version,
107 display_name: Some(input.display_name),
108 description: None,
109 input_token_limit: Some(input.max_input_tokens),
110 output_token_limit: Some(input.max_tokens),
111 supported_generation_methods: None,
112 thinking: Some(input.capabilities.thinking.supported),
113 temperature: None,
114 max_temperature: None,
115 top_p: None,
116 top_k: None,
117 rest: Default::default(),
118 };
119 Ok(common::converted(value, report))
120}
121
122pub fn openai_to_claude(
123 input: openai_models::Model,
124 supplement: &ClaudeModelSupplement,
125) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
126 let created_at = match input.created {
127 Some(created) => {
128 if let Some(supplied) = &supplement.created_at
129 && common::parse_iso(supplied)?.unix_timestamp_nanos()
130 != created as i128 * 1_000_000_000
131 {
132 return Err(TransformError::new(
133 crate::transform::TransformErrorKind::Conflict,
134 "model.created_at",
135 "source and supplied metadata disagree",
136 ));
137 }
138 common::epoch_to_iso(created)?
139 }
140 None => {
141 let supplied = common::require(supplement.created_at.clone(), "model.created_at")?;
142 common::parse_iso(&supplied)?;
143 supplied
144 }
145 };
146 let display_name = common::choose(
147 input.display_name.clone(),
148 supplement.display_name.clone(),
149 "model.display_name",
150 )?;
151 let source_input = input
152 .context_window
153 .map(i64::try_from)
154 .transpose()
155 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?;
156 let source_output = input
157 .max_output_tokens
158 .map(i64::try_from)
159 .transpose()
160 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?;
161 let max_input_tokens = common::choose(
162 source_input,
163 supplement.max_input_tokens,
164 "model.max_input_tokens",
165 )?;
166 let max_tokens = common::choose(source_output, supplement.max_tokens, "model.max_tokens")?;
167 let mut report = Report::default();
168 common::report_openai_loss(&mut report);
169 report.omitted(
170 "model.object",
171 "Claude model objects have a distinct type field",
172 );
173 Ok(common::converted(
174 common::claude_model(
175 common::openai_model_id(&input.id)?,
176 created_at,
177 display_name,
178 max_input_tokens,
179 max_tokens,
180 supplement.clone(),
181 ),
182 report,
183 ))
184}
185
186pub fn openai_to_gemini(
187 input: openai_models::Model,
188 supplement: &GeminiModelSupplement,
189) -> Result<Converted<gemini_models::Model>, TransformError> {
190 let mut report = Report::default();
191 report.omitted("model.owned_by", "Gemini has no owner field");
192 report.omitted("model.object", "Gemini has no OpenAI object discriminator");
193 report.omitted("model.created", "Gemini has no model creation timestamp");
194 let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
195 let version = common::non_empty(&supplement.version, "model.version")?;
196 let value = gemini_models::Model {
197 name: common::gemini_name(&input.id)?,
198 base_model_id,
199 version,
200 display_name: input.display_name,
201 description: input.description,
202 input_token_limit: input
203 .context_window
204 .map(i64::try_from)
205 .transpose()
206 .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?,
207 output_token_limit: input
208 .max_output_tokens
209 .map(i64::try_from)
210 .transpose()
211 .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?,
212 supported_generation_methods: input.generation_methods,
213 thinking: input.thinking_supported,
214 temperature: None,
215 max_temperature: None,
216 top_p: None,
217 top_k: None,
218 rest: Default::default(),
219 };
220 Ok(common::converted(value, report))
221}
222
223pub fn gemini_to_openai(
224 input: gemini_models::Model,
225 supplement: &OpenAiModelSupplement,
226) -> Result<Converted<openai_models::Model>, TransformError> {
227 let created = common::require(supplement.created, "model.created")?;
228 if supplement.owned_by.is_empty() {
229 return Err(TransformError::missing_metadata("model.owned_by"));
230 }
231 let mut report = Report::default();
232 common::report_gemini_loss(&mut report);
233 let mut value = common::openai_model(
234 common::bare_gemini_id(&input.name)?,
235 created,
236 supplement.owned_by.clone(),
237 );
238 value.display_name = input.display_name;
239 value.description = input.description;
240 value.context_window = input
241 .input_token_limit
242 .map(u64::try_from)
243 .transpose()
244 .map_err(|e| TransformError::shape("model.input_token_limit", e.to_string()))?;
245 value.max_output_tokens = input
246 .output_token_limit
247 .map(u64::try_from)
248 .transpose()
249 .map_err(|e| TransformError::shape("model.output_token_limit", e.to_string()))?;
250 value.thinking_supported = input.thinking;
251 value.generation_methods = input.supported_generation_methods;
252 Ok(common::converted(value, report))
253}
254
255pub fn gemini_to_claude(
256 input: gemini_models::Model,
257 supplement: &ClaudeModelSupplement,
258) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
259 let created_at = common::require(supplement.created_at.clone(), "model.created_at")?;
260 common::parse_iso(&created_at)?;
261 let display_name = common::choose(
262 input.display_name.clone(),
263 supplement.display_name.clone(),
264 "model.display_name",
265 )?;
266 let max_input_tokens = common::choose(
267 input.input_token_limit,
268 supplement.max_input_tokens,
269 "model.max_input_tokens",
270 )?;
271 let max_tokens = common::choose(
272 input.output_token_limit,
273 supplement.max_tokens,
274 "model.max_tokens",
275 )?;
276 let mut report = Report::default();
277 report.omitted(
278 "model.description",
279 "Claude model objects have no description",
280 );
281 report.omitted(
282 "model.supported_generation_methods",
283 "Claude model objects have no generation-method list",
284 );
285 report.omitted(
286 "model.base_model_id",
287 "Claude model objects have no base-model id",
288 );
289 report.omitted(
290 "model.version",
291 "Claude model objects have no model version",
292 );
293 report.omitted(
294 "model.temperature",
295 "Claude model objects have no temperature field",
296 );
297 report.omitted(
298 "model.max_temperature",
299 "Claude model objects have no max_temperature field",
300 );
301 report.omitted("model.top_p", "Claude model objects have no top_p field");
302 report.omitted("model.top_k", "Claude model objects have no top_k field");
303 if input.thinking.is_some() {
304 report.changed(
305 "model.thinking",
306 "Claude thinking support was reconciled with source metadata",
307 );
308 }
309 let mut supplement = supplement.clone();
310 supplement.capabilities.thinking.supported = common::choose(
311 input.thinking,
312 Some(supplement.capabilities.thinking.supported),
313 "model.thinking",
314 )?;
315 Ok(common::converted(
316 common::claude_model(
317 common::bare_gemini_id(&input.name)?,
318 created_at,
319 display_name,
320 max_input_tokens,
321 max_tokens,
322 supplement,
323 ),
324 report,
325 ))
326}