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_EXP_03_25: &str = "gemini-2.5-pro-exp-03-25";
25pub const GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
27pub 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::message::{
40 AssistantContent, DocumentData, DocumentMediaType, DocumentSourceKind as Source, Message,
41 MimeType, ToolChoice, ToolResultContent, UserContent,
42};
43use crate::operation::Completion;
44use crate::providers::internal::set_fields;
45use crate::providers::internal::wire_ids::WireIds;
46use crate::telemetry::GenAiOperation;
47use crate::wire::{Descriptor, Encoded, Framing, Mode, Wire};
48
49pub const PROVIDER_NAME: &str = "gcp.gemini";
51
52#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
55pub struct GenerateContent {
56 pub provider: super::GeminiConfig,
58 pub model: String,
60 pub cached_content: Option<String>,
63 #[serde(default)]
65 pub thought_replay: ThoughtReplay,
66 #[serde(skip)]
68 pub facts: crate::catalog::ModelFacts,
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
86pub enum ThoughtReplay {
87 #[default]
89 All,
90 CurrentTurn,
94}
95
96impl GenerateContent {
97 pub fn new(provider: super::GeminiConfig, model: impl Into<String>) -> Self {
99 Self {
100 provider,
101 model: model.into(),
102 cached_content: None,
103 thought_replay: ThoughtReplay::All,
104 facts: crate::catalog::ModelFacts::default(),
105 }
106 }
107
108 pub fn with_facts(mut self, facts: crate::catalog::ModelFacts) -> Self {
111 self.facts = facts;
112 self
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 Event = super::streaming::GenerateContentChunk;
165 type Decoder<'id> = super::streaming::GenerateContentDecoder;
166 type Reassembler = super::streaming::document::GenerateContentResponse;
167
168 fn describe(&self) -> Descriptor<'_> {
169 Descriptor::new(PROVIDER_NAME)
170 .model(self.model.as_str())
171 .facts(&self.facts)
172 .telemetry(|_| GenAiOperation::GenerateContent)
173 .replay(self)
174 }
175
176 fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
177 let model = request.model.clone().unwrap_or_else(|| self.model.clone());
179 let replay = self.thought_replay;
180 let body = request_body(
181 &request,
182 self,
183 &model,
184 self.cached_content.as_deref(),
185 |body| {
186 if let (ThoughtReplay::CurrentTurn, Some(Value::Array(contents))) =
187 (replay, body.get_mut("contents"))
188 {
189 drop_finished_signatures(contents);
190 }
191 },
192 )?;
193 use crate::providers::internal::LogTarget;
194 let (verb, framing, target) = match mode {
197 Mode::Unary => ("generateContent", Framing::Whole, LogTarget::Completions),
198 Mode::Streaming => (
199 "streamGenerateContent?alt=sse",
200 Framing::Sse,
201 LogTarget::Streaming,
202 ),
203 };
204 crate::providers::internal::trace_json(target, "Gemini completion request", &body);
205 let uri = self.provider.uri(&format!("/v1beta/models/{model}:{verb}"));
206 let request = http::Request::post(uri)
207 .header("Content-Type", "application/json")
208 .body(body.into_body())?;
209 Ok(Encoded::new(request, framing)
211 .with_projection(super::streaming::GenerateContentDecoder::project)
212 .with_analysis_only(super::streaming::GenerateContentDecoder::is_analysis_only))
213 }
214
215 fn decoder<'id>(&self) -> Self::Decoder<'id> {
216 super::streaming::GenerateContentDecoder::default()
217 }
218}
219
220impl ReplayTarget for GenerateContent {
221 fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
222 Some(&self.facts)
223 }
224
225 fn api(&self) -> crate::message::Api {
226 crate::message::Api::from_static("gemini.generate_content")
227 }
228
229 fn provider(&self) -> &str {
230 PROVIDER_NAME
231 }
232
233 fn model(&self) -> &str {
234 &self.model
235 }
236
237 fn accepts(&self, model: &str) -> Accepts {
238 accepts(model)
239 }
240
241 fn encodes(&self, _model: &str, media: Media<'_>) -> bool {
242 encodes(media, false)
243 }
244
245 fn normalize_tool_call_id(
246 &self,
247 id: &str,
248 model: &str,
249 _source: Option<&crate::message::Origin>,
250 ) -> String {
251 normalize_tool_call_id(model, id)
252 }
253
254 fn later_system(&self, _model: &str) -> crate::completion::LaterSystem {
257 crate::completion::LaterSystem::Leading
258 }
259
260 fn call_id_slot(&self) -> Option<&'static str> {
261 CALL_ID_SLOT
262 }
263
264 fn declares_tools(&self, request: &crate::completion::CompletionRequest) -> bool {
265 self.cached_content.is_some() || declares_tools(self, request)
266 }
267
268 fn map_options(
270 &self,
271 request: &CompletionRequest,
272 fields: crate::completion::options::OptionFields<'_>,
273 ) -> crate::completion::options::OptionMap {
274 let model = request.model.as_deref().unwrap_or(&self.model);
275 generate_content_options(&self.facts, model, Route::Rest, fields)
276 }
277}
278
279pub fn declares_tools(
284 target: &dyn ReplayTarget,
285 request: &crate::completion::CompletionRequest,
286) -> bool {
287 let param = |key: &str| crate::completion::options::param(target, request, key);
288 !request.tools.is_empty()
289 || param("tools").is_some_and(|tools| !tools.as_array().is_none_or(Vec::is_empty))
290 || CACHED_CONTENT
291 .iter()
292 .any(|spelling| param(spelling).is_some_and(|value| !value.is_null()))
293}
294
295pub const CALL_ID_SLOT: Option<&str> = Some("/functionCall/id");
298
299pub(super) fn gemini_major(model: &str) -> Option<u32> {
305 let model = model.to_ascii_lowercase();
306 let model = model.strip_prefix("models/").unwrap_or(&model);
307 let rest = model.strip_prefix("gemini-")?;
308 let rest = rest.strip_prefix("live-").unwrap_or(rest);
309 let end = rest
310 .find(|c: char| !c.is_ascii_digit())
311 .unwrap_or(rest.len());
312 Some(
313 rest.get(..end)
314 .and_then(|major| major.parse().ok())
315 .unwrap_or(u32::MAX),
316 )
317}
318
319pub(super) fn gemini_3_or_later(model: &str) -> bool {
322 gemini_major(model).is_some_and(|major| major >= 3)
323}
324
325pub fn requires_tool_call_id(model: &str) -> bool {
328 let model = model.to_ascii_lowercase();
329 model.starts_with("claude-") || model.starts_with("gpt-oss-") || gemini_3_or_later(&model)
330}
331
332pub fn accepts(model: &str) -> Accepts {
338 Accepts {
339 tool_result_images: gemini_major(model).is_none_or(|major| major >= 3),
340 ..Accepts::ALL
341 }
342}
343
344pub fn encodes(media: Media<'_>, response_files: bool) -> bool {
350 match media {
351 Media::Image(image, place) => {
352 reads_image(image.media_type.as_ref(), place)
353 && match image.data {
354 Source::Base64(_) => true,
355 Source::Url(_) => place != Place::ToolResult || response_files,
356 _ => false,
357 }
358 }
359 Media::Audio(audio) => {
360 audio.media_type.is_some() && matches!(audio.data, Source::Url(_) | Source::Base64(_))
361 }
362 Media::Video(video) => match &video.data {
363 Source::Url(url) if url.starts_with("https://www.youtube.com") => true,
364 data => {
365 video.media_type.is_some() && matches!(data, Source::Url(_) | Source::Base64(_))
366 }
367 },
368 Media::Document(document) => match (&document.media_type, &document.data) {
369 (_, DocumentData::Text(_)) => true,
370 (Some(_), DocumentData::File(Source::Url(_))) => true,
371 (Some(media_type), DocumentData::File(Source::Base64(_))) => {
372 *media_type == DocumentMediaType::PDF
373 }
374 _ => false,
375 },
376 }
377}
378
379pub(crate) fn reads_image(
382 media_type: Option<&crate::message::ImageMediaType>,
383 place: Place,
384) -> bool {
385 use crate::message::ImageMediaType::{HEIC, HEIF, JPEG, PNG, WEBP};
386 match place {
387 Place::ToolResult => matches!(media_type, Some(JPEG | PNG | WEBP)),
388 _ => matches!(media_type, Some(JPEG | PNG | WEBP | HEIC | HEIF)),
389 }
390}
391
392pub fn normalize_tool_call_id(model: &str, id: &str) -> String {
396 if !requires_tool_call_id(model) {
397 return id.to_owned();
398 }
399 crate::providers::internal::wire_ids::legal_call_id(id, 64)
400}
401
402pub fn request_body(
421 request: &CompletionRequest,
422 target: &dyn ReplayTarget,
423 model: &str,
424 cached_content: Option<&str>,
425 adjust: impl FnOnce(&mut Map<String, Value>),
426) -> Result<FinalBody, EncodeError> {
427 request_params(
428 target,
429 request,
430 |input| {
431 let mut body = base(request, target, model, input)?;
432 adjust(&mut body);
433 Ok(body)
434 },
435 RawAt::Top,
436 &[Rewrite::GeminiCachedContent(
437 cached_content.map(str::to_owned),
438 )],
439 )
440}
441
442fn base(
445 request: &CompletionRequest,
446 target: &dyn ReplayTarget,
447 model: &str,
448 input: &mut BaseInput<'_>,
449) -> Result<Map<String, Value>, EncodeError> {
450 let extra_tools = input.raw_tools()?;
451 let schema = request
452 .output_schema
453 .clone()
454 .map(|schema| schema.to_value());
455 let mime = schema.as_ref().map(|_| json!("application/json"));
456 let (temperature, max_tokens) = (
457 request.temperature.map(Value::from),
458 request.max_tokens.map(Value::from),
459 );
460 let config = set_fields([
461 ("responseMimeType", mime),
462 ("responseJsonSchema", schema),
463 ("temperature", temperature),
464 ("maxOutputTokens", max_tokens),
465 ]);
466 let (mut system, mut history) = (Vec::new(), Vec::new());
467 for message in &request.chat_history {
468 match &**message {
469 Message::System { content } if content.is_empty() => {}
470 Message::System { content } => system.push(text_part(content)),
471 message => history.push(message),
472 }
473 }
474 let preamble = "system instruction twice: as a preamble or system message";
477 let choice = "tool choice twice: as `tool_choice`, which Gemini merges,";
478 let twice = [
479 (!system.is_empty(), &SYSTEM_INSTRUCTION, preamble),
480 (request.tool_choice.is_some(), &TOOL_CONFIG, choice),
481 ];
482 for (set, spellings, what) in twice {
483 let raw = spellings
484 .iter()
485 .find(|spelling| input.param(spelling).is_some_and(|value| !value.is_null()));
486 if let (true, Some(spelling)) = (set, raw) {
487 return Err(EncodeError::request(format!(
488 "a Gemini request set the {what} and as `additional_params.{spelling}`. Set it \
489 one way or the other"
490 )));
491 }
492 }
493 let declared: Vec<Value> = request.tools.iter().map(|tool| declaration(tool)).collect();
494 let declared = (!declared.is_empty()).then(|| {
495 Value::Object(set_fields([
496 ("functionDeclarations", Some(Value::Array(declared))),
497 ("codeExecution", Some(Value::Null)),
498 ]))
499 });
500 let tools: Vec<Value> = declared.into_iter().chain(extra_tools).collect();
501 let contents = Value::Array(contents(&history, target, model)?);
502 let system = (!system.is_empty()).then(|| content(system, "model"));
503 let tool_config = request
504 .tool_choice
505 .clone()
506 .map_or(Value::Null, calling_config);
507 Ok(set_fields([
508 ("contents", Some(contents)),
509 (
510 "generationConfig",
511 Some(if config.is_empty() {
512 Value::Null
513 } else {
514 Value::Object(config)
515 }),
516 ),
517 ("safetySettings", Some(Value::Null)),
518 ("toolConfig", Some(tool_config)),
519 ("systemInstruction", Some(system.unwrap_or(Value::Null))),
520 ("tools", (!tools.is_empty()).then_some(Value::Array(tools))),
521 ]))
522}
523
524fn declaration(tool: &crate::completion::ToolDefinition) -> Value {
528 let parameters = (!tool.parameters.is_null()).then(|| tool.parameters.clone());
529 Value::Object(set_fields([
530 ("name", Some(Value::from(tool.name.as_str()))),
531 ("description", Some(Value::from(tool.description.as_str()))),
532 ("parametersJsonSchema", parameters),
533 ]))
534}
535
536pub(super) const SYSTEM_INSTRUCTION: [&str; 2] = ["systemInstruction", "system_instruction"];
538pub(super) const TOOL_CONFIG: [&str; 2] = ["toolConfig", "tool_config"];
539const CACHED_CONTENT: [&str; 2] = ["cachedContent", "cached_content"];
540
541pub(super) fn present<'a>(body: &Map<String, Value>, spellings: &[&'a str]) -> Option<&'a str> {
543 let set = |spelling: &&&str| body.get(**spelling).is_some_and(|value| !value.is_null());
544 spellings.iter().find(set).copied()
545}
546
547fn calling_config(choice: ToolChoice) -> Value {
549 let (mode, names) = match choice {
550 ToolChoice::Auto => ("AUTO", None),
551 ToolChoice::None => ("NONE", None),
552 ToolChoice::Required => ("ANY", None),
553 ToolChoice::Specific { function_names } => ("ANY", Some(json!(function_names))),
554 };
555 let config = set_fields([
556 ("mode", Some(json!(mode))),
557 ("allowed_function_names", names),
558 ]);
559 json!({ "functionCallingConfig": config })
560}
561
562fn contents(
569 history: &[&Message],
570 target: &dyn ReplayTarget,
571 model: &str,
572) -> Result<Vec<Value>, EncodeError> {
573 let ids = WireIds::for_target(history, target, model);
574 let (with_ids, gemini3) = (requires_tool_call_id(model), gemini_3_or_later(model));
575 let mut contents = Vec::with_capacity(history.len());
576 let mut push = |role: &str, parts: Vec<Value>| {
577 if !parts.is_empty() {
579 contents.push(content(parts, role));
580 }
581 };
582 for message in history {
583 match message {
584 Message::System { content } => push("user", vec![text_part(content)]),
585 Message::User { content } => {
586 let mut run = Vec::new();
587 let mut responses = false;
588 for part in content {
589 let id = match part {
590 UserContent::ToolResult(result) => {
591 Some(ids.of(&result.call).filter(|_| with_ids))
592 }
593 _ => None,
594 };
595 if id.is_some() != responses {
596 push("user", std::mem::take(&mut run));
597 }
598 responses = id.is_some();
599 run.push(user_part(part, id.flatten())?);
600 }
601 push("user", run);
602 }
603 Message::Assistant(turn) => {
604 let parts = turn
605 .content
606 .iter()
607 .map(|block| assistant_part(block, target, &ids, with_ids, gemini3));
608 push(
609 "model",
610 parts
611 .collect::<Result<Vec<_>, _>>()?
612 .into_iter()
613 .flatten()
614 .collect(),
615 );
616 }
617 }
618 }
619 Ok(contents)
620}
621
622fn content(parts: Vec<Value>, role: &str) -> Value {
624 Value::Object(set_fields([
625 ("parts", Some(Value::Array(parts))),
626 ("role", Some(Value::from(role))),
627 ]))
628}
629
630fn text_part(text: &str) -> Value {
631 json!({ "text": text, "thought": false })
632}
633
634pub(super) fn mime<M: MimeType>(media_type: Option<M>) -> Option<String> {
635 media_type.map(|media_type| media_type.to_mime_type().to_owned())
636}
637
638pub(super) fn carried(source: &Source) -> Result<(bool, &str), EncodeError> {
642 match source {
643 Source::Url(uri) => Ok((true, uri)),
644 Source::Base64(data) => Ok((false, data)),
645 _ => Err(EncodeError::request(
646 "Gemini cannot receive this media in its form",
647 )),
648 }
649}
650
651pub(super) fn media(
655 mime_type: Option<String>,
656 source: &Source,
657 part: bool,
658) -> Result<Value, EncodeError> {
659 let (uri, data) = carried(source)?;
660 let data = match (uri, mime_type) {
661 (true, mime) => ("fileData", json!({ "mimeType": mime, "fileUri": data })),
662 (false, Some(mime)) => ("inlineData", json!({ "mimeType": mime, "data": data })),
663 (false, None) => {
664 return Err(EncodeError::request(
665 "Gemini cannot receive media without its type",
666 ));
667 }
668 };
669 Ok(Value::Object(set_fields([
670 (data.0, Some(data.1)),
671 ("thought", part.then_some(Value::Bool(false))),
672 ])))
673}
674
675fn user_part(part: &UserContent, id: Option<&str>) -> Result<Value, EncodeError> {
681 Ok(match part {
682 UserContent::Text(text) => text_part(&text.text),
683 UserContent::ToolResult(result) => {
684 let (mut values, mut parts) = (Vec::new(), Vec::new());
685 for item in &result.content {
686 match item {
687 ToolResultContent::Text(text) => values.push(Value::from(text.text.as_str())),
688 ToolResultContent::Json { value } => values.push(value.clone()),
689 ToolResultContent::Image(image) => {
690 parts.push(media(mime(image.media_type.clone()), &image.data, false)?);
691 }
692 }
693 }
694 let key = if result.is_error { "error" } else { "result" };
695 let value = match values.len() {
696 0 => None,
697 1 => values.pop(),
698 _ => Some(Value::Array(values)),
699 };
700 let response = set_fields([
701 ("name", Some(json!(result.name))),
702 ("id", id.map(Value::from)),
703 (
704 "response",
705 value.map(|value| Value::Object(Map::from_iter([(key.into(), value)]))),
706 ),
707 ("parts", (!parts.is_empty()).then_some(Value::Array(parts))),
708 ]);
709 Value::Object(set_fields([
710 ("functionResponse", Some(Value::Object(response))),
711 ("thought", Some(Value::Bool(false))),
712 ]))
713 }
714 UserContent::Image(image) => media(mime(image.media_type.clone()), &image.data, true)?,
715 UserContent::Document(document) => match &document.data {
717 DocumentData::Text(text) => text_part(text),
718 DocumentData::File(data) => media(mime(document.media_type.clone()), data, true)?,
719 },
720 UserContent::Audio(audio) => media(mime(audio.media_type.clone()), &audio.data, true)?,
721 UserContent::Video(video) => {
722 let mut part = media(mime(video.media_type.clone()), &video.data, true)?;
723 if let (Some(Value::Object(extra)), Some(part)) =
724 (&video.additional_params, part.as_object_mut())
725 {
726 part.extend(extra.clone());
727 }
728 part
729 }
730 })
731}
732
733fn assistant_part(
740 block: &AssistantContent,
741 target: &dyn ReplayTarget,
742 ids: &WireIds,
743 with_ids: bool,
744 gemini3: bool,
745) -> Result<Option<Value>, EncodeError> {
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 = gemini3.then(|| json!("skip_thought_signature_validator"));
779 let call = Value::Object(set_fields([("name", name), ("args", args), ("id", id)]));
780 Value::Object(set_fields([
781 ("functionCall", Some(call)),
782 ("thoughtSignature", signature),
783 ]))
784 }
785 AssistantContent::Image(image) => media(mime(image.media_type.clone()), &image.data, true)?,
786 AssistantContent::Opaque(opaque) => opaque.item.clone(),
787 }))
788}
789
790fn is_base64(text: &str) -> bool {
792 let body = text.trim_end_matches('=');
793 let alphabet = |byte: u8| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/');
794 !body.is_empty()
795 && text.len().is_multiple_of(4)
796 && text.len() - body.len() <= 2
797 && body.bytes().all(alphabet)
798}
799
800pub(crate) fn blocked_prompt_error(feedback: &Value) -> Option<ProviderError> {
806 let reason = match feedback.get("blockReason")? {
807 Value::String(reason) => reason.clone(),
808 Value::Number(number) => format!("BLOCK_REASON_{number}"),
809 _ => return None,
810 };
811 if matches!(
812 reason.as_str(),
813 "BLOCK_REASON_UNSPECIFIED" | "BLOCKED_REASON_UNSPECIFIED"
814 ) {
815 return None;
816 }
817 let spelled = |value: Option<&Value>| match value {
818 Some(Value::String(name)) => name.clone(),
819 Some(other) => other.to_string(),
820 None => "<unset>".to_owned(),
821 };
822 let ratings = feedback.arr("safetyRatings").iter();
823 let ratings = ratings.map(|rating| {
824 format!(
825 "{}={}",
826 spelled(rating.get("category")),
827 spelled(rating.get("probability"))
828 )
829 });
830 let ratings: Vec<String> = ratings.collect();
831 let ratings = match ratings.is_empty() {
832 true => String::new(),
833 false => format!(", safety_ratings=[{}]", ratings.join(", ")),
834 };
835 let message = format!("Gemini blocked the prompt: block_reason={reason}{ratings}");
836 let error = crate::provider_response::ProviderResponseError::without_status(message)
837 .with_code(Some(reason.clone()));
838 Some(ProviderError::ProviderResponse(match reason.as_str() {
839 "SAFETY" | "BLOCKLIST" | "PROHIBITED_CONTENT" | "IMAGE_SAFETY" | "MODEL_ARMOR"
840 | "JAILBREAK" => error.with_refusal(true),
841 _ => error.with_transient(Some(true)),
842 }))
843}
844
845pub fn map_google_finish_reason(wire_name: &str) -> crate::completion::FinishReason {
853 use crate::completion::FinishReason;
854 match wire_name {
855 "STOP" => FinishReason::Stop,
856 "MAX_TOKENS" => FinishReason::Length,
857 "SAFETY"
858 | "BLOCKLIST"
859 | "PROHIBITED_CONTENT"
860 | "SPII"
861 | "IMAGE_SAFETY"
862 | "IMAGE_PROHIBITED_CONTENT"
863 | "MODEL_ARMOR" => FinishReason::ContentFilter,
864 other => FinishReason::Other(other.to_owned()),
865 }
866}
867
868pub(crate) fn classify_reply(body: &str) -> crate::wire::WireEvent<Value> {
871 crate::providers::internal::wire::classify_marker_keyed_frame(
872 body,
873 &["candidates", "promptFeedback", "usageMetadata"],
874 )
875}
876
877pub fn usage_of(usage: &Value) -> crate::completion::Usage {
884 let count = |key: &str| usage.get(key).and_then(Value::as_u64);
885 let tool_use = count("toolUsePromptTokenCount");
886 let thoughts = count("thoughtsTokenCount");
887 let input = count("promptTokenCount")
888 .unwrap_or(0)
889 .saturating_add(tool_use.unwrap_or(0));
890 let output = count("candidatesTokenCount")
891 .unwrap_or(0)
892 .saturating_add(thoughts.unwrap_or(0));
893 crate::completion::Usage {
894 input_tokens: Some(input),
895 output_tokens: Some(output),
896 cached_input_tokens: count("cachedContentTokenCount"),
897 reasoning_tokens: thoughts,
898 tool_use_prompt_tokens: tool_use,
899 total_tokens: Some(input.saturating_add(output)),
900 cache_creation_input_tokens: None,
901 cost: None,
902 }
903}
904
905#[cfg(test)]
906mod tests;
907
908#[cfg(test)]
909mod cached_content_conflict_matrix;
910#[cfg(test)]
911mod cached_content_request_tests;