1use std::collections::BTreeMap;
19
20use serde::de::Error as _;
21use serde::ser::SerializeMap;
22use serde::{Deserialize, Serialize, Serializer};
23use serde_json::Value;
24
25use crate::completion::provider_options::{ExtensionOptions, ProviderExtension, ReplyExtras};
26use crate::message::Api;
27
28use super::ImageSize;
29use super::interactions_api::InteractionSpeechConfig;
30
31const GENERATE_CONTENT: &str = "gemini.generate_content";
33
34const INTERACTIONS: &str = "gemini.interactions";
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub struct GeminiExt;
40
41impl ProviderExtension for GeminiExt {
42 const PROVIDER: &'static str = super::PROVIDER_NAME;
43 type Options = GeminiOptions;
44 type Extras = GeminiExtras;
45}
46
47fn is_default<T: Default + PartialEq>(value: &T) -> bool {
50 *value == T::default()
51}
52
53#[non_exhaustive]
62#[derive(Clone, Debug, Default, PartialEq, Serialize)]
63pub struct GeminiOptions {
64 #[serde(rename = "*")]
66 pub shared: GeminiShared,
67 #[serde(rename = "gemini.generate_content")]
69 pub generate_content: GenerateContentOptions,
70 #[serde(rename = "gemini.interactions")]
72 pub interactions: InteractionsOptions,
73}
74
75impl GeminiOptions {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn store(mut self, store: bool) -> Self {
83 self.shared.store = Some(store);
84 self
85 }
86
87 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
90 self.shared.labels.insert(key.into(), value.into());
91 self
92 }
93
94 pub fn generate_content(mut self, section: GenerateContentOptions) -> Self {
96 self.generate_content = section;
97 self
98 }
99
100 pub fn interactions(mut self, section: InteractionsOptions) -> Self {
102 self.interactions = section;
103 self
104 }
105
106 fn generation_config_entry(
108 mut self,
109 set: impl FnOnce(GenerationConfig) -> GenerationConfig,
110 ) -> Self {
111 let common = std::mem::take(&mut self.generate_content.generation_config.common);
112 self.generate_content.generation_config.common = set(common);
113 self
114 }
115
116 pub fn include_thoughts(self, include: bool) -> Self {
120 self.generation_config_entry(|config| config.include_thoughts(include))
121 }
122
123 pub fn top_k(self, top_k: u32) -> Self {
126 self.generation_config_entry(|config| config.top_k(top_k))
127 }
128
129 pub fn presence_penalty(self, penalty: f64) -> Self {
132 self.generation_config_entry(|config| config.presence_penalty(penalty))
133 }
134
135 pub fn frequency_penalty(self, penalty: f64) -> Self {
139 self.generation_config_entry(|config| config.frequency_penalty(penalty))
140 }
141
142 pub fn response_logprobs(self, enable: bool) -> Self {
146 self.generation_config_entry(|config| config.response_logprobs(enable))
147 }
148
149 pub fn logprobs(self, top: u32) -> Self {
152 self.generation_config_entry(|config| config.logprobs(top))
153 }
154
155 pub fn candidate_count(self, count: CandidateCount) -> Self {
158 self.generation_config_entry(|config| config.candidate_count(count))
159 }
160
161 pub fn response_modalities(
165 self,
166 modalities: impl IntoIterator<Item = ResponseModality>,
167 ) -> Self {
168 self.generation_config_entry(|config| config.response_modalities(modalities))
169 }
170
171 pub fn media_resolution(self, resolution: MediaResolution) -> Self {
175 self.generation_config_entry(|config| config.media_resolution(resolution))
176 }
177}
178
179impl ExtensionOptions for GeminiOptions {
180 type Ext = GeminiExt;
181}
182
183#[non_exhaustive]
185#[derive(Clone, Debug, Default, PartialEq, Serialize)]
186pub struct GeminiShared {
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub store: Option<bool>,
190 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
193 pub labels: BTreeMap<String, String>,
194}
195
196#[non_exhaustive]
199#[derive(Clone, Debug, Default, PartialEq, Serialize)]
200pub struct GenerateContentOptions {
201 #[serde(rename = "generationConfig", skip_serializing_if = "is_default")]
204 pub generation_config: GeminiGenerationConfig,
205 #[serde(rename = "safetySettings", skip_serializing_if = "Vec::is_empty")]
207 pub safety_settings: Vec<SafetySetting>,
208}
209
210impl GenerateContentOptions {
211 pub fn new() -> Self {
213 Self::default()
214 }
215
216 pub fn generation_config(mut self, config: GenerationConfig) -> Self {
218 self.generation_config.common = config;
219 self
220 }
221
222 pub fn enable_enhanced_civic_answers(mut self, enable: bool) -> Self {
224 self.generation_config.enable_enhanced_civic_answers = Some(enable);
225 self
226 }
227
228 pub fn safety_setting(mut self, category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
230 self.safety_settings
231 .push(SafetySetting::new(category, threshold));
232 self
233 }
234}
235
236#[non_exhaustive]
239#[derive(Clone, Debug, Default, PartialEq, Serialize)]
240pub struct GeminiGenerationConfig {
241 #[serde(flatten)]
243 pub common: GenerationConfig,
244 #[serde(
246 rename = "enableEnhancedCivicAnswers",
247 skip_serializing_if = "Option::is_none"
248 )]
249 pub enable_enhanced_civic_answers: Option<bool>,
250}
251
252#[non_exhaustive]
257#[derive(Clone, Debug, Default, PartialEq, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct GenerationConfig {
260 #[serde(skip_serializing_if = "is_default")]
263 pub thinking_config: ThinkingConfig,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub top_k: Option<u32>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub presence_penalty: Option<f64>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub frequency_penalty: Option<f64>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub response_logprobs: Option<bool>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub logprobs: Option<u32>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub candidate_count: Option<CandidateCount>,
282 #[serde(skip_serializing_if = "Vec::is_empty")]
284 pub response_modalities: Vec<ResponseModality>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub image_config: Option<ImageConfig>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub speech_config: Option<SpeechConfig>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub media_resolution: Option<MediaResolution>,
294}
295
296impl GenerationConfig {
297 pub fn new() -> Self {
299 Self::default()
300 }
301
302 pub fn include_thoughts(mut self, include: bool) -> Self {
304 self.thinking_config.include_thoughts = Some(include);
305 self
306 }
307
308 pub fn top_k(mut self, top_k: u32) -> Self {
310 self.top_k = Some(top_k);
311 self
312 }
313
314 pub fn presence_penalty(mut self, penalty: f64) -> Self {
316 self.presence_penalty = Some(penalty);
317 self
318 }
319
320 pub fn frequency_penalty(mut self, penalty: f64) -> Self {
322 self.frequency_penalty = Some(penalty);
323 self
324 }
325
326 pub fn response_logprobs(mut self, enable: bool) -> Self {
328 self.response_logprobs = Some(enable);
329 self
330 }
331
332 pub fn logprobs(mut self, top: u32) -> Self {
334 self.logprobs = Some(top);
335 self
336 }
337
338 pub fn candidate_count(mut self, count: CandidateCount) -> Self {
340 self.candidate_count = Some(count);
341 self
342 }
343
344 pub fn response_modalities(
346 mut self,
347 modalities: impl IntoIterator<Item = ResponseModality>,
348 ) -> Self {
349 self.response_modalities = modalities.into_iter().collect();
350 self
351 }
352
353 pub fn image_config(mut self, config: ImageConfig) -> Self {
355 self.image_config = Some(config);
356 self
357 }
358
359 pub fn speech_config(mut self, config: SpeechConfig) -> Self {
361 self.speech_config = Some(config);
362 self
363 }
364
365 pub fn media_resolution(mut self, resolution: MediaResolution) -> Self {
367 self.media_resolution = Some(resolution);
368 self
369 }
370}
371
372#[non_exhaustive]
374#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
375#[serde(rename_all = "camelCase")]
376pub struct ThinkingConfig {
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub include_thoughts: Option<bool>,
380}
381
382#[non_exhaustive]
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum CandidateCount {
387 One,
389}
390
391impl Serialize for CandidateCount {
392 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
393 match self {
394 Self::One => serializer.serialize_u32(1),
395 }
396 }
397}
398
399#[non_exhaustive]
401#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
402#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
403pub enum ResponseModality {
404 Text,
406 Image,
408 Audio,
410}
411
412#[non_exhaustive]
414#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
415pub enum MediaResolution {
416 #[serde(rename = "MEDIA_RESOLUTION_LOW")]
418 Low,
419 #[serde(rename = "MEDIA_RESOLUTION_MEDIUM")]
421 Medium,
422 #[serde(rename = "MEDIA_RESOLUTION_HIGH")]
424 High,
425}
426
427#[non_exhaustive]
429#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
430#[serde(rename_all = "camelCase")]
431pub struct ImageConfig {
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub aspect_ratio: Option<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub image_size: Option<ImageSize>,
438}
439
440impl ImageConfig {
441 pub fn new() -> Self {
443 Self::default()
444 }
445
446 pub fn aspect_ratio(mut self, ratio: impl Into<String>) -> Self {
448 self.aspect_ratio = Some(ratio.into());
449 self
450 }
451
452 pub fn image_size(mut self, size: ImageSize) -> Self {
454 self.image_size = Some(size);
455 self
456 }
457}
458
459#[non_exhaustive]
461#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct SpeechConfig {
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub voice_config: Option<VoiceConfig>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub multi_speaker_voice_config: Option<MultiSpeakerVoiceConfig>,
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub language_code: Option<String>,
473}
474
475impl SpeechConfig {
476 pub fn voice(voice_name: impl Into<String>) -> Self {
478 Self {
479 voice_config: Some(VoiceConfig::prebuilt(voice_name)),
480 multi_speaker_voice_config: None,
481 language_code: None,
482 }
483 }
484
485 pub fn speakers<S: Into<String>, V: Into<String>>(
488 speakers: impl IntoIterator<Item = (S, V)>,
489 ) -> Self {
490 let speaker_voice_configs = speakers
491 .into_iter()
492 .map(|(speaker, voice)| SpeakerVoiceConfig {
493 speaker: speaker.into(),
494 voice_config: VoiceConfig::prebuilt(voice),
495 })
496 .collect();
497 Self {
498 voice_config: None,
499 multi_speaker_voice_config: Some(MultiSpeakerVoiceConfig {
500 speaker_voice_configs,
501 }),
502 language_code: None,
503 }
504 }
505
506 pub fn language_code(mut self, code: impl Into<String>) -> Self {
508 self.language_code = Some(code.into());
509 self
510 }
511}
512
513#[non_exhaustive]
515#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
516#[serde(rename_all = "camelCase")]
517pub struct VoiceConfig {
518 pub prebuilt_voice_config: PrebuiltVoiceConfig,
520}
521
522impl VoiceConfig {
523 pub fn prebuilt(voice_name: impl Into<String>) -> Self {
525 Self {
526 prebuilt_voice_config: PrebuiltVoiceConfig {
527 voice_name: voice_name.into(),
528 },
529 }
530 }
531}
532
533#[non_exhaustive]
535#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
536#[serde(rename_all = "camelCase")]
537pub struct PrebuiltVoiceConfig {
538 pub voice_name: String,
540}
541
542#[non_exhaustive]
544#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
545#[serde(rename_all = "camelCase")]
546pub struct MultiSpeakerVoiceConfig {
547 pub speaker_voice_configs: Vec<SpeakerVoiceConfig>,
549}
550
551#[non_exhaustive]
553#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
554#[serde(rename_all = "camelCase")]
555pub struct SpeakerVoiceConfig {
556 pub speaker: String,
558 pub voice_config: VoiceConfig,
560}
561
562#[non_exhaustive]
564#[derive(Clone, Copy, Debug, PartialEq, Eq)]
565pub enum HarmCategory {
566 HateSpeech,
568 DangerousContent,
570 Harassment,
572 SexuallyExplicit,
574 CivicIntegrity,
576 Jailbreak,
578}
579
580impl HarmCategory {
581 pub fn as_str(self) -> &'static str {
583 match self {
584 Self::HateSpeech => "HARM_CATEGORY_HATE_SPEECH",
585 Self::DangerousContent => "HARM_CATEGORY_DANGEROUS_CONTENT",
586 Self::Harassment => "HARM_CATEGORY_HARASSMENT",
587 Self::SexuallyExplicit => "HARM_CATEGORY_SEXUALLY_EXPLICIT",
588 Self::CivicIntegrity => "HARM_CATEGORY_CIVIC_INTEGRITY",
589 Self::Jailbreak => "HARM_CATEGORY_JAILBREAK",
590 }
591 }
592
593 fn interactions(self) -> &'static str {
595 match self {
596 Self::HateSpeech => "hate_speech",
597 Self::DangerousContent => "dangerous_content",
598 Self::Harassment => "harassment",
599 Self::SexuallyExplicit => "sexually_explicit",
600 Self::CivicIntegrity => "civic_integrity",
601 Self::Jailbreak => "jailbreak",
602 }
603 }
604}
605
606#[non_exhaustive]
608#[derive(Clone, Copy, Debug, PartialEq, Eq)]
609pub enum HarmBlockThreshold {
610 BlockLowAndAbove,
612 BlockMediumAndAbove,
614 BlockOnlyHigh,
616 BlockNone,
618 Off,
620}
621
622impl HarmBlockThreshold {
623 pub fn as_str(self) -> &'static str {
625 match self {
626 Self::BlockLowAndAbove => "BLOCK_LOW_AND_ABOVE",
627 Self::BlockMediumAndAbove => "BLOCK_MEDIUM_AND_ABOVE",
628 Self::BlockOnlyHigh => "BLOCK_ONLY_HIGH",
629 Self::BlockNone => "BLOCK_NONE",
630 Self::Off => "OFF",
631 }
632 }
633
634 fn interactions(self) -> &'static str {
636 match self {
637 Self::BlockLowAndAbove => "block_low_and_above",
638 Self::BlockMediumAndAbove => "block_medium_and_above",
639 Self::BlockOnlyHigh => "block_only_high",
640 Self::BlockNone => "block_none",
641 Self::Off => "off",
642 }
643 }
644}
645
646#[non_exhaustive]
651#[derive(Clone, Copy, Debug, PartialEq, Eq)]
652pub struct SafetySetting {
653 pub category: HarmCategory,
655 pub threshold: HarmBlockThreshold,
657}
658
659impl SafetySetting {
660 pub fn new(category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
662 Self {
663 category,
664 threshold,
665 }
666 }
667}
668
669impl Serialize for SafetySetting {
670 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
671 let mut map = serializer.serialize_map(Some(2))?;
672 map.serialize_entry("category", self.category.as_str())?;
673 map.serialize_entry("threshold", self.threshold.as_str())?;
674 map.end()
675 }
676}
677
678fn interactions_safety<S: Serializer>(
680 settings: &[SafetySetting],
681 serializer: S,
682) -> Result<S::Ok, S::Error> {
683 serializer.collect_seq(settings.iter().map(|setting| {
684 BTreeMap::from([
685 ("type", setting.category.interactions()),
686 ("threshold", setting.threshold.interactions()),
687 ])
688 }))
689}
690
691#[non_exhaustive]
694#[derive(Clone, Debug, Default, PartialEq, Serialize)]
695pub struct InteractionsOptions {
696 #[serde(skip_serializing_if = "Option::is_none")]
699 pub agent: Option<String>,
700 #[serde(skip_serializing_if = "Option::is_none")]
702 pub agent_config: Option<AgentConfig>,
703 #[serde(skip_serializing_if = "Option::is_none")]
705 pub background: Option<bool>,
706 #[serde(skip_serializing_if = "Option::is_none")]
708 pub previous_interaction_id: Option<String>,
709 #[serde(
711 skip_serializing_if = "Vec::is_empty",
712 serialize_with = "interactions_safety"
713 )]
714 pub safety_settings: Vec<SafetySetting>,
715 #[serde(skip_serializing_if = "is_default")]
718 pub generation_config: InteractionsGenerationConfig,
719}
720
721impl InteractionsOptions {
722 pub fn new() -> Self {
724 Self::default()
725 }
726
727 pub fn agent(mut self, agent: impl Into<String>) -> Self {
729 self.agent = Some(agent.into());
730 self
731 }
732
733 pub fn agent_config(mut self, config: AgentConfig) -> Self {
735 self.agent_config = Some(config);
736 self
737 }
738
739 pub fn background(mut self, background: bool) -> Self {
741 self.background = Some(background);
742 self
743 }
744
745 pub fn previous_interaction_id(mut self, id: impl Into<String>) -> Self {
747 self.previous_interaction_id = Some(id.into());
748 self
749 }
750
751 pub fn safety_setting(mut self, category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
753 self.safety_settings
754 .push(SafetySetting::new(category, threshold));
755 self
756 }
757
758 pub fn thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
760 self.generation_config.thinking_summaries = Some(summaries);
761 self
762 }
763
764 pub fn speech_config(mut self, config: InteractionSpeechConfig) -> Self {
766 self.generation_config.speech_config = Some(config);
767 self
768 }
769}
770
771#[non_exhaustive]
773#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
774pub struct InteractionsGenerationConfig {
775 #[serde(skip_serializing_if = "Option::is_none")]
777 pub thinking_summaries: Option<ThinkingSummaries>,
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub speech_config: Option<InteractionSpeechConfig>,
781}
782
783#[non_exhaustive]
785#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
786#[serde(rename_all = "snake_case")]
787pub enum ThinkingSummaries {
788 Auto,
790 None,
792}
793
794#[non_exhaustive]
796#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
797#[serde(tag = "type", rename_all = "kebab-case")]
798pub enum AgentConfig {
799 Dynamic,
801 DeepResearch(DeepResearchConfig),
803}
804
805#[non_exhaustive]
808#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
809pub struct DeepResearchConfig {
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub thinking_summaries: Option<ThinkingSummaries>,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub visualization: Option<Visualization>,
816 #[serde(skip_serializing_if = "Option::is_none")]
819 pub collaborative_planning: Option<bool>,
820}
821
822impl DeepResearchConfig {
823 pub fn new() -> Self {
825 Self::default()
826 }
827
828 pub fn thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
830 self.thinking_summaries = Some(summaries);
831 self
832 }
833
834 pub fn visualization(mut self, visualization: Visualization) -> Self {
836 self.visualization = Some(visualization);
837 self
838 }
839
840 pub fn collaborative_planning(mut self, plan: bool) -> Self {
842 self.collaborative_planning = Some(plan);
843 self
844 }
845}
846
847impl From<DeepResearchConfig> for AgentConfig {
848 fn from(config: DeepResearchConfig) -> Self {
849 Self::DeepResearch(config)
850 }
851}
852
853#[non_exhaustive]
855#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
856#[serde(rename_all = "snake_case")]
857pub enum Visualization {
858 Auto,
860 Off,
862}
863
864#[non_exhaustive]
869#[derive(Clone, Debug, Default, PartialEq)]
870pub struct GeminiExtras {
871 pub model_version: Option<String>,
873 pub response_id: Option<String>,
875 pub service_tier: Option<String>,
878 pub prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
880 pub cache_tokens_details: Option<Vec<ModalityTokenCount>>,
882 pub candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
884 pub tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
886 pub prompt_feedback: Option<PromptFeedback>,
888 pub safety_ratings: Option<Vec<SafetyRating>>,
890 pub finish_message: Option<String>,
892 pub citation_metadata: Option<CitationMetadata>,
894 pub grounding_metadata: Option<GroundingMetadata>,
896 pub url_context_metadata: Option<UrlContextMetadata>,
898 pub avg_logprobs: Option<f64>,
900 pub logprobs_result: Option<LogprobsResult>,
902 pub id: Option<String>,
904 pub status: Option<InteractionStatus>,
906 pub created: Option<String>,
908 pub updated: Option<String>,
910 pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
912 pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
914 pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
916 pub grounding_tool_count: Option<Vec<GroundingToolCount>>,
918}
919
920impl ReplyExtras for GeminiExtras {
921 fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
922 match api.as_str() {
923 GENERATE_CONTENT => Ok(GenerateContentReply::deserialize(raw)?.into()),
924 INTERACTIONS => Ok(InteractionReply::deserialize(raw)?.into()),
925 other => Err(serde_json::Error::custom(format!(
926 "the Gemini API returns no `{other}` reply"
927 ))),
928 }
929 }
930}
931
932#[derive(Deserialize)]
934#[serde(rename_all = "camelCase")]
935struct GenerateContentReply {
936 model_version: Option<String>,
937 response_id: Option<String>,
938 usage_metadata: Option<UsageDetails>,
939 prompt_feedback: Option<PromptFeedback>,
940 #[serde(default)]
941 candidates: Vec<CandidateDetails>,
942}
943
944#[derive(Deserialize)]
945#[serde(rename_all = "camelCase")]
946struct UsageDetails {
947 service_tier: Option<String>,
948 prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
949 cache_tokens_details: Option<Vec<ModalityTokenCount>>,
950 candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
951 tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
952}
953
954#[derive(Deserialize)]
955#[serde(rename_all = "camelCase")]
956struct CandidateDetails {
957 safety_ratings: Option<Vec<SafetyRating>>,
958 finish_message: Option<String>,
959 citation_metadata: Option<CitationMetadata>,
960 grounding_metadata: Option<GroundingMetadata>,
961 url_context_metadata: Option<UrlContextMetadata>,
962 avg_logprobs: Option<f64>,
963 logprobs_result: Option<LogprobsResult>,
964}
965
966impl From<GenerateContentReply> for GeminiExtras {
967 fn from(reply: GenerateContentReply) -> Self {
968 let mut extras = Self {
969 model_version: reply.model_version,
970 response_id: reply.response_id,
971 prompt_feedback: reply.prompt_feedback,
972 ..Self::default()
973 };
974 if let Some(usage) = reply.usage_metadata {
975 extras.service_tier = usage.service_tier;
976 extras.prompt_tokens_details = usage.prompt_tokens_details;
977 extras.cache_tokens_details = usage.cache_tokens_details;
978 extras.candidates_tokens_details = usage.candidates_tokens_details;
979 extras.tool_use_prompt_tokens_details = usage.tool_use_prompt_tokens_details;
980 }
981 if let Some(candidate) = reply.candidates.into_iter().next() {
982 extras.safety_ratings = candidate.safety_ratings;
983 extras.finish_message = candidate.finish_message;
984 extras.citation_metadata = candidate.citation_metadata;
985 extras.grounding_metadata = candidate.grounding_metadata;
986 extras.url_context_metadata = candidate.url_context_metadata;
987 extras.avg_logprobs = candidate.avg_logprobs;
988 extras.logprobs_result = candidate.logprobs_result;
989 }
990 extras
991 }
992}
993
994#[derive(Deserialize)]
996struct InteractionReply {
997 id: Option<String>,
998 status: Option<InteractionStatus>,
999 service_tier: Option<String>,
1000 created: Option<String>,
1001 updated: Option<String>,
1002 usage: Option<InteractionUsage>,
1003}
1004
1005#[derive(Deserialize)]
1006struct InteractionUsage {
1007 input_tokens_by_modality: Option<Vec<ModalityTokens>>,
1008 output_tokens_by_modality: Option<Vec<ModalityTokens>>,
1009 cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
1010 grounding_tool_count: Option<Vec<GroundingToolCount>>,
1011}
1012
1013impl From<InteractionReply> for GeminiExtras {
1014 fn from(reply: InteractionReply) -> Self {
1015 let mut extras = Self {
1016 id: reply.id,
1017 status: reply.status,
1018 service_tier: reply.service_tier,
1019 created: reply.created,
1020 updated: reply.updated,
1021 ..Self::default()
1022 };
1023 if let Some(usage) = reply.usage {
1024 extras.input_tokens_by_modality = usage.input_tokens_by_modality;
1025 extras.output_tokens_by_modality = usage.output_tokens_by_modality;
1026 extras.cached_tokens_by_modality = usage.cached_tokens_by_modality;
1027 extras.grounding_tool_count = usage.grounding_tool_count;
1028 }
1029 extras
1030 }
1031}
1032
1033#[non_exhaustive]
1035#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1036#[serde(rename_all = "camelCase")]
1037pub struct ModalityTokenCount {
1038 pub modality: Option<String>,
1040 pub token_count: Option<u64>,
1042}
1043
1044#[non_exhaustive]
1046#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct PromptFeedback {
1049 pub block_reason: Option<String>,
1051 pub safety_ratings: Option<Vec<SafetyRating>>,
1053}
1054
1055#[non_exhaustive]
1057#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct SafetyRating {
1060 pub category: Option<String>,
1062 pub probability: Option<String>,
1064 pub blocked: Option<bool>,
1066}
1067
1068#[non_exhaustive]
1070#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1071#[serde(rename_all = "camelCase")]
1072pub struct CitationMetadata {
1073 pub citation_sources: Option<Vec<CitationSource>>,
1075}
1076
1077#[non_exhaustive]
1079#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1080#[serde(rename_all = "camelCase")]
1081pub struct CitationSource {
1082 pub start_index: Option<u64>,
1084 pub end_index: Option<u64>,
1086 pub uri: Option<String>,
1088 pub license: Option<String>,
1090}
1091
1092#[non_exhaustive]
1094#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct GroundingMetadata {
1097 pub web_search_queries: Option<Vec<String>>,
1099 pub search_entry_point: Option<SearchEntryPoint>,
1101 pub grounding_chunks: Option<Vec<GroundingChunk>>,
1103 pub grounding_supports: Option<Vec<GroundingSupport>>,
1105}
1106
1107#[non_exhaustive]
1109#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1110#[serde(rename_all = "camelCase")]
1111pub struct SearchEntryPoint {
1112 pub rendered_content: Option<String>,
1114}
1115
1116#[non_exhaustive]
1118#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub struct GroundingChunk {
1121 pub web: Option<WebChunk>,
1123 pub retrieved_context: Option<RetrievedContext>,
1125}
1126
1127#[non_exhaustive]
1129#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1130pub struct WebChunk {
1131 pub uri: Option<String>,
1133 pub title: Option<String>,
1135}
1136
1137#[non_exhaustive]
1139#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1140pub struct RetrievedContext {
1141 pub uri: Option<String>,
1143 pub title: Option<String>,
1145 pub text: Option<String>,
1147}
1148
1149#[non_exhaustive]
1151#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1152#[serde(rename_all = "camelCase")]
1153pub struct GroundingSupport {
1154 pub segment: Option<Segment>,
1156 pub grounding_chunk_indices: Option<Vec<u32>>,
1158 pub confidence_scores: Option<Vec<f64>>,
1160}
1161
1162#[non_exhaustive]
1164#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1165#[serde(rename_all = "camelCase")]
1166pub struct Segment {
1167 pub part_index: Option<u32>,
1169 pub start_index: Option<u64>,
1171 pub end_index: Option<u64>,
1173 pub text: Option<String>,
1175}
1176
1177#[non_exhaustive]
1179#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1180#[serde(rename_all = "camelCase")]
1181pub struct UrlContextMetadata {
1182 pub url_metadata: Option<Vec<UrlMetadata>>,
1184}
1185
1186#[non_exhaustive]
1188#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1189#[serde(rename_all = "camelCase")]
1190pub struct UrlMetadata {
1191 pub retrieved_url: Option<String>,
1193 pub url_retrieval_status: Option<String>,
1195}
1196
1197#[non_exhaustive]
1199#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1200#[serde(rename_all = "camelCase")]
1201pub struct LogprobsResult {
1202 pub top_candidates: Option<Vec<TopCandidates>>,
1204 pub chosen_candidates: Option<Vec<LogprobsCandidate>>,
1206 pub log_probability_sum: Option<f64>,
1208}
1209
1210#[non_exhaustive]
1212#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1213pub struct TopCandidates {
1214 pub candidates: Option<Vec<LogprobsCandidate>>,
1216}
1217
1218#[non_exhaustive]
1220#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1221#[serde(rename_all = "camelCase")]
1222pub struct LogprobsCandidate {
1223 pub token: Option<String>,
1225 pub token_id: Option<i64>,
1227 pub log_probability: Option<f64>,
1229}
1230
1231#[non_exhaustive]
1233#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1234#[serde(rename_all = "snake_case")]
1235pub enum InteractionStatus {
1236 InProgress,
1238 RequiresAction,
1240 Incomplete,
1242 BudgetExceeded,
1244 Completed,
1246 Failed,
1248 Cancelled,
1250 Queued,
1252 #[serde(untagged)]
1254 Unknown(String),
1255}
1256
1257#[non_exhaustive]
1259#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1260pub struct ModalityTokens {
1261 pub modality: Option<String>,
1263 pub tokens: Option<u64>,
1265}
1266
1267#[non_exhaustive]
1269#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1270pub struct GroundingToolCount {
1271 #[serde(rename = "type")]
1273 pub kind: Option<String>,
1274 pub count: Option<u64>,
1276 pub search_query_count: Option<u64>,
1278}
1279
1280#[cfg(test)]
1281mod tests;