gproxy_protocol/transform/gemini/generate_content/openai_response/
response.rs1use 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}