Skip to main content

rig_core/providers/gemini/
completion.rs

1// ================================================================
2//! Google Gemini Completion Integration
3//! From [Gemini API Reference](https://ai.google.dev/api/generate-content)
4// ================================================================
5/// `gemini-3.1-flash-lite-preview` completion model
6pub const GEMINI_3_1_FLASH_LITE_PREVIEW: &str = "gemini-3.1-flash-lite-preview";
7/// `gemini-3-flash-preview` completion model
8pub const GEMINI_3_FLASH_PREVIEW: &str = "gemini-3-flash-preview";
9/// `gemini-2.5-pro-preview-06-05` completion model
10pub const GEMINI_2_5_PRO_PREVIEW_06_05: &str = "gemini-2.5-pro-preview-06-05";
11/// `gemini-2.5-pro-preview-05-06` completion model
12pub const GEMINI_2_5_PRO_PREVIEW_05_06: &str = "gemini-2.5-pro-preview-05-06";
13/// `gemini-2.5-pro-preview-03-25` completion model
14pub const GEMINI_2_5_PRO_PREVIEW_03_25: &str = "gemini-2.5-pro-preview-03-25";
15/// `gemini-2.5-flash-preview-04-17` completion model
16pub const GEMINI_2_5_FLASH_PREVIEW_04_17: &str = "gemini-2.5-flash-preview-04-17";
17/// `gemini-2.5-pro-exp-03-25` experimental completion model
18pub const GEMINI_2_5_PRO_EXP_03_25: &str = "gemini-2.5-pro-exp-03-25";
19/// `gemini-2.5-flash` completion model
20pub const GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
21/// `gemini-2.5-flash-image` image generation model, commonly referred to as Nano Banana.
22#[cfg(feature = "image")]
23#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
24pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
25/// `gemini-2.0-flash-lite` completion model
26pub const GEMINI_2_0_FLASH_LITE: &str = "gemini-2.0-flash-lite";
27/// `gemini-2.0-flash` completion model
28pub 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// =================================================================
55// Rig Implementation Types
56// =================================================================
57
58#[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    // Apply output_schema to generation_config, creating one if needed
234    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    // =================================================================
539    // Gemini API Types
540    // =================================================================
541    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        /// Change your Gemini request configuration.
556        pub generation_config: Option<GenerationConfig>,
557        /// Any additional parameters that you want.
558        #[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    /// Response from the model supporting multiple candidate responses.
575    /// Safety ratings and content filtering are reported for both prompt in GenerateContentResponse.prompt_feedback
576    /// and for each candidate in finishReason and in safetyRatings.
577    /// The API:
578    ///     - Returns either all requested candidates or none of them
579    ///     - Returns no candidates at all only if there was something wrong with the prompt (check promptFeedback)
580    ///     - Reports feedback on each candidate in finishReason and safetyRatings.
581    #[derive(Debug, Deserialize, Serialize)]
582    #[serde(rename_all = "camelCase")]
583    pub struct GenerateContentResponse {
584        #[serde(default)]
585        pub response_id: String,
586        /// Candidate responses from the model.
587        #[serde(default)]
588        pub candidates: Vec<ContentCandidate>,
589        /// Returns the prompt's feedback related to the content filters.
590        pub prompt_feedback: Option<PromptFeedback>,
591        /// Output only. Metadata on the generation requests' token usage.
592        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    /// A response candidate generated from the model.
649    #[derive(Clone, Debug, Deserialize, Serialize)]
650    #[serde(rename_all = "camelCase")]
651    pub struct ContentCandidate {
652        /// Output only. Generated content returned from the model.
653        #[serde(skip_serializing_if = "Option::is_none")]
654        pub content: Option<Content>,
655        /// Optional. Output only. The reason why the model stopped generating tokens.
656        /// If empty, the model has not stopped generating tokens.
657        pub finish_reason: Option<FinishReason>,
658        /// List of ratings for the safety of a response candidate.
659        /// There is at most one rating per category.
660        pub safety_ratings: Option<Vec<SafetyRating>>,
661        /// Output only. Citation information for model-generated candidate.
662        /// This field may be populated with recitation information for any text included in the content.
663        /// These are passages that are "recited" from copyrighted material in the foundational LLM's training data.
664        pub citation_metadata: Option<CitationMetadata>,
665        /// Output only. Token count for this candidate.
666        pub token_count: Option<i32>,
667        /// Output only.
668        pub avg_logprobs: Option<f64>,
669        /// Output only. Log-likelihood scores for the response tokens and top tokens
670        pub logprobs_result: Option<LogprobsResult>,
671        /// Output only. Index of the candidate in the list of response candidates.
672        pub index: Option<i32>,
673        /// Output only. Additional information about why the model stopped generating tokens.
674        pub finish_message: Option<String>,
675    }
676
677    #[derive(Clone, Debug, Deserialize, Serialize)]
678    pub struct Content {
679        /// Ordered Parts that constitute a single message. Parts may have different MIME types.
680        #[serde(default)]
681        pub parts: Vec<Part>,
682        /// The producer of the content. Must be either 'user' or 'model'.
683        /// Useful to set for multi-turn conversations, otherwise can be left blank or unset.
684        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        /// whether or not the part is a reasoning/thinking text or not
725        #[serde(skip_serializing_if = "Option::is_none")]
726        pub thought: Option<bool>,
727        /// an opaque sig for the thought so it can be reused - is a base64 string
728        #[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    /// A datatype containing media that is part of a multi-part [Content] message.
737    /// A Part consists of data which has an associated datatype. A Part can only contain one of the accepted types in Part.data.
738    /// A Part must have a fixed IANA MIME type identifying the type and subtype of the media if the inlineData field is filled with raw bytes.
739    #[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    // This default instance is primarily so we can easily fill in the optional fields of `Part`
752    // So this instance for `PartKind` (and the allocation it would cause) should be optimized away
753    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                    // For text-like documents (RAG context), convert inline content to plain text.
948                    // URL-backed files should stay as file_data references so Gemini can fetch them.
949                    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                                // Decode base64 text payloads.
966                                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    /// Raw media bytes.
1222    /// Text should not be sent as raw bytes, use the 'text' field.
1223    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1224    #[serde(rename_all = "camelCase")]
1225    pub struct Blob {
1226        /// The IANA standard MIME type of the source data. Examples: - image/png - image/jpeg
1227        /// If an unsupported MIME type is provided, an error will be returned.
1228        pub mime_type: String,
1229        /// Raw bytes for media formats. A base64-encoded string.
1230        pub data: String,
1231    }
1232
1233    /// A predicted FunctionCall returned from the model that contains a string representing the
1234    /// FunctionDeclaration.name with the arguments and their values.
1235    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1236    pub struct FunctionCall {
1237        /// Required. The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores
1238        /// and dashes, with a maximum length of 63.
1239        pub name: String,
1240        /// Optional. The function parameters and values in JSON object format.
1241        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    /// The result output from a FunctionCall that contains a string representing the FunctionDeclaration.name
1254    /// and a structured JSON object containing any output from the function is used as context to the model.
1255    /// This should contain the result of aFunctionCall made based on model prediction.
1256    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1257    pub struct FunctionResponse {
1258        /// The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores and dashes,
1259        /// with a maximum length of 63.
1260        pub name: String,
1261        /// The function response in JSON object format.
1262        #[serde(skip_serializing_if = "Option::is_none")]
1263        pub response: Option<serde_json::Value>,
1264        /// Multimodal parts for the function response (e.g., images).
1265        #[serde(skip_serializing_if = "Option::is_none")]
1266        pub parts: Option<Vec<FunctionResponsePart>>,
1267    }
1268
1269    /// A part of a multimodal function response.
1270    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1271    #[serde(rename_all = "camelCase")]
1272    pub struct FunctionResponsePart {
1273        /// Inline data containing base64-encoded media content.
1274        #[serde(skip_serializing_if = "Option::is_none")]
1275        pub inline_data: Option<FunctionResponseInlineData>,
1276        /// File data containing a URI reference.
1277        #[serde(skip_serializing_if = "Option::is_none")]
1278        pub file_data: Option<FileData>,
1279    }
1280
1281    /// Inline data for function response parts.
1282    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1283    #[serde(rename_all = "camelCase")]
1284    pub struct FunctionResponseInlineData {
1285        /// The IANA standard MIME type of the source data.
1286        pub mime_type: String,
1287        /// Raw bytes for media formats. A base64-encoded string.
1288        pub data: String,
1289        /// Optional display name for the content.
1290        #[serde(skip_serializing_if = "Option::is_none")]
1291        pub display_name: Option<String>,
1292    }
1293
1294    /// URI based data.
1295    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1296    #[serde(rename_all = "camelCase")]
1297    pub struct FileData {
1298        /// Optional. The IANA standard MIME type of the source data.
1299        pub mime_type: Option<String>,
1300        /// Required. URI.
1301        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    /// A set of the feedback metadata the prompt specified in [GenerateContentRequest.contents](GenerateContentRequest).
1427    #[derive(Debug, Deserialize, Serialize)]
1428    #[serde(rename_all = "camelCase")]
1429    pub struct PromptFeedback {
1430        /// Optional. If set, the prompt was blocked and no candidates are returned. Rephrase the prompt.
1431        pub block_reason: Option<BlockReason>,
1432        /// Ratings for safety of the prompt. There is at most one rating per category.
1433        pub safety_ratings: Option<Vec<SafetyRating>>,
1434    }
1435
1436    /// Reason why a prompt was blocked by the model
1437    #[derive(Debug, Deserialize, Serialize)]
1438    #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1439    pub enum BlockReason {
1440        /// Default value. This value is unused.
1441        BlockReasonUnspecified,
1442        /// Prompt was blocked due to safety reasons. Inspect safetyRatings to understand which safety category blocked it.
1443        Safety,
1444        /// Prompt was blocked due to unknown reasons.
1445        Other,
1446        /// Prompt was blocked due to the terms which are included from the terminology blocklist.
1447        Blocklist,
1448        /// Prompt was blocked due to prohibited content.
1449        ProhibitedContent,
1450    }
1451
1452    #[derive(Clone, Debug, Deserialize, Serialize)]
1453    #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1454    pub enum FinishReason {
1455        /// Default value. This value is unused.
1456        FinishReasonUnspecified,
1457        /// Natural stop point of the model or provided stop sequence.
1458        Stop,
1459        /// The maximum number of tokens as specified in the request was reached.
1460        MaxTokens,
1461        /// The response candidate content was flagged for safety reasons.
1462        Safety,
1463        /// The response candidate content was flagged for recitation reasons.
1464        Recitation,
1465        /// The response candidate content was flagged for using an unsupported language.
1466        Language,
1467        /// Unknown reason.
1468        Other,
1469        /// Token generation stopped because the content contains forbidden terms.
1470        Blocklist,
1471        /// Token generation stopped for potentially containing prohibited content.
1472        ProhibitedContent,
1473        /// Token generation stopped because the content potentially contains Sensitive Personally Identifiable Information (SPII).
1474        Spii,
1475        /// The function call generated by the model is invalid.
1476        MalformedFunctionCall,
1477        /// The model emitted a tool call that was not expected by the request.
1478        UnexpectedToolCall,
1479        /// The response omitted a thought signature required for a tool-calling turn.
1480        MissingThoughtSignature,
1481        /// The model emitted more tool calls than the provider allows for the request.
1482        TooManyToolCalls,
1483        /// The provider could not parse the generated response into a valid protocol shape.
1484        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    /// Gemini API Configuration options for model generation and outputs. Not all parameters are
1536    /// configurable for every model. From [Gemini API Reference](https://ai.google.dev/api/generate-content#generationconfig)
1537    /// ### Rig Note:
1538    /// Can be used to construct a typesafe `additional_params` in rig_core::[AgentBuilder](crate::agent::AgentBuilder).
1539    #[derive(Debug, Deserialize, Serialize)]
1540    #[serde(rename_all = "camelCase")]
1541    pub struct GenerationConfig {
1542        /// The set of character sequences (up to 5) that will stop output generation. If specified, the API will stop
1543        /// at the first appearance of a stop_sequence. The stop sequence will not be included as part of the response.
1544        #[serde(skip_serializing_if = "Option::is_none")]
1545        pub stop_sequences: Option<Vec<String>>,
1546        /// MIME type of the generated candidate text. Supported MIME types are:
1547        ///     - text/plain:  (default) Text output
1548        ///     - application/json: JSON response in the response candidates.
1549        ///     - text/x.enum: ENUM as a string response in the response candidates.
1550        /// Refer to the docs for a list of all supported text MIME types
1551        #[serde(skip_serializing_if = "Option::is_none")]
1552        pub response_mime_type: Option<String>,
1553        /// Output schema of the generated candidate text. Schemas must be a subset of the OpenAPI schema and can be
1554        /// objects, primitives or arrays. If set, a compatible responseMimeType must also  be set. Compatible MIME
1555        /// types: application/json: Schema for JSON response. Refer to the JSON text generation guide for more details.
1556        #[serde(skip_serializing_if = "Option::is_none")]
1557        pub response_schema: Option<Schema>,
1558        /// Optional. The output schema of the generated response.
1559        /// This is an alternative to responseSchema that accepts a standard JSON Schema.
1560        /// If this is set, responseSchema must be omitted.
1561        /// Compatible MIME type: application/json.
1562        /// Supported properties: $id, $defs, $ref, type, properties, etc.
1563        #[serde(
1564            skip_serializing_if = "Option::is_none",
1565            rename = "_responseJsonSchema"
1566        )]
1567        pub _response_json_schema: Option<Value>,
1568        /// Internal or alternative representation for `response_json_schema`.
1569        #[serde(skip_serializing_if = "Option::is_none")]
1570        pub response_json_schema: Option<Value>,
1571        /// Number of generated responses to return. Currently, this value can only be set to 1. If
1572        /// unset, this will default to 1.
1573        #[serde(skip_serializing_if = "Option::is_none")]
1574        pub candidate_count: Option<i32>,
1575        /// The maximum number of tokens to include in a response candidate. Note: The default value varies by model, see
1576        /// the Model.output_token_limit attribute of the Model returned from the getModel function.
1577        #[serde(skip_serializing_if = "Option::is_none")]
1578        pub max_output_tokens: Option<u64>,
1579        /// Controls the randomness of the output. Note: The default value varies by model, see the Model.temperature
1580        /// attribute of the Model returned from the getModel function. Values can range from [0.0, 2.0].
1581        #[serde(skip_serializing_if = "Option::is_none")]
1582        pub temperature: Option<f64>,
1583        /// The maximum cumulative probability of tokens to consider when sampling. The model uses combined Top-k and
1584        /// Top-p (nucleus) sampling. Tokens are sorted based on their assigned probabilities so that only the most
1585        /// likely tokens are considered. Top-k sampling directly limits the maximum number of tokens to consider, while
1586        /// Nucleus sampling limits the number of tokens based on the cumulative probability. Note: The default value
1587        /// varies by Model and is specified by theModel.top_p attribute returned from the getModel function. An empty
1588        /// topK attribute indicates that the model doesn't apply top-k sampling and doesn't allow setting topK on requests.
1589        #[serde(skip_serializing_if = "Option::is_none")]
1590        pub top_p: Option<f64>,
1591        /// The maximum number of tokens to consider when sampling. Gemini models use Top-p (nucleus) sampling or a
1592        /// combination of Top-k and nucleus sampling. Top-k sampling considers the set of topK most probable tokens.
1593        /// Models running with nucleus sampling don't allow topK setting. Note: The default value varies by Model and is
1594        /// specified by theModel.top_p attribute returned from the getModel function. An empty topK attribute indicates
1595        /// that the model doesn't apply top-k sampling and doesn't allow setting topK on requests.
1596        #[serde(skip_serializing_if = "Option::is_none")]
1597        pub top_k: Option<i32>,
1598        /// Presence penalty applied to the next token's logprobs if the token has already been seen in the response.
1599        /// This penalty is binary on/off and not dependent on the number of times the token is used (after the first).
1600        /// Use frequencyPenalty for a penalty that increases with each use. A positive penalty will discourage the use
1601        /// of tokens that have already been used in the response, increasing the vocabulary. A negative penalty will
1602        /// encourage the use of tokens that have already been used in the response, decreasing the vocabulary.
1603        #[serde(skip_serializing_if = "Option::is_none")]
1604        pub presence_penalty: Option<f64>,
1605        /// Frequency penalty applied to the next token's logprobs, multiplied by the number of times each token has been
1606        /// seen in the response so far. A positive penalty will discourage the use of tokens that have already been
1607        /// used, proportional to the number of times the token has been used: The more a token is used, the more
1608        /// difficult it is for the  model to use that token again increasing the vocabulary of responses. Caution: A
1609        /// negative penalty will encourage the model to reuse tokens proportional to the number of times the token has
1610        /// been used. Small negative values will reduce the vocabulary of a response. Larger negative values will cause
1611        /// the model to  repeating a common token until it hits the maxOutputTokens limit: "...the the the the the...".
1612        #[serde(skip_serializing_if = "Option::is_none")]
1613        pub frequency_penalty: Option<f64>,
1614        /// If true, export the logprobs results in response.
1615        #[serde(skip_serializing_if = "Option::is_none")]
1616        pub response_logprobs: Option<bool>,
1617        /// Only valid if responseLogprobs=True. This sets the number of top logprobs to return at each decoding step in
1618        /// [Candidate.logprobs_result].
1619        #[serde(skip_serializing_if = "Option::is_none")]
1620        pub logprobs: Option<i32>,
1621        /// Configuration for thinking/reasoning.
1622        #[serde(skip_serializing_if = "Option::is_none")]
1623        pub thinking_config: Option<ThinkingConfig>,
1624        /// Response modalities requested from models that support multimodal output.
1625        #[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    /// Response modalities supported by Gemini multimodal output models.
1656    #[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    /// Thinking depth level for Gemini 3 models.
1665    #[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    /// Configuration for the model's thinking/reasoning process.
1675    /// Note: `thinking_budget` (Gemini 2.5) and `thinking_level` (Gemini 3) are mutually exclusive
1676    /// and cannot be set in the same request.
1677    #[derive(Debug, Deserialize, Serialize)]
1678    #[serde(rename_all = "camelCase")]
1679    pub struct ThinkingConfig {
1680        /// Token budget for thinking. Used by Gemini 2.5 models. Range: 0 to 32768.
1681        #[serde(skip_serializing_if = "Option::is_none")]
1682        pub thinking_budget: Option<u32>,
1683        /// Thinking depth level. Used by Gemini 3 models.
1684        #[serde(skip_serializing_if = "Option::is_none")]
1685        pub thinking_level: Option<ThinkingLevel>,
1686        /// When true, includes summarized versions of the model's reasoning in the response.
1687        #[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    /// The Schema object allows the definition of input and output data types. These types can be objects, but also
1701    /// primitives and arrays. Represents a select subset of an OpenAPI 3.0 schema object.
1702    /// From [Gemini API Reference](https://ai.google.dev/api/caching#Schema)
1703    #[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    /// Converts Rig tool parameters into Gemini's schema representation.
1727    ///
1728    /// Gemini does not need a `parameters` object for no-argument tools, and it
1729    /// does not support JSON Schema references, so this helper keeps those
1730    /// conventions centralized for all Gemini transports.
1731    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    /// Flattens a JSON schema by resolving all `$ref` references inline.
1740    /// It takes a JSON schema that may contain `$ref` references to definitions
1741    /// in `$defs` or `definitions` sections and returns a new schema with all references
1742    /// resolved and inlined. This is necessary for APIs like Gemini that don't support
1743    /// schema references.
1744    pub fn flatten_schema(mut schema: Value) -> Result<Value, CompletionError> {
1745        // extracting $defs if they exist
1746        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        // removing $defs from the final schema because we have inlined everything
1765        if let Some(obj) = schema.as_object_mut() {
1766            obj.remove("$defs");
1767            obj.remove("definitions");
1768        }
1769
1770        Ok(schema)
1771    }
1772
1773    /// Recursively resolves all `$ref` references in a JSON value by
1774    /// replacing them with their definitions.
1775    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                    // "#/$defs/Person" -> "Person"
1785                    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    /// Parses a JSON Schema `$ref` path to extract the definition name.
1813    ///
1814    /// JSON Schema references use URI fragment syntax to point to definitions within
1815    /// the same document. This function extracts the definition name from common
1816    /// reference patterns used in JSON Schema.
1817    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    /// Helper function to extract the type string from a JSON value.
1838    /// Handles both direct string types and array types.
1839    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    /// Helper function to extract type from anyOf, oneOf, or allOf schemas.
1878    /// Returns the type of the first non-null schema found.
1879    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                        // Enum schemas without explicit type are string-backed
1892                        Some("string".to_string())
1893                    } else {
1894                        None
1895                    }
1896                })
1897            })
1898        })
1899    }
1900
1901    /// Helper function to extract the first non-null schema from anyOf, oneOf, or allOf.
1902    /// Returns the schema object that should be used for properties, required, etc.
1903    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    /// Helper function to infer the type of a schema object.
1928    /// Checks for explicit type, then anyOf/oneOf/allOf, then infers from properties.
1929    fn infer_type(obj: &serde_json::Map<String, Value>) -> String {
1930        // First, try direct type field
1931        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        // Then try anyOf, oneOf, allOf (in that order)
1938        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        // Finally, infer object type if properties are present
1957        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                // Determine which object to use for extracting properties and required fields.
1973                // If this object has anyOf/oneOf/allOf, we need to extract properties from the composition.
1974                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                // Gemini requires `items` on array-typed schemas; default to
1989                // string items when the source schema omits it.
1990                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        /// Optional. Configuration options for model generation and outputs.
2079        pub generation_config: Option<GenerationConfig>,
2080        /// Optional. A list of unique SafetySetting instances for blocking unsafe content. This will be enforced on the
2081        /// [GenerateContentRequest.contents] and [GenerateContentResponse.candidates]. There should not be more than one
2082        /// setting for each SafetyCategory type. The API will block any contents and responses that fail to meet the
2083        /// thresholds set by these settings. This list overrides the default settings for each SafetyCategory specified
2084        /// in the safetySettings. If there is no SafetySetting for a given SafetyCategory provided in the list, the API
2085        /// will use the default safety setting for that category. Harm categories:
2086        ///     - HARM_CATEGORY_HATE_SPEECH,
2087        ///     - HARM_CATEGORY_SEXUALLY_EXPLICIT
2088        ///     - HARM_CATEGORY_DANGEROUS_CONTENT
2089        ///     - HARM_CATEGORY_HARASSMENT
2090        /// are supported.
2091        /// Refer to the guide for detailed information on available safety settings. Also refer to the Safety guidance
2092        /// to learn how to incorporate safety considerations in your AI applications.
2093        pub safety_settings: Option<Vec<SafetySetting>>,
2094        /// Optional. Developer set system instruction(s). Currently, text only.
2095        /// From [Gemini API Reference](https://ai.google.dev/gemini-api/docs/system-instructions?lang=rest)
2096        pub system_instruction: Option<Content>,
2097        // cachedContent: Optional<String>
2098        /// Additional parameters.
2099        #[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        // Gemini's proto3-JSON encoding omits fields whose value is the default (0),
2194        // so `totalTokenCount` is absent on short/empty/blocked generations.
2195        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        // Blocked prompt responses can omit default-valued proto fields, including
2204        // empty repeated `candidates` and empty string `responseId`.
2205        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        // Test that TXT documents are converted to plain text parts, not inline data
2938        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        // Test that a ToolResult with image content converts correctly to Gemini's Part format
2969        use crate::OneOrMany;
2970        use crate::message::{
2971            DocumentSourceKind, Image, ImageMediaType, ToolResult, ToolResultContent,
2972        };
2973
2974        // Create a tool result with both text and image content
2975        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        // Convert to Gemini Content
2995        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        // Verify the part is a FunctionResponse with both response and parts
3000        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            // Check that response JSON is present
3008            assert!(function_response.response.is_some());
3009            let response = function_response.response.as_ref().unwrap();
3010            assert!(response.get("result").is_some());
3011
3012            // Check that parts with image data are present
3013            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        // Test that MARKDOWN documents are converted to plain text parts
3030        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        // URL-backed MARKDOWN documents should be represented as file_data.
3060        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        // Test that a ToolResult with a URL-based image converts to file_data
3097        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            // URL images should have parts with file_data
3130            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        // Test that documents are injected into chat history
3147        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        // Should have 2 contents: 1 for documents, 1 for user message
3199        assert_eq!(
3200            request.contents.len(),
3201            2,
3202            "Expected 2 contents (documents + user message)"
3203        );
3204
3205        // First content should be documents with role User
3206        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        // Check that documents are text parts
3214        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        // Second content should be the user message
3230        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        // Test backward compatibility: requests without documents work as before
3245        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![], // No documents
3253            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        // Should have only 1 content (the user message)
3265        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        // Test the ToolResultContent::from_tool_output helper with image JSON
3282        use crate::message::{DocumentSourceKind, ToolResultContent};
3283
3284        // Test simple image JSON format
3285        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        // Test the ToolResultContent::from_tool_output helper with hybrid response/parts format
3303        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        // Should have 3 items: 1 text (response) + 2 images (parts)
3316        assert_eq!(result.len(), 3);
3317
3318        let items: Vec<_> = result.iter().collect();
3319
3320        // First should be text with the response JSON
3321        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        // Second should be base64 image
3329        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        // Third should be URL image
3336        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    /// E2E test that verifies Gemini can process tool results containing images.
3344    /// This test creates an agent with a tool that returns an image, invokes it,
3345    /// and verifies that Gemini can interpret the image in the tool result.
3346    #[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        // This prompt should trigger the tool, which returns an image that Gemini should process
3363        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        // Gemini should have been able to see the image and potentially describe its color
3368        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}