Skip to main content

gproxy_protocol/transform/models/
get.rs

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