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