1use super::{
2 common, get,
3 supplement::{
4 ClaudeModelSupplement, Converted, GeminiModelSupplement, ListPageFacts,
5 OpenAiModelSupplement, missing_pagination,
6 },
7};
8use crate::transform::{Report, TransformError};
9use crate::wire::DeclaredFields;
10use crate::wire::{
11 claude::models as claude_models, gemini::models as gemini_models,
12 openai::models as openai_models,
13};
14use std::collections::{BTreeMap, BTreeSet};
15
16fn supplement_for<'a, T>(
19 supplements: &'a BTreeMap<String, T>,
20 source_key: &str,
21) -> Result<&'a T, TransformError> {
22 supplements
23 .get(source_key)
24 .ok_or_else(|| TransformError::missing_metadata(format!("model[{source_key}].supplement")))
25}
26
27pub fn normalize_openai_list(
30 input: openai_models::ListModelsResponseBody,
31) -> Result<openai_models::ListModelsResponseBody, TransformError> {
32 let mut input = input.into_declared();
33 for model in &mut input.data {
34 model.id = common::openai_identity(model)?.to_owned();
35 }
36 let mut index: BTreeMap<String, usize> = input
37 .data
38 .iter()
39 .enumerate()
40 .map(|(index, model)| (model.id.clone(), index))
41 .collect();
42 let mut seen = BTreeSet::new();
43 for mut model in input.models.take().unwrap_or_default() {
44 model.id = common::openai_identity(&model)?.to_owned();
45 if !seen.insert(model.id.clone()) {
46 return Err(TransformError::shape("models", "repeated model identity"));
47 }
48 if let Some(&position) = index.get(&model.id) {
49 merge_openai_model(&mut input.data[position], model)?;
50 } else {
51 index.insert(model.id.clone(), input.data.len());
52 input.data.push(model);
53 }
54 }
55 Ok(input)
56}
57
58fn merge_openai_model(
59 target: &mut openai_models::Model,
60 mut source: openai_models::Model,
61) -> Result<(), TransformError> {
62 macro_rules! merge {
63 ($($field:ident),* $(,)?) => {$(
64 target.$field = common::choose_optional(
65 target.$field.take(), source.$field.take(),
66 &format!("model[{}].{}", target.id, stringify!($field)),
67 )?;
68 )*};
69 }
70 merge!(
71 slug,
72 created,
73 object,
74 owned_by,
75 display_name,
76 description,
77 instructions,
78 context_window,
79 max_context_window,
80 max_output_tokens,
81 thinking_supported,
82 input_modalities,
83 output_modalities,
84 supported_parameters,
85 supported_reasoning_levels,
86 default_reasoning_level,
87 service_tiers,
88 default_service_tier,
89 generation_methods,
90 supported_actions,
91 guardian,
92 shell_type,
93 visibility,
94 supported_in_api,
95 priority,
96 additional_speed_tiers,
97 available_access_programs,
98 availability_nux,
99 upgrade,
100 model_messages,
101 include_skills_usage_instructions,
102 include_plugin_usage_instructions,
103 include_apps_usage_instructions,
104 supports_reasoning_summary_parameter,
105 default_reasoning_summary,
106 support_verbosity,
107 default_verbosity,
108 apply_patch_tool_type,
109 web_search_tool_type,
110 truncation_policy,
111 supports_image_detail_original,
112 auto_compact_token_limit,
113 comp_hash,
114 effective_context_window_percent,
115 experimental_supported_tools,
116 supports_search_tool,
117 supports_experimental_context,
118 use_responses_lite,
119 supports_reasoning_effort_updates,
120 node_repl_auto_review_required,
121 node_repl_disabled,
122 auto_review_model_override,
123 model_specialty,
124 tool_mode,
125 multi_agent_version,
126 multi_agent_reasoning_effort,
127 base_instructions,
128 prefer_websockets,
129 supports_parallel_tool_calls,
130 supports_reasoning_summaries,
131 requires_sandboxed_review,
132 available_in_plans,
133 minimal_client_version,
134 );
135 Ok(())
136}
137
138pub fn claude_to_openai_list(
139 input: claude_models::ListModelsResponseBody,
140 supplements: &BTreeMap<String, OpenAiModelSupplement>,
141 _page: &ListPageFacts,
142) -> Result<Converted<openai_models::ListModelsResponseBody>, TransformError> {
143 let mut report = Report::default();
144 let mut data = Vec::with_capacity(input.data.len());
145 for model in input.data {
146 let supplement = supplement_for(supplements, &model.id)?;
147 let converted = get::claude_to_openai(model, supplement)?;
148 common::merge_report(&mut report, converted.report);
149 data.push(converted.value);
150 }
151
152 report.omitted("pagination", "OpenAI model lists have no cursor envelope");
153 Ok(common::converted(
154 openai_models::ListModelsResponseBody {
155 data,
156 object: openai_models::ListObject::List,
157 models: None,
158 rest: Default::default(),
159 },
160 report,
161 ))
162}
163
164pub fn claude_to_gemini_list(
165 input: claude_models::ListModelsResponseBody,
166 supplements: &BTreeMap<String, GeminiModelSupplement>,
167 page: &ListPageFacts,
168) -> Result<Converted<gemini_models::ListModelsResponseBody>, TransformError> {
169 let mut report = Report::default();
170 let mut models = Vec::with_capacity(input.data.len());
171 for model in input.data {
172 let supplement = supplement_for(supplements, &model.id)?;
173 let converted = get::claude_to_gemini(model, supplement)?;
174 common::merge_report(&mut report, converted.report);
175 models.push(converted.value);
176 }
177
178 Ok(common::converted(
179 gemini_models::ListModelsResponseBody {
180 models: Some(models),
181 next_page_token: page.next_page_token.clone(),
182 rest: Default::default(),
183 },
184 report,
185 ))
186}
187
188pub fn openai_to_claude_list(
189 input: openai_models::ListModelsResponseBody,
190 supplements: &BTreeMap<String, ClaudeModelSupplement>,
191 page: &ListPageFacts,
192) -> Result<Converted<claude_models::ListModelsResponseBody>, TransformError> {
193 let mut report = Report::default();
194 if input.models.is_some() {
195 report.changed(
196 "models",
197 "alternate catalog reconciled with standard model data",
198 );
199 }
200 let input = normalize_openai_list(input)?;
201 let mut data = Vec::with_capacity(input.data.len());
202 for model in input.data {
203 let supplement = supplement_for(supplements, &model.id)?;
204 let converted = get::openai_to_claude(model, supplement)?;
205 common::merge_report(&mut report, converted.report);
206 data.push(converted.value);
207 }
208
209 let (first_id, last_id, has_more) = claude_page(&data, page)?;
210 Ok(common::converted(
211 claude_models::ListModelsResponseBody {
212 first_id,
213 last_id,
214 has_more,
215 data,
216 rest: Default::default(),
217 },
218 report,
219 ))
220}
221
222pub fn openai_to_gemini_list(
223 input: openai_models::ListModelsResponseBody,
224 supplements: &BTreeMap<String, GeminiModelSupplement>,
225 page: &ListPageFacts,
226) -> Result<Converted<gemini_models::ListModelsResponseBody>, TransformError> {
227 let mut report = Report::default();
228 if input.models.is_some() {
229 report.changed(
230 "models",
231 "alternate catalog reconciled with standard model data",
232 );
233 }
234 let input = normalize_openai_list(input)?;
235 let mut models = Vec::with_capacity(input.data.len());
236 for model in input.data {
237 let supplement = supplement_for(supplements, &model.id)?;
238 let converted = get::openai_to_gemini(model, supplement)?;
239 common::merge_report(&mut report, converted.report);
240 models.push(converted.value);
241 }
242
243 Ok(common::converted(
244 gemini_models::ListModelsResponseBody {
245 models: Some(models),
246 next_page_token: page.next_page_token.clone(),
247 rest: Default::default(),
248 },
249 report,
250 ))
251}
252
253pub fn gemini_to_openai_list(
254 input: gemini_models::ListModelsResponseBody,
255 supplements: &BTreeMap<String, OpenAiModelSupplement>,
256 _page: &ListPageFacts,
257) -> Result<Converted<openai_models::ListModelsResponseBody>, TransformError> {
258 let mut report = Report::default();
259 let source_models = input.models.unwrap_or_default();
260 let mut data = Vec::with_capacity(source_models.len());
261 for model in source_models {
262 let supplement = supplement_for(supplements, &model.name)?;
263 let converted = get::gemini_to_openai(model, supplement)?;
264 common::merge_report(&mut report, converted.report);
265 data.push(converted.value);
266 }
267
268 report.omitted("pagination", "OpenAI model lists have no cursor envelope");
269 Ok(common::converted(
270 openai_models::ListModelsResponseBody {
271 data,
272 object: openai_models::ListObject::List,
273 models: None,
274 rest: Default::default(),
275 },
276 report,
277 ))
278}
279
280pub fn gemini_to_claude_list(
281 input: gemini_models::ListModelsResponseBody,
282 supplements: &BTreeMap<String, ClaudeModelSupplement>,
283 page: &ListPageFacts,
284) -> Result<Converted<claude_models::ListModelsResponseBody>, TransformError> {
285 let mut report = Report::default();
286 let source_models = input.models.unwrap_or_default();
287 let mut data = Vec::with_capacity(source_models.len());
288 for model in source_models {
289 let supplement = supplement_for(supplements, &model.name)?;
290 let converted = get::gemini_to_claude(model, supplement)?;
291 common::merge_report(&mut report, converted.report);
292 data.push(converted.value);
293 }
294
295 let (first_id, last_id, has_more) = claude_page(&data, page)?;
296 Ok(common::converted(
297 claude_models::ListModelsResponseBody {
298 first_id,
299 last_id,
300 has_more,
301 data,
302 rest: Default::default(),
303 },
304 report,
305 ))
306}
307
308fn claude_page(
309 models: &[claude_models::ModelInfo],
310 facts: &ListPageFacts,
311) -> Result<(Option<String>, Option<String>, bool), TransformError> {
312 let derived_first = models.first().map(|model| model.id.clone());
313 let derived_last = models.last().map(|model| model.id.clone());
314 let first_id = match (&facts.first_id, derived_first) {
315 (Some(supplied), Some(derived)) if supplied != &derived => {
316 return Err(TransformError::new(
317 crate::transform::TransformErrorKind::Conflict,
318 "pagination.first_id",
319 "page fact disagrees with converted list",
320 ));
321 }
322 (Some(supplied), _) => Some(supplied.clone()),
323 (None, derived) if facts.complete => derived,
324 _ => {
325 return Err(missing_pagination(
326 "Claude first_id requires complete list or explicit page facts (NeedsPagination)",
327 ));
328 }
329 };
330 let last_id = match (&facts.last_id, derived_last) {
331 (Some(supplied), Some(derived)) if supplied != &derived => {
332 return Err(TransformError::new(
333 crate::transform::TransformErrorKind::Conflict,
334 "pagination.last_id",
335 "page fact disagrees with converted list",
336 ));
337 }
338 (Some(supplied), _) => Some(supplied.clone()),
339 (None, derived) if facts.complete => derived,
340 _ => {
341 return Err(missing_pagination(
342 "Claude last_id requires complete list or explicit page facts (NeedsPagination)",
343 ));
344 }
345 };
346 let has_more = match (facts.has_more, facts.complete) {
347 (Some(value), _) => value,
348 (None, true) => false,
349 (None, false) => {
350 return Err(missing_pagination(
351 "Claude has_more requires explicit page facts (NeedsPagination)",
352 ));
353 }
354 };
355 Ok((first_id, last_id, has_more))
356}