1pub const GEMINI_3_1_FLASH_LITE_PREVIEW: &str = "gemini-3.1-flash-lite-preview";
7pub const GEMINI_3_FLASH_PREVIEW: &str = "gemini-3-flash-preview";
9pub const GEMINI_2_5_PRO_PREVIEW_06_05: &str = "gemini-2.5-pro-preview-06-05";
11pub const GEMINI_2_5_PRO_PREVIEW_05_06: &str = "gemini-2.5-pro-preview-05-06";
13pub const GEMINI_2_5_PRO_PREVIEW_03_25: &str = "gemini-2.5-pro-preview-03-25";
15pub const GEMINI_2_5_FLASH_PREVIEW_04_17: &str = "gemini-2.5-flash-preview-04-17";
17pub const GEMINI_2_5_PRO_EXP_03_25: &str = "gemini-2.5-pro-exp-03-25";
19pub const GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
21#[cfg(feature = "image")]
23#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
24pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
25pub const GEMINI_2_0_FLASH_LITE: &str = "gemini-2.0-flash-lite";
27pub const GEMINI_2_0_FLASH: &str = "gemini-2.0-flash";
29
30use self::gemini_api_types::tool_parameters_to_schema;
31use crate::http_client::HttpClientExt;
32use crate::message::{self, MimeType, Reasoning};
33use crate::providers::gemini::completion::gemini_api_types::{
34 AdditionalParameters, FunctionCallingMode, ToolConfig,
35};
36use crate::providers::gemini::streaming::StreamingCompletionResponse;
37use crate::telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator};
38use crate::{
39 OneOrMany,
40 completion::{self, CompletionError, CompletionRequest, GetTokenUsage},
41};
42use gemini_api_types::{
43 Content, FinishReason, FunctionDeclaration, GenerateContentRequest, GenerateContentResponse,
44 GenerationConfig, Part, PartKind, Role, Tool,
45};
46use serde_json::{Map, Value};
47use std::convert::TryFrom;
48use tracing::{Level, enabled};
49use tracing_futures::Instrument;
50
51use super::Client;
52
53#[derive(Clone, Debug)]
58pub struct CompletionModel<T = reqwest::Client> {
59 pub(crate) client: Client<T>,
60 pub model: String,
61}
62
63impl<T> CompletionModel<T> {
64 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
65 Self {
66 client,
67 model: model.into(),
68 }
69 }
70
71 pub fn with_model(client: Client<T>, model: &str) -> Self {
72 Self {
73 client,
74 model: model.into(),
75 }
76 }
77}
78
79impl<T> completion::CompletionModel for CompletionModel<T>
80where
81 T: HttpClientExt + Clone + 'static,
82{
83 type Response = GenerateContentResponse;
84 type StreamingResponse = StreamingCompletionResponse;
85 type Client = super::Client<T>;
86
87 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
88 Self::new(client.clone(), model)
89 }
90
91 async fn completion(
92 &self,
93 completion_request: CompletionRequest,
94 ) -> Result<completion::CompletionResponse<GenerateContentResponse>, CompletionError> {
95 let request_model = resolve_request_model(&self.model, &completion_request);
96 let span = CompletionSpanBuilder::new(
97 "gcp.gemini",
98 &request_model,
99 CompletionOperation::GenerateContent,
100 )
101 .system_instructions(
102 completion_request.preamble.as_deref(),
103 completion_request.record_telemetry_content,
104 )
105 .build();
106
107 let request = create_request_body(completion_request)?;
108
109 if enabled!(Level::TRACE) {
110 tracing::trace!(
111 target: "rig::completions",
112 "Gemini completion request: {}",
113 serde_json::to_string_pretty(&request)?
114 );
115 }
116
117 let body = serde_json::to_vec(&request)?;
118
119 let path = completion_endpoint(&request_model);
120
121 let request = self
122 .client
123 .post(path.as_str())?
124 .body(body)
125 .map_err(|e| CompletionError::HttpError(e.into()))?;
126
127 async move {
128 let response = self.client.send::<_, Vec<u8>>(request).await?;
129
130 if response.status().is_success() {
131 let response_body = response
132 .into_body()
133 .await
134 .map_err(CompletionError::HttpError)?;
135
136 let response_text = String::from_utf8_lossy(&response_body).to_string();
137
138 let response: GenerateContentResponse = serde_json::from_slice(&response_body)
139 .map_err(|err| {
140 tracing::error!(
141 error = %err,
142 body = %response_text,
143 "Failed to deserialize Gemini completion response"
144 );
145 CompletionError::JsonError(err)
146 })?;
147
148 let span = tracing::Span::current();
149 span.record_response_metadata(&response);
150 span.record_token_usage(&response.usage_metadata);
151
152 if enabled!(Level::TRACE) {
153 tracing::trace!(
154 target: "rig::completions",
155 "Gemini completion response: {}",
156 serde_json::to_string_pretty(&response)?
157 );
158 }
159
160 response.try_into()
161 } else {
162 let status = response.status();
163 let body = response
164 .into_body()
165 .await
166 .map_err(CompletionError::HttpError)?;
167
168 Err(CompletionError::from_http_response(
169 status,
170 String::from_utf8_lossy(&body),
171 ))
172 }
173 }
174 .instrument(span)
175 .await
176 }
177
178 async fn stream(
179 &self,
180 request: CompletionRequest,
181 ) -> Result<
182 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
183 CompletionError,
184 > {
185 CompletionModel::stream(self, request).await
186 }
187}
188
189pub(crate) fn create_request_body(
190 completion_request: CompletionRequest,
191) -> Result<GenerateContentRequest, CompletionError> {
192 let chat_history = completion_request.chat_history_with_documents();
193
194 let CompletionRequest {
195 model: _,
196 preamble,
197 chat_history: _,
198 documents: _,
199 tools: function_tools,
200 temperature,
201 max_tokens,
202 tool_choice,
203 mut additional_params,
204 output_schema,
205 record_telemetry_content: _,
206 } = completion_request;
207
208 let mut full_history = Vec::new();
209 full_history.extend(chat_history);
210 let (history_system, full_history) = split_system_messages_from_history(full_history);
211
212 let mut additional_params_payload = additional_params
213 .take()
214 .unwrap_or_else(|| Value::Object(Map::new()));
215 let mut additional_tools =
216 extract_tools_from_additional_params(&mut additional_params_payload)?;
217
218 let AdditionalParameters {
219 mut generation_config,
220 additional_params,
221 } = serde_json::from_value::<AdditionalParameters>(additional_params_payload)?;
222
223 if let Some(schema) = output_schema {
225 let cfg = generation_config.get_or_insert_with(GenerationConfig::default);
226 cfg.response_mime_type = Some("application/json".to_string());
227 cfg.response_json_schema = Some(schema.to_value());
228 }
229
230 generation_config = generation_config.map(|mut cfg| {
231 if let Some(temp) = temperature {
232 cfg.temperature = Some(temp);
233 };
234
235 if let Some(max_tokens) = max_tokens {
236 cfg.max_output_tokens = Some(max_tokens);
237 };
238
239 cfg
240 });
241
242 let mut system_parts: Vec<Part> = Vec::new();
243 if let Some(preamble) = preamble.filter(|preamble| !preamble.is_empty()) {
244 system_parts.push(preamble.into());
245 }
246 for content in history_system {
247 if !content.is_empty() {
248 system_parts.push(content.into());
249 }
250 }
251 let system_instruction = if system_parts.is_empty() {
252 None
253 } else {
254 Some(Content {
255 parts: system_parts,
256 role: Some(Role::Model),
257 })
258 };
259
260 let mut tools = if function_tools.is_empty() {
261 Vec::new()
262 } else {
263 vec![serde_json::to_value(Tool::try_from(function_tools)?)?]
264 };
265 tools.append(&mut additional_tools);
266 let tools = if tools.is_empty() { None } else { Some(tools) };
267
268 let tool_config = if let Some(cfg) = tool_choice {
269 Some(ToolConfig {
270 function_calling_config: Some(FunctionCallingMode::try_from(cfg)?),
271 })
272 } else {
273 None
274 };
275
276 let request = GenerateContentRequest {
277 contents: full_history
278 .into_iter()
279 .map(|msg| {
280 msg.try_into()
281 .map_err(|e| CompletionError::RequestError(Box::new(e)))
282 })
283 .collect::<Result<Vec<_>, _>>()?,
284 generation_config,
285 safety_settings: None,
286 tools,
287 tool_config,
288 system_instruction,
289 additional_params,
290 };
291
292 Ok(request)
293}
294
295fn split_system_messages_from_history(
296 history: Vec<completion::Message>,
297) -> (Vec<String>, Vec<completion::Message>) {
298 let mut system = Vec::new();
299 let mut remaining = Vec::new();
300
301 for message in history {
302 match message {
303 completion::Message::System { content } => system.push(content),
304 other => remaining.push(other),
305 }
306 }
307
308 (system, remaining)
309}
310
311fn extract_tools_from_additional_params(
312 additional_params: &mut Value,
313) -> Result<Vec<Value>, CompletionError> {
314 if let Some(map) = additional_params.as_object_mut()
315 && let Some(raw_tools) = map.remove("tools")
316 {
317 return serde_json::from_value::<Vec<Value>>(raw_tools).map_err(|err| {
318 CompletionError::RequestError(
319 format!("Invalid Gemini `additional_params.tools` payload: {err}").into(),
320 )
321 });
322 }
323
324 Ok(Vec::new())
325}
326
327pub(crate) fn resolve_request_model(
328 default_model: &str,
329 completion_request: &CompletionRequest,
330) -> String {
331 completion_request
332 .model
333 .clone()
334 .unwrap_or_else(|| default_model.to_string())
335}
336
337pub(crate) fn completion_endpoint(model: &str) -> String {
338 format!("/v1beta/models/{model}:generateContent")
339}
340
341pub(crate) fn streaming_endpoint(model: &str) -> String {
342 format!("/v1beta/models/{model}:streamGenerateContent")
343}
344
345impl TryFrom<completion::ToolDefinition> for Tool {
346 type Error = CompletionError;
347
348 fn try_from(tool: completion::ToolDefinition) -> Result<Self, Self::Error> {
349 let parameters = tool_parameters_to_schema(tool.parameters)?;
350
351 Ok(Self {
352 function_declarations: vec![FunctionDeclaration {
353 name: tool.name,
354 description: tool.description,
355 parameters,
356 }],
357 code_execution: None,
358 })
359 }
360}
361
362impl TryFrom<Vec<completion::ToolDefinition>> for Tool {
363 type Error = CompletionError;
364
365 fn try_from(tools: Vec<completion::ToolDefinition>) -> Result<Self, Self::Error> {
366 let mut function_declarations = Vec::new();
367
368 for tool in tools {
369 let parameters = tool_parameters_to_schema(tool.parameters).map_err(|e| {
370 CompletionError::ProviderError(format!(
371 "Tool '{}' could not be converted to a schema: {:?}",
372 tool.name, e,
373 ))
374 })?;
375
376 function_declarations.push(FunctionDeclaration {
377 name: tool.name,
378 description: tool.description,
379 parameters,
380 });
381 }
382
383 Ok(Self {
384 function_declarations,
385 code_execution: None,
386 })
387 }
388}
389
390pub(crate) fn function_call_finish_reason_error(
391 reason: &FinishReason,
392 finish_message: Option<&str>,
393) -> Option<CompletionError> {
394 match reason {
395 FinishReason::MalformedFunctionCall
396 | FinishReason::UnexpectedToolCall
397 | FinishReason::MissingThoughtSignature
398 | FinishReason::TooManyToolCalls
399 | FinishReason::MalformedResponse => {
400 let message = finish_message.unwrap_or("no finish message provided");
401 Some(CompletionError::ResponseError(format!(
402 "Gemini stopped with finish_reason={reason:?}: {message}"
403 )))
404 }
405 _ => None,
406 }
407}
408
409impl TryFrom<GenerateContentResponse> for completion::CompletionResponse<GenerateContentResponse> {
410 type Error = CompletionError;
411
412 fn try_from(response: GenerateContentResponse) -> Result<Self, Self::Error> {
413 let candidate = response.candidates.first().ok_or_else(|| {
414 CompletionError::ResponseError("No response candidates in response".into())
415 })?;
416
417 if let Some(reason) = candidate.finish_reason.as_ref()
418 && let Some(err) =
419 function_call_finish_reason_error(reason, candidate.finish_message.as_deref())
420 {
421 return Err(err);
422 }
423
424 let content = candidate
425 .content
426 .as_ref()
427 .ok_or_else(|| {
428 let reason = candidate
429 .finish_reason
430 .as_ref()
431 .map(|r| format!("finish_reason={r:?}"))
432 .unwrap_or_else(|| "finish_reason=<unknown>".to_string());
433 let message = candidate
434 .finish_message
435 .as_deref()
436 .unwrap_or("no finish message provided");
437 CompletionError::ResponseError(format!(
438 "Gemini candidate missing content ({reason}, finish_message={message})"
439 ))
440 })?
441 .parts
442 .iter()
443 .map(
444 |Part {
445 thought,
446 thought_signature,
447 part,
448 ..
449 }| {
450 Ok(match part {
451 PartKind::Text(text) => {
452 if let Some(thought) = thought
453 && *thought
454 {
455 completion::AssistantContent::Reasoning(
456 Reasoning::new_with_signature(text, thought_signature.clone()),
457 )
458 } else {
459 completion::AssistantContent::text(text)
460 }
461 }
462 PartKind::InlineData(inline_data) => {
463 let mime_type =
464 message::MediaType::from_mime_type(&inline_data.mime_type);
465
466 match mime_type {
467 Some(message::MediaType::Image(media_type)) => {
468 message::AssistantContent::image_base64(
469 &inline_data.data,
470 Some(media_type),
471 Some(message::ImageDetail::default()),
472 )
473 }
474 _ => {
475 return Err(CompletionError::ResponseError(format!(
476 "Unsupported media type {mime_type:?}"
477 )));
478 }
479 }
480 }
481 PartKind::FunctionCall(function_call) => {
482 let tool_call = message::ToolCall::new(
483 function_call.name.clone(),
484 message::ToolFunction::new(
485 function_call.name.clone(),
486 function_call.args.clone(),
487 ),
488 )
489 .with_signature(thought_signature.clone());
490 let tool_call = if let Some(id) = &function_call.id {
491 tool_call.with_call_id(id.clone())
492 } else {
493 tool_call
494 };
495 completion::AssistantContent::ToolCall(tool_call)
496 }
497 _ => {
498 return Err(CompletionError::ResponseError(
499 "Response did not contain a message or tool call".into(),
500 ));
501 }
502 })
503 },
504 )
505 .collect::<Result<Vec<_>, _>>()?;
506
507 let choice = OneOrMany::many(content).map_err(|_| {
508 CompletionError::ResponseError(
509 "Response contained no message or tool call (empty)".to_owned(),
510 )
511 })?;
512
513 let usage = response
514 .usage_metadata
515 .as_ref()
516 .map(GetTokenUsage::token_usage)
517 .unwrap_or_default();
518
519 Ok(completion::CompletionResponse {
520 choice,
521 usage,
522 raw_response: response,
523 message_id: None,
524 })
525 }
526}
527
528pub mod gemini_api_types {
529 use crate::telemetry::ProviderResponseExt;
530 use std::{collections::HashMap, convert::Infallible, str::FromStr};
531
532 use serde::{Deserialize, Serialize};
536 use serde_json::{Value, json};
537
538 use crate::completion::GetTokenUsage;
539 use crate::message::{DocumentSourceKind, ImageMediaType, MessageError, MimeType};
540 use crate::{
541 completion::CompletionError,
542 message::{self},
543 providers::gemini::gemini_api_types::{CodeExecutionResult, ExecutableCode},
544 };
545
546 #[derive(Debug, Deserialize, Serialize, Default)]
547 #[serde(rename_all = "camelCase")]
548 pub struct AdditionalParameters {
549 pub generation_config: Option<GenerationConfig>,
551 #[serde(flatten, skip_serializing_if = "Option::is_none")]
553 pub additional_params: Option<serde_json::Value>,
554 }
555
556 impl AdditionalParameters {
557 pub fn with_config(mut self, cfg: GenerationConfig) -> Self {
558 self.generation_config = Some(cfg);
559 self
560 }
561
562 pub fn with_params(mut self, params: serde_json::Value) -> Self {
563 self.additional_params = Some(params);
564 self
565 }
566 }
567
568 #[derive(Debug, Deserialize, Serialize)]
576 #[serde(rename_all = "camelCase")]
577 pub struct GenerateContentResponse {
578 #[serde(default)]
579 pub response_id: String,
580 #[serde(default)]
582 pub candidates: Vec<ContentCandidate>,
583 pub prompt_feedback: Option<PromptFeedback>,
585 pub usage_metadata: Option<UsageMetadata>,
587 pub model_version: Option<String>,
588 }
589
590 impl ProviderResponseExt for GenerateContentResponse {
591 type OutputMessage = ContentCandidate;
592 type Usage = UsageMetadata;
593
594 fn get_response_id(&self) -> Option<String> {
595 Some(self.response_id.clone())
596 }
597
598 fn get_response_model_name(&self) -> Option<String> {
599 self.model_version.clone()
600 }
601
602 fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
603 self.candidates.clone()
604 }
605
606 fn get_text_response(&self) -> Option<String> {
607 let str = self
608 .candidates
609 .iter()
610 .filter_map(|x| {
611 let content = x.content.as_ref()?;
612 if content.role.as_ref().is_none_or(|y| y != &Role::Model) {
613 return None;
614 }
615
616 let res = content
617 .parts
618 .iter()
619 .filter_map(|part| {
620 if let PartKind::Text(ref str) = part.part {
621 Some(str.to_owned())
622 } else {
623 None
624 }
625 })
626 .collect::<Vec<String>>()
627 .join("\n");
628
629 Some(res)
630 })
631 .collect::<Vec<String>>()
632 .join("\n");
633
634 if str.is_empty() { None } else { Some(str) }
635 }
636
637 fn get_usage(&self) -> Option<Self::Usage> {
638 self.usage_metadata.clone()
639 }
640 }
641
642 #[derive(Clone, Debug, Deserialize, Serialize)]
644 #[serde(rename_all = "camelCase")]
645 pub struct ContentCandidate {
646 #[serde(skip_serializing_if = "Option::is_none")]
648 pub content: Option<Content>,
649 pub finish_reason: Option<FinishReason>,
652 pub safety_ratings: Option<Vec<SafetyRating>>,
655 pub citation_metadata: Option<CitationMetadata>,
659 pub token_count: Option<i32>,
661 pub avg_logprobs: Option<f64>,
663 pub logprobs_result: Option<LogprobsResult>,
665 pub index: Option<i32>,
667 pub finish_message: Option<String>,
669 }
670
671 #[derive(Clone, Debug, Deserialize, Serialize)]
672 pub struct Content {
673 #[serde(default)]
675 pub parts: Vec<Part>,
676 pub role: Option<Role>,
679 }
680
681 impl TryFrom<message::Message> for Content {
682 type Error = message::MessageError;
683
684 fn try_from(msg: message::Message) -> Result<Self, Self::Error> {
685 Ok(match msg {
686 message::Message::System { content } => Content {
687 parts: vec![content.into()],
688 role: Some(Role::User),
689 },
690 message::Message::User { content } => Content {
691 parts: content
692 .into_iter()
693 .map(|c| c.try_into())
694 .collect::<Result<Vec<_>, _>>()?,
695 role: Some(Role::User),
696 },
697 message::Message::Assistant { content, .. } => Content {
698 role: Some(Role::Model),
699 parts: content
700 .into_iter()
701 .map(|content| content.try_into())
702 .collect::<Result<Vec<_>, _>>()?,
703 },
704 })
705 }
706 }
707
708 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
709 #[serde(rename_all = "lowercase")]
710 pub enum Role {
711 User,
712 Model,
713 }
714
715 #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
716 #[serde(rename_all = "camelCase")]
717 pub struct Part {
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub thought: Option<bool>,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub thought_signature: Option<String>,
724 #[serde(flatten)]
725 pub part: PartKind,
726 #[serde(flatten, skip_serializing_if = "Option::is_none")]
727 pub additional_params: Option<Value>,
728 }
729
730 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
734 #[serde(rename_all = "camelCase")]
735 pub enum PartKind {
736 Text(String),
737 InlineData(Blob),
738 FunctionCall(FunctionCall),
739 FunctionResponse(FunctionResponse),
740 FileData(FileData),
741 ExecutableCode(ExecutableCode),
742 CodeExecutionResult(CodeExecutionResult),
743 }
744
745 impl Default for PartKind {
748 fn default() -> Self {
749 Self::Text(String::new())
750 }
751 }
752
753 impl From<String> for Part {
754 fn from(text: String) -> Self {
755 Self {
756 thought: Some(false),
757 thought_signature: None,
758 part: PartKind::Text(text),
759 additional_params: None,
760 }
761 }
762 }
763
764 impl From<&str> for Part {
765 fn from(text: &str) -> Self {
766 Self::from(text.to_string())
767 }
768 }
769
770 impl FromStr for Part {
771 type Err = Infallible;
772
773 fn from_str(s: &str) -> Result<Self, Self::Err> {
774 Ok(s.into())
775 }
776 }
777
778 impl TryFrom<(ImageMediaType, DocumentSourceKind)> for PartKind {
779 type Error = message::MessageError;
780 fn try_from(
781 (mime_type, doc_src): (ImageMediaType, DocumentSourceKind),
782 ) -> Result<Self, Self::Error> {
783 let mime_type = mime_type.to_mime_type().to_string();
784 let part = match doc_src {
785 DocumentSourceKind::Url(url) => PartKind::FileData(FileData {
786 mime_type: Some(mime_type),
787 file_uri: url,
788 }),
789 DocumentSourceKind::Base64(data) | DocumentSourceKind::String(data) => {
790 PartKind::InlineData(Blob { mime_type, data })
791 }
792 DocumentSourceKind::Raw(_) => {
793 return Err(message::MessageError::ConversionError(
794 "Raw files not supported, encode as base64 first".into(),
795 ));
796 }
797 DocumentSourceKind::FileId(_) => {
798 return Err(message::MessageError::ConversionError(
799 "Provider file IDs are not supported for Gemini image inputs".into(),
800 ));
801 }
802 DocumentSourceKind::Unknown => {
803 return Err(message::MessageError::ConversionError(
804 "Can't convert an unknown document source".to_string(),
805 ));
806 }
807 };
808
809 Ok(part)
810 }
811 }
812
813 fn gemini_tool_result_image_mime_type(
814 media_type: Option<&ImageMediaType>,
815 ) -> Result<&'static str, MessageError> {
816 let media_type = media_type.ok_or_else(|| {
817 MessageError::ConversionError(
818 "Image media type is required for Gemini tool results".to_string(),
819 )
820 })?;
821
822 match media_type {
823 ImageMediaType::JPEG | ImageMediaType::PNG | ImageMediaType::WEBP => {
824 Ok(media_type.to_mime_type())
825 }
826 _ => Err(MessageError::ConversionError(format!(
827 "Unsupported image media type {media_type:?} for Gemini tool results; supported types are JPEG, PNG, and WEBP"
828 ))),
829 }
830 }
831
832 impl TryFrom<message::UserContent> for Part {
833 type Error = message::MessageError;
834
835 fn try_from(content: message::UserContent) -> Result<Self, Self::Error> {
836 match content {
837 message::UserContent::Text(message::Text { text, .. }) => Ok(Part {
838 thought: Some(false),
839 thought_signature: None,
840 part: PartKind::Text(text),
841 additional_params: None,
842 }),
843 message::UserContent::ToolResult(message::ToolResult {
844 id,
845 call_id,
846 content,
847 }) => {
848 let mut response_values = Vec::new();
849 let mut parts: Vec<FunctionResponsePart> = Vec::new();
850
851 for item in content.iter() {
852 match item {
853 message::ToolResultContent::Text(text) => {
854 response_values.push(json!(&text.text));
855 }
856 message::ToolResultContent::Json { value } => {
857 response_values.push(value.clone());
858 }
859 message::ToolResultContent::Image(image) => {
860 let part = match &image.data {
861 DocumentSourceKind::Base64(b64) => {
862 let mime_type = gemini_tool_result_image_mime_type(
863 image.media_type.as_ref(),
864 )?;
865
866 FunctionResponsePart {
873 inline_data: Some(FunctionResponseInlineData {
874 mime_type: mime_type.to_string(),
875 data: b64.clone(),
876 display_name: None,
877 }),
878 file_data: None,
879 }
880 }
881 DocumentSourceKind::Url(_) => {
882 return Err(message::MessageError::ConversionError(
883 "Gemini tool result images must use base64 inline data; URL-backed images are not supported"
884 .to_string(),
885 ));
886 }
887 _ => {
888 return Err(message::MessageError::ConversionError(
889 "Unsupported image source kind for tool results"
890 .to_string(),
891 ));
892 }
893 };
894 parts.push(part);
895 }
896 }
897 }
898
899 let response_json = if response_values.is_empty() {
900 None
901 } else {
902 let result = if response_values.len() == 1 {
903 response_values.remove(0)
904 } else {
905 serde_json::Value::Array(response_values)
906 };
907 Some(json!({ "result": result }))
908 };
909
910 Ok(Part {
911 thought: Some(false),
912 thought_signature: None,
913 part: PartKind::FunctionResponse(FunctionResponse {
914 name: id,
915 id: call_id,
916 response: response_json,
917 parts: if parts.is_empty() { None } else { Some(parts) },
918 }),
919 additional_params: None,
920 })
921 }
922 message::UserContent::Image(message::Image {
923 data, media_type, ..
924 }) => match media_type {
925 Some(media_type) => match media_type {
926 message::ImageMediaType::JPEG
927 | message::ImageMediaType::PNG
928 | message::ImageMediaType::WEBP
929 | message::ImageMediaType::HEIC
930 | message::ImageMediaType::HEIF => {
931 let part = PartKind::try_from((media_type, data))?;
932 Ok(Part {
933 thought: Some(false),
934 thought_signature: None,
935 part,
936 additional_params: None,
937 })
938 }
939 _ => Err(message::MessageError::ConversionError(format!(
940 "Unsupported image media type {media_type:?}"
941 ))),
942 },
943 None => Err(message::MessageError::ConversionError(
944 "Media type for image is required for Gemini".to_string(),
945 )),
946 },
947 message::UserContent::Document(message::Document {
948 data, media_type, ..
949 }) => {
950 let Some(media_type) = media_type else {
951 return Err(MessageError::ConversionError(
952 "A mime type is required for document inputs to Gemini".to_string(),
953 ));
954 };
955
956 if matches!(
959 media_type,
960 message::DocumentMediaType::TXT
961 | message::DocumentMediaType::RTF
962 | message::DocumentMediaType::HTML
963 | message::DocumentMediaType::CSS
964 | message::DocumentMediaType::MARKDOWN
965 | message::DocumentMediaType::CSV
966 | message::DocumentMediaType::XML
967 | message::DocumentMediaType::Javascript
968 | message::DocumentMediaType::Python
969 ) {
970 use base64::Engine;
971 let part = match data {
972 DocumentSourceKind::String(text) => PartKind::Text(text),
973 DocumentSourceKind::Base64(data) => {
974 let text = String::from_utf8(
976 base64::engine::general_purpose::STANDARD
977 .decode(&data)
978 .map_err(|e| {
979 MessageError::ConversionError(format!(
980 "Failed to decode base64: {e}"
981 ))
982 })?,
983 )
984 .map_err(|e| {
985 MessageError::ConversionError(format!(
986 "Invalid UTF-8 in document: {e}"
987 ))
988 })?;
989 PartKind::Text(text)
990 }
991 DocumentSourceKind::Url(file_uri) => PartKind::FileData(FileData {
992 mime_type: Some(media_type.to_mime_type().to_string()),
993 file_uri,
994 }),
995 DocumentSourceKind::Raw(_) => {
996 return Err(MessageError::ConversionError(
997 "Raw files not supported, encode as base64 first".to_string(),
998 ));
999 }
1000 DocumentSourceKind::FileId(_) => {
1001 return Err(MessageError::ConversionError(
1002 "Provider file IDs are not supported for Gemini documents"
1003 .to_string(),
1004 ));
1005 }
1006 DocumentSourceKind::Unknown => {
1007 return Err(MessageError::ConversionError(
1008 "Document has no body".to_string(),
1009 ));
1010 }
1011 };
1012
1013 Ok(Part {
1014 thought: Some(false),
1015 part,
1016 ..Default::default()
1017 })
1018 } else if !media_type.is_code() {
1019 let mime_type = media_type.to_mime_type().to_string();
1020
1021 let part = match data {
1022 DocumentSourceKind::Url(file_uri) => PartKind::FileData(FileData {
1023 mime_type: Some(mime_type),
1024 file_uri,
1025 }),
1026 DocumentSourceKind::Base64(data) | DocumentSourceKind::String(data) => {
1027 PartKind::InlineData(Blob { mime_type, data })
1028 }
1029 DocumentSourceKind::Raw(_) => {
1030 return Err(message::MessageError::ConversionError(
1031 "Raw files not supported, encode as base64 first".into(),
1032 ));
1033 }
1034 _ => {
1035 return Err(message::MessageError::ConversionError(
1036 "Document has no body".to_string(),
1037 ));
1038 }
1039 };
1040
1041 Ok(Part {
1042 thought: Some(false),
1043 part,
1044 ..Default::default()
1045 })
1046 } else {
1047 Err(message::MessageError::ConversionError(format!(
1048 "Unsupported document media type {media_type:?}"
1049 )))
1050 }
1051 }
1052
1053 message::UserContent::Audio(message::Audio {
1054 data, media_type, ..
1055 }) => {
1056 let Some(media_type) = media_type else {
1057 return Err(MessageError::ConversionError(
1058 "A mime type is required for audio inputs to Gemini".to_string(),
1059 ));
1060 };
1061
1062 let mime_type = media_type.to_mime_type().to_string();
1063
1064 let part = match data {
1065 DocumentSourceKind::Base64(data) => {
1066 PartKind::InlineData(Blob { data, mime_type })
1067 }
1068
1069 DocumentSourceKind::Url(file_uri) => PartKind::FileData(FileData {
1070 mime_type: Some(mime_type),
1071 file_uri,
1072 }),
1073 DocumentSourceKind::String(_) => {
1074 return Err(message::MessageError::ConversionError(
1075 "Strings cannot be used as audio files!".into(),
1076 ));
1077 }
1078 DocumentSourceKind::Raw(_) => {
1079 return Err(message::MessageError::ConversionError(
1080 "Raw files not supported, encode as base64 first".into(),
1081 ));
1082 }
1083 DocumentSourceKind::FileId(_) => {
1084 return Err(message::MessageError::ConversionError(
1085 "Provider file IDs are not supported for Gemini audio inputs"
1086 .into(),
1087 ));
1088 }
1089 DocumentSourceKind::Unknown => {
1090 return Err(message::MessageError::ConversionError(
1091 "Content has no body".to_string(),
1092 ));
1093 }
1094 };
1095
1096 Ok(Part {
1097 thought: Some(false),
1098 part,
1099 ..Default::default()
1100 })
1101 }
1102 message::UserContent::Video(message::Video {
1103 data,
1104 media_type,
1105 additional_params,
1106 ..
1107 }) => {
1108 let mime_type = media_type.map(|media_ty| media_ty.to_mime_type().to_string());
1109
1110 let part = match data {
1111 DocumentSourceKind::Url(file_uri) => {
1112 if file_uri.starts_with("https://www.youtube.com") {
1113 PartKind::FileData(FileData {
1114 mime_type,
1115 file_uri,
1116 })
1117 } else {
1118 if mime_type.is_none() {
1119 return Err(MessageError::ConversionError(
1120 "A mime type is required for non-Youtube video file inputs to Gemini"
1121 .to_string(),
1122 ));
1123 }
1124
1125 PartKind::FileData(FileData {
1126 mime_type,
1127 file_uri,
1128 })
1129 }
1130 }
1131 DocumentSourceKind::Base64(data) => {
1132 let Some(mime_type) = mime_type else {
1133 return Err(MessageError::ConversionError(
1134 "A media type is expected for base64 encoded strings"
1135 .to_string(),
1136 ));
1137 };
1138 PartKind::InlineData(Blob { mime_type, data })
1139 }
1140 DocumentSourceKind::String(_) => {
1141 return Err(message::MessageError::ConversionError(
1142 "Strings cannot be used as audio files!".into(),
1143 ));
1144 }
1145 DocumentSourceKind::Raw(_) => {
1146 return Err(message::MessageError::ConversionError(
1147 "Raw file data not supported, encode as base64 first".into(),
1148 ));
1149 }
1150 DocumentSourceKind::FileId(_) => {
1151 return Err(message::MessageError::ConversionError(
1152 "Provider file IDs are not supported for Gemini video inputs"
1153 .into(),
1154 ));
1155 }
1156 DocumentSourceKind::Unknown => {
1157 return Err(message::MessageError::ConversionError(
1158 "Media type for video is required for Gemini".to_string(),
1159 ));
1160 }
1161 };
1162
1163 Ok(Part {
1164 thought: Some(false),
1165 thought_signature: None,
1166 part,
1167 additional_params,
1168 })
1169 }
1170 }
1171 }
1172 }
1173
1174 impl TryFrom<message::AssistantContent> for Part {
1175 type Error = message::MessageError;
1176
1177 fn try_from(content: message::AssistantContent) -> Result<Self, Self::Error> {
1178 match content {
1179 message::AssistantContent::Text(message::Text { text, .. }) => Ok(text.into()),
1180 message::AssistantContent::Image(message::Image {
1181 data, media_type, ..
1182 }) => match media_type {
1183 Some(media_type) => match media_type {
1184 message::ImageMediaType::JPEG
1185 | message::ImageMediaType::PNG
1186 | message::ImageMediaType::WEBP
1187 | message::ImageMediaType::HEIC
1188 | message::ImageMediaType::HEIF => {
1189 let part = PartKind::try_from((media_type, data))?;
1190 Ok(Part {
1191 thought: Some(false),
1192 thought_signature: None,
1193 part,
1194 additional_params: None,
1195 })
1196 }
1197 _ => Err(message::MessageError::ConversionError(format!(
1198 "Unsupported image media type {media_type:?}"
1199 ))),
1200 },
1201 None => Err(message::MessageError::ConversionError(
1202 "Media type for image is required for Gemini".to_string(),
1203 )),
1204 },
1205 message::AssistantContent::ToolCall(tool_call) => Ok(tool_call.into()),
1206 message::AssistantContent::Reasoning(reasoning) => Ok(Part {
1207 thought: Some(true),
1208 thought_signature: reasoning.first_signature().map(str::to_owned),
1209 part: PartKind::Text(reasoning.display_text()),
1210 additional_params: None,
1211 }),
1212 }
1213 }
1214 }
1215
1216 impl From<message::ToolCall> for Part {
1217 fn from(tool_call: message::ToolCall) -> Self {
1218 Self {
1219 thought: Some(false),
1220 thought_signature: tool_call.signature,
1221 part: PartKind::FunctionCall(FunctionCall {
1222 name: tool_call.function.name,
1223 args: tool_call.function.arguments,
1224 id: tool_call.call_id,
1225 }),
1226 additional_params: None,
1227 }
1228 }
1229 }
1230
1231 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1234 #[serde(rename_all = "camelCase")]
1235 pub struct Blob {
1236 pub mime_type: String,
1239 pub data: String,
1241 }
1242
1243 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1246 pub struct FunctionCall {
1247 pub name: String,
1250 pub args: serde_json::Value,
1252 #[serde(skip_serializing_if = "Option::is_none")]
1254 pub id: Option<String>,
1255 }
1256
1257 impl From<message::ToolCall> for FunctionCall {
1258 fn from(tool_call: message::ToolCall) -> Self {
1259 Self {
1260 name: tool_call.function.name,
1261 args: tool_call.function.arguments,
1262 id: tool_call.call_id,
1263 }
1264 }
1265 }
1266
1267 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1271 pub struct FunctionResponse {
1272 pub name: String,
1275 #[serde(skip_serializing_if = "Option::is_none")]
1277 pub id: Option<String>,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1280 pub response: Option<serde_json::Value>,
1281 #[serde(skip_serializing_if = "Option::is_none")]
1283 pub parts: Option<Vec<FunctionResponsePart>>,
1284 }
1285
1286 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1288 #[serde(rename_all = "camelCase")]
1289 pub struct FunctionResponsePart {
1290 #[serde(skip_serializing_if = "Option::is_none")]
1292 pub inline_data: Option<FunctionResponseInlineData>,
1293 #[serde(skip_serializing_if = "Option::is_none")]
1295 pub file_data: Option<FileData>,
1296 }
1297
1298 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1300 #[serde(rename_all = "camelCase")]
1301 pub struct FunctionResponseInlineData {
1302 pub mime_type: String,
1304 pub data: String,
1306 #[serde(skip_serializing_if = "Option::is_none")]
1308 pub display_name: Option<String>,
1309 }
1310
1311 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1313 #[serde(rename_all = "camelCase")]
1314 pub struct FileData {
1315 pub mime_type: Option<String>,
1317 pub file_uri: String,
1319 }
1320
1321 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1322 pub struct SafetyRating {
1323 pub category: HarmCategory,
1324 pub probability: HarmProbability,
1325 }
1326
1327 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1328 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1329 pub enum HarmProbability {
1330 HarmProbabilityUnspecified,
1331 Negligible,
1332 Low,
1333 Medium,
1334 High,
1335 }
1336
1337 #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1338 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1339 pub enum HarmCategory {
1340 HarmCategoryUnspecified,
1341 HarmCategoryDerogatory,
1342 HarmCategoryToxicity,
1343 HarmCategoryViolence,
1344 HarmCategorySexually,
1345 HarmCategoryMedical,
1346 HarmCategoryDangerous,
1347 HarmCategoryHarassment,
1348 HarmCategoryHateSpeech,
1349 HarmCategorySexuallyExplicit,
1350 HarmCategoryDangerousContent,
1351 HarmCategoryCivicIntegrity,
1352 }
1353
1354 #[derive(Debug, Deserialize, Clone, Default, Serialize)]
1355 #[serde(rename_all = "camelCase")]
1356 pub struct UsageMetadata {
1357 #[serde(default)]
1358 pub prompt_token_count: i32,
1359 #[serde(skip_serializing_if = "Option::is_none")]
1360 pub cached_content_token_count: Option<i32>,
1361 #[serde(skip_serializing_if = "Option::is_none")]
1362 pub candidates_token_count: Option<i32>,
1363 #[serde(default)]
1364 pub total_token_count: i32,
1365 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub thoughts_token_count: Option<i32>,
1367 #[serde(default, skip_serializing_if = "Option::is_none")]
1368 pub prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
1369 #[serde(default, skip_serializing_if = "Option::is_none")]
1370 pub cache_tokens_details: Option<Vec<ModalityTokenCount>>,
1371 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1374 pub tool_use_prompt_token_count: Option<i32>,
1375 #[serde(default, skip_serializing_if = "Option::is_none")]
1376 pub tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
1377 #[serde(default, skip_serializing_if = "Option::is_none")]
1378 pub traffic_type: Option<TrafficType>,
1379 }
1380
1381 #[derive(Clone, Debug, Deserialize, Serialize)]
1382 #[serde(rename_all = "camelCase")]
1383 pub struct ModalityTokenCount {
1384 pub modality: Modality,
1385 #[serde(default)]
1386 pub token_count: i32,
1387 }
1388
1389 #[derive(Clone, Debug, Deserialize, Serialize)]
1390 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1391 pub enum Modality {
1392 ModalityUnspecified,
1393 Text,
1394 Image,
1395 Video,
1396 Audio,
1397 Document,
1398 }
1399
1400 #[derive(Clone, Debug, Deserialize, Serialize)]
1401 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1402 pub enum TrafficType {
1403 TrafficTypeUnspecified,
1404 OnDemand,
1405 ProvisionedThroughput,
1406 }
1407
1408 impl std::fmt::Display for UsageMetadata {
1409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1410 write!(
1411 f,
1412 "Prompt token count: {}\nCached content token count: {}\nCandidates token count: {}\nTotal token count: {}",
1413 self.prompt_token_count,
1414 match self.cached_content_token_count {
1415 Some(count) => count.to_string(),
1416 None => "n/a".to_string(),
1417 },
1418 match self.candidates_token_count {
1419 Some(count) => count.to_string(),
1420 None => "n/a".to_string(),
1421 },
1422 self.total_token_count
1423 )
1424 }
1425 }
1426
1427 impl GetTokenUsage for UsageMetadata {
1428 fn token_usage(&self) -> crate::completion::Usage {
1429 let mut usage = crate::completion::Usage::new();
1430
1431 usage.input_tokens = self.prompt_token_count as u64;
1432 usage.output_tokens = self.candidates_token_count.unwrap_or_default() as u64;
1433 usage.cached_input_tokens = self.cached_content_token_count.unwrap_or_default() as u64;
1434 usage.reasoning_tokens = self.thoughts_token_count.unwrap_or_default() as u64;
1435 usage.tool_use_prompt_tokens =
1436 self.tool_use_prompt_token_count.unwrap_or_default() as u64;
1437 usage.total_tokens = self.total_token_count as u64;
1438
1439 usage
1440 }
1441 }
1442
1443 #[derive(Debug, Deserialize, Serialize)]
1445 #[serde(rename_all = "camelCase")]
1446 pub struct PromptFeedback {
1447 pub block_reason: Option<BlockReason>,
1449 pub safety_ratings: Option<Vec<SafetyRating>>,
1451 }
1452
1453 #[derive(Debug, Deserialize, Serialize)]
1455 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1456 pub enum BlockReason {
1457 BlockReasonUnspecified,
1459 Safety,
1461 Other,
1463 Blocklist,
1465 ProhibitedContent,
1467 }
1468
1469 #[derive(Clone, Debug, Deserialize, Serialize)]
1470 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1471 pub enum FinishReason {
1472 FinishReasonUnspecified,
1474 Stop,
1476 MaxTokens,
1478 Safety,
1480 Recitation,
1482 Language,
1484 Other,
1486 Blocklist,
1488 ProhibitedContent,
1490 Spii,
1492 MalformedFunctionCall,
1494 UnexpectedToolCall,
1496 MissingThoughtSignature,
1498 TooManyToolCalls,
1500 MalformedResponse,
1502 }
1503
1504 #[derive(Clone, Debug, Deserialize, Serialize)]
1505 #[serde(rename_all = "camelCase")]
1506 pub struct CitationMetadata {
1507 #[serde(default)]
1508 pub citation_sources: Vec<CitationSource>,
1509 }
1510
1511 #[derive(Clone, Debug, Deserialize, Serialize)]
1512 #[serde(rename_all = "camelCase")]
1513 pub struct CitationSource {
1514 #[serde(skip_serializing_if = "Option::is_none")]
1515 pub uri: Option<String>,
1516 #[serde(skip_serializing_if = "Option::is_none")]
1517 pub start_index: Option<i32>,
1518 #[serde(skip_serializing_if = "Option::is_none")]
1519 pub end_index: Option<i32>,
1520 #[serde(skip_serializing_if = "Option::is_none")]
1521 pub license: Option<String>,
1522 }
1523
1524 #[derive(Clone, Debug, Deserialize, Serialize)]
1525 #[serde(rename_all = "camelCase")]
1526 pub struct LogprobsResult {
1527 #[serde(default)]
1528 pub top_candidates: Vec<TopCandidate>,
1529 #[serde(skip_serializing_if = "Option::is_none")]
1530 pub log_probability_sum: Option<f64>,
1531 #[serde(default)]
1532 pub chosen_candidates: Vec<LogProbCandidate>,
1533 }
1534
1535 #[derive(Clone, Debug, Deserialize, Serialize)]
1536 pub struct TopCandidate {
1537 #[serde(default)]
1538 pub candidates: Vec<LogProbCandidate>,
1539 }
1540
1541 #[derive(Clone, Debug, Deserialize, Serialize)]
1542 #[serde(rename_all = "camelCase")]
1543 pub struct LogProbCandidate {
1544 #[serde(skip_serializing_if = "Option::is_none")]
1545 pub token: Option<String>,
1546 #[serde(skip_serializing_if = "Option::is_none")]
1547 pub token_id: Option<i32>,
1548 #[serde(skip_serializing_if = "Option::is_none")]
1549 pub log_probability: Option<f64>,
1550 }
1551
1552 #[derive(Debug, Deserialize, Serialize)]
1559 #[serde(rename_all = "camelCase")]
1560 pub struct GenerationConfig {
1561 #[serde(skip_serializing_if = "Option::is_none")]
1564 pub stop_sequences: Option<Vec<String>>,
1565 #[serde(skip_serializing_if = "Option::is_none")]
1571 pub response_mime_type: Option<String>,
1572 #[serde(skip_serializing_if = "Option::is_none")]
1576 pub response_schema: Option<Schema>,
1577 #[serde(
1583 skip_serializing_if = "Option::is_none",
1584 rename = "_responseJsonSchema"
1585 )]
1586 pub _response_json_schema: Option<Value>,
1587 #[serde(skip_serializing_if = "Option::is_none")]
1589 pub response_json_schema: Option<Value>,
1590 #[serde(skip_serializing_if = "Option::is_none")]
1593 pub candidate_count: Option<i32>,
1594 #[serde(skip_serializing_if = "Option::is_none")]
1597 pub max_output_tokens: Option<u64>,
1598 #[serde(skip_serializing_if = "Option::is_none")]
1601 pub temperature: Option<f64>,
1602 #[serde(skip_serializing_if = "Option::is_none")]
1609 pub top_p: Option<f64>,
1610 #[serde(skip_serializing_if = "Option::is_none")]
1616 pub top_k: Option<i32>,
1617 #[serde(skip_serializing_if = "Option::is_none")]
1623 pub presence_penalty: Option<f64>,
1624 #[serde(skip_serializing_if = "Option::is_none")]
1632 pub frequency_penalty: Option<f64>,
1633 #[serde(skip_serializing_if = "Option::is_none")]
1635 pub response_logprobs: Option<bool>,
1636 #[serde(skip_serializing_if = "Option::is_none")]
1639 pub logprobs: Option<i32>,
1640 #[serde(skip_serializing_if = "Option::is_none")]
1642 pub thinking_config: Option<ThinkingConfig>,
1643 #[serde(skip_serializing_if = "Option::is_none")]
1645 pub response_modalities: Option<Vec<ResponseModality>>,
1646 #[serde(skip_serializing_if = "Option::is_none")]
1647 pub image_config: Option<ImageConfig>,
1648 }
1649
1650 impl Default for GenerationConfig {
1651 fn default() -> Self {
1652 Self {
1653 temperature: Some(1.0),
1654 max_output_tokens: Some(4096),
1655 stop_sequences: None,
1656 response_mime_type: None,
1657 response_schema: None,
1658 _response_json_schema: None,
1659 response_json_schema: None,
1660 candidate_count: None,
1661 top_p: None,
1662 top_k: None,
1663 presence_penalty: None,
1664 frequency_penalty: None,
1665 response_logprobs: None,
1666 logprobs: None,
1667 thinking_config: None,
1668 response_modalities: None,
1669 image_config: None,
1670 }
1671 }
1672 }
1673
1674 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1676 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1677 pub enum ResponseModality {
1678 Text,
1679 Image,
1680 Audio,
1681 }
1682
1683 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1685 #[serde(rename_all = "snake_case")]
1686 pub enum ThinkingLevel {
1687 Minimal,
1688 Low,
1689 Medium,
1690 High,
1691 }
1692
1693 #[derive(Debug, Deserialize, Serialize)]
1697 #[serde(rename_all = "camelCase")]
1698 pub struct ThinkingConfig {
1699 #[serde(skip_serializing_if = "Option::is_none")]
1701 pub thinking_budget: Option<u32>,
1702 #[serde(skip_serializing_if = "Option::is_none")]
1704 pub thinking_level: Option<ThinkingLevel>,
1705 #[serde(skip_serializing_if = "Option::is_none")]
1707 pub include_thoughts: Option<bool>,
1708 }
1709
1710 #[derive(Debug, Deserialize, Serialize)]
1711 #[serde(rename_all = "camelCase")]
1712 pub struct ImageConfig {
1713 #[serde(skip_serializing_if = "Option::is_none")]
1714 pub aspect_ratio: Option<String>,
1715 #[serde(skip_serializing_if = "Option::is_none")]
1716 pub image_size: Option<String>,
1717 }
1718
1719 #[derive(Debug, Deserialize, Serialize, Clone)]
1723 pub struct Schema {
1724 pub r#type: String,
1725 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub format: Option<String>,
1727 #[serde(skip_serializing_if = "Option::is_none")]
1728 pub description: Option<String>,
1729 #[serde(skip_serializing_if = "Option::is_none")]
1730 pub nullable: Option<bool>,
1731 #[serde(skip_serializing_if = "Option::is_none")]
1732 pub r#enum: Option<Vec<String>>,
1733 #[serde(skip_serializing_if = "Option::is_none")]
1734 pub max_items: Option<i32>,
1735 #[serde(skip_serializing_if = "Option::is_none")]
1736 pub min_items: Option<i32>,
1737 #[serde(skip_serializing_if = "Option::is_none")]
1738 pub properties: Option<HashMap<String, Schema>>,
1739 #[serde(skip_serializing_if = "Option::is_none")]
1740 pub required: Option<Vec<String>>,
1741 #[serde(skip_serializing_if = "Option::is_none")]
1742 pub items: Option<Box<Schema>>,
1743 }
1744
1745 pub fn tool_parameters_to_schema(parameters: Value) -> Result<Option<Schema>, CompletionError> {
1751 if parameters.is_null() || parameters == json!({"type": "object", "properties": {}}) {
1752 Ok(None)
1753 } else {
1754 parameters.try_into().map(Some)
1755 }
1756 }
1757
1758 pub fn flatten_schema(mut schema: Value) -> Result<Value, CompletionError> {
1764 let defs = if let Some(obj) = schema.as_object() {
1766 obj.get("$defs").or_else(|| obj.get("definitions")).cloned()
1767 } else {
1768 None
1769 };
1770
1771 let Some(defs_value) = defs else {
1772 return Ok(schema);
1773 };
1774
1775 let Some(defs_obj) = defs_value.as_object() else {
1776 return Err(CompletionError::ResponseError(
1777 "$defs must be an object".into(),
1778 ));
1779 };
1780
1781 resolve_refs(&mut schema, defs_obj)?;
1782
1783 if let Some(obj) = schema.as_object_mut() {
1785 obj.remove("$defs");
1786 obj.remove("definitions");
1787 }
1788
1789 Ok(schema)
1790 }
1791
1792 fn resolve_refs(
1795 value: &mut Value,
1796 defs: &serde_json::Map<String, Value>,
1797 ) -> Result<(), CompletionError> {
1798 match value {
1799 Value::Object(obj) => {
1800 if let Some(ref_value) = obj.get("$ref")
1801 && let Some(ref_str) = ref_value.as_str()
1802 {
1803 let def_name = parse_ref_path(ref_str)?;
1805
1806 let def = defs.get(&def_name).ok_or_else(|| {
1807 CompletionError::ResponseError(format!("Reference not found: {}", ref_str))
1808 })?;
1809
1810 let mut resolved = def.clone();
1811 resolve_refs(&mut resolved, defs)?;
1812 *value = resolved;
1813 return Ok(());
1814 }
1815
1816 for (_, v) in obj.iter_mut() {
1817 resolve_refs(v, defs)?;
1818 }
1819 }
1820 Value::Array(arr) => {
1821 for item in arr.iter_mut() {
1822 resolve_refs(item, defs)?;
1823 }
1824 }
1825 _ => {}
1826 }
1827
1828 Ok(())
1829 }
1830
1831 fn parse_ref_path(ref_str: &str) -> Result<String, CompletionError> {
1837 if let Some(fragment) = ref_str.strip_prefix('#') {
1838 if let Some(name) = fragment.strip_prefix("/$defs/") {
1839 Ok(name.to_string())
1840 } else if let Some(name) = fragment.strip_prefix("/definitions/") {
1841 Ok(name.to_string())
1842 } else {
1843 Err(CompletionError::ResponseError(format!(
1844 "Unsupported reference format: {}",
1845 ref_str
1846 )))
1847 }
1848 } else {
1849 Err(CompletionError::ResponseError(format!(
1850 "Only fragment references (#/...) are supported: {}",
1851 ref_str
1852 )))
1853 }
1854 }
1855
1856 fn extract_type(type_value: &Value) -> Option<String> {
1859 if let Some(t) = type_value.as_str() {
1860 return Some(t.to_string());
1861 }
1862
1863 type_value.as_array().and_then(|arr| {
1864 arr.iter()
1865 .filter_map(|v| v.as_str())
1866 .find(|t| *t != "null")
1867 .or_else(|| arr.iter().find_map(|v| v.as_str()))
1868 .map(str::to_owned)
1869 })
1870 }
1871
1872 fn schema_is_null(obj: &serde_json::Map<String, Value>) -> bool {
1873 obj.get("type")
1874 .and_then(extract_type)
1875 .as_deref()
1876 .is_some_and(|t| t == "null")
1877 }
1878
1879 fn schema_is_nullable(obj: &serde_json::Map<String, Value>) -> bool {
1880 obj.get("nullable")
1881 .and_then(|v| v.as_bool())
1882 .unwrap_or(false)
1883 || obj
1884 .get("type")
1885 .and_then(|v| v.as_array())
1886 .is_some_and(|arr| arr.iter().any(|v| v.as_str() == Some("null")))
1887 || ["anyOf", "oneOf", "allOf"].iter().any(|key| {
1888 obj.get(*key).and_then(|v| v.as_array()).is_some_and(|arr| {
1889 arr.iter()
1890 .filter_map(|schema| schema.as_object())
1891 .any(schema_is_null)
1892 })
1893 })
1894 }
1895
1896 fn extract_type_from_composition(composition: &Value) -> Option<String> {
1899 composition.as_array().and_then(|arr| {
1900 arr.iter().find_map(|schema| {
1901 let obj = schema.as_object()?;
1902 if schema_is_null(obj) {
1903 return None;
1904 }
1905
1906 obj.get("type").and_then(extract_type).or_else(|| {
1907 if obj.contains_key("properties") {
1908 Some("object".to_string())
1909 } else if obj.contains_key("enum") {
1910 Some("string".to_string())
1912 } else {
1913 None
1914 }
1915 })
1916 })
1917 })
1918 }
1919
1920 fn extract_schema_from_composition(
1923 composition: &Value,
1924 ) -> Option<serde_json::Map<String, Value>> {
1925 composition.as_array().and_then(|arr| {
1926 arr.iter().find_map(|schema| {
1927 let obj = schema.as_object()?;
1928 if schema_is_null(obj) {
1929 None
1930 } else {
1931 Some(obj.clone())
1932 }
1933 })
1934 })
1935 }
1936
1937 fn extract_schema_from_composition_obj(
1938 obj: &serde_json::Map<String, Value>,
1939 ) -> Option<serde_json::Map<String, Value>> {
1940 obj.get("anyOf")
1941 .and_then(extract_schema_from_composition)
1942 .or_else(|| obj.get("oneOf").and_then(extract_schema_from_composition))
1943 .or_else(|| obj.get("allOf").and_then(extract_schema_from_composition))
1944 }
1945
1946 fn infer_type(obj: &serde_json::Map<String, Value>) -> String {
1949 if let Some(type_val) = obj.get("type")
1951 && let Some(type_str) = extract_type(type_val)
1952 {
1953 return type_str;
1954 }
1955
1956 if let Some(any_of) = obj.get("anyOf")
1958 && let Some(type_str) = extract_type_from_composition(any_of)
1959 {
1960 return type_str;
1961 }
1962
1963 if let Some(one_of) = obj.get("oneOf")
1964 && let Some(type_str) = extract_type_from_composition(one_of)
1965 {
1966 return type_str;
1967 }
1968
1969 if let Some(all_of) = obj.get("allOf")
1970 && let Some(type_str) = extract_type_from_composition(all_of)
1971 {
1972 return type_str;
1973 }
1974
1975 if obj.contains_key("properties") {
1977 "object".to_string()
1978 } else if obj.contains_key("enum") {
1979 "string".to_string()
1980 } else {
1981 String::new()
1982 }
1983 }
1984
1985 impl TryFrom<Value> for Schema {
1986 type Error = CompletionError;
1987
1988 fn try_from(value: Value) -> Result<Self, Self::Error> {
1989 let flattened_val = flatten_schema(value)?;
1990 if let Some(obj) = flattened_val.as_object() {
1991 let composition_source = extract_schema_from_composition_obj(obj);
1994 let props_source = if obj.get("properties").is_none() {
1995 composition_source.clone().unwrap_or(obj.clone())
1996 } else {
1997 obj.clone()
1998 };
1999
2000 let schema_type = infer_type(obj);
2001 let items = obj
2002 .get("items")
2003 .or_else(|| props_source.get("items"))
2004 .and_then(|v| v.clone().try_into().ok())
2005 .map(Box::new);
2006
2007 let items = if schema_type == "array" && items.is_none() {
2010 Some(Box::new(Schema {
2011 r#type: "string".to_string(),
2012 format: None,
2013 description: None,
2014 nullable: None,
2015 r#enum: None,
2016 max_items: None,
2017 min_items: None,
2018 properties: None,
2019 required: None,
2020 items: None,
2021 }))
2022 } else {
2023 items
2024 };
2025
2026 Ok(Schema {
2027 r#type: schema_type,
2028 format: obj
2029 .get("format")
2030 .or_else(|| props_source.get("format"))
2031 .and_then(|v| v.as_str())
2032 .map(String::from),
2033 description: obj
2034 .get("description")
2035 .or_else(|| props_source.get("description"))
2036 .and_then(|v| v.as_str())
2037 .map(String::from),
2038 nullable: if schema_is_nullable(obj)
2039 || composition_source.as_ref().is_some_and(schema_is_nullable)
2040 {
2041 Some(true)
2042 } else {
2043 None
2044 },
2045 r#enum: obj
2046 .get("enum")
2047 .or_else(|| props_source.get("enum"))
2048 .and_then(|v| v.as_array())
2049 .map(|arr| {
2050 arr.iter()
2051 .filter_map(|v| v.as_str().map(String::from))
2052 .collect()
2053 }),
2054 max_items: obj
2055 .get("maxItems")
2056 .and_then(|v| v.as_i64())
2057 .map(|v| v as i32),
2058 min_items: obj
2059 .get("minItems")
2060 .and_then(|v| v.as_i64())
2061 .map(|v| v as i32),
2062 properties: props_source
2063 .get("properties")
2064 .and_then(|v| v.as_object())
2065 .map(|map| {
2066 map.iter()
2067 .filter_map(|(k, v)| {
2068 v.clone().try_into().ok().map(|schema| (k.clone(), schema))
2069 })
2070 .collect()
2071 }),
2072 required: props_source
2073 .get("required")
2074 .and_then(|v| v.as_array())
2075 .map(|arr| {
2076 arr.iter()
2077 .filter_map(|v| v.as_str().map(String::from))
2078 .collect()
2079 }),
2080 items,
2081 })
2082 } else {
2083 Err(CompletionError::ResponseError(
2084 "Expected a JSON object for Schema".into(),
2085 ))
2086 }
2087 }
2088 }
2089
2090 #[derive(Debug, Serialize)]
2091 #[serde(rename_all = "camelCase")]
2092 pub struct GenerateContentRequest {
2093 pub contents: Vec<Content>,
2094 #[serde(skip_serializing_if = "Option::is_none")]
2095 pub tools: Option<Vec<Value>>,
2096 pub tool_config: Option<ToolConfig>,
2097 pub generation_config: Option<GenerationConfig>,
2099 pub safety_settings: Option<Vec<SafetySetting>>,
2113 pub system_instruction: Option<Content>,
2116 #[serde(flatten, skip_serializing_if = "Option::is_none")]
2119 pub additional_params: Option<serde_json::Value>,
2120 }
2121
2122 #[derive(Debug, Serialize)]
2123 #[serde(rename_all = "camelCase")]
2124 pub struct Tool {
2125 pub function_declarations: Vec<FunctionDeclaration>,
2126 pub code_execution: Option<CodeExecution>,
2127 }
2128
2129 #[derive(Debug, Serialize, Clone)]
2130 #[serde(rename_all = "camelCase")]
2131 pub struct FunctionDeclaration {
2132 pub name: String,
2133 pub description: String,
2134 #[serde(skip_serializing_if = "Option::is_none")]
2135 pub parameters: Option<Schema>,
2136 }
2137
2138 #[derive(Debug, Serialize, Deserialize)]
2139 #[serde(rename_all = "camelCase")]
2140 pub struct ToolConfig {
2141 pub function_calling_config: Option<FunctionCallingMode>,
2142 }
2143
2144 #[derive(Debug, Serialize, Deserialize, Default)]
2145 #[serde(tag = "mode", rename_all = "UPPERCASE")]
2146 pub enum FunctionCallingMode {
2147 #[default]
2148 Auto,
2149 None,
2150 Any {
2151 #[serde(skip_serializing_if = "Option::is_none")]
2152 allowed_function_names: Option<Vec<String>>,
2153 },
2154 }
2155
2156 impl TryFrom<message::ToolChoice> for FunctionCallingMode {
2157 type Error = CompletionError;
2158 fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
2159 let res = match value {
2160 message::ToolChoice::Auto => Self::Auto,
2161 message::ToolChoice::None => Self::None,
2162 message::ToolChoice::Required => Self::Any {
2163 allowed_function_names: None,
2164 },
2165 message::ToolChoice::Specific { function_names } => Self::Any {
2166 allowed_function_names: Some(function_names),
2167 },
2168 };
2169
2170 Ok(res)
2171 }
2172 }
2173
2174 #[derive(Debug, Serialize)]
2175 pub struct CodeExecution {}
2176
2177 #[derive(Debug, Serialize)]
2178 #[serde(rename_all = "camelCase")]
2179 pub struct SafetySetting {
2180 pub category: HarmCategory,
2181 pub threshold: HarmBlockThreshold,
2182 }
2183
2184 #[derive(Debug, Serialize)]
2185 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2186 pub enum HarmBlockThreshold {
2187 HarmBlockThresholdUnspecified,
2188 BlockLowAndAbove,
2189 BlockMediumAndAbove,
2190 BlockOnlyHigh,
2191 BlockNone,
2192 Off,
2193 }
2194}
2195
2196#[cfg(test)]
2197mod tests {
2198 use crate::{
2199 message,
2200 providers::gemini::completion::gemini_api_types::{
2201 CitationMetadata, ContentCandidate, FinishReason, FunctionCall,
2202 GenerateContentResponse, LogprobsResult, ModalityTokenCount, Schema, TopCandidate,
2203 UsageMetadata, flatten_schema, tool_parameters_to_schema,
2204 },
2205 };
2206
2207 use super::*;
2208 use serde_json::json;
2209
2210 #[test]
2211 fn test_usage_metadata_deserializes_without_total_token_count() {
2212 let usage: UsageMetadata =
2215 serde_json::from_str(r#"{"promptTokenCount": 12}"#).expect("should deserialize");
2216 assert_eq!(usage.total_token_count, 0);
2217 assert_eq!(usage.prompt_token_count, 12);
2218 }
2219
2220 #[test]
2221 fn test_generate_content_response_deserializes_without_candidates_or_response_id() {
2222 let response: GenerateContentResponse = serde_json::from_value(json!({
2225 "promptFeedback": {
2226 "blockReason": "SAFETY"
2227 }
2228 }))
2229 .expect("blocked prompt response should deserialize");
2230
2231 assert!(response.response_id.is_empty());
2232 assert!(response.candidates.is_empty());
2233
2234 let error = completion::CompletionResponse::try_from(response)
2235 .expect_err("empty candidates should become a response error");
2236 assert!(error.to_string().contains("No response candidates"));
2237 }
2238
2239 #[test]
2240 fn test_modality_token_count_deserializes_without_zero_token_count() {
2241 let count: ModalityTokenCount = serde_json::from_value(json!({
2242 "modality": "TEXT"
2243 }))
2244 .expect("zero tokenCount may be omitted");
2245
2246 assert_eq!(count.token_count, 0);
2247 }
2248
2249 #[test]
2250 fn test_response_metadata_repeated_fields_deserialize_when_omitted() {
2251 let citation_metadata: CitationMetadata =
2252 serde_json::from_value(json!({})).expect("empty citation metadata should deserialize");
2253 assert!(citation_metadata.citation_sources.is_empty());
2254
2255 let logprobs: LogprobsResult =
2256 serde_json::from_value(json!({})).expect("empty logprobs result should deserialize");
2257 assert!(logprobs.top_candidates.is_empty());
2258 assert_eq!(logprobs.log_probability_sum, None);
2259 assert!(logprobs.chosen_candidates.is_empty());
2260
2261 let top_candidate: TopCandidate =
2262 serde_json::from_value(json!({})).expect("empty top candidate should deserialize");
2263 assert!(top_candidate.candidates.is_empty());
2264 }
2265
2266 #[test]
2267 fn test_logprobs_result_deserializes_official_json_field_names() {
2268 let logprobs: LogprobsResult = serde_json::from_value(json!({
2269 "topCandidates": [
2270 {
2271 "candidates": [
2272 {
2273 "token": "Hello",
2274 "tokenId": 123,
2275 "logProbability": -0.1
2276 },
2277 {
2278 "token": "Hi",
2279 "tokenId": 124,
2280 "logProbability": -1.25
2281 }
2282 ]
2283 }
2284 ],
2285 "logProbabilitySum": -0.1,
2286 "chosenCandidates": [
2287 {
2288 "token": "Hello",
2289 "tokenId": 123,
2290 "logProbability": -0.1
2291 }
2292 ]
2293 }))
2294 .expect("official Gemini logprobs result should deserialize");
2295
2296 assert_eq!(logprobs.top_candidates.len(), 1);
2297 assert_eq!(logprobs.top_candidates[0].candidates.len(), 2);
2298 assert_eq!(
2299 logprobs.top_candidates[0].candidates[0].token.as_deref(),
2300 Some("Hello")
2301 );
2302 assert_eq!(logprobs.top_candidates[0].candidates[0].token_id, Some(123));
2303 assert_eq!(
2304 logprobs.top_candidates[0].candidates[0].log_probability,
2305 Some(-0.1)
2306 );
2307 assert_eq!(logprobs.log_probability_sum, Some(-0.1));
2308 assert_eq!(logprobs.chosen_candidates.len(), 1);
2309 assert_eq!(
2310 logprobs.chosen_candidates[0].token.as_deref(),
2311 Some("Hello")
2312 );
2313 assert_eq!(logprobs.chosen_candidates[0].token_id, Some(123));
2314 assert_eq!(logprobs.chosen_candidates[0].log_probability, Some(-0.1));
2315 }
2316
2317 #[test]
2318 fn test_resolve_request_model_uses_override() {
2319 let request = CompletionRequest {
2320 model: Some("gemini-2.5-flash".to_string()),
2321 preamble: None,
2322 chat_history: crate::OneOrMany::one("Hello".into()),
2323 documents: vec![],
2324 tools: vec![],
2325 temperature: None,
2326 max_tokens: None,
2327 tool_choice: None,
2328 additional_params: None,
2329 output_schema: None,
2330 record_telemetry_content: false,
2331 };
2332
2333 let request_model = resolve_request_model("gemini-2.0-flash", &request);
2334 assert_eq!(request_model, "gemini-2.5-flash");
2335 assert_eq!(
2336 completion_endpoint(&request_model),
2337 "/v1beta/models/gemini-2.5-flash:generateContent"
2338 );
2339 assert_eq!(
2340 streaming_endpoint(&request_model),
2341 "/v1beta/models/gemini-2.5-flash:streamGenerateContent"
2342 );
2343 }
2344
2345 #[test]
2346 fn test_resolve_request_model_uses_default_when_unset() {
2347 let request = CompletionRequest {
2348 model: None,
2349 preamble: None,
2350 chat_history: crate::OneOrMany::one("Hello".into()),
2351 documents: vec![],
2352 tools: vec![],
2353 temperature: None,
2354 max_tokens: None,
2355 tool_choice: None,
2356 additional_params: None,
2357 output_schema: None,
2358 record_telemetry_content: false,
2359 };
2360
2361 assert_eq!(
2362 resolve_request_model("gemini-2.0-flash", &request),
2363 "gemini-2.0-flash"
2364 );
2365 }
2366
2367 #[test]
2368 fn test_deserialize_message_user() {
2369 let raw_message = r#"{
2370 "parts": [
2371 {"text": "Hello, world!"},
2372 {"inlineData": {"mimeType": "image/png", "data": "base64encodeddata"}},
2373 {"functionCall": {"name": "test_function", "args": {"arg1": "value1"}}},
2374 {"functionResponse": {"name": "test_function", "response": {"result": "success"}}},
2375 {"fileData": {"mimeType": "application/pdf", "fileUri": "http://example.com/file.pdf"}},
2376 {"executableCode": {"code": "print('Hello, world!')", "language": "PYTHON"}},
2377 {"codeExecutionResult": {"output": "Hello, world!", "outcome": "OUTCOME_OK"}}
2378 ],
2379 "role": "user"
2380 }"#;
2381
2382 let content: Content = {
2383 let jd = &mut serde_json::Deserializer::from_str(raw_message);
2384 serde_path_to_error::deserialize(jd).unwrap_or_else(|err| {
2385 panic!("Deserialization error at {}: {}", err.path(), err);
2386 })
2387 };
2388 assert_eq!(content.role, Some(Role::User));
2389 assert_eq!(content.parts.len(), 7);
2390
2391 let parts: Vec<Part> = content.parts.into_iter().collect();
2392
2393 if let Part {
2394 part: PartKind::Text(text),
2395 ..
2396 } = &parts[0]
2397 {
2398 assert_eq!(text, "Hello, world!");
2399 } else {
2400 panic!("Expected text part");
2401 }
2402
2403 if let Part {
2404 part: PartKind::InlineData(inline_data),
2405 ..
2406 } = &parts[1]
2407 {
2408 assert_eq!(inline_data.mime_type, "image/png");
2409 assert_eq!(inline_data.data, "base64encodeddata");
2410 } else {
2411 panic!("Expected inline data part");
2412 }
2413
2414 if let Part {
2415 part: PartKind::FunctionCall(function_call),
2416 ..
2417 } = &parts[2]
2418 {
2419 assert_eq!(function_call.name, "test_function");
2420 assert_eq!(
2421 function_call.args.as_object().unwrap().get("arg1").unwrap(),
2422 "value1"
2423 );
2424 } else {
2425 panic!("Expected function call part");
2426 }
2427
2428 if let Part {
2429 part: PartKind::FunctionResponse(function_response),
2430 ..
2431 } = &parts[3]
2432 {
2433 assert_eq!(function_response.name, "test_function");
2434 assert_eq!(
2435 function_response
2436 .response
2437 .as_ref()
2438 .unwrap()
2439 .get("result")
2440 .unwrap(),
2441 "success"
2442 );
2443 } else {
2444 panic!("Expected function response part");
2445 }
2446
2447 if let Part {
2448 part: PartKind::FileData(file_data),
2449 ..
2450 } = &parts[4]
2451 {
2452 assert_eq!(file_data.mime_type.as_ref().unwrap(), "application/pdf");
2453 assert_eq!(file_data.file_uri, "http://example.com/file.pdf");
2454 } else {
2455 panic!("Expected file data part");
2456 }
2457
2458 if let Part {
2459 part: PartKind::ExecutableCode(executable_code),
2460 ..
2461 } = &parts[5]
2462 {
2463 assert_eq!(executable_code.code, "print('Hello, world!')");
2464 } else {
2465 panic!("Expected executable code part");
2466 }
2467
2468 if let Part {
2469 part: PartKind::CodeExecutionResult(code_execution_result),
2470 ..
2471 } = &parts[6]
2472 {
2473 assert_eq!(
2474 code_execution_result.clone().output.unwrap(),
2475 "Hello, world!"
2476 );
2477 } else {
2478 panic!("Expected code execution result part");
2479 }
2480 }
2481
2482 #[test]
2483 fn test_deserialize_message_model() {
2484 let json_data = json!({
2485 "parts": [{"text": "Hello, user!"}],
2486 "role": "model"
2487 });
2488
2489 let content: Content = serde_json::from_value(json_data).unwrap();
2490 assert_eq!(content.role, Some(Role::Model));
2491 assert_eq!(content.parts.len(), 1);
2492 if let Some(Part {
2493 part: PartKind::Text(text),
2494 ..
2495 }) = content.parts.first()
2496 {
2497 assert_eq!(text, "Hello, user!");
2498 } else {
2499 panic!("Expected text part");
2500 }
2501 }
2502
2503 #[test]
2504 fn test_message_conversion_user() {
2505 let msg = message::Message::user("Hello, world!");
2506 let content: Content = msg.try_into().unwrap();
2507 assert_eq!(content.role, Some(Role::User));
2508 assert_eq!(content.parts.len(), 1);
2509 if let Some(Part {
2510 part: PartKind::Text(text),
2511 ..
2512 }) = &content.parts.first()
2513 {
2514 assert_eq!(text, "Hello, world!");
2515 } else {
2516 panic!("Expected text part");
2517 }
2518 }
2519
2520 #[test]
2521 fn test_message_conversion_model() {
2522 let msg = message::Message::assistant("Hello, user!");
2523
2524 let content: Content = msg.try_into().unwrap();
2525 assert_eq!(content.role, Some(Role::Model));
2526 assert_eq!(content.parts.len(), 1);
2527 if let Some(Part {
2528 part: PartKind::Text(text),
2529 ..
2530 }) = &content.parts.first()
2531 {
2532 assert_eq!(text, "Hello, user!");
2533 } else {
2534 panic!("Expected text part");
2535 }
2536 }
2537
2538 #[test]
2539 fn test_thought_signature_is_preserved_from_response_reasoning_part() {
2540 let response = GenerateContentResponse {
2541 response_id: "resp_1".to_string(),
2542 candidates: vec![ContentCandidate {
2543 content: Some(Content {
2544 parts: vec![Part {
2545 thought: Some(true),
2546 thought_signature: Some("thought_sig_123".to_string()),
2547 part: PartKind::Text("thinking text".to_string()),
2548 additional_params: None,
2549 }],
2550 role: Some(Role::Model),
2551 }),
2552 finish_reason: Some(FinishReason::Stop),
2553 safety_ratings: None,
2554 citation_metadata: None,
2555 token_count: None,
2556 avg_logprobs: None,
2557 logprobs_result: None,
2558 index: Some(0),
2559 finish_message: None,
2560 }],
2561 prompt_feedback: None,
2562 usage_metadata: None,
2563 model_version: None,
2564 };
2565
2566 let converted: crate::completion::CompletionResponse<GenerateContentResponse> =
2567 response.try_into().expect("convert response");
2568 let first = converted.choice.first();
2569 assert!(matches!(
2570 first,
2571 message::AssistantContent::Reasoning(message::Reasoning { content, .. })
2572 if matches!(
2573 content.first(),
2574 Some(message::ReasoningContent::Text {
2575 text,
2576 signature: Some(signature)
2577 }) if text == "thinking text" && signature == "thought_sig_123"
2578 )
2579 ));
2580 }
2581
2582 #[test]
2583 fn test_tool_protocol_finish_reason_returns_response_error() {
2584 for (reason, finish_message) in [
2585 (
2586 FinishReason::MalformedFunctionCall,
2587 "malformed function call: default_api",
2588 ),
2589 (
2590 FinishReason::UnexpectedToolCall,
2591 "unexpected tool call: default_api",
2592 ),
2593 (
2594 FinishReason::MissingThoughtSignature,
2595 "missing thought signature for tool call",
2596 ),
2597 (
2598 FinishReason::TooManyToolCalls,
2599 "too many tool calls in response",
2600 ),
2601 (
2602 FinishReason::MalformedResponse,
2603 "malformed response from provider",
2604 ),
2605 ] {
2606 let reason_name = format!("{reason:?}");
2607 let response = GenerateContentResponse {
2608 response_id: "resp_tool_protocol_error".to_string(),
2609 candidates: vec![ContentCandidate {
2610 content: Some(Content {
2611 parts: vec![Part {
2612 thought: None,
2613 thought_signature: None,
2614 part: PartKind::FunctionCall(FunctionCall {
2615 name: "default_api".to_string(),
2616 args: json!({"x": 1}),
2617 id: None,
2618 }),
2619 additional_params: None,
2620 }],
2621 role: Some(Role::Model),
2622 }),
2623 finish_reason: Some(reason),
2624 safety_ratings: None,
2625 citation_metadata: None,
2626 token_count: None,
2627 avg_logprobs: None,
2628 logprobs_result: None,
2629 index: Some(0),
2630 finish_message: Some(finish_message.to_string()),
2631 }],
2632 prompt_feedback: None,
2633 usage_metadata: None,
2634 model_version: None,
2635 };
2636
2637 let err = crate::completion::CompletionResponse::<GenerateContentResponse>::try_from(
2638 response,
2639 )
2640 .expect_err("tool protocol finish reason should fail");
2641
2642 assert!(matches!(
2643 err,
2644 CompletionError::ResponseError(message)
2645 if message.contains(&reason_name)
2646 && message.contains(finish_message)
2647 ));
2648 }
2649 }
2650
2651 #[test]
2652 fn test_completion_response_usage_preserves_cached_and_reasoning_tokens() {
2653 let response = GenerateContentResponse {
2654 response_id: "resp_1".to_string(),
2655 candidates: vec![ContentCandidate {
2656 content: Some(Content {
2657 parts: vec![Part {
2658 thought: None,
2659 thought_signature: None,
2660 part: PartKind::Text("answer".to_string()),
2661 additional_params: None,
2662 }],
2663 role: Some(Role::Model),
2664 }),
2665 finish_reason: Some(FinishReason::Stop),
2666 safety_ratings: None,
2667 citation_metadata: None,
2668 token_count: None,
2669 avg_logprobs: None,
2670 logprobs_result: None,
2671 index: Some(0),
2672 finish_message: None,
2673 }],
2674 prompt_feedback: None,
2675 usage_metadata: Some(UsageMetadata {
2676 prompt_token_count: 40,
2677 cached_content_token_count: Some(20),
2678 candidates_token_count: Some(30),
2679 total_token_count: 100,
2680 thoughts_token_count: Some(10),
2681 prompt_tokens_details: None,
2682 cache_tokens_details: None,
2683 candidates_tokens_details: None,
2684 tool_use_prompt_token_count: Some(12),
2685 tool_use_prompt_tokens_details: None,
2686 traffic_type: None,
2687 }),
2688 model_version: Some("gemini-2.0-flash-001".to_string()),
2689 };
2690
2691 let converted: crate::completion::CompletionResponse<GenerateContentResponse> =
2692 response.try_into().expect("convert response");
2693
2694 assert_eq!(converted.usage.input_tokens, 40);
2695 assert_eq!(converted.usage.cached_input_tokens, 20);
2696 assert_eq!(converted.usage.output_tokens, 30);
2697 assert_eq!(converted.usage.reasoning_tokens, 10);
2698 assert_eq!(converted.usage.tool_use_prompt_tokens, 12);
2699 assert_eq!(converted.usage.total_tokens, 100);
2700 }
2701
2702 #[test]
2703 fn test_reasoning_signature_is_emitted_in_gemini_part() {
2704 let msg = message::Message::Assistant {
2705 id: None,
2706 content: OneOrMany::one(message::AssistantContent::Reasoning(
2707 message::Reasoning::new_with_signature(
2708 "structured thought",
2709 Some("reuse_sig_456".to_string()),
2710 ),
2711 )),
2712 };
2713
2714 let converted: Content = msg.try_into().expect("convert message");
2715 let first = converted.parts.first().expect("reasoning part");
2716 assert_eq!(first.thought, Some(true));
2717 assert_eq!(first.thought_signature.as_deref(), Some("reuse_sig_456"));
2718 assert!(matches!(
2719 &first.part,
2720 PartKind::Text(text) if text == "structured thought"
2721 ));
2722 }
2723
2724 #[test]
2725 fn test_message_conversion_tool_call() {
2726 let tool_call = message::ToolCall {
2727 id: "test_tool".to_string(),
2728 call_id: Some("call-123".to_string()),
2729 function: message::ToolFunction {
2730 name: "test_function".to_string(),
2731 arguments: json!({"arg1": "value1"}),
2732 },
2733 signature: None,
2734 additional_params: None,
2735 };
2736
2737 let msg = message::Message::Assistant {
2738 id: None,
2739 content: OneOrMany::one(message::AssistantContent::ToolCall(tool_call)),
2740 };
2741
2742 let content: Content = msg.try_into().unwrap();
2743 assert_eq!(content.role, Some(Role::Model));
2744 assert_eq!(content.parts.len(), 1);
2745 if let Some(Part {
2746 part: PartKind::FunctionCall(function_call),
2747 ..
2748 }) = content.parts.first()
2749 {
2750 assert_eq!(function_call.name, "test_function");
2751 assert_eq!(
2752 function_call.args.as_object().unwrap().get("arg1").unwrap(),
2753 "value1"
2754 );
2755 assert_eq!(function_call.id.as_deref(), Some("call-123"));
2756 } else {
2757 panic!("Expected function call part");
2758 }
2759 }
2760
2761 #[test]
2762 fn test_response_function_call_preserves_correlation_id() {
2763 let response: GenerateContentResponse = serde_json::from_value(json!({
2764 "responseId": "response-123",
2765 "candidates": [{
2766 "content": {
2767 "parts": [{
2768 "functionCall": {
2769 "name": "test_function",
2770 "args": {"arg1": "value1"},
2771 "id": "call-123"
2772 }
2773 }],
2774 "role": "model"
2775 },
2776 "finishReason": "STOP"
2777 }]
2778 }))
2779 .expect("response should deserialize");
2780
2781 let converted: crate::completion::CompletionResponse<GenerateContentResponse> =
2782 response.try_into().expect("response should convert");
2783 let message::AssistantContent::ToolCall(tool_call) = converted.choice.first() else {
2784 panic!("expected a tool call");
2785 };
2786 assert_eq!(tool_call.id, "test_function");
2787 assert_eq!(tool_call.call_id.as_deref(), Some("call-123"));
2788 }
2789
2790 #[test]
2791 fn test_vec_schema_conversion() {
2792 let schema_with_ref = json!({
2793 "type": "array",
2794 "items": {
2795 "$ref": "#/$defs/Person"
2796 },
2797 "$defs": {
2798 "Person": {
2799 "type": "object",
2800 "properties": {
2801 "first_name": {
2802 "type": ["string", "null"],
2803 "description": "The person's first name, if provided (null otherwise)"
2804 },
2805 "last_name": {
2806 "type": ["string", "null"],
2807 "description": "The person's last name, if provided (null otherwise)"
2808 },
2809 "job": {
2810 "type": ["string", "null"],
2811 "description": "The person's job, if provided (null otherwise)"
2812 }
2813 },
2814 "required": []
2815 }
2816 }
2817 });
2818
2819 let result: Result<Schema, _> = schema_with_ref.try_into();
2820
2821 match result {
2822 Ok(schema) => {
2823 assert_eq!(schema.r#type, "array");
2824
2825 if let Some(items) = schema.items {
2826 println!("item types: {}", items.r#type);
2827
2828 assert_ne!(items.r#type, "", "Items type should not be empty string!");
2829 assert_eq!(items.r#type, "object", "Items should be object type");
2830 } else {
2831 panic!("Schema should have items field for array type");
2832 }
2833 }
2834 Err(e) => println!("Schema conversion failed: {:?}", e),
2835 }
2836 }
2837
2838 #[test]
2839 fn test_object_schema() {
2840 let simple_schema = json!({
2841 "type": "object",
2842 "properties": {
2843 "name": {
2844 "type": "string"
2845 }
2846 }
2847 });
2848
2849 let schema: Schema = simple_schema.try_into().unwrap();
2850 assert_eq!(schema.r#type, "object");
2851 assert!(schema.properties.is_some());
2852 }
2853
2854 #[test]
2855 fn test_array_with_inline_items() {
2856 let inline_schema = json!({
2857 "type": "array",
2858 "items": {
2859 "type": "object",
2860 "properties": {
2861 "name": {
2862 "type": "string"
2863 }
2864 }
2865 }
2866 });
2867
2868 let schema: Schema = inline_schema.try_into().unwrap();
2869 assert_eq!(schema.r#type, "array");
2870
2871 if let Some(items) = schema.items {
2872 assert_eq!(items.r#type, "object");
2873 assert!(items.properties.is_some());
2874 } else {
2875 panic!("Schema should have items field");
2876 }
2877 }
2878 #[test]
2879 fn test_flattened_schema() {
2880 let ref_schema = json!({
2881 "type": "array",
2882 "items": {
2883 "$ref": "#/$defs/Person"
2884 },
2885 "$defs": {
2886 "Person": {
2887 "type": "object",
2888 "properties": {
2889 "name": { "type": "string" }
2890 }
2891 }
2892 }
2893 });
2894
2895 let flattened = flatten_schema(ref_schema).unwrap();
2896 let schema: Schema = flattened.try_into().unwrap();
2897
2898 assert_eq!(schema.r#type, "array");
2899
2900 if let Some(items) = schema.items {
2901 println!("Flattened items type: '{}'", items.r#type);
2902
2903 assert_eq!(items.r#type, "object");
2904 assert!(items.properties.is_some());
2905 }
2906 }
2907
2908 #[test]
2909 fn test_array_without_items_gets_default() {
2910 let schema_json = json!({
2911 "type": "object",
2912 "properties": {
2913 "service_ids": {
2914 "type": "array",
2915 "description": "A list of service IDs"
2916 }
2917 }
2918 });
2919
2920 let schema: Schema = schema_json.try_into().unwrap();
2921 let props = schema.properties.unwrap();
2922 let service_ids = props.get("service_ids").unwrap();
2923 assert_eq!(service_ids.r#type, "array");
2924 let items = service_ids
2925 .items
2926 .as_ref()
2927 .expect("array schema missing items should get a default");
2928 assert_eq!(items.r#type, "string");
2929 }
2930
2931 #[test]
2932 fn test_tool_parameters_to_schema_maps_no_arg_tool_to_none() {
2933 let schema = tool_parameters_to_schema(json!({"type": "object", "properties": {}}))
2934 .expect("schema conversion");
2935
2936 assert!(schema.is_none());
2937 }
2938
2939 #[test]
2940 fn test_tool_parameters_to_schema_resolves_defs_ref() {
2941 let schema_json = json!({
2942 "type": "object",
2943 "properties": {
2944 "destination": { "$ref": "#/$defs/Destination" }
2945 },
2946 "required": ["destination"],
2947 "$defs": {
2948 "Destination": {
2949 "type": "object",
2950 "properties": {
2951 "city": { "type": "string" }
2952 },
2953 "required": ["city"]
2954 }
2955 }
2956 });
2957
2958 let schema = tool_parameters_to_schema(schema_json)
2959 .expect("schema conversion")
2960 .expect("schema");
2961 let props = schema.properties.expect("properties");
2962 let destination = props.get("destination").expect("destination prop");
2963
2964 assert_eq!(destination.r#type, "object");
2965 assert_eq!(destination.required, Some(vec!["city".to_string()]));
2966 }
2967
2968 #[test]
2969 fn test_tool_parameters_to_schema_handles_nullable_type_arrays() {
2970 let schema_json = json!({
2971 "type": "object",
2972 "properties": {
2973 "nickname": { "type": ["null", "string"] }
2974 }
2975 });
2976
2977 let schema = tool_parameters_to_schema(schema_json)
2978 .expect("schema conversion")
2979 .expect("schema");
2980 let props = schema.properties.expect("properties");
2981 let nickname = props.get("nickname").expect("nickname prop");
2982
2983 assert_eq!(nickname.r#type, "string");
2984 assert_eq!(nickname.nullable, Some(true));
2985 }
2986
2987 #[test]
2988 fn test_txt_document_conversion_to_text_part() {
2989 use crate::message::{DocumentMediaType, UserContent};
2991
2992 let doc = UserContent::document(
2993 "Note: test.md\nPath: /test.md\nContent: Hello World!",
2994 Some(DocumentMediaType::TXT),
2995 );
2996
2997 let content: Content = message::Message::User {
2998 content: crate::OneOrMany::one(doc),
2999 }
3000 .try_into()
3001 .unwrap();
3002
3003 if let Part {
3004 part: PartKind::Text(text),
3005 ..
3006 } = &content.parts[0]
3007 {
3008 assert!(text.contains("Note: test.md"));
3009 assert!(text.contains("Hello World!"));
3010 } else {
3011 panic!(
3012 "Expected text part for TXT document, got: {:?}",
3013 content.parts[0]
3014 );
3015 }
3016 }
3017
3018 #[test]
3019 fn test_tool_result_with_image_content() {
3020 use crate::OneOrMany;
3022 use crate::message::{
3023 DocumentSourceKind, Image, ImageMediaType, ToolResult, ToolResultContent,
3024 };
3025
3026 let tool_result = ToolResult {
3028 id: "test_tool".to_string(),
3029 call_id: Some("call-123".to_string()),
3030 content: OneOrMany::many(vec![
3031 ToolResultContent::Text(message::Text::new(r#"{"status": "success"}"#.to_string())),
3032 ToolResultContent::Image(Image {
3033 data: DocumentSourceKind::Base64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==".to_string()),
3034 media_type: Some(ImageMediaType::PNG),
3035 detail: None,
3036 additional_params: None,
3037 }),
3038 ]).expect("Should create OneOrMany with multiple items"),
3039 };
3040
3041 let user_content = message::UserContent::ToolResult(tool_result);
3042 let msg = message::Message::User {
3043 content: OneOrMany::one(user_content),
3044 };
3045
3046 let content: Content = msg.try_into().expect("Should convert to Gemini Content");
3048 assert_eq!(content.role, Some(Role::User));
3049 assert_eq!(content.parts.len(), 1);
3050
3051 if let Some(Part {
3053 part: PartKind::FunctionResponse(function_response),
3054 ..
3055 }) = content.parts.first()
3056 {
3057 assert_eq!(function_response.name, "test_tool");
3058 assert_eq!(function_response.id.as_deref(), Some("call-123"));
3059
3060 assert!(function_response.response.is_some());
3062 let response = function_response.response.as_ref().unwrap();
3063 assert_eq!(
3064 response,
3065 &json!({
3066 "result": r#"{"status": "success"}"#
3067 })
3068 );
3069
3070 assert!(function_response.parts.is_some());
3072 let parts = function_response.parts.as_ref().unwrap();
3073 assert_eq!(parts.len(), 1);
3074
3075 let image_part = &parts[0];
3076 assert!(image_part.inline_data.is_some());
3077 let inline_data = image_part.inline_data.as_ref().unwrap();
3078 assert_eq!(inline_data.mime_type, "image/png");
3079 assert!(!inline_data.data.is_empty());
3080 assert_eq!(inline_data.display_name, None);
3081 } else {
3082 panic!("Expected FunctionResponse part");
3083 }
3084 }
3085
3086 #[test]
3087 fn mixed_inline_images_and_text_keep_text_response_and_ordered_parts() {
3088 use crate::OneOrMany;
3089 use crate::message::{ImageMediaType, ToolResult, ToolResultContent};
3090
3091 let message = message::Message::User {
3092 content: OneOrMany::one(message::UserContent::ToolResult(ToolResult {
3093 id: "ordered_tool".to_string(),
3094 call_id: None,
3095 content: OneOrMany::many(vec![
3096 ToolResultContent::image_base64("first-image", Some(ImageMediaType::PNG), None),
3097 ToolResultContent::text("between-images"),
3098 ToolResultContent::image_base64(
3099 "second-image",
3100 Some(ImageMediaType::JPEG),
3101 None,
3102 ),
3103 ])
3104 .expect("mixed tool result content should be non-empty"),
3105 })),
3106 };
3107
3108 let content: Content = message.try_into().expect("tool result should convert");
3109 let PartKind::FunctionResponse(response) = &content.parts[0].part else {
3110 panic!("expected a function response");
3111 };
3112
3113 assert_eq!(
3114 response.response,
3115 Some(json!({ "result": "between-images" }))
3116 );
3117
3118 let parts = response
3119 .parts
3120 .as_ref()
3121 .expect("images should be inline parts");
3122 assert_eq!(parts.len(), 2);
3123 let first = parts[0].inline_data.as_ref().expect("first inline image");
3124 assert_eq!(first.mime_type, "image/png");
3125 assert_eq!(first.data, "first-image");
3126 assert_eq!(first.display_name, None);
3127 let second = parts[1].inline_data.as_ref().expect("second inline image");
3128 assert_eq!(second.mime_type, "image/jpeg");
3129 assert_eq!(second.data, "second-image");
3130 assert_eq!(second.display_name, None);
3131 }
3132
3133 #[test]
3134 fn mixed_inline_image_and_json_keep_structured_value_and_media_part() {
3135 use crate::OneOrMany;
3136 use crate::message::{ImageMediaType, ToolResult, ToolResultContent};
3137
3138 let message = message::Message::User {
3139 content: OneOrMany::one(message::UserContent::ToolResult(ToolResult {
3140 id: "ordered_tool".to_string(),
3141 call_id: None,
3142 content: OneOrMany::many(vec![
3143 ToolResultContent::json(json!({ "status": "ok" })),
3144 ToolResultContent::image_base64("image-data", Some(ImageMediaType::PNG), None),
3145 ])
3146 .expect("mixed tool result content should be non-empty"),
3147 })),
3148 };
3149
3150 let content: Content = message.try_into().expect("tool result should convert");
3151 let PartKind::FunctionResponse(response) = &content.parts[0].part else {
3152 panic!("expected a function response");
3153 };
3154
3155 assert_eq!(
3156 response.response,
3157 Some(json!({ "result": { "status": "ok" } }))
3158 );
3159 let parts = response
3160 .parts
3161 .as_ref()
3162 .expect("image should be an inline part");
3163 assert_eq!(parts.len(), 1);
3164 let inline_data = parts[0].inline_data.as_ref().expect("inline image data");
3165 assert_eq!(inline_data.data, "image-data");
3166 assert_eq!(inline_data.display_name, None);
3167 }
3168
3169 #[test]
3170 fn mixed_url_image_and_response_value_is_rejected() {
3171 use crate::OneOrMany;
3172 use crate::message::{DocumentSourceKind, Image, ImageMediaType, ToolResultContent};
3173
3174 let tool_result = message::Message::User {
3175 content: OneOrMany::one(message::UserContent::ToolResult(message::ToolResult {
3176 id: "url_tool".to_string(),
3177 call_id: None,
3178 content: OneOrMany::many(vec![
3179 ToolResultContent::Image(Image {
3180 data: DocumentSourceKind::Url("https://example.com/image.png".to_string()),
3181 media_type: Some(ImageMediaType::PNG),
3182 detail: None,
3183 additional_params: None,
3184 }),
3185 ToolResultContent::text("after-image"),
3186 ])
3187 .expect("mixed tool result content should be non-empty"),
3188 })),
3189 };
3190
3191 let error = Content::try_from(tool_result)
3192 .expect_err("URL-backed tool result images should be rejected");
3193 assert!(
3194 error
3195 .to_string()
3196 .contains("URL-backed images are not supported"),
3197 "unexpected error: {error}"
3198 );
3199 }
3200
3201 #[test]
3202 fn tool_result_rejects_unsupported_image_media_types() {
3203 use crate::OneOrMany;
3204 use crate::message::{ImageMediaType, ToolResult, ToolResultContent};
3205
3206 for media_type in [
3207 ImageMediaType::GIF,
3208 ImageMediaType::HEIC,
3209 ImageMediaType::HEIF,
3210 ImageMediaType::SVG,
3211 ] {
3212 let message = message::Message::User {
3213 content: OneOrMany::one(message::UserContent::ToolResult(ToolResult {
3214 id: "image_tool".to_string(),
3215 call_id: None,
3216 content: OneOrMany::one(ToolResultContent::image_base64(
3217 "image-data",
3218 Some(media_type),
3219 None,
3220 )),
3221 })),
3222 };
3223
3224 let error = Content::try_from(message)
3225 .expect_err("unsupported tool result image type should be rejected");
3226 assert!(
3227 error
3228 .to_string()
3229 .contains("supported types are JPEG, PNG, and WEBP"),
3230 "unexpected error: {error}"
3231 );
3232 }
3233 }
3234
3235 #[test]
3236 fn structured_json_refs_remain_literal_with_unreferenced_image_parts() {
3237 use crate::OneOrMany;
3238 use crate::message::{ImageMediaType, ToolResult, ToolResultContent};
3239
3240 let message = message::Message::User {
3241 content: OneOrMany::one(message::UserContent::ToolResult(ToolResult {
3242 id: "collision_tool".to_string(),
3243 call_id: None,
3244 content: OneOrMany::many(vec![
3245 ToolResultContent::json(json!({
3246 "literal": {
3247 "$ref": "tool_result_image_0"
3248 }
3249 })),
3250 ToolResultContent::image_base64("image-data", Some(ImageMediaType::PNG), None),
3251 ])
3252 .expect("mixed tool result content should be non-empty"),
3253 })),
3254 };
3255
3256 let content: Content = message.try_into().expect("tool result should convert");
3257 let PartKind::FunctionResponse(response) = &content.parts[0].part else {
3258 panic!("expected a function response");
3259 };
3260
3261 assert_eq!(
3262 response.response,
3263 Some(json!({
3264 "result": {
3265 "literal": {
3266 "$ref": "tool_result_image_0"
3267 }
3268 }
3269 }))
3270 );
3271 assert_eq!(
3272 response.parts.as_ref().and_then(|parts| {
3273 parts
3274 .first()
3275 .and_then(|part| part.inline_data.as_ref())
3276 .and_then(|part| part.display_name.as_deref())
3277 }),
3278 None
3279 );
3280 }
3281
3282 #[test]
3283 fn tool_result_literal_text_and_structured_json_remain_distinct() {
3284 use crate::OneOrMany;
3285 use crate::message::{ToolResult, ToolResultContent};
3286
3287 let cases = [
3288 (
3289 ToolResultContent::text(r#"{"status":"ok"}"#),
3290 json!({ "result": "{\"status\":\"ok\"}" }),
3291 ),
3292 (
3293 ToolResultContent::json(json!({ "status": "ok" })),
3294 json!({ "result": { "status": "ok" } }),
3295 ),
3296 ];
3297
3298 for (tool_content, expected) in cases {
3299 let message = message::Message::User {
3300 content: OneOrMany::one(message::UserContent::ToolResult(ToolResult {
3301 id: "test_tool".to_string(),
3302 call_id: None,
3303 content: OneOrMany::one(tool_content),
3304 })),
3305 };
3306 let content: Content = message.try_into().expect("tool result should convert");
3307
3308 let PartKind::FunctionResponse(response) = &content.parts[0].part else {
3309 panic!("expected a function response");
3310 };
3311 assert_eq!(response.response.as_ref(), Some(&expected));
3312 }
3313 }
3314
3315 #[test]
3316 fn test_markdown_document_conversion_to_text_part() {
3317 use crate::message::{DocumentMediaType, UserContent};
3319
3320 let doc = UserContent::document(
3321 "# Heading\n\n* List item",
3322 Some(DocumentMediaType::MARKDOWN),
3323 );
3324
3325 let content: Content = message::Message::User {
3326 content: crate::OneOrMany::one(doc),
3327 }
3328 .try_into()
3329 .unwrap();
3330
3331 if let Part {
3332 part: PartKind::Text(text),
3333 ..
3334 } = &content.parts[0]
3335 {
3336 assert_eq!(text, "# Heading\n\n* List item");
3337 } else {
3338 panic!(
3339 "Expected text part for MARKDOWN document, got: {:?}",
3340 content.parts[0]
3341 );
3342 }
3343 }
3344
3345 #[test]
3346 fn test_markdown_url_document_conversion_to_file_data_part() {
3347 use crate::message::{DocumentMediaType, DocumentSourceKind, UserContent};
3349
3350 let doc = UserContent::Document(message::Document {
3351 data: DocumentSourceKind::Url(
3352 "https://generativelanguage.googleapis.com/v1beta/files/test-markdown".to_string(),
3353 ),
3354 media_type: Some(DocumentMediaType::MARKDOWN),
3355 additional_params: None,
3356 });
3357
3358 let content: Content = message::Message::User {
3359 content: crate::OneOrMany::one(doc),
3360 }
3361 .try_into()
3362 .unwrap();
3363
3364 if let Part {
3365 part: PartKind::FileData(file_data),
3366 ..
3367 } = &content.parts[0]
3368 {
3369 assert_eq!(
3370 file_data.file_uri,
3371 "https://generativelanguage.googleapis.com/v1beta/files/test-markdown"
3372 );
3373 assert_eq!(file_data.mime_type.as_deref(), Some("text/markdown"));
3374 } else {
3375 panic!(
3376 "Expected file_data part for URL MARKDOWN document, got: {:?}",
3377 content.parts[0]
3378 );
3379 }
3380 }
3381
3382 #[test]
3383 fn test_tool_result_with_url_image_is_rejected() {
3384 use crate::OneOrMany;
3385 use crate::message::{
3386 DocumentSourceKind, Image, ImageMediaType, ToolResult, ToolResultContent,
3387 };
3388
3389 let tool_result = ToolResult {
3390 id: "screenshot_tool".to_string(),
3391 call_id: None,
3392 content: OneOrMany::one(ToolResultContent::Image(Image {
3393 data: DocumentSourceKind::Url("https://example.com/image.png".to_string()),
3394 media_type: Some(ImageMediaType::PNG),
3395 detail: None,
3396 additional_params: None,
3397 })),
3398 };
3399
3400 let user_content = message::UserContent::ToolResult(tool_result);
3401 let msg = message::Message::User {
3402 content: OneOrMany::one(user_content),
3403 };
3404
3405 let error =
3406 Content::try_from(msg).expect_err("URL-backed tool result images should be rejected");
3407 assert!(
3408 error
3409 .to_string()
3410 .contains("URL-backed images are not supported"),
3411 "unexpected error: {error}"
3412 );
3413 }
3414
3415 #[test]
3416 fn test_create_request_body_with_documents() {
3417 use crate::OneOrMany;
3419 use crate::completion::request::{CompletionRequest, Document};
3420 use crate::message::Message;
3421
3422 let documents = vec![
3423 Document {
3424 id: "doc1".to_string(),
3425 text: "Note: first.md\nContent: First note".to_string(),
3426 additional_props: std::collections::HashMap::new(),
3427 },
3428 Document {
3429 id: "doc2".to_string(),
3430 text: "Note: second.md\nContent: Second note".to_string(),
3431 additional_props: std::collections::HashMap::new(),
3432 },
3433 ];
3434
3435 let documents_message = CompletionRequest {
3436 preamble: None,
3437 chat_history: OneOrMany::one(Message::user("placeholder")),
3438 documents,
3439 tools: vec![],
3440 temperature: None,
3441 model: None,
3442 output_schema: None,
3443 record_telemetry_content: false,
3444 max_tokens: None,
3445 tool_choice: None,
3446 additional_params: None,
3447 }
3448 .normalized_documents()
3449 .unwrap();
3450
3451 let completion_request = CompletionRequest {
3452 preamble: Some("You are a helpful assistant".to_string()),
3453 chat_history: OneOrMany::many(vec![
3454 documents_message,
3455 Message::user("What are my notes about?"),
3456 ])
3457 .unwrap(),
3458 documents: vec![],
3459 tools: vec![],
3460 temperature: None,
3461 model: None,
3462 output_schema: None,
3463 record_telemetry_content: false,
3464 max_tokens: None,
3465 tool_choice: None,
3466 additional_params: None,
3467 };
3468
3469 let request = create_request_body(completion_request).unwrap();
3470
3471 assert_eq!(
3473 request.contents.len(),
3474 2,
3475 "Expected 2 contents (documents + user message)"
3476 );
3477
3478 assert_eq!(request.contents[0].role, Some(Role::User));
3480 assert_eq!(
3481 request.contents[0].parts.len(),
3482 2,
3483 "Expected 2 document parts"
3484 );
3485
3486 for part in &request.contents[0].parts {
3488 if let Part {
3489 part: PartKind::Text(text),
3490 ..
3491 } = part
3492 {
3493 assert!(
3494 text.contains("Note:") && text.contains("Content:"),
3495 "Document should contain note metadata"
3496 );
3497 } else {
3498 panic!("Document parts should be text, not {:?}", part);
3499 }
3500 }
3501
3502 assert_eq!(request.contents[1].role, Some(Role::User));
3504 if let Part {
3505 part: PartKind::Text(text),
3506 ..
3507 } = &request.contents[1].parts[0]
3508 {
3509 assert_eq!(text, "What are my notes about?");
3510 } else {
3511 panic!("Expected user message to be text");
3512 }
3513 }
3514
3515 #[test]
3516 fn test_create_request_body_without_documents() {
3517 use crate::OneOrMany;
3519 use crate::completion::request::CompletionRequest;
3520 use crate::message::Message;
3521
3522 let completion_request = CompletionRequest {
3523 preamble: Some("You are a helpful assistant".to_string()),
3524 chat_history: OneOrMany::one(Message::user("Hello")),
3525 documents: vec![], tools: vec![],
3527 temperature: None,
3528 max_tokens: None,
3529 tool_choice: None,
3530 model: None,
3531 output_schema: None,
3532 record_telemetry_content: false,
3533 additional_params: None,
3534 };
3535
3536 let request = create_request_body(completion_request).unwrap();
3537
3538 assert_eq!(request.contents.len(), 1, "Expected only user message");
3540 assert_eq!(request.contents[0].role, Some(Role::User));
3541
3542 if let Part {
3543 part: PartKind::Text(text),
3544 ..
3545 } = &request.contents[0].parts[0]
3546 {
3547 assert_eq!(text, "Hello");
3548 } else {
3549 panic!("Expected user message to be text");
3550 }
3551 }
3552
3553 #[tokio::test]
3554 async fn completion_non_success_preserves_status_and_body() {
3555 use crate::client::completion::CompletionClient;
3556 use crate::completion::CompletionModel as _;
3557 use crate::providers::gemini::Client;
3558 use crate::test_utils::RecordingHttpClient;
3559
3560 let body = r#"{"error":{"code":503,"message":"boom","status":"UNAVAILABLE"}}"#;
3561 let http_client =
3562 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
3563 let client = Client::builder()
3564 .api_key("test-key")
3565 .http_client(http_client)
3566 .build()
3567 .expect("build client");
3568 let model = client.completion_model(super::GEMINI_3_FLASH_PREVIEW);
3569 let request = model.completion_request("hello").build();
3570
3571 let error = model
3572 .completion(request)
3573 .await
3574 .expect_err("should fail with non-success status");
3575
3576 assert!(matches!(error, CompletionError::HttpError(_)));
3577 assert_eq!(
3578 error.provider_response_status(),
3579 Some(http::StatusCode::SERVICE_UNAVAILABLE)
3580 );
3581 assert_eq!(error.provider_response_body(), Some(body));
3582 }
3583}