gproxy_protocol/transform/openai/compact/gemini/
response.rs1use crate::gemini::generate_content::response::GeminiGenerateContentResponse;
2use crate::openai::compact_response::response::OpenAiCompactResponse;
3use crate::openai::compact_response::response::{
4 OpenAiCompactedResponseObject, ResponseBody as CompactResponseBody,
5};
6use crate::openai::compact_response::types as cpt;
7use crate::openai::count_tokens::types as ot;
8use crate::transform::openai::generate_content::openai_chat_completions::gemini::utils::{
9 gemini_function_response_to_text, json_object_to_string, prompt_feedback_refusal_text,
10};
11use crate::transform::openai::generate_content::openai_response::gemini::utils::{
12 gemini_citation_annotations, gemini_logprobs,
13};
14use crate::transform::openai::model_list::gemini::utils::openai_error_response_from_gemini;
15use crate::transform::utils::TransformError;
16
17impl TryFrom<GeminiGenerateContentResponse> for OpenAiCompactResponse {
18 type Error = TransformError;
19
20 fn try_from(value: GeminiGenerateContentResponse) -> Result<Self, TransformError> {
21 Ok(match value {
22 GeminiGenerateContentResponse::Success {
23 stats_code,
24 headers,
25 body,
26 } => {
27 let response_id = body.response_id.unwrap_or_else(|| "compact".to_string());
28 let prompt_feedback = body.prompt_feedback;
29 let mut output = Vec::new();
30
31 for (candidate_index, candidate) in
32 body.candidates.unwrap_or_default().into_iter().enumerate()
33 {
34 let mut message_content = Vec::new();
35
36 if let Some(content) = candidate.content.clone() {
37 for part in content.parts {
38 if let Some(text) = part.text
39 && !text.is_empty()
40 {
41 message_content.push(
42 cpt::CompactedResponseMessageContent::OutputText(
43 ot::ResponseOutputText {
44 annotations: gemini_citation_annotations(
45 candidate.citation_metadata.as_ref(),
46 ),
47 logprobs: gemini_logprobs(
48 candidate.logprobs_result.as_ref(),
49 ),
50 text,
51 type_: ot::ResponseOutputTextType::OutputText,
52 },
53 ),
54 );
55 }
56
57 if let Some(function_call) = part.function_call {
58 output.push(cpt::CompactedResponseOutputItem::FunctionToolCall(
59 ot::ResponseFunctionToolCall {
60 arguments: function_call
61 .args
62 .as_ref()
63 .map(json_object_to_string)
64 .unwrap_or_else(|| "{}".to_string()),
65 call_id: function_call.id.unwrap_or_else(|| {
66 format!(
67 "{}_candidate_{}_tool_call",
68 response_id, candidate_index
69 )
70 }),
71 name: function_call.name,
72 type_: ot::ResponseFunctionToolCallType::FunctionCall,
73 id: None,
74 status: Some(ot::ResponseItemStatus::Completed),
75 },
76 ));
77 }
78
79 if let Some(function_response) = part.function_response {
80 output.push(cpt::CompactedResponseOutputItem::FunctionCallOutput(
81 ot::ResponseFunctionCallOutput {
82 call_id: function_response
83 .id
84 .clone()
85 .unwrap_or_else(|| function_response.name.clone()),
86 output: ot::ResponseFunctionCallOutputContent::Text(
87 gemini_function_response_to_text(function_response),
88 ),
89 type_:
90 ot::ResponseFunctionCallOutputType::FunctionCallOutput,
91 id: None,
92 status: Some(ot::ResponseItemStatus::Completed),
93 },
94 ));
95 }
96
97 if let Some(inline_data) = part.inline_data {
98 message_content.push(
99 cpt::CompactedResponseMessageContent::OutputText(
100 ot::ResponseOutputText {
101 annotations: Vec::new(),
102 logprobs: None,
103 text: format!(
104 "data:{};base64,{}",
105 inline_data.mime_type, inline_data.data
106 ),
107 type_: ot::ResponseOutputTextType::OutputText,
108 },
109 ),
110 );
111 }
112
113 if let Some(file_data) = part.file_data {
114 message_content.push(
115 cpt::CompactedResponseMessageContent::OutputText(
116 ot::ResponseOutputText {
117 annotations: Vec::new(),
118 logprobs: None,
119 text: file_data.file_uri,
120 type_: ot::ResponseOutputTextType::OutputText,
121 },
122 ),
123 );
124 }
125
126 if let Some(executable_code) = part.executable_code {
127 message_content.push(
128 cpt::CompactedResponseMessageContent::OutputText(
129 ot::ResponseOutputText {
130 annotations: Vec::new(),
131 logprobs: None,
132 text: executable_code.code,
133 type_: ot::ResponseOutputTextType::OutputText,
134 },
135 ),
136 );
137 }
138
139 if let Some(code_execution_result) = part.code_execution_result
140 && let Some(output_text) = code_execution_result.output
141 && !output_text.is_empty()
142 {
143 message_content.push(
144 cpt::CompactedResponseMessageContent::OutputText(
145 ot::ResponseOutputText {
146 annotations: Vec::new(),
147 logprobs: None,
148 text: output_text,
149 type_: ot::ResponseOutputTextType::OutputText,
150 },
151 ),
152 );
153 }
154 }
155 }
156
157 if message_content.is_empty()
158 && let Some(finish_message) = candidate.finish_message
159 && !finish_message.is_empty()
160 {
161 message_content.push(cpt::CompactedResponseMessageContent::OutputText(
162 ot::ResponseOutputText {
163 annotations: Vec::new(),
164 logprobs: None,
165 text: finish_message,
166 type_: ot::ResponseOutputTextType::OutputText,
167 },
168 ));
169 }
170
171 if !message_content.is_empty() {
172 let status = match candidate.finish_reason.as_ref() {
173 Some(
174 crate::gemini::generate_content::types::GeminiFinishReason::MaxTokens
175 | crate::gemini::generate_content::types::GeminiFinishReason::Safety
176 | crate::gemini::generate_content::types::GeminiFinishReason::Recitation
177 | crate::gemini::generate_content::types::GeminiFinishReason::Blocklist
178 | crate::gemini::generate_content::types::GeminiFinishReason::ProhibitedContent
179 | crate::gemini::generate_content::types::GeminiFinishReason::Spii
180 | crate::gemini::generate_content::types::GeminiFinishReason::ImageSafety
181 | crate::gemini::generate_content::types::GeminiFinishReason::ImageProhibitedContent
182 | crate::gemini::generate_content::types::GeminiFinishReason::ImageRecitation,
183 ) => ot::ResponseItemStatus::Incomplete,
184 _ => ot::ResponseItemStatus::Completed,
185 };
186 output.push(cpt::CompactedResponseOutputItem::Message(
187 cpt::CompactedResponseMessage {
188 id: format!("{}_message_{}", response_id, candidate_index),
189 content: message_content,
190 role: cpt::CompactedResponseMessageRole::Assistant,
191 status,
192 type_: cpt::CompactedResponseMessageType::Message,
193 },
194 ));
195 }
196 }
197
198 if output.is_empty()
199 && let Some(refusal) = prompt_feedback_refusal_text(prompt_feedback.as_ref())
200 {
201 output.push(cpt::CompactedResponseOutputItem::Message(
202 cpt::CompactedResponseMessage {
203 id: format!("{}_message_0", response_id),
204 content: vec![cpt::CompactedResponseMessageContent::Refusal(
205 ot::ResponseOutputRefusal {
206 refusal,
207 type_: ot::ResponseOutputRefusalType::Refusal,
208 },
209 )],
210 role: cpt::CompactedResponseMessageRole::Assistant,
211 status: ot::ResponseItemStatus::Incomplete,
212 type_: cpt::CompactedResponseMessageType::Message,
213 },
214 ));
215 }
216
217 let usage = body
218 .usage_metadata
219 .map(|usage| {
220 let input_tokens = usage
221 .prompt_token_count
222 .unwrap_or(0)
223 .saturating_add(usage.tool_use_prompt_token_count.unwrap_or(0));
224 let output_tokens = usage
225 .candidates_token_count
226 .unwrap_or(0)
227 .saturating_add(usage.thoughts_token_count.unwrap_or(0));
228 cpt::ResponseUsage {
229 input_tokens,
230 input_tokens_details: cpt::ResponseInputTokensDetails {
231 cached_tokens: usage.cached_content_token_count.unwrap_or(0),
232 },
233 output_tokens,
234 output_tokens_details: cpt::ResponseOutputTokensDetails {
235 reasoning_tokens: usage.thoughts_token_count.unwrap_or(0),
236 },
237 total_tokens: usage
238 .total_token_count
239 .unwrap_or_else(|| input_tokens.saturating_add(output_tokens)),
240 }
241 })
242 .unwrap_or(cpt::ResponseUsage {
243 input_tokens: 0,
244 input_tokens_details: cpt::ResponseInputTokensDetails { cached_tokens: 0 },
245 output_tokens: 0,
246 output_tokens_details: cpt::ResponseOutputTokensDetails {
247 reasoning_tokens: 0,
248 },
249 total_tokens: 0,
250 });
251
252 OpenAiCompactResponse::Success {
253 stats_code,
254 headers: crate::openai::types::OpenAiResponseHeaders {
255 extra: headers.extra,
256 },
257 body: CompactResponseBody {
258 id: response_id,
259 created_at: 0,
260 object: OpenAiCompactedResponseObject::ResponseCompaction,
261 output,
262 usage,
263 },
264 }
265 }
266 GeminiGenerateContentResponse::Error {
267 stats_code,
268 headers,
269 body,
270 } => OpenAiCompactResponse::Error {
271 stats_code,
272 headers: crate::openai::types::OpenAiResponseHeaders {
273 extra: headers.extra,
274 },
275 body: openai_error_response_from_gemini(stats_code, body),
276 },
277 })
278 }
279}