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
28const GENERATE_CONTENT: &str = "gemini.generate_content";
30
31const INTERACTIONS: &str = "gemini.interactions";
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct GeminiExt;
37
38impl ProviderExtension for GeminiExt {
39 const PROVIDER: &'static str = super::PROVIDER_NAME;
40 type Options = GeminiOptions;
41 type Extras = GeminiExtras;
42}
43
44fn is_default<T: Default + PartialEq>(value: &T) -> bool {
47 *value == T::default()
48}
49
50#[non_exhaustive]
59#[derive(Clone, Debug, Default, PartialEq, Serialize)]
60pub struct GeminiOptions {
61 #[serde(rename = "*")]
63 pub shared: GeminiShared,
64 #[serde(rename = "gemini.generate_content")]
66 pub generate_content: GenerateContentOptions,
67 #[serde(rename = "gemini.interactions")]
69 pub interactions: InteractionsOptions,
70}
71
72impl GeminiOptions {
73 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn store(mut self, store: bool) -> Self {
80 self.shared.store = Some(store);
81 self
82 }
83
84 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
87 self.shared.labels.insert(key.into(), value.into());
88 self
89 }
90
91 pub fn generate_content(mut self, section: GenerateContentOptions) -> Self {
93 self.generate_content = section;
94 self
95 }
96
97 pub fn interactions(mut self, section: InteractionsOptions) -> Self {
99 self.interactions = section;
100 self
101 }
102
103 fn generation_config_entry(
105 mut self,
106 set: impl FnOnce(GenerationConfig) -> GenerationConfig,
107 ) -> Self {
108 let common = std::mem::take(&mut self.generate_content.generation_config.common);
109 self.generate_content.generation_config.common = set(common);
110 self
111 }
112
113 pub fn include_thoughts(self, include: bool) -> Self {
117 self.generation_config_entry(|config| config.include_thoughts(include))
118 }
119
120 pub fn top_k(self, top_k: u32) -> Self {
123 self.generation_config_entry(|config| config.top_k(top_k))
124 }
125
126 pub fn presence_penalty(self, penalty: f64) -> Self {
129 self.generation_config_entry(|config| config.presence_penalty(penalty))
130 }
131
132 pub fn frequency_penalty(self, penalty: f64) -> Self {
136 self.generation_config_entry(|config| config.frequency_penalty(penalty))
137 }
138
139 pub fn response_logprobs(self, enable: bool) -> Self {
143 self.generation_config_entry(|config| config.response_logprobs(enable))
144 }
145
146 pub fn logprobs(self, top: u32) -> Self {
149 self.generation_config_entry(|config| config.logprobs(top))
150 }
151
152 pub fn candidate_count(self, count: CandidateCount) -> Self {
155 self.generation_config_entry(|config| config.candidate_count(count))
156 }
157
158 pub fn response_modalities(
162 self,
163 modalities: impl IntoIterator<Item = ResponseModality>,
164 ) -> Self {
165 self.generation_config_entry(|config| config.response_modalities(modalities))
166 }
167
168 pub fn media_resolution(self, resolution: MediaResolution) -> Self {
172 self.generation_config_entry(|config| config.media_resolution(resolution))
173 }
174}
175
176impl ExtensionOptions for GeminiOptions {
177 type Ext = GeminiExt;
178}
179
180#[non_exhaustive]
182#[derive(Clone, Debug, Default, PartialEq, Serialize)]
183pub struct GeminiShared {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub store: Option<bool>,
187 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
190 pub labels: BTreeMap<String, String>,
191}
192
193#[non_exhaustive]
196#[derive(Clone, Debug, Default, PartialEq, Serialize)]
197pub struct GenerateContentOptions {
198 #[serde(rename = "generationConfig", skip_serializing_if = "is_default")]
201 pub generation_config: GeminiGenerationConfig,
202 #[serde(rename = "safetySettings", skip_serializing_if = "Vec::is_empty")]
204 pub safety_settings: Vec<SafetySetting>,
205}
206
207impl GenerateContentOptions {
208 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn generation_config(mut self, config: GenerationConfig) -> Self {
215 self.generation_config.common = config;
216 self
217 }
218
219 pub fn enable_enhanced_civic_answers(mut self, enable: bool) -> Self {
221 self.generation_config.enable_enhanced_civic_answers = Some(enable);
222 self
223 }
224
225 pub fn safety_setting(mut self, category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
227 self.safety_settings
228 .push(SafetySetting::new(category, threshold));
229 self
230 }
231}
232
233#[non_exhaustive]
236#[derive(Clone, Debug, Default, PartialEq, Serialize)]
237pub struct GeminiGenerationConfig {
238 #[serde(flatten)]
240 pub common: GenerationConfig,
241 #[serde(
243 rename = "enableEnhancedCivicAnswers",
244 skip_serializing_if = "Option::is_none"
245 )]
246 pub enable_enhanced_civic_answers: Option<bool>,
247}
248
249#[non_exhaustive]
254#[derive(Clone, Debug, Default, PartialEq, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub struct GenerationConfig {
257 #[serde(skip_serializing_if = "is_default")]
260 pub thinking_config: ThinkingConfig,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub top_k: Option<u32>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub presence_penalty: Option<f64>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub frequency_penalty: Option<f64>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub response_logprobs: Option<bool>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub logprobs: Option<u32>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub candidate_count: Option<CandidateCount>,
279 #[serde(skip_serializing_if = "Vec::is_empty")]
281 pub response_modalities: Vec<ResponseModality>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub image_config: Option<ImageConfig>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub speech_config: Option<SpeechConfig>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub media_resolution: Option<MediaResolution>,
291}
292
293impl GenerationConfig {
294 pub fn new() -> Self {
296 Self::default()
297 }
298
299 pub fn include_thoughts(mut self, include: bool) -> Self {
301 self.thinking_config.include_thoughts = Some(include);
302 self
303 }
304
305 pub fn top_k(mut self, top_k: u32) -> Self {
307 self.top_k = Some(top_k);
308 self
309 }
310
311 pub fn presence_penalty(mut self, penalty: f64) -> Self {
313 self.presence_penalty = Some(penalty);
314 self
315 }
316
317 pub fn frequency_penalty(mut self, penalty: f64) -> Self {
319 self.frequency_penalty = Some(penalty);
320 self
321 }
322
323 pub fn response_logprobs(mut self, enable: bool) -> Self {
325 self.response_logprobs = Some(enable);
326 self
327 }
328
329 pub fn logprobs(mut self, top: u32) -> Self {
331 self.logprobs = Some(top);
332 self
333 }
334
335 pub fn candidate_count(mut self, count: CandidateCount) -> Self {
337 self.candidate_count = Some(count);
338 self
339 }
340
341 pub fn response_modalities(
343 mut self,
344 modalities: impl IntoIterator<Item = ResponseModality>,
345 ) -> Self {
346 self.response_modalities = modalities.into_iter().collect();
347 self
348 }
349
350 pub fn image_config(mut self, config: ImageConfig) -> Self {
352 self.image_config = Some(config);
353 self
354 }
355
356 pub fn speech_config(mut self, config: SpeechConfig) -> Self {
358 self.speech_config = Some(config);
359 self
360 }
361
362 pub fn media_resolution(mut self, resolution: MediaResolution) -> Self {
364 self.media_resolution = Some(resolution);
365 self
366 }
367}
368
369#[non_exhaustive]
371#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
372#[serde(rename_all = "camelCase")]
373pub struct ThinkingConfig {
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub include_thoughts: Option<bool>,
377}
378
379#[non_exhaustive]
382#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub enum CandidateCount {
384 One,
386}
387
388impl Serialize for CandidateCount {
389 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
390 match self {
391 Self::One => serializer.serialize_u32(1),
392 }
393 }
394}
395
396#[non_exhaustive]
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
399#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
400pub enum ResponseModality {
401 Text,
403 Image,
405 Audio,
407}
408
409#[non_exhaustive]
411#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
412pub enum MediaResolution {
413 #[serde(rename = "MEDIA_RESOLUTION_LOW")]
415 Low,
416 #[serde(rename = "MEDIA_RESOLUTION_MEDIUM")]
418 Medium,
419 #[serde(rename = "MEDIA_RESOLUTION_HIGH")]
421 High,
422}
423
424#[non_exhaustive]
426#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
427#[serde(rename_all = "camelCase")]
428pub struct ImageConfig {
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub aspect_ratio: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub image_size: Option<String>,
435}
436
437impl ImageConfig {
438 pub fn new() -> Self {
440 Self::default()
441 }
442
443 pub fn aspect_ratio(mut self, ratio: impl Into<String>) -> Self {
445 self.aspect_ratio = Some(ratio.into());
446 self
447 }
448
449 pub fn image_size(mut self, size: impl Into<String>) -> Self {
451 self.image_size = Some(size.into());
452 self
453 }
454}
455
456#[non_exhaustive]
458#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
459#[serde(rename_all = "camelCase")]
460pub struct SpeechConfig {
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub voice_config: Option<VoiceConfig>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub multi_speaker_voice_config: Option<MultiSpeakerVoiceConfig>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub language_code: Option<String>,
470}
471
472impl SpeechConfig {
473 pub fn voice(voice_name: impl Into<String>) -> Self {
475 Self {
476 voice_config: Some(VoiceConfig::prebuilt(voice_name)),
477 multi_speaker_voice_config: None,
478 language_code: None,
479 }
480 }
481
482 pub fn speakers<S: Into<String>, V: Into<String>>(
485 speakers: impl IntoIterator<Item = (S, V)>,
486 ) -> Self {
487 let speaker_voice_configs = speakers
488 .into_iter()
489 .map(|(speaker, voice)| SpeakerVoiceConfig {
490 speaker: speaker.into(),
491 voice_config: VoiceConfig::prebuilt(voice),
492 })
493 .collect();
494 Self {
495 voice_config: None,
496 multi_speaker_voice_config: Some(MultiSpeakerVoiceConfig {
497 speaker_voice_configs,
498 }),
499 language_code: None,
500 }
501 }
502
503 pub fn language_code(mut self, code: impl Into<String>) -> Self {
505 self.language_code = Some(code.into());
506 self
507 }
508}
509
510#[non_exhaustive]
512#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
513#[serde(rename_all = "camelCase")]
514pub struct VoiceConfig {
515 pub prebuilt_voice_config: PrebuiltVoiceConfig,
517}
518
519impl VoiceConfig {
520 pub fn prebuilt(voice_name: impl Into<String>) -> Self {
522 Self {
523 prebuilt_voice_config: PrebuiltVoiceConfig {
524 voice_name: voice_name.into(),
525 },
526 }
527 }
528}
529
530#[non_exhaustive]
532#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
533#[serde(rename_all = "camelCase")]
534pub struct PrebuiltVoiceConfig {
535 pub voice_name: String,
537}
538
539#[non_exhaustive]
541#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
542#[serde(rename_all = "camelCase")]
543pub struct MultiSpeakerVoiceConfig {
544 pub speaker_voice_configs: Vec<SpeakerVoiceConfig>,
546}
547
548#[non_exhaustive]
550#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
551#[serde(rename_all = "camelCase")]
552pub struct SpeakerVoiceConfig {
553 pub speaker: String,
555 pub voice_config: VoiceConfig,
557}
558
559#[non_exhaustive]
561#[derive(Clone, Copy, Debug, PartialEq, Eq)]
562pub enum HarmCategory {
563 HateSpeech,
565 DangerousContent,
567 Harassment,
569 SexuallyExplicit,
571 CivicIntegrity,
573 Jailbreak,
575}
576
577impl HarmCategory {
578 pub fn as_str(self) -> &'static str {
580 match self {
581 Self::HateSpeech => "HARM_CATEGORY_HATE_SPEECH",
582 Self::DangerousContent => "HARM_CATEGORY_DANGEROUS_CONTENT",
583 Self::Harassment => "HARM_CATEGORY_HARASSMENT",
584 Self::SexuallyExplicit => "HARM_CATEGORY_SEXUALLY_EXPLICIT",
585 Self::CivicIntegrity => "HARM_CATEGORY_CIVIC_INTEGRITY",
586 Self::Jailbreak => "HARM_CATEGORY_JAILBREAK",
587 }
588 }
589
590 fn interactions(self) -> &'static str {
592 match self {
593 Self::HateSpeech => "hate_speech",
594 Self::DangerousContent => "dangerous_content",
595 Self::Harassment => "harassment",
596 Self::SexuallyExplicit => "sexually_explicit",
597 Self::CivicIntegrity => "civic_integrity",
598 Self::Jailbreak => "jailbreak",
599 }
600 }
601}
602
603#[non_exhaustive]
605#[derive(Clone, Copy, Debug, PartialEq, Eq)]
606pub enum HarmBlockThreshold {
607 BlockLowAndAbove,
609 BlockMediumAndAbove,
611 BlockOnlyHigh,
613 BlockNone,
615 Off,
617}
618
619impl HarmBlockThreshold {
620 pub fn as_str(self) -> &'static str {
622 match self {
623 Self::BlockLowAndAbove => "BLOCK_LOW_AND_ABOVE",
624 Self::BlockMediumAndAbove => "BLOCK_MEDIUM_AND_ABOVE",
625 Self::BlockOnlyHigh => "BLOCK_ONLY_HIGH",
626 Self::BlockNone => "BLOCK_NONE",
627 Self::Off => "OFF",
628 }
629 }
630
631 fn interactions(self) -> &'static str {
633 match self {
634 Self::BlockLowAndAbove => "block_low_and_above",
635 Self::BlockMediumAndAbove => "block_medium_and_above",
636 Self::BlockOnlyHigh => "block_only_high",
637 Self::BlockNone => "block_none",
638 Self::Off => "off",
639 }
640 }
641}
642
643#[non_exhaustive]
648#[derive(Clone, Copy, Debug, PartialEq, Eq)]
649pub struct SafetySetting {
650 pub category: HarmCategory,
652 pub threshold: HarmBlockThreshold,
654}
655
656impl SafetySetting {
657 pub fn new(category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
659 Self {
660 category,
661 threshold,
662 }
663 }
664}
665
666impl Serialize for SafetySetting {
667 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
668 let mut map = serializer.serialize_map(Some(2))?;
669 map.serialize_entry("category", self.category.as_str())?;
670 map.serialize_entry("threshold", self.threshold.as_str())?;
671 map.end()
672 }
673}
674
675fn interactions_safety<S: Serializer>(
677 settings: &[SafetySetting],
678 serializer: S,
679) -> Result<S::Ok, S::Error> {
680 serializer.collect_seq(settings.iter().map(|setting| {
681 BTreeMap::from([
682 ("type", setting.category.interactions()),
683 ("threshold", setting.threshold.interactions()),
684 ])
685 }))
686}
687
688#[non_exhaustive]
691#[derive(Clone, Debug, Default, PartialEq, Serialize)]
692pub struct InteractionsOptions {
693 #[serde(skip_serializing_if = "Option::is_none")]
696 pub agent: Option<String>,
697 #[serde(skip_serializing_if = "Option::is_none")]
699 pub agent_config: Option<AgentConfig>,
700 #[serde(skip_serializing_if = "Option::is_none")]
702 pub background: Option<bool>,
703 #[serde(skip_serializing_if = "Option::is_none")]
705 pub previous_interaction_id: Option<String>,
706 #[serde(
708 skip_serializing_if = "Vec::is_empty",
709 serialize_with = "interactions_safety"
710 )]
711 pub safety_settings: Vec<SafetySetting>,
712 #[serde(skip_serializing_if = "is_default")]
715 pub generation_config: InteractionsGenerationConfig,
716}
717
718impl InteractionsOptions {
719 pub fn new() -> Self {
721 Self::default()
722 }
723
724 pub fn agent(mut self, agent: impl Into<String>) -> Self {
726 self.agent = Some(agent.into());
727 self
728 }
729
730 pub fn agent_config(mut self, config: AgentConfig) -> Self {
732 self.agent_config = Some(config);
733 self
734 }
735
736 pub fn background(mut self, background: bool) -> Self {
738 self.background = Some(background);
739 self
740 }
741
742 pub fn previous_interaction_id(mut self, id: impl Into<String>) -> Self {
744 self.previous_interaction_id = Some(id.into());
745 self
746 }
747
748 pub fn safety_setting(mut self, category: HarmCategory, threshold: HarmBlockThreshold) -> Self {
750 self.safety_settings
751 .push(SafetySetting::new(category, threshold));
752 self
753 }
754
755 pub fn thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
757 self.generation_config.thinking_summaries = Some(summaries);
758 self
759 }
760
761 pub fn speech(mut self, speech: InteractionSpeech) -> Self {
763 self.generation_config.speech_config.push(speech);
764 self
765 }
766}
767
768#[non_exhaustive]
770#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
771pub struct InteractionsGenerationConfig {
772 #[serde(skip_serializing_if = "Option::is_none")]
774 pub thinking_summaries: Option<ThinkingSummaries>,
775 #[serde(skip_serializing_if = "Vec::is_empty")]
777 pub speech_config: Vec<InteractionSpeech>,
778}
779
780#[non_exhaustive]
782#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
783#[serde(rename_all = "snake_case")]
784pub enum ThinkingSummaries {
785 Auto,
787 None,
789}
790
791#[non_exhaustive]
793#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
794#[serde(tag = "type", rename_all = "kebab-case")]
795pub enum AgentConfig {
796 Dynamic,
798 DeepResearch {
800 #[serde(skip_serializing_if = "Option::is_none")]
802 thinking_summaries: Option<ThinkingSummaries>,
803 },
804}
805
806#[non_exhaustive]
808#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
809pub struct InteractionSpeech {
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub voice: Option<String>,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub language: Option<String>,
816 #[serde(skip_serializing_if = "Option::is_none")]
818 pub speaker: Option<String>,
819}
820
821impl InteractionSpeech {
822 pub fn voice(voice: impl Into<String>) -> Self {
824 Self {
825 voice: Some(voice.into()),
826 ..Self::default()
827 }
828 }
829
830 pub fn language(mut self, language: impl Into<String>) -> Self {
832 self.language = Some(language.into());
833 self
834 }
835
836 pub fn speaker(mut self, speaker: impl Into<String>) -> Self {
838 self.speaker = Some(speaker.into());
839 self
840 }
841}
842
843#[non_exhaustive]
848#[derive(Clone, Debug, Default, PartialEq)]
849pub struct GeminiExtras {
850 pub model_version: Option<String>,
852 pub response_id: Option<String>,
854 pub service_tier: Option<String>,
857 pub prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
859 pub cache_tokens_details: Option<Vec<ModalityTokenCount>>,
861 pub candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
863 pub tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
865 pub prompt_feedback: Option<PromptFeedback>,
867 pub safety_ratings: Option<Vec<SafetyRating>>,
869 pub finish_message: Option<String>,
871 pub citation_metadata: Option<CitationMetadata>,
873 pub grounding_metadata: Option<GroundingMetadata>,
875 pub url_context_metadata: Option<UrlContextMetadata>,
877 pub avg_logprobs: Option<f64>,
879 pub logprobs_result: Option<LogprobsResult>,
881 pub id: Option<String>,
883 pub status: Option<InteractionStatus>,
885 pub created: Option<String>,
887 pub updated: Option<String>,
889 pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
891 pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
893 pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
895 pub grounding_tool_count: Option<Vec<GroundingToolCount>>,
897}
898
899impl ReplyExtras for GeminiExtras {
900 fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
901 match api.as_str() {
902 GENERATE_CONTENT => Ok(GenerateContentReply::deserialize(raw)?.into()),
903 INTERACTIONS => Ok(InteractionReply::deserialize(raw)?.into()),
904 other => Err(serde_json::Error::custom(format!(
905 "the Gemini API returns no `{other}` reply"
906 ))),
907 }
908 }
909}
910
911#[derive(Deserialize)]
913#[serde(rename_all = "camelCase")]
914struct GenerateContentReply {
915 model_version: Option<String>,
916 response_id: Option<String>,
917 usage_metadata: Option<UsageDetails>,
918 prompt_feedback: Option<PromptFeedback>,
919 #[serde(default)]
920 candidates: Vec<CandidateDetails>,
921}
922
923#[derive(Deserialize)]
924#[serde(rename_all = "camelCase")]
925struct UsageDetails {
926 service_tier: Option<String>,
927 prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
928 cache_tokens_details: Option<Vec<ModalityTokenCount>>,
929 candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
930 tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
931}
932
933#[derive(Deserialize)]
934#[serde(rename_all = "camelCase")]
935struct CandidateDetails {
936 safety_ratings: Option<Vec<SafetyRating>>,
937 finish_message: Option<String>,
938 citation_metadata: Option<CitationMetadata>,
939 grounding_metadata: Option<GroundingMetadata>,
940 url_context_metadata: Option<UrlContextMetadata>,
941 avg_logprobs: Option<f64>,
942 logprobs_result: Option<LogprobsResult>,
943}
944
945impl From<GenerateContentReply> for GeminiExtras {
946 fn from(reply: GenerateContentReply) -> Self {
947 let mut extras = Self {
948 model_version: reply.model_version,
949 response_id: reply.response_id,
950 prompt_feedback: reply.prompt_feedback,
951 ..Self::default()
952 };
953 if let Some(usage) = reply.usage_metadata {
954 extras.service_tier = usage.service_tier;
955 extras.prompt_tokens_details = usage.prompt_tokens_details;
956 extras.cache_tokens_details = usage.cache_tokens_details;
957 extras.candidates_tokens_details = usage.candidates_tokens_details;
958 extras.tool_use_prompt_tokens_details = usage.tool_use_prompt_tokens_details;
959 }
960 if let Some(candidate) = reply.candidates.into_iter().next() {
961 extras.safety_ratings = candidate.safety_ratings;
962 extras.finish_message = candidate.finish_message;
963 extras.citation_metadata = candidate.citation_metadata;
964 extras.grounding_metadata = candidate.grounding_metadata;
965 extras.url_context_metadata = candidate.url_context_metadata;
966 extras.avg_logprobs = candidate.avg_logprobs;
967 extras.logprobs_result = candidate.logprobs_result;
968 }
969 extras
970 }
971}
972
973#[derive(Deserialize)]
975struct InteractionReply {
976 id: Option<String>,
977 status: Option<InteractionStatus>,
978 service_tier: Option<String>,
979 created: Option<String>,
980 updated: Option<String>,
981 usage: Option<InteractionUsage>,
982}
983
984#[derive(Deserialize)]
985struct InteractionUsage {
986 input_tokens_by_modality: Option<Vec<ModalityTokens>>,
987 output_tokens_by_modality: Option<Vec<ModalityTokens>>,
988 cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
989 grounding_tool_count: Option<Vec<GroundingToolCount>>,
990}
991
992impl From<InteractionReply> for GeminiExtras {
993 fn from(reply: InteractionReply) -> Self {
994 let mut extras = Self {
995 id: reply.id,
996 status: reply.status,
997 service_tier: reply.service_tier,
998 created: reply.created,
999 updated: reply.updated,
1000 ..Self::default()
1001 };
1002 if let Some(usage) = reply.usage {
1003 extras.input_tokens_by_modality = usage.input_tokens_by_modality;
1004 extras.output_tokens_by_modality = usage.output_tokens_by_modality;
1005 extras.cached_tokens_by_modality = usage.cached_tokens_by_modality;
1006 extras.grounding_tool_count = usage.grounding_tool_count;
1007 }
1008 extras
1009 }
1010}
1011
1012#[non_exhaustive]
1014#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1015#[serde(rename_all = "camelCase")]
1016pub struct ModalityTokenCount {
1017 pub modality: Option<String>,
1019 pub token_count: Option<u64>,
1021}
1022
1023#[non_exhaustive]
1025#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1026#[serde(rename_all = "camelCase")]
1027pub struct PromptFeedback {
1028 pub block_reason: Option<String>,
1030 pub safety_ratings: Option<Vec<SafetyRating>>,
1032}
1033
1034#[non_exhaustive]
1036#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1037#[serde(rename_all = "camelCase")]
1038pub struct SafetyRating {
1039 pub category: Option<String>,
1041 pub probability: Option<String>,
1043 pub blocked: Option<bool>,
1045}
1046
1047#[non_exhaustive]
1049#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1050#[serde(rename_all = "camelCase")]
1051pub struct CitationMetadata {
1052 pub citation_sources: Option<Vec<CitationSource>>,
1054}
1055
1056#[non_exhaustive]
1058#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1059#[serde(rename_all = "camelCase")]
1060pub struct CitationSource {
1061 pub start_index: Option<u64>,
1063 pub end_index: Option<u64>,
1065 pub uri: Option<String>,
1067 pub license: Option<String>,
1069}
1070
1071#[non_exhaustive]
1073#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1074#[serde(rename_all = "camelCase")]
1075pub struct GroundingMetadata {
1076 pub web_search_queries: Option<Vec<String>>,
1078 pub search_entry_point: Option<SearchEntryPoint>,
1080 pub grounding_chunks: Option<Vec<GroundingChunk>>,
1082 pub grounding_supports: Option<Vec<GroundingSupport>>,
1084}
1085
1086#[non_exhaustive]
1088#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1089#[serde(rename_all = "camelCase")]
1090pub struct SearchEntryPoint {
1091 pub rendered_content: Option<String>,
1093}
1094
1095#[non_exhaustive]
1097#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct GroundingChunk {
1100 pub web: Option<WebChunk>,
1102 pub retrieved_context: Option<RetrievedContext>,
1104}
1105
1106#[non_exhaustive]
1108#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1109pub struct WebChunk {
1110 pub uri: Option<String>,
1112 pub title: Option<String>,
1114}
1115
1116#[non_exhaustive]
1118#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1119pub struct RetrievedContext {
1120 pub uri: Option<String>,
1122 pub title: Option<String>,
1124 pub text: Option<String>,
1126}
1127
1128#[non_exhaustive]
1130#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1131#[serde(rename_all = "camelCase")]
1132pub struct GroundingSupport {
1133 pub segment: Option<Segment>,
1135 pub grounding_chunk_indices: Option<Vec<u32>>,
1137 pub confidence_scores: Option<Vec<f64>>,
1139}
1140
1141#[non_exhaustive]
1143#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct Segment {
1146 pub part_index: Option<u32>,
1148 pub start_index: Option<u64>,
1150 pub end_index: Option<u64>,
1152 pub text: Option<String>,
1154}
1155
1156#[non_exhaustive]
1158#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1159#[serde(rename_all = "camelCase")]
1160pub struct UrlContextMetadata {
1161 pub url_metadata: Option<Vec<UrlMetadata>>,
1163}
1164
1165#[non_exhaustive]
1167#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1168#[serde(rename_all = "camelCase")]
1169pub struct UrlMetadata {
1170 pub retrieved_url: Option<String>,
1172 pub url_retrieval_status: Option<String>,
1174}
1175
1176#[non_exhaustive]
1178#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1179#[serde(rename_all = "camelCase")]
1180pub struct LogprobsResult {
1181 pub top_candidates: Option<Vec<TopCandidates>>,
1183 pub chosen_candidates: Option<Vec<LogprobsCandidate>>,
1185 pub log_probability_sum: Option<f64>,
1187}
1188
1189#[non_exhaustive]
1191#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1192pub struct TopCandidates {
1193 pub candidates: Option<Vec<LogprobsCandidate>>,
1195}
1196
1197#[non_exhaustive]
1199#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
1200#[serde(rename_all = "camelCase")]
1201pub struct LogprobsCandidate {
1202 pub token: Option<String>,
1204 pub token_id: Option<i64>,
1206 pub log_probability: Option<f64>,
1208}
1209
1210#[non_exhaustive]
1212#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1213#[serde(rename_all = "snake_case")]
1214pub enum InteractionStatus {
1215 InProgress,
1217 RequiresAction,
1219 Incomplete,
1221 BudgetExceeded,
1223 Completed,
1225 Failed,
1227 Cancelled,
1229 #[serde(untagged)]
1231 Unknown(String),
1232}
1233
1234#[non_exhaustive]
1236#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1237pub struct ModalityTokens {
1238 pub modality: Option<String>,
1240 pub tokens: Option<u64>,
1242}
1243
1244#[non_exhaustive]
1246#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
1247pub struct GroundingToolCount {
1248 #[serde(rename = "type")]
1250 pub kind: Option<String>,
1251 pub count: Option<u64>,
1253 pub search_query_count: Option<u64>,
1255}
1256
1257#[cfg(test)]
1258mod tests;