Skip to main content

rig_core/providers/gemini/
completion.rs

1//! The [Gemini GenerateContent API](https://ai.google.dev/api/generate-content)
2//! completion wire. Its request is built as REST JSON by [`request_body`],
3//! which the Vertex AI and gRPC wires transcode, and its replies are read by
4//! [`GenerateContentDecoder`](super::streaming::GenerateContentDecoder).
5//!
6//! ```no_run
7//! use rig_core::providers::gemini::{Gemini, completion::GEMINI_2_5_FLASH};
8//!
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! let wire = Gemini::from_env()?.completion(GEMINI_2_5_FLASH);
11//! # Ok(())
12//! # }
13//! ```
14
15/// `gemini-3.8-flash` completion model
16pub const GEMINI_3_8_FLASH: &str = "gemini-3.8-flash";
17/// `gemini-3.1-flash-lite-preview` completion model
18pub const GEMINI_3_1_FLASH_LITE_PREVIEW: &str = "gemini-3.1-flash-lite-preview";
19/// `gemini-3-flash-preview` completion model
20pub const GEMINI_3_FLASH_PREVIEW: &str = "gemini-3-flash-preview";
21/// `gemini-2.5-pro-preview-06-05` completion model
22pub const GEMINI_2_5_PRO_PREVIEW_06_05: &str = "gemini-2.5-pro-preview-06-05";
23/// `gemini-2.5-pro-preview-05-06` completion model
24pub const GEMINI_2_5_PRO_PREVIEW_05_06: &str = "gemini-2.5-pro-preview-05-06";
25/// `gemini-2.5-pro-preview-03-25` completion model
26pub const GEMINI_2_5_PRO_PREVIEW_03_25: &str = "gemini-2.5-pro-preview-03-25";
27/// `gemini-2.5-flash-preview-04-17` completion model
28pub const GEMINI_2_5_FLASH_PREVIEW_04_17: &str = "gemini-2.5-flash-preview-04-17";
29/// `gemini-2.5-pro-exp-03-25` experimental completion model
30pub const GEMINI_2_5_PRO_EXP_03_25: &str = "gemini-2.5-pro-exp-03-25";
31/// `gemini-2.5-flash` completion model
32pub const GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
33/// `gemini-2.5-flash-image` image generation model, commonly referred to as Nano Banana.
34#[cfg(feature = "image")]
35#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
36pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
37/// `gemini-2.0-flash-lite` completion model
38pub const GEMINI_2_0_FLASH_LITE: &str = "gemini-2.0-flash-lite";
39/// `gemini-2.0-flash` completion model
40pub const GEMINI_2_0_FLASH: &str = "gemini-2.0-flash";
41
42use serde_json::{Map, Value, json};
43
44pub use super::cached_content::with_cached_content;
45#[doc(hidden)]
46pub use super::options::{Route, generate_content_options};
47use crate::completion::options::{BaseInput, FinalBody, RawAt, Rewrite, request_params};
48use crate::completion::{Accepts, CompletionRequest, Media, Place, Replay, ReplayTarget};
49use crate::error::{EncodeError, ProviderError};
50use crate::json_utils::Lenient;
51use crate::message::{
52    AssistantContent, DocumentMediaType, DocumentSourceKind as Source, Message, MimeType,
53    ToolChoice, ToolResultContent, UserContent,
54};
55use crate::operation::Completion;
56use crate::providers::internal::wire_ids::WireIds;
57use crate::telemetry::GenAiOperation;
58use crate::wire::{Descriptor, Encoded, Framing, Mode, Wire};
59
60/// Provider name used in normalized responses, streams, and telemetry.
61pub const PROVIDER_NAME: &str = "gcp.gemini";
62
63/// Completion wire for unary `generateContent` and SSE `streamGenerateContent`.
64/// Both modes use [`GenerateContentDecoder`](super::streaming::GenerateContentDecoder).
65#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
66pub struct GenerateContent {
67    /// The key and the API root.
68    pub provider: super::GeminiConfig,
69    /// The model to address, e.g. [`GEMINI_2_5_FLASH`].
70    pub model: String,
71    /// Handle of a `cachedContents` resource every request reads its prefix
72    /// from. See [`Self::with_cached_content`].
73    pub cached_content: Option<String>,
74    /// Which thought signatures requests re-send. See [`ThoughtReplay`].
75    #[serde(default)]
76    pub thought_replay: ThoughtReplay,
77}
78
79/// Which earlier thought signatures a request re-sends.
80///
81/// On Gemini 3, every thought signature a request carries is expanded back
82/// into the reasoning it came from, and that reasoning is billed again as
83/// input on every later call. No explicit cache holds it. Measured on
84/// gemini-3.8-flash: a history whose earlier turns carried their signatures
85/// cost 6,708 prompt tokens; the same history without them cost 2,681, and
86/// Gemini answered normally. Over a 30-turn chat, dropping them cut input by
87/// 42%, and by 83% together with automatic caching.
88///
89/// Google's thinking guide says to send thought signatures back exactly as
90/// received. [`ThoughtReplay::CurrentTurn`] departs from that for turns the
91/// model has finished; its effect on answer quality is unmeasured. Hence
92/// [`ThoughtReplay::All`] is the default.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94pub enum ThoughtReplay {
95    /// Re-send every signature, as Google's guidance asks.
96    #[default]
97    All,
98    /// Re-send only the signatures at or after the newest user message: the
99    /// current turn, whose function calls Gemini validates. Parts before it
100    /// go out without their signature. The history itself is unchanged.
101    CurrentTurn,
102}
103
104impl GenerateContent {
105    /// The wire for `model`.
106    pub fn new(provider: super::GeminiConfig, model: impl Into<String>) -> Self {
107        Self {
108            provider,
109            model: model.into(),
110            cached_content: None,
111            thought_replay: ThoughtReplay::All,
112        }
113    }
114
115    /// Re-send thought signatures as `replay` says. See [`ThoughtReplay`].
116    pub fn thought_replay(mut self, replay: ThoughtReplay) -> Self {
117        self.thought_replay = replay;
118        self
119    }
120
121    /// Use an explicit `cachedContents/<id>` handle as every request's prefix.
122    /// Encoding rejects requests with their own system instruction, tools, or
123    /// tool choice. See [`crate::providers::gemini::cached_content`] for cache ownership.
124    pub fn with_cached_content(mut self, name: impl Into<String>) -> Self {
125        self.cached_content = Some(name.into());
126        self
127    }
128}
129
130impl<T> crate::driver::Model<GenerateContent, T> {
131    /// This model, re-sending thought signatures as `replay` says. See
132    /// [`ThoughtReplay`]. Call it before [`caching`](Self::caching), which
133    /// reads it.
134    pub fn thought_replay(mut self, replay: ThoughtReplay) -> Self {
135        self.wire.thought_replay = replay;
136        self
137    }
138}
139
140/// Remove thought signatures from every part before the newest user text
141/// content: the turns the model has finished. Only the signature keys go;
142/// every other field is unchanged.
143fn drop_finished_signatures(contents: &mut [Value]) {
144    let current = contents.iter().rposition(|content| {
145        let parts = content.arr("parts");
146        content.str("role") == Some("user")
147            && parts.iter().any(|part| part.get("text").is_some())
148            && !parts
149                .iter()
150                .any(|part| part.get("functionResponse").is_some())
151    });
152    for content in contents.iter_mut().take(current.unwrap_or(0)) {
153        let parts = content.get_mut("parts").and_then(Value::as_array_mut);
154        for part in parts.into_iter().flatten().filter_map(Value::as_object_mut) {
155            part.shift_remove("thoughtSignature");
156        }
157    }
158}
159
160impl Wire for GenerateContent {
161    type Op = Completion;
162    type Payload = Encoded;
163    type Frame = crate::wire::WireFrame;
164    type Decoder<'id> = super::streaming::GenerateContentDecoder;
165    type Reassembler = super::streaming::document::GenerateContentResponse;
166
167    fn describe(&self) -> Descriptor<'_> {
168        Descriptor::new(PROVIDER_NAME)
169            .model(self.model.as_str())
170            .telemetry(|_| GenAiOperation::GenerateContent)
171            .replay(self)
172    }
173
174    fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
175        // The request may name a model of its own; the wire's is the default.
176        let model = request.model.clone().unwrap_or_else(|| self.model.clone());
177        let replay = self.thought_replay;
178        let body = request_body(
179            &request,
180            self,
181            &model,
182            self.cached_content.as_deref(),
183            |body| {
184                if let (ThoughtReplay::CurrentTurn, Some(Value::Array(contents))) =
185                    (replay, body.get_mut("contents"))
186                {
187                    drop_finished_signatures(contents);
188                }
189            },
190        )?;
191        use crate::providers::internal::LogTarget;
192        // `alt=sse` is what makes the streamed reply an event stream rather
193        // than a JSON array of the same chunks.
194        let (verb, framing, target) = match mode {
195            Mode::Unary => ("generateContent", Framing::Whole, LogTarget::Completions),
196            Mode::Streaming => (
197                "streamGenerateContent?alt=sse",
198                Framing::Sse,
199                LogTarget::Streaming,
200            ),
201        };
202        crate::providers::internal::trace_json(target, "Gemini completion request", &body);
203        let uri = self.provider.uri(&format!("/v1beta/models/{model}:{verb}"));
204        let request = http::Request::post(uri)
205            .header("Content-Type", "application/json")
206            .body(body.into_body())?;
207        // Gemini supplies no transport request-id response header.
208        Ok(Encoded::new(request, framing)
209            .with_projection(super::streaming::GenerateContentDecoder::project)
210            .with_analysis_only(super::streaming::GenerateContentDecoder::is_analysis_only))
211    }
212
213    fn decoder<'id>(&self) -> Self::Decoder<'id> {
214        super::streaming::GenerateContentDecoder::default()
215    }
216}
217
218impl ReplayTarget for GenerateContent {
219    fn api(&self) -> crate::message::Api {
220        crate::message::Api::from_static("gemini.generate_content")
221    }
222
223    fn provider(&self) -> &str {
224        PROVIDER_NAME
225    }
226
227    fn model(&self) -> &str {
228        &self.model
229    }
230
231    fn accepts(&self, model: &str) -> Accepts {
232        accepts(model)
233    }
234
235    fn encodes(&self, _model: &str, media: Media<'_>) -> bool {
236        encodes(media, false)
237    }
238
239    fn normalize_tool_call_id(
240        &self,
241        id: &str,
242        model: &str,
243        _source: Option<&crate::message::Origin>,
244    ) -> String {
245        normalize_tool_call_id(model, id)
246    }
247
248    /// Gemini takes system text only in `systemInstruction`: later system
249    /// messages fold into the leading one, as pi's `collapseSystemMessages`.
250    fn later_system(&self, _model: &str) -> crate::completion::LaterSystem {
251        crate::completion::LaterSystem::Leading
252    }
253
254    fn call_id_slot(&self) -> Option<&'static str> {
255        CALL_ID_SLOT
256    }
257
258    fn declares_tools(&self, request: &crate::completion::CompletionRequest) -> bool {
259        self.cached_content.is_some() || declares_tools(self, request)
260    }
261
262    /// Section 6.4 of the typed-options design, for the Gemini API.
263    fn map_options(
264        &self,
265        request: &CompletionRequest,
266        fields: crate::completion::options::OptionFields<'_>,
267    ) -> crate::completion::options::OptionMap {
268        let model = request.model.as_deref().unwrap_or(&self.model);
269        generate_content_options(model, Route::Rest, fields)
270    }
271}
272
273/// Whether `request` declares tools on `target`, a GenerateContent wire: in
274/// `tools`, in the `tools` of its `additional_params`, or through a
275/// `cachedContent` handle there, since a cache holds its function
276/// declarations.
277pub fn declares_tools(
278    target: &dyn ReplayTarget,
279    request: &crate::completion::CompletionRequest,
280) -> bool {
281    let param = |key: &str| crate::completion::options::param(target, request, key);
282    !request.tools.is_empty()
283        || param("tools").is_some_and(|tools| !tools.as_array().is_none_or(Vec::is_empty))
284        || CACHED_CONTENT
285            .iter()
286            .any(|spelling| param(spelling).is_some_and(|value| !value.is_null()))
287}
288
289/// Where a GenerateContent part holds its call's id, on every wire that
290/// speaks it.
291pub const CALL_ID_SLOT: Option<&str> = Some("/functionCall/id");
292
293/// The major version of a Gemini model id, read past a `models/` prefix:
294/// `gemini-<major>…` or `gemini-live-<major>…`, or `None` for an id that is
295/// not Gemini's. A Gemini id that names no version, such as the alias
296/// `gemini-flash-latest`, is the current generation (`u32::MAX`), so every
297/// rule reads an alias as the newest model.
298pub(super) fn gemini_major(model: &str) -> Option<u32> {
299    let model = model.to_ascii_lowercase();
300    let model = model.strip_prefix("models/").unwrap_or(&model);
301    let rest = model.strip_prefix("gemini-")?;
302    let rest = rest.strip_prefix("live-").unwrap_or(rest);
303    let end = rest
304        .find(|c: char| !c.is_ascii_digit())
305        .unwrap_or(rest.len());
306    Some(
307        rest.get(..end)
308            .and_then(|major| major.parse().ok())
309            .unwrap_or(u32::MAX),
310    )
311}
312
313/// Whether `model` is Gemini 3 or later, which validates the thought
314/// signatures of function calls. A Gemini alias counts as current.
315pub(super) fn gemini_3_or_later(model: &str) -> bool {
316    gemini_major(model).is_some_and(|major| major >= 3)
317}
318
319/// Whether `model` takes function-call ids on its calls and their
320/// responses: Claude, gpt-oss, and Gemini 3 or later (pi's rule).
321pub fn requires_tool_call_id(model: &str) -> bool {
322    let model = model.to_ascii_lowercase();
323    model.starts_with("claude-") || model.starts_with("gpt-oss-") || gemini_3_or_later(&model)
324}
325
326/// What `model` reads on every Gemini wire (REST, Vertex AI, gRPC and
327/// Interactions): images in every role, and images inside function
328/// responses only from Gemini 3 on. A Gemini alias reads them as the
329/// current generation, and a model that is not Gemini (Claude behind
330/// Vertex AI) is assumed to read them, as pi assumes.
331pub fn accepts(model: &str) -> Accepts {
332    Accepts {
333        tool_result_images: gemini_major(model).is_none_or(|major| major >= 3),
334        ..Accepts::ALL
335    }
336}
337
338/// Whether a GenerateContent wire takes `media`: data or a URL of a media
339/// type Gemini reads. A YouTube video needs no media type, a file id is
340/// never taken, and a text document's data is left to the adapter, which
341/// sends its text. A function response takes image data, and a URL only when
342/// `response_files`: Vertex AI declares `fileData` there and the Gemini API
343/// does not.
344pub fn encodes(media: Media<'_>, response_files: bool) -> bool {
345    match media {
346        Media::Image(image, place) => {
347            reads_image(image.media_type.as_ref(), place)
348                && match image.data {
349                    Source::Base64(_) | Source::String(_) => true,
350                    Source::Url(_) => place != Place::ToolResult || response_files,
351                    _ => false,
352                }
353        }
354        Media::Audio(audio) => {
355            audio.media_type.is_some() && matches!(audio.data, Source::Url(_) | Source::Base64(_))
356        }
357        Media::Video(video) => match &video.data {
358            Source::Url(url) if url.starts_with("https://www.youtube.com") => true,
359            data => {
360                video.media_type.is_some() && matches!(data, Source::Url(_) | Source::Base64(_))
361            }
362        },
363        Media::Document(document) => match (&document.media_type, &document.data) {
364            (None, _) => false,
365            (Some(_), Source::Url(_) | Source::String(_)) => true,
366            (Some(media_type), Source::Base64(_)) => *media_type == DocumentMediaType::PDF,
367            (Some(_), _) => false,
368        },
369    }
370}
371
372/// Whether Gemini reads an image of `media_type` at `place`: JPEG, PNG,
373/// WEBP, HEIC or HEIF, and only the first three in a function response.
374pub(crate) fn reads_image(
375    media_type: Option<&crate::message::ImageMediaType>,
376    place: Place,
377) -> bool {
378    use crate::message::ImageMediaType::{HEIC, HEIF, JPEG, PNG, WEBP};
379    match place {
380        Place::ToolResult => matches!(media_type, Some(JPEG | PNG | WEBP)),
381        _ => matches!(media_type, Some(JPEG | PNG | WEBP | HEIC | HEIF)),
382    }
383}
384
385/// `id` as `model` takes another model's call id: when the model takes ids
386/// at all, characters outside `[a-zA-Z0-9_-]` become `_` and it keeps at
387/// most 64 of them.
388pub fn normalize_tool_call_id(model: &str, id: &str) -> String {
389    if !requires_tool_call_id(model) {
390        return id.to_owned();
391    }
392    crate::providers::internal::wire_ids::legal_call_id(id, 64)
393}
394
395/// The REST `GenerateContentRequest` body `request` sends to `model` on
396/// `target`, a GenerateContent wire. The REST wire sends it; the Vertex AI
397/// and gRPC wires read it into their SDK and protobuf requests.
398///
399/// System messages become the system instruction, and the rest the
400/// contents. `adjust` finishes the wire's own encoding (a transport's
401/// spellings of `contents`, a `model` key) before [`request_params`] merges
402/// the mapped options and then `additional_params` over it: its `tools`
403/// add to the request's, its `generationConfig` merges over the typed
404/// fields key by key, and a `cachedContent` handle, like `cached_content`,
405/// is checked as [`GenerateContent::with_cached_content`] checks one. The
406/// fields Gemini's request shape leaves unset are sent as `null`, as
407/// recorded requests spell them.
408///
409/// # Errors
410///
411/// When an option is refused, `additional_params` is malformed, a tool's
412/// parameters are no schema Gemini reads, or a field is set two ways.
413pub fn request_body(
414    request: &CompletionRequest,
415    target: &dyn ReplayTarget,
416    model: &str,
417    cached_content: Option<&str>,
418    adjust: impl FnOnce(&mut Map<String, Value>),
419) -> Result<FinalBody, EncodeError> {
420    request_params(
421        target,
422        request,
423        |input| {
424            let mut body = base(request, target, model, input)?;
425            adjust(&mut body);
426            Ok(body)
427        },
428        RawAt::Top,
429        &[Rewrite::GeminiCachedContent(
430            cached_content.map(str::to_owned),
431        )],
432    )
433}
434
435/// The wire's own encoding of `request`: contents, system instruction,
436/// tools, tool choice and the typed fields in `generationConfig`.
437fn base(
438    request: &CompletionRequest,
439    target: &dyn ReplayTarget,
440    model: &str,
441    input: &mut BaseInput<'_>,
442) -> Result<Map<String, Value>, EncodeError> {
443    let extra_tools = input.raw_tools()?;
444    let schema = request
445        .output_schema
446        .clone()
447        .map(|schema| schema.to_value());
448    let mime = schema.as_ref().map(|_| json!("application/json"));
449    let (temperature, max_tokens) = (
450        request.temperature.map(Value::from),
451        request.max_tokens.map(Value::from),
452    );
453    let typed = [
454        ("responseMimeType", mime),
455        ("responseJsonSchema", schema),
456        ("temperature", temperature),
457        ("maxOutputTokens", max_tokens),
458    ];
459    let config: Map<String, Value> = typed
460        .into_iter()
461        .filter_map(|(key, value)| Some((key.to_owned(), value?)))
462        .collect();
463    let (mut system, mut history) = (Vec::new(), Vec::new());
464    for message in &request.chat_history {
465        match message {
466            Message::System { content } if content.is_empty() => {}
467            Message::System { content } => system.push(text_part(content.clone())),
468            message => history.push(message.clone()),
469        }
470    }
471    // Gemini rejects a field set twice, and merges a tool choice set twice,
472    // which unions the allowed functions.
473    let preamble = "system instruction twice: as a preamble or system message";
474    let choice = "tool choice twice: as `tool_choice`, which Gemini merges,";
475    let twice = [
476        (!system.is_empty(), &SYSTEM_INSTRUCTION, preamble),
477        (request.tool_choice.is_some(), &TOOL_CONFIG, choice),
478    ];
479    for (set, spellings, what) in twice {
480        let raw = spellings
481            .iter()
482            .find(|spelling| input.param(spelling).is_some_and(|value| !value.is_null()));
483        if let (true, Some(spelling)) = (set, raw) {
484            return Err(EncodeError::request(format!(
485                "a Gemini request set the {what} and as `additional_params.{spelling}`. Set it \
486                 one way or the other"
487            )));
488        }
489    }
490    let declared = request
491        .tools
492        .iter()
493        .cloned()
494        .map(declaration)
495        .collect::<Vec<_>>();
496    let declared = (!declared.is_empty())
497        .then(|| json!({ "functionDeclarations": declared, "codeExecution": null }));
498    let tools: Vec<Value> = declared.into_iter().chain(extra_tools).collect();
499    let contents = Value::Array(contents(history, target, model)?);
500    let system = (!system.is_empty()).then(|| json!({ "parts": system, "role": "model" }));
501    let tool_config = request
502        .tool_choice
503        .clone()
504        .map_or(Value::Null, calling_config);
505    Ok(object([
506        ("contents", Some(contents)),
507        (
508            "generationConfig",
509            Some(if config.is_empty() {
510                Value::Null
511            } else {
512                Value::Object(config)
513            }),
514        ),
515        ("safetySettings", Some(Value::Null)),
516        ("toolConfig", Some(tool_config)),
517        ("systemInstruction", Some(system.unwrap_or(Value::Null))),
518        ("tools", (!tools.is_empty()).then_some(Value::Array(tools))),
519    ]))
520}
521
522/// A tool as its function declaration: its JSON Schema goes as
523/// `parametersJsonSchema`, which reads full JSON Schema, as pi sends it. A
524/// tool with no schema declares none.
525fn declaration(tool: crate::completion::ToolDefinition) -> Value {
526    let parameters = (!tool.parameters.is_null()).then_some(tool.parameters);
527    Value::Object(object([
528        ("name", Some(json!(tool.name))),
529        ("description", Some(json!(tool.description))),
530        ("parametersJsonSchema", parameters),
531    ]))
532}
533
534/// An object of the entries that are set.
535fn object<const N: usize>(entries: [(&str, Option<Value>); N]) -> Map<String, Value> {
536    let entries = entries.into_iter();
537    entries
538        .filter_map(|(key, value)| Some((key.to_owned(), value?)))
539        .collect()
540}
541
542/// Proto3 JSON accepts both lowerCamelCase and original proto field names.
543pub(super) const SYSTEM_INSTRUCTION: [&str; 2] = ["systemInstruction", "system_instruction"];
544pub(super) const TOOL_CONFIG: [&str; 2] = ["toolConfig", "tool_config"];
545const CACHED_CONTENT: [&str; 2] = ["cachedContent", "cached_content"];
546
547/// The first of `spellings` set to a value other than `null` in `body`.
548pub(super) fn present<'a>(body: &Map<String, Value>, spellings: &[&'a str]) -> Option<&'a str> {
549    let set = |spelling: &&&str| body.get(**spelling).is_some_and(|value| !value.is_null());
550    spellings.iter().find(set).copied()
551}
552
553/// The `functionCallingConfig` of `choice`.
554fn calling_config(choice: ToolChoice) -> Value {
555    let (mode, names) = match choice {
556        ToolChoice::Auto => ("AUTO", None),
557        ToolChoice::None => ("NONE", None),
558        ToolChoice::Required => ("ANY", None),
559        ToolChoice::Specific { function_names } => ("ANY", Some(json!(function_names))),
560    };
561    let config = object([
562        ("mode", Some(json!(mode))),
563        ("allowed_function_names", names),
564    ]);
565    json!({ "functionCallingConfig": config })
566}
567
568/// The Gemini contents for `history`, a history without system messages,
569/// sent to `model` on `target`. A user message's function responses and
570/// its other parts go in contents of their own, in order, as pi sends
571/// them: Gemini answers text that shares a content with function responses
572/// poorly. Calls and their responses carry the ids [`WireIds`] spells when
573/// `model` takes ids.
574fn contents(
575    history: Vec<Message>,
576    target: &dyn ReplayTarget,
577    model: &str,
578) -> Result<Vec<Value>, EncodeError> {
579    let ids = WireIds::for_target(&history, target, model);
580    let with_ids = requires_tool_call_id(model);
581    let mut contents = Vec::with_capacity(history.len());
582    let mut push = |role: &str, parts: Vec<Value>| {
583        // Gemini rejects a content with no parts, as pi skips one.
584        if !parts.is_empty() {
585            contents.push(json!({ "parts": parts, "role": role }));
586        }
587    };
588    for message in history {
589        match message {
590            Message::System { content } => push("user", vec![text_part(content)]),
591            Message::User { content } => {
592                let mut run = Vec::new();
593                let mut responses = false;
594                for part in content {
595                    let id = match &part {
596                        UserContent::ToolResult(result) => {
597                            Some(ids.of(&result.call).filter(|_| with_ids))
598                        }
599                        _ => None,
600                    };
601                    if id.is_some() != responses {
602                        push("user", std::mem::take(&mut run));
603                    }
604                    responses = id.is_some();
605                    run.push(user_part(part, id.flatten())?);
606                }
607                push("user", run);
608            }
609            Message::Assistant(turn) => {
610                let parts = turn
611                    .content
612                    .iter()
613                    .map(|block| assistant_part(block, target, &ids, model));
614                push(
615                    "model",
616                    parts
617                        .collect::<Result<Vec<_>, _>>()?
618                        .into_iter()
619                        .flatten()
620                        .collect(),
621                );
622            }
623        }
624    }
625    Ok(contents)
626}
627
628fn text_part(text: String) -> Value {
629    json!({ "text": text, "thought": false })
630}
631
632fn mime<M: MimeType>(media_type: Option<M>) -> Option<String> {
633    media_type.map(|media_type| media_type.to_mime_type().to_owned())
634}
635
636/// Where `source` puts media on every Gemini wire, REST and Interactions
637/// alike: a URL by reference (`true`) and base64 data inline. A string is
638/// inline as it stands when `verbatim`, else as the base64 of its bytes.
639/// Each wire's `encodes` refuses every other form, so the adapter passes
640/// none.
641pub(super) fn carried(source: Source, verbatim: bool) -> Result<(bool, String), EncodeError> {
642    use base64::Engine as _;
643    match source {
644        Source::Url(uri) => Ok((true, uri)),
645        Source::Base64(data) => Ok((false, data)),
646        Source::String(data) if verbatim => Ok((false, data)),
647        Source::String(data) => Ok((false, base64::prelude::BASE64_STANDARD.encode(data))),
648        _ => Err(EncodeError::request(
649            "Gemini cannot receive this media in its form",
650        )),
651    }
652}
653
654/// `source` of `mime_type` as GenerateContent part data, file data by URI
655/// or inline data, which needs a media type; a whole `part` is marked as no
656/// thought.
657fn media(
658    mime_type: Option<String>,
659    source: Source,
660    string_is_data: bool,
661    part: bool,
662) -> Result<Value, EncodeError> {
663    let (uri, data) = carried(source, string_is_data)?;
664    let data = match (uri, mime_type) {
665        (true, mime) => ("fileData", json!({ "mimeType": mime, "fileUri": data })),
666        (false, Some(mime)) => ("inlineData", json!({ "mimeType": mime, "data": data })),
667        (false, None) => {
668            return Err(EncodeError::request(
669                "Gemini cannot receive media without its type",
670            ));
671        }
672    };
673    Ok(Value::Object(object([
674        (data.0, Some(data.1)),
675        ("thought", part.then_some(Value::Bool(false))),
676    ])))
677}
678
679/// A user part as Gemini takes it. A function response carries `id`, its
680/// call's wire spelling, when the model takes ids, and the error key when
681/// the tool failed, as the Gemini SDKs spell a function's failure. Images
682/// the adapter leaves in a result (a model that reads them) go in its
683/// `parts`, in order.
684fn user_part(part: UserContent, id: Option<&str>) -> Result<Value, EncodeError> {
685    Ok(match part {
686        UserContent::Text(text) => text_part(text.text),
687        UserContent::ToolResult(result) => {
688            let (mut values, mut parts) = (Vec::new(), Vec::new());
689            for item in result.content {
690                match item {
691                    ToolResultContent::Text(text) => values.push(Value::String(text.text)),
692                    ToolResultContent::Json { value } => values.push(value),
693                    ToolResultContent::Image(image) => {
694                        parts.push(media(mime(image.media_type), image.data, true, false)?);
695                    }
696                }
697            }
698            let key = if result.is_error { "error" } else { "result" };
699            let value = match values.len() {
700                0 => None,
701                1 => values.pop(),
702                _ => Some(Value::Array(values)),
703            };
704            let response = object([
705                ("name", Some(json!(result.name))),
706                ("id", id.map(Value::from)),
707                ("response", value.map(|value| json!({ key: value }))),
708                ("parts", (!parts.is_empty()).then_some(Value::Array(parts))),
709            ]);
710            json!({ "functionResponse": response, "thought": false })
711        }
712        UserContent::Image(image) => media(mime(image.media_type), image.data, true, true)?,
713        // A text document goes as text, so that RAG context reads as prose.
714        UserContent::Document(document) => match (document.media_type, document.data) {
715            (Some(media_type), Source::String(text)) if media_type != DocumentMediaType::PDF => {
716                text_part(text)
717            }
718            (media_type, data) => media(mime(media_type), data, true, true)?,
719        },
720        UserContent::Audio(audio) => media(mime(audio.media_type), audio.data, false, true)?,
721        UserContent::Video(video) => {
722            let mut part = media(mime(video.media_type), video.data, false, true)?;
723            if let (Some(Value::Object(extra)), Some(part)) =
724                (video.additional_params, part.as_object_mut())
725            {
726                part.extend(extra);
727            }
728            part
729        }
730    })
731}
732
733/// One assistant block as a Gemini part: its provider item while that is
734/// current, else a part rebuilt from its canonical fields, or `None` when
735/// it has nothing to send. Calls carry ids only when `model` takes them. A
736/// rebuilt call on Gemini 3 carries Google's placeholder signature, since
737/// Gemini 3 rejects a call it did not sign without one ("Function call is
738/// missing a thought_signature in functionCall parts").
739fn assistant_part(
740    block: &AssistantContent,
741    target: &dyn ReplayTarget,
742    ids: &WireIds,
743    model: &str,
744) -> Result<Option<Value>, EncodeError> {
745    let with_ids = requires_tool_call_id(model);
746    if let Replay::Item(item) = block.replay(target, ids) {
747        let mut item = item.into_owned();
748        if let Some(part) = item.as_object_mut() {
749            if let (false, Some(Value::Object(call))) = (with_ids, part.get_mut("functionCall")) {
750                call.shift_remove("id");
751            }
752            // Gemini rejects the whole request over a signature that is
753            // not base64 ("Base64 decoding failed").
754            if part
755                .get("thoughtSignature")
756                .and_then(Value::as_str)
757                .is_some_and(|signature| !is_base64(signature))
758            {
759                part.shift_remove("thoughtSignature");
760            }
761        }
762        return Ok(Some(item));
763    }
764    Ok(Some(match block {
765        AssistantContent::Text(text) => json!({ "text": text.text }),
766        AssistantContent::Reasoning(reasoning)
767            if reasoning.redacted || reasoning.text.trim().is_empty() =>
768        {
769            return Ok(None);
770        }
771        AssistantContent::Reasoning(reasoning) => {
772            json!({ "thought": true, "text": reasoning.text })
773        }
774        AssistantContent::ToolCall(call) => {
775            let id = ids.of(&call.id).filter(|_| with_ids).map(Value::from);
776            let name = Some(json!(call.function.name));
777            let args = Some(json!(call.function.arguments));
778            let signature =
779                gemini_3_or_later(model).then(|| json!("skip_thought_signature_validator"));
780            let call = Value::Object(object([("name", name), ("args", args), ("id", id)]));
781            Value::Object(object([
782                ("functionCall", Some(call)),
783                ("thoughtSignature", signature),
784            ]))
785        }
786        AssistantContent::Image(image) => media(
787            mime(image.media_type.clone()),
788            image.data.clone(),
789            true,
790            true,
791        )?,
792        AssistantContent::Opaque(opaque) => opaque.item.clone(),
793    }))
794}
795
796/// Whether `text` is padded standard base64.
797fn is_base64(text: &str) -> bool {
798    let body = text.trim_end_matches('=');
799    let alphabet = |byte: u8| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/');
800    !body.is_empty()
801        && text.len().is_multiple_of(4)
802        && text.len() - body.len() <= 2
803        && body.bytes().all(alphabet)
804}
805
806/// Convert a specified prompt block reason into a provider error with its
807/// safety ratings. `feedback` is the reply's `promptFeedback` JSON, read
808/// leniently. Content refusals are final; `OTHER` and unknown reasons are
809/// transient. The zero value names no block: Gemini spells it
810/// `BLOCK_REASON_`, Vertex AI `BLOCKED_REASON_`.
811pub(crate) fn blocked_prompt_error(feedback: &Value) -> Option<ProviderError> {
812    let reason = match feedback.get("blockReason")? {
813        Value::String(reason) => reason.clone(),
814        Value::Number(number) => format!("BLOCK_REASON_{number}"),
815        _ => return None,
816    };
817    if matches!(
818        reason.as_str(),
819        "BLOCK_REASON_UNSPECIFIED" | "BLOCKED_REASON_UNSPECIFIED"
820    ) {
821        return None;
822    }
823    let spelled = |value: Option<&Value>| match value {
824        Some(Value::String(name)) => name.clone(),
825        Some(other) => other.to_string(),
826        None => "<unset>".to_owned(),
827    };
828    let ratings = feedback.arr("safetyRatings").iter();
829    let ratings = ratings.map(|rating| {
830        format!(
831            "{}={}",
832            spelled(rating.get("category")),
833            spelled(rating.get("probability"))
834        )
835    });
836    let ratings: Vec<String> = ratings.collect();
837    let ratings = match ratings.is_empty() {
838        true => String::new(),
839        false => format!(", safety_ratings=[{}]", ratings.join(", ")),
840    };
841    let message = format!("Gemini blocked the prompt: block_reason={reason}{ratings}");
842    let error = crate::provider_response::ProviderResponseError::without_status(message)
843        .with_code(Some(reason.clone()));
844    Some(ProviderError::ProviderResponse(match reason.as_str() {
845        "SAFETY" | "BLOCKLIST" | "PROHIBITED_CONTENT" | "IMAGE_SAFETY" | "MODEL_ARMOR"
846        | "JAILBREAK" => error.with_refusal(true),
847        _ => error.with_transient(Some(true)),
848    }))
849}
850
851/// Map a Google `finishReason` in its SCREAMING_SNAKE_CASE wire spelling.
852/// `STOP` is a stop (a tool use when the turn holds calls) and `MAX_TOKENS`
853/// a length stop. Every other reason, documented or not, is a failure: the
854/// content filters as
855/// [`ContentFilter`](crate::completion::FinishReason::ContentFilter), the
856/// rest as [`Other`](crate::completion::FinishReason::Other) with the
857/// reason's name.
858pub fn map_google_finish_reason(wire_name: &str) -> crate::completion::FinishReason {
859    use crate::completion::FinishReason;
860    match wire_name {
861        "STOP" => FinishReason::Stop,
862        "MAX_TOKENS" => FinishReason::Length,
863        "SAFETY"
864        | "BLOCKLIST"
865        | "PROHIBITED_CONTENT"
866        | "SPII"
867        | "IMAGE_SAFETY"
868        | "IMAGE_PROHIBITED_CONTENT"
869        | "MODEL_ARMOR" => FinishReason::ContentFilter,
870        other => FinishReason::Other(other.to_owned()),
871    }
872}
873
874/// Rig's usage for a `usageMetadata` document, read leniently: a count that
875/// is absent or not a non-negative integer is unreported, and no other field
876/// is read, so usage never fails a reply. Rig's input is the prompt plus the
877/// tool-use prompt, its output the candidates plus the thoughts, and its
878/// total their sum. A count Gemini leaves out is zero in those sums, as its
879/// JSON omits zeros. The REST, Vertex AI and gRPC wires all read usage here.
880pub fn usage_of(usage: &Value) -> crate::completion::Usage {
881    let count = |key: &str| usage.get(key).and_then(Value::as_u64);
882    let tool_use = count("toolUsePromptTokenCount");
883    let thoughts = count("thoughtsTokenCount");
884    let input = count("promptTokenCount")
885        .unwrap_or(0)
886        .saturating_add(tool_use.unwrap_or(0));
887    let output = count("candidatesTokenCount")
888        .unwrap_or(0)
889        .saturating_add(thoughts.unwrap_or(0));
890    crate::completion::Usage {
891        input_tokens: Some(input),
892        output_tokens: Some(output),
893        cached_input_tokens: count("cachedContentTokenCount"),
894        reasoning_tokens: thoughts,
895        tool_use_prompt_tokens: tool_use,
896        total_tokens: Some(input.saturating_add(output)),
897        cache_creation_input_tokens: None,
898        cost: None,
899    }
900}
901
902#[cfg(test)]
903mod tests;
904
905#[cfg(test)]
906mod cached_content_conflict_matrix;
907#[cfg(test)]
908mod cached_content_request_tests;