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    Ok(common::converted(value, report))
51}
52
53pub fn claude_to_gemini(
54    input: claude_models::ModelInfo,
55    supplement: &GeminiModelSupplement,
56) -> Result<Converted<gemini_models::Model>, TransformError> {
57    let name = common::gemini_name(&input.id)?;
58    let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
59    let version = common::non_empty(&supplement.version, "model.version")?;
60    let mut report = Report::default();
61    report.omitted(
62        "model.capabilities.batch",
63        "Gemini has no Claude batch capability",
64    );
65    report.omitted(
66        "model.capabilities.citations",
67        "Gemini has no Claude citations capability",
68    );
69    report.omitted(
70        "model.capabilities.code_execution",
71        "Gemini has no Claude code-execution capability",
72    );
73    report.omitted(
74        "model.capabilities.context_management",
75        "Gemini has no Claude context-management capability",
76    );
77    report.omitted(
78        "model.capabilities.effort",
79        "Gemini has no Claude effort capability",
80    );
81    report.omitted(
82        "model.capabilities.image_input",
83        "Gemini has no Claude image-input capability",
84    );
85    report.omitted(
86        "model.capabilities.pdf_input",
87        "Gemini has no Claude PDF-input capability",
88    );
89    report.omitted(
90        "model.capabilities.structured_outputs",
91        "Gemini has no Claude structured-output capability",
92    );
93    report.omitted(
94        "model.capabilities.thinking.types",
95        "Gemini represents thinking support but not Claude thinking modes",
96    );
97    report.omitted(
98        "model.allowed_fallback_models",
99        "Gemini has no fallback-model list",
100    );
101    report.omitted("model.created_at", "Gemini has no model creation timestamp");
102    report.omitted("model.type", "Gemini has no model type field");
103    let value = gemini_models::Model {
104        name,
105        base_model_id,
106        version,
107        display_name: Some(input.display_name),
108        description: None,
109        input_token_limit: Some(input.max_input_tokens),
110        output_token_limit: Some(input.max_tokens),
111        supported_generation_methods: None,
112        thinking: Some(input.capabilities.thinking.supported),
113        temperature: None,
114        max_temperature: None,
115        top_p: None,
116        top_k: None,
117        rest: Default::default(),
118    };
119    Ok(common::converted(value, report))
120}
121
122pub fn openai_to_claude(
123    input: openai_models::Model,
124    supplement: &ClaudeModelSupplement,
125) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
126    let created_at = match input.created {
127        Some(created) => {
128            if let Some(supplied) = &supplement.created_at
129                && common::parse_iso(supplied)?.unix_timestamp_nanos()
130                    != created as i128 * 1_000_000_000
131            {
132                return Err(TransformError::new(
133                    crate::transform::TransformErrorKind::Conflict,
134                    "model.created_at",
135                    "source and supplied metadata disagree",
136                ));
137            }
138            common::epoch_to_iso(created)?
139        }
140        None => {
141            let supplied = common::require(supplement.created_at.clone(), "model.created_at")?;
142            common::parse_iso(&supplied)?;
143            supplied
144        }
145    };
146    let display_name = common::choose(
147        input.display_name.clone(),
148        supplement.display_name.clone(),
149        "model.display_name",
150    )?;
151    let source_input = input
152        .context_window
153        .map(i64::try_from)
154        .transpose()
155        .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?;
156    let source_output = input
157        .max_output_tokens
158        .map(i64::try_from)
159        .transpose()
160        .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?;
161    let max_input_tokens = common::choose(
162        source_input,
163        supplement.max_input_tokens,
164        "model.max_input_tokens",
165    )?;
166    let max_tokens = common::choose(source_output, supplement.max_tokens, "model.max_tokens")?;
167    let mut report = Report::default();
168    common::report_openai_loss(&mut report);
169    report.omitted(
170        "model.object",
171        "Claude model objects have a distinct type field",
172    );
173    Ok(common::converted(
174        common::claude_model(
175            common::openai_model_id(&input.id)?,
176            created_at,
177            display_name,
178            max_input_tokens,
179            max_tokens,
180            supplement.clone(),
181        ),
182        report,
183    ))
184}
185
186pub fn openai_to_gemini(
187    input: openai_models::Model,
188    supplement: &GeminiModelSupplement,
189) -> Result<Converted<gemini_models::Model>, TransformError> {
190    let mut report = Report::default();
191    report.omitted("model.owned_by", "Gemini has no owner field");
192    report.omitted("model.object", "Gemini has no OpenAI object discriminator");
193    report.omitted("model.created", "Gemini has no model creation timestamp");
194    let base_model_id = common::non_empty(&supplement.base_model_id, "model.base_model_id")?;
195    let version = common::non_empty(&supplement.version, "model.version")?;
196    let value = gemini_models::Model {
197        name: common::gemini_name(&input.id)?,
198        base_model_id,
199        version,
200        display_name: input.display_name,
201        description: input.description,
202        input_token_limit: input
203            .context_window
204            .map(i64::try_from)
205            .transpose()
206            .map_err(|e| TransformError::shape("model.context_window", e.to_string()))?,
207        output_token_limit: input
208            .max_output_tokens
209            .map(i64::try_from)
210            .transpose()
211            .map_err(|e| TransformError::shape("model.max_output_tokens", e.to_string()))?,
212        supported_generation_methods: input.generation_methods,
213        thinking: input.thinking_supported,
214        temperature: None,
215        max_temperature: None,
216        top_p: None,
217        top_k: None,
218        rest: Default::default(),
219    };
220    Ok(common::converted(value, report))
221}
222
223pub fn gemini_to_openai(
224    input: gemini_models::Model,
225    supplement: &OpenAiModelSupplement,
226) -> Result<Converted<openai_models::Model>, TransformError> {
227    let created = common::require(supplement.created, "model.created")?;
228    if supplement.owned_by.is_empty() {
229        return Err(TransformError::missing_metadata("model.owned_by"));
230    }
231    let mut report = Report::default();
232    common::report_gemini_loss(&mut report);
233    let mut value = common::openai_model(
234        common::bare_gemini_id(&input.name)?,
235        created,
236        supplement.owned_by.clone(),
237    );
238    value.display_name = input.display_name;
239    value.description = input.description;
240    value.context_window = input
241        .input_token_limit
242        .map(u64::try_from)
243        .transpose()
244        .map_err(|e| TransformError::shape("model.input_token_limit", e.to_string()))?;
245    value.max_output_tokens = input
246        .output_token_limit
247        .map(u64::try_from)
248        .transpose()
249        .map_err(|e| TransformError::shape("model.output_token_limit", e.to_string()))?;
250    value.thinking_supported = input.thinking;
251    value.generation_methods = input.supported_generation_methods;
252    Ok(common::converted(value, report))
253}
254
255pub fn gemini_to_claude(
256    input: gemini_models::Model,
257    supplement: &ClaudeModelSupplement,
258) -> Result<Converted<claude_models::ModelInfo>, TransformError> {
259    let created_at = common::require(supplement.created_at.clone(), "model.created_at")?;
260    common::parse_iso(&created_at)?;
261    let display_name = common::choose(
262        input.display_name.clone(),
263        supplement.display_name.clone(),
264        "model.display_name",
265    )?;
266    let max_input_tokens = common::choose(
267        input.input_token_limit,
268        supplement.max_input_tokens,
269        "model.max_input_tokens",
270    )?;
271    let max_tokens = common::choose(
272        input.output_token_limit,
273        supplement.max_tokens,
274        "model.max_tokens",
275    )?;
276    let mut report = Report::default();
277    report.omitted(
278        "model.description",
279        "Claude model objects have no description",
280    );
281    report.omitted(
282        "model.supported_generation_methods",
283        "Claude model objects have no generation-method list",
284    );
285    report.omitted(
286        "model.base_model_id",
287        "Claude model objects have no base-model id",
288    );
289    report.omitted(
290        "model.version",
291        "Claude model objects have no model version",
292    );
293    report.omitted(
294        "model.temperature",
295        "Claude model objects have no temperature field",
296    );
297    report.omitted(
298        "model.max_temperature",
299        "Claude model objects have no max_temperature field",
300    );
301    report.omitted("model.top_p", "Claude model objects have no top_p field");
302    report.omitted("model.top_k", "Claude model objects have no top_k field");
303    if input.thinking.is_some() {
304        report.changed(
305            "model.thinking",
306            "Claude thinking support was reconciled with source metadata",
307        );
308    }
309    let mut supplement = supplement.clone();
310    supplement.capabilities.thinking.supported = common::choose(
311        input.thinking,
312        Some(supplement.capabilities.thinking.supported),
313        "model.thinking",
314    )?;
315    Ok(common::converted(
316        common::claude_model(
317            common::bare_gemini_id(&input.name)?,
318            created_at,
319            display_name,
320            max_input_tokens,
321            max_tokens,
322            supplement,
323        ),
324        report,
325    ))
326}