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