Skip to main content

gproxy_protocol/transform/models/
list.rs

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::{
10    claude::models as claude_models, gemini::models as gemini_models,
11    openai::models as openai_models,
12};
13use std::collections::BTreeMap;
14
15/// List supplement maps use the exact source resource field: `id` for
16/// Claude/OpenAI and `name` (including its `models/` prefix) for Gemini.
17fn supplement_for<'a, T>(
18    supplements: &'a BTreeMap<String, T>,
19    source_key: &str,
20) -> Result<&'a T, TransformError> {
21    supplements
22        .get(source_key)
23        .ok_or_else(|| TransformError::missing_metadata(format!("model[{source_key}].supplement")))
24}
25
26pub fn claude_to_openai_list(
27    input: claude_models::ListModelsResponseBody,
28    supplements: &BTreeMap<String, OpenAiModelSupplement>,
29    _page: &ListPageFacts,
30) -> Result<Converted<openai_models::ListModelsResponseBody>, TransformError> {
31    let mut report = Report::default();
32    let mut data = Vec::with_capacity(input.data.len());
33    for model in input.data {
34        let supplement = supplement_for(supplements, &model.id)?;
35        let converted = get::claude_to_openai(model, supplement)?;
36        common::merge_report(&mut report, converted.report);
37        data.push(converted.value);
38    }
39
40    report.omitted("pagination", "OpenAI model lists have no cursor envelope");
41    Ok(common::converted(
42        openai_models::ListModelsResponseBody {
43            data,
44            object: openai_models::ListObject::List,
45            rest: Default::default(),
46        },
47        report,
48    ))
49}
50
51pub fn claude_to_gemini_list(
52    input: claude_models::ListModelsResponseBody,
53    supplements: &BTreeMap<String, GeminiModelSupplement>,
54    page: &ListPageFacts,
55) -> Result<Converted<gemini_models::ListModelsResponseBody>, TransformError> {
56    let mut report = Report::default();
57    let mut models = Vec::with_capacity(input.data.len());
58    for model in input.data {
59        let supplement = supplement_for(supplements, &model.id)?;
60        let converted = get::claude_to_gemini(model, supplement)?;
61        common::merge_report(&mut report, converted.report);
62        models.push(converted.value);
63    }
64
65    Ok(common::converted(
66        gemini_models::ListModelsResponseBody {
67            models: Some(models),
68            next_page_token: page.next_page_token.clone(),
69            rest: Default::default(),
70        },
71        report,
72    ))
73}
74
75pub fn openai_to_claude_list(
76    input: openai_models::ListModelsResponseBody,
77    supplements: &BTreeMap<String, ClaudeModelSupplement>,
78    page: &ListPageFacts,
79) -> Result<Converted<claude_models::ListModelsResponseBody>, TransformError> {
80    let mut report = Report::default();
81    let mut data = Vec::with_capacity(input.data.len());
82    for model in input.data {
83        let supplement = supplement_for(supplements, &model.id)?;
84        let converted = get::openai_to_claude(model, supplement)?;
85        common::merge_report(&mut report, converted.report);
86        data.push(converted.value);
87    }
88
89    let (first_id, last_id, has_more) = claude_page(&data, page)?;
90    Ok(common::converted(
91        claude_models::ListModelsResponseBody {
92            first_id,
93            last_id,
94            has_more,
95            data,
96            rest: Default::default(),
97        },
98        report,
99    ))
100}
101
102pub fn openai_to_gemini_list(
103    input: openai_models::ListModelsResponseBody,
104    supplements: &BTreeMap<String, GeminiModelSupplement>,
105    page: &ListPageFacts,
106) -> Result<Converted<gemini_models::ListModelsResponseBody>, TransformError> {
107    let mut report = Report::default();
108    let mut models = Vec::with_capacity(input.data.len());
109    for model in input.data {
110        let supplement = supplement_for(supplements, &model.id)?;
111        let converted = get::openai_to_gemini(model, supplement)?;
112        common::merge_report(&mut report, converted.report);
113        models.push(converted.value);
114    }
115
116    Ok(common::converted(
117        gemini_models::ListModelsResponseBody {
118            models: Some(models),
119            next_page_token: page.next_page_token.clone(),
120            rest: Default::default(),
121        },
122        report,
123    ))
124}
125
126pub fn gemini_to_openai_list(
127    input: gemini_models::ListModelsResponseBody,
128    supplements: &BTreeMap<String, OpenAiModelSupplement>,
129    _page: &ListPageFacts,
130) -> Result<Converted<openai_models::ListModelsResponseBody>, TransformError> {
131    let mut report = Report::default();
132    let source_models = input.models.unwrap_or_default();
133    let mut data = Vec::with_capacity(source_models.len());
134    for model in source_models {
135        let supplement = supplement_for(supplements, &model.name)?;
136        let converted = get::gemini_to_openai(model, supplement)?;
137        common::merge_report(&mut report, converted.report);
138        data.push(converted.value);
139    }
140
141    report.omitted("pagination", "OpenAI model lists have no cursor envelope");
142    Ok(common::converted(
143        openai_models::ListModelsResponseBody {
144            data,
145            object: openai_models::ListObject::List,
146            rest: Default::default(),
147        },
148        report,
149    ))
150}
151
152pub fn gemini_to_claude_list(
153    input: gemini_models::ListModelsResponseBody,
154    supplements: &BTreeMap<String, ClaudeModelSupplement>,
155    page: &ListPageFacts,
156) -> Result<Converted<claude_models::ListModelsResponseBody>, TransformError> {
157    let mut report = Report::default();
158    let source_models = input.models.unwrap_or_default();
159    let mut data = Vec::with_capacity(source_models.len());
160    for model in source_models {
161        let supplement = supplement_for(supplements, &model.name)?;
162        let converted = get::gemini_to_claude(model, supplement)?;
163        common::merge_report(&mut report, converted.report);
164        data.push(converted.value);
165    }
166
167    let (first_id, last_id, has_more) = claude_page(&data, page)?;
168    Ok(common::converted(
169        claude_models::ListModelsResponseBody {
170            first_id,
171            last_id,
172            has_more,
173            data,
174            rest: Default::default(),
175        },
176        report,
177    ))
178}
179
180fn claude_page(
181    models: &[claude_models::ModelInfo],
182    facts: &ListPageFacts,
183) -> Result<(String, String, bool), TransformError> {
184    let derived_first = models.first().map(|model| model.id.clone());
185    let derived_last = models.last().map(|model| model.id.clone());
186    let first_id = match (&facts.first_id, derived_first) {
187        (Some(supplied), Some(derived)) if supplied != &derived => {
188            return Err(TransformError::new(
189                crate::transform::TransformErrorKind::Conflict,
190                "pagination.first_id",
191                "page fact disagrees with converted list",
192            ));
193        }
194        (Some(supplied), _) => supplied.clone(),
195        (None, Some(derived)) if facts.complete => derived,
196        _ => {
197            return Err(missing_pagination(
198                "Claude first_id requires complete list or explicit page facts (NeedsPagination)",
199            ));
200        }
201    };
202    let last_id = match (&facts.last_id, derived_last) {
203        (Some(supplied), Some(derived)) if supplied != &derived => {
204            return Err(TransformError::new(
205                crate::transform::TransformErrorKind::Conflict,
206                "pagination.last_id",
207                "page fact disagrees with converted list",
208            ));
209        }
210        (Some(supplied), _) => supplied.clone(),
211        (None, Some(derived)) if facts.complete => derived,
212        _ => {
213            return Err(missing_pagination(
214                "Claude last_id requires complete list or explicit page facts (NeedsPagination)",
215            ));
216        }
217    };
218    let has_more = match (facts.has_more, facts.complete) {
219        (Some(value), _) => value,
220        (None, true) => false,
221        (None, false) => {
222            return Err(missing_pagination(
223                "Claude has_more requires explicit page facts (NeedsPagination)",
224            ));
225        }
226    };
227    Ok((first_id, last_id, has_more))
228}