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