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