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