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