Skip to main content

gproxy_protocol/transform/gemini/generate_content/openai_response/
response.rs

1use crate::gemini::count_tokens::types::{GeminiContentRole, GeminiFunctionCall, GeminiPart};
2use crate::gemini::generate_content::response::{GeminiGenerateContentResponse, ResponseBody};
3use crate::gemini::generate_content::types::{
4    GeminiBlockReason, GeminiCandidate, GeminiContent, GeminiFinishReason, GeminiPromptFeedback,
5    GeminiUsageMetadata,
6};
7use crate::gemini::types::GeminiResponseHeaders;
8use crate::openai::count_tokens::types::{
9    ResponseCustomToolCallOutputContent, ResponseFunctionCallOutputContent, ResponseInputContent,
10};
11use crate::openai::create_response::response::OpenAiCreateResponseResponse;
12use crate::openai::create_response::types::{ResponseIncompleteReason, ResponseOutputItem};
13use crate::transform::gemini::generate_content::utils::{
14    gemini_error_response_from_openai, parse_json_object_or_empty,
15};
16use crate::transform::utils::TransformError;
17
18impl TryFrom<OpenAiCreateResponseResponse> for GeminiGenerateContentResponse {
19    type Error = TransformError;
20
21    fn try_from(value: OpenAiCreateResponseResponse) -> Result<Self, TransformError> {
22        Ok(match value {
23            OpenAiCreateResponseResponse::Success {
24                stats_code,
25                headers,
26                body,
27            } => {
28                let input_content_to_text = |items: Vec<ResponseInputContent>| {
29                    items
30                        .into_iter()
31                        .filter_map(|item| match item {
32                            ResponseInputContent::Text(text) => Some(text.text),
33                            ResponseInputContent::Image(image) => {
34                                if let Some(url) = image.image_url {
35                                    Some(url)
36                                } else {
37                                    image.file_id.map(|file_id| format!("file:{file_id}"))
38                                }
39                            }
40                            ResponseInputContent::File(file) => {
41                                if let Some(data) = file.file_data {
42                                    Some(data)
43                                } else if let Some(url) = file.file_url {
44                                    Some(url)
45                                } else if let Some(file_id) = file.file_id {
46                                    Some(format!("file:{file_id}"))
47                                } else {
48                                    file.filename
49                                }
50                            }
51                        })
52                        .collect::<Vec<_>>()
53                        .join("\n")
54                };
55
56                let mut parts = Vec::new();
57                let mut has_tool_call = false;
58                let mut has_refusal = false;
59
60                for item in body.output {
61                    match item {
62                        ResponseOutputItem::Message(message) => {
63                            for content in message.content {
64                                match content {
65                                    crate::openai::count_tokens::types::ResponseOutputContent::Text(
66                                        text,
67                                    ) => {
68                                        if !text.text.is_empty() {
69                                            parts.push(GeminiPart {
70                                                text: Some(text.text),
71                                                ..GeminiPart::default()
72                                            });
73                                        }
74                                    }
75                                    crate::openai::count_tokens::types::ResponseOutputContent::Refusal(
76                                        refusal,
77                                    ) => {
78                                        has_refusal = true;
79                                        if !refusal.refusal.is_empty() {
80                                            parts.push(GeminiPart {
81                                                text: Some(refusal.refusal),
82                                                ..GeminiPart::default()
83                                            });
84                                        }
85                                    }
86                                }
87                            }
88                        }
89                        ResponseOutputItem::FunctionToolCall(call) => {
90                            has_tool_call = true;
91                            parts.push(GeminiPart {
92                                function_call: Some(GeminiFunctionCall {
93                                    id: call.id.or(Some(call.call_id)),
94                                    name: call.name,
95                                    args: Some(parse_json_object_or_empty(&call.arguments)),
96                                }),
97                                ..GeminiPart::default()
98                            });
99                        }
100                        ResponseOutputItem::CustomToolCall(call) => {
101                            has_tool_call = true;
102                            parts.push(GeminiPart {
103                                function_call: Some(GeminiFunctionCall {
104                                    id: call.id.or(Some(call.call_id)),
105                                    name: call.name,
106                                    args: Some(parse_json_object_or_empty(&call.input)),
107                                }),
108                                ..GeminiPart::default()
109                            });
110                        }
111                        ResponseOutputItem::ReasoningItem(item) => {
112                            let thought_signature = item.id.clone().filter(|id| !id.is_empty());
113                            for summary in item.summary {
114                                if !summary.text.is_empty() {
115                                    parts.push(GeminiPart {
116                                        thought: Some(true),
117                                        thought_signature: thought_signature.clone(),
118                                        text: Some(summary.text),
119                                        ..GeminiPart::default()
120                                    });
121                                }
122                            }
123                            if let Some(content) = item.content {
124                                for reasoning_text in content {
125                                    if !reasoning_text.text.is_empty() {
126                                        parts.push(GeminiPart {
127                                            thought: Some(true),
128                                            thought_signature: thought_signature.clone(),
129                                            text: Some(reasoning_text.text),
130                                            ..GeminiPart::default()
131                                        });
132                                    }
133                                }
134                            }
135                            if let Some(encrypted_content) = item.encrypted_content
136                                && !encrypted_content.is_empty()
137                            {
138                                parts.push(GeminiPart {
139                                    thought: Some(true),
140                                    thought_signature,
141                                    text: Some(encrypted_content),
142                                    ..GeminiPart::default()
143                                });
144                            }
145                        }
146                        ResponseOutputItem::FunctionCallOutput(call) => {
147                            let output = match call.output {
148                                ResponseFunctionCallOutputContent::Text(text) => text,
149                                ResponseFunctionCallOutputContent::Content(items) => {
150                                    input_content_to_text(items)
151                                }
152                            };
153                            if !output.is_empty() {
154                                parts.push(GeminiPart {
155                                    text: Some(output),
156                                    ..GeminiPart::default()
157                                });
158                            }
159                        }
160                        ResponseOutputItem::CustomToolCallOutput(call) => {
161                            let output = match call.output {
162                                ResponseCustomToolCallOutputContent::Text(text) => text,
163                                ResponseCustomToolCallOutputContent::Content(items) => {
164                                    input_content_to_text(items)
165                                }
166                            };
167                            if !output.is_empty() {
168                                parts.push(GeminiPart {
169                                    text: Some(output),
170                                    ..GeminiPart::default()
171                                });
172                            }
173                        }
174                        ResponseOutputItem::ShellCallOutput(call) => {
175                            let output = call
176                                .output
177                                .into_iter()
178                                .map(|entry| {
179                                    format!("stdout: {}\nstderr: {}", entry.stdout, entry.stderr)
180                                })
181                                .collect::<Vec<_>>()
182                                .join("\n");
183                            if !output.is_empty() {
184                                parts.push(GeminiPart {
185                                    text: Some(output),
186                                    ..GeminiPart::default()
187                                });
188                            }
189                        }
190                        ResponseOutputItem::LocalShellCallOutput(call)
191                            if !call.output.is_empty() =>
192                        {
193                            parts.push(GeminiPart {
194                                text: Some(call.output),
195                                ..GeminiPart::default()
196                            });
197                        }
198                        ResponseOutputItem::McpCall(call) => {
199                            if let Some(output) = call.output
200                                && !output.is_empty()
201                            {
202                                parts.push(GeminiPart {
203                                    text: Some(output),
204                                    ..GeminiPart::default()
205                                });
206                            }
207                            if let Some(error) = call.error
208                                && !error.is_empty()
209                            {
210                                has_refusal = true;
211                                parts.push(GeminiPart {
212                                    text: Some(error),
213                                    ..GeminiPart::default()
214                                });
215                            }
216                        }
217                        ResponseOutputItem::ImageGenerationCall(call)
218                            if call.result.as_deref().is_some_and(|s| !s.is_empty()) =>
219                        {
220                            parts.push(GeminiPart {
221                                text: Some(call.result.unwrap_or_default()),
222                                ..GeminiPart::default()
223                            });
224                        }
225                        _ => {}
226                    }
227                }
228
229                if parts.is_empty() {
230                    parts.push(GeminiPart {
231                        text: body.output_text.clone().or(Some(String::new())),
232                        ..GeminiPart::default()
233                    });
234                }
235
236                let incomplete_reason = body
237                    .incomplete_details
238                    .as_ref()
239                    .and_then(|details| details.reason.as_ref());
240                let finish_reason = match incomplete_reason {
241                    Some(ResponseIncompleteReason::MaxOutputTokens) => {
242                        Some(GeminiFinishReason::MaxTokens)
243                    }
244                    Some(ResponseIncompleteReason::ContentFilter) => {
245                        Some(GeminiFinishReason::Safety)
246                    }
247                    None => {
248                        if has_tool_call {
249                            Some(GeminiFinishReason::UnexpectedToolCall)
250                        } else if has_refusal {
251                            Some(GeminiFinishReason::Safety)
252                        } else {
253                            Some(GeminiFinishReason::Stop)
254                        }
255                    }
256                };
257
258                let prompt_feedback = if matches!(
259                    incomplete_reason,
260                    Some(ResponseIncompleteReason::ContentFilter)
261                ) {
262                    Some(GeminiPromptFeedback {
263                        block_reason: Some(GeminiBlockReason::Safety),
264                        safety_ratings: None,
265                    })
266                } else {
267                    None
268                };
269
270                let usage_metadata = body.usage.map(|usage| GeminiUsageMetadata {
271                    prompt_token_count: Some(usage.input_tokens),
272                    cached_content_token_count: Some(usage.input_tokens_details.cached_tokens),
273                    candidates_token_count: Some(usage.output_tokens),
274                    thoughts_token_count: Some(usage.output_tokens_details.reasoning_tokens),
275                    total_token_count: Some(usage.total_tokens),
276                    ..GeminiUsageMetadata::default()
277                });
278
279                GeminiGenerateContentResponse::Success {
280                    stats_code,
281                    headers: GeminiResponseHeaders {
282                        extra: headers.extra,
283                    },
284                    body: ResponseBody {
285                        candidates: Some(vec![GeminiCandidate {
286                            content: Some(GeminiContent {
287                                parts,
288                                role: Some(GeminiContentRole::Model),
289                            }),
290                            finish_reason,
291                            index: Some(0),
292                            ..GeminiCandidate::default()
293                        }]),
294                        prompt_feedback,
295                        usage_metadata,
296                        model_version: Some(body.model),
297                        response_id: Some(body.id),
298                        model_status: None,
299                    },
300                }
301            }
302            OpenAiCreateResponseResponse::Error {
303                stats_code,
304                headers,
305                body,
306            } => GeminiGenerateContentResponse::Error {
307                stats_code,
308                headers: GeminiResponseHeaders {
309                    extra: headers.extra,
310                },
311                body: gemini_error_response_from_openai(stats_code, body),
312            },
313        })
314    }
315}