1pub const GEMINI_3_8_FLASH: &str = "gemini-3.8-flash";
17pub const GEMINI_3_1_FLASH_LITE_PREVIEW: &str = "gemini-3.1-flash-lite-preview";
19pub const GEMINI_3_FLASH_PREVIEW: &str = "gemini-3-flash-preview";
21pub const GEMINI_2_5_PRO_PREVIEW_06_05: &str = "gemini-2.5-pro-preview-06-05";
23pub const GEMINI_2_5_PRO_PREVIEW_05_06: &str = "gemini-2.5-pro-preview-05-06";
25pub const GEMINI_2_5_PRO_PREVIEW_03_25: &str = "gemini-2.5-pro-preview-03-25";
27pub const GEMINI_2_5_FLASH_PREVIEW_04_17: &str = "gemini-2.5-flash-preview-04-17";
29pub const GEMINI_2_5_PRO_EXP_03_25: &str = "gemini-2.5-pro-exp-03-25";
31pub const GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
33#[cfg(feature = "image")]
35#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
36pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
37pub const GEMINI_2_0_FLASH_LITE: &str = "gemini-2.0-flash-lite";
39pub 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
60pub const PROVIDER_NAME: &str = "gcp.gemini";
62
63#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
66pub struct GenerateContent {
67 pub provider: super::GeminiConfig,
69 pub model: String,
71 pub cached_content: Option<String>,
74 #[serde(default)]
76 pub thought_replay: ThoughtReplay,
77}
78
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94pub enum ThoughtReplay {
95 #[default]
97 All,
98 CurrentTurn,
102}
103
104impl GenerateContent {
105 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 pub fn thought_replay(mut self, replay: ThoughtReplay) -> Self {
117 self.thought_replay = replay;
118 self
119 }
120
121 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 pub fn thought_replay(mut self, replay: ThoughtReplay) -> Self {
135 self.wire.thought_replay = replay;
136 self
137 }
138}
139
140fn 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 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 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 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 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 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
273pub 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
289pub const CALL_ID_SLOT: Option<&str> = Some("/functionCall/id");
292
293pub(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
313pub(super) fn gemini_3_or_later(model: &str) -> bool {
316 gemini_major(model).is_some_and(|major| major >= 3)
317}
318
319pub 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
326pub 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
338pub 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
372pub(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
385pub 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
395pub 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
435fn 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 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
522fn 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
534fn 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
542pub(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
547pub(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
553fn 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
568fn 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 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
636pub(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
654fn 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
679fn 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 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
733fn 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 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
796fn 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
806pub(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
851pub 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
874pub 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;