1mod error;
11#[cfg(test)]
12mod tests;
13mod types;
14
15pub use error::GeminiError;
16
17use async_trait::async_trait;
18use futures_util::{Stream, StreamExt};
19use schemars::JsonSchema;
20use serde::de::DeserializeOwned;
21use serde_json::json;
22use std::env;
23use std::marker::PhantomData;
24use std::pin::Pin;
25use std::sync::{Arc, Mutex};
26
27use self::types::*;
28use crate::openai::sse::SseByteFramer;
29use crate::ProviderError;
30use lc_callbacks::RunType;
31use lc_core::language_models::{
32 BaseChatModel, BaseLanguageModel, LLMResult, StreamChunk, TokenUsage,
33};
34use lc_core::runnables::{run_tree_from_config, Runnable};
35use lc_core::tools::{StructuredOutput, ToolDefinition};
36use lc_core::RunnableConfig;
37use lc_schema::{Message, MessageType};
38
39pub const GEMINI_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
41
42pub const GEMINI_MODELS: [&str; 6] = [
44 "gemini-3-pro", "gemini-3-flash", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", ];
51
52#[derive(Clone)]
54pub struct GeminiConfig {
55 pub api_key: String,
57 pub base_url: String,
59 pub model: String,
61 pub temperature: Option<f32>,
63 pub max_output_tokens: Option<usize>,
65 pub top_p: Option<f32>,
67 pub top_k: Option<i32>,
69 pub tools: Option<Vec<ToolDefinition>>,
71 pub tool_choice: Option<String>,
73}
74
75impl std::fmt::Debug for GeminiConfig {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("GeminiConfig")
79 .field("api_key", &"***")
80 .field("base_url", &self.base_url)
81 .field("model", &self.model)
82 .field("temperature", &self.temperature)
83 .field("max_output_tokens", &self.max_output_tokens)
84 .field("top_p", &self.top_p)
85 .field("top_k", &self.top_k)
86 .field("tools", &self.tools)
87 .field("tool_choice", &self.tool_choice)
88 .finish()
89 }
90}
91
92impl Default for GeminiConfig {
93 fn default() -> Self {
94 Self {
95 api_key: String::new(),
96 base_url: GEMINI_BASE_URL.to_string(),
97 model: "gemini-1.5-flash".to_string(),
98 temperature: None,
99 max_output_tokens: None,
100 top_p: None,
101 top_k: None,
102 tools: None,
103 tool_choice: None,
104 }
105 }
106}
107
108impl GeminiConfig {
109 pub fn new(api_key: impl Into<String>) -> Self {
111 Self {
112 api_key: api_key.into(),
113 ..Default::default()
114 }
115 }
116
117 pub fn from_env_result() -> Result<Self, ProviderError> {
124 let api_key = env::var("GEMINI_API_KEY")
125 .or_else(|_| env::var("GOOGLE_API_KEY"))
126 .map_err(|_| {
127 ProviderError::Config(
128 "GEMINI_API_KEY or GOOGLE_API_KEY environment variable not set".to_string(),
129 )
130 })?;
131
132 let base_url = env::var("GEMINI_BASE_URL").unwrap_or_else(|_| GEMINI_BASE_URL.to_string());
133
134 let model = env::var("GEMINI_MODEL").unwrap_or_else(|_| "gemini-1.5-flash".to_string());
135
136 Ok(Self {
137 api_key,
138 base_url,
139 model,
140 ..Default::default()
141 })
142 }
143
144 pub fn with_model(mut self, model: impl Into<String>) -> Self {
146 self.model = model.into();
147 self
148 }
149
150 pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
152 self.base_url = url.into();
153 self
154 }
155
156 pub fn with_temperature(mut self, temp: f32) -> Self {
158 self.temperature = Some(temp);
159 self
160 }
161
162 pub fn with_max_output_tokens(mut self, max: usize) -> Self {
164 self.max_output_tokens = Some(max);
165 self
166 }
167
168 pub fn with_max_tokens(self, max: usize) -> Self {
170 self.with_max_output_tokens(max)
171 }
172}
173
174#[derive(Clone, Debug)]
176pub struct GeminiChat {
177 config: GeminiConfig,
178 client: reqwest::Client,
179}
180
181impl GeminiChat {
182 pub fn new(config: GeminiConfig) -> Self {
184 Self {
185 config,
186 client: crate::retry::default_client(),
188 }
189 }
190
191 pub fn from_env() -> Result<Self, ProviderError> {
193 Self::from_env_result()
194 }
195
196 #[allow(deprecated)]
198 pub fn from_env_result() -> Result<Self, ProviderError> {
199 Ok(Self::new(GeminiConfig::from_env_result()?))
200 }
201
202 pub fn bind_tools(&self, tools: Vec<ToolDefinition>) -> Self {
208 let config = GeminiConfig {
209 tools: Some(tools),
210 ..self.config.clone()
211 };
212 Self {
213 config,
214 client: self.client.clone(),
215 }
216 }
217
218 pub fn with_tool_choice(mut self, choice: impl Into<String>) -> Self {
222 self.config.tool_choice = Some(choice.into());
223 self
224 }
225
226 pub fn with_structured_output<T: DeserializeOwned + JsonSchema>(
231 &self,
232 ) -> GeminiStructuredOutputMethod<T> {
233 use schemars::schema_for;
234 let schema = serde_json::to_value(schema_for!(T))
235 .unwrap_or_else(|_| serde_json::json!({"type": "object", "properties": {}}));
236
237 let tool = ToolDefinition::new("structured_output", "Return structured JSON output")
238 .with_parameters(schema);
239
240 let config = GeminiConfig {
241 tools: Some(vec![tool]),
242 tool_choice: Some("auto".to_string()),
243 ..self.config.clone()
244 };
245
246 GeminiStructuredOutputMethod {
247 config,
248 client: self.client.clone(),
249 _phantom: PhantomData,
250 }
251 }
252
253 fn build_contents(&self, messages: Vec<Message>) -> (Vec<GeminiContent>, Option<String>) {
255 let mut contents = Vec::new();
256 let mut system_prompt: Option<String> = None;
257
258 for msg in messages {
259 match msg.message_type {
260 MessageType::System => {
261 system_prompt = Some(match system_prompt {
263 Some(prev) => format!("{}\n{}", prev, msg.content),
264 None => msg.content,
265 });
266 }
267 MessageType::Human => {
268 let mut parts: Vec<GeminiPart> = Vec::new();
272 if !msg.content.is_empty() {
273 parts.push(GeminiPart {
274 text: Some(msg.content.clone()),
275 function_call: None,
276 function_response: None,
277 inline_data: None,
278 file_data: None,
279 });
280 }
281 for media in msg.media_parts() {
282 if let Some(media_part) = Self::media_to_part(&media) {
283 parts.push(media_part);
284 }
285 }
286 if parts.is_empty() {
287 parts.push(GeminiPart {
290 text: Some(msg.content),
291 function_call: None,
292 function_response: None,
293 inline_data: None,
294 file_data: None,
295 });
296 }
297 contents.push(GeminiContent {
298 role: Some("user".to_string()),
299 parts,
300 });
301 }
302 MessageType::AI => {
303 contents.push(GeminiContent {
304 role: Some("model".to_string()),
305 parts: vec![GeminiPart {
306 text: Some(msg.content),
307 function_call: None,
308 function_response: None,
309 inline_data: None,
310 file_data: None,
311 }],
312 });
313 }
314 MessageType::Tool { ref tool_call_id } => {
315 let function_name = tool_call_id.strip_prefix("call_").unwrap_or(tool_call_id);
323 contents.push(GeminiContent {
324 role: Some("function".to_string()),
325 parts: vec![GeminiPart {
326 text: None,
327 function_call: None,
328 function_response: Some(GeminiFunctionResponse {
329 name: function_name.to_string(),
330 response: json!({"result": msg.content}),
331 }),
332 inline_data: None,
333 file_data: None,
334 }],
335 });
336 }
337 }
338 }
339
340 (contents, system_prompt)
341 }
342
343 fn media_to_part(part: &lc_schema::MediaPart<'_>) -> Option<GeminiPart> {
352 let url = part.url();
353
354 let (inline_data, file_data) = if let Some(gs_path) = url.strip_prefix("gs://") {
355 let mime = part
356 .mime_type()
357 .or_else(|| crate::media::mime_from_extension(url))?
358 .to_string();
359 (
360 None,
361 Some(GeminiFileData {
362 file_uri: format!("gs://{gs_path}"),
363 mime_type: mime,
364 }),
365 )
366 } else if let Some((uri_mime, data)) = crate::media::data_uri_parts(url) {
367 let mime = part.mime_type().unwrap_or(uri_mime).to_string();
369 (
370 Some(GeminiInlineData {
371 mime_type: mime,
372 data: data.to_string(),
373 }),
374 None,
375 )
376 } else {
377 return None;
378 };
379
380 Some(GeminiPart {
381 text: None,
382 function_call: None,
383 function_response: None,
384 inline_data,
385 file_data,
386 })
387 }
388
389 fn build_request(&self, messages: Vec<Message>) -> GeminiRequest {
391 let (contents, system_text) = self.build_contents(messages);
392
393 let system_instruction = system_text.map(|text| GeminiSystemInstruction {
394 parts: vec![GeminiPart {
395 text: Some(text),
396 function_call: None,
397 function_response: None,
398 inline_data: None,
399 file_data: None,
400 }],
401 });
402
403 let generation_config = {
404 let has_config = self.config.temperature.is_some()
405 || self.config.max_output_tokens.is_some()
406 || self.config.top_p.is_some()
407 || self.config.top_k.is_some();
408
409 if has_config {
410 Some(GeminiGenerationConfig {
411 temperature: self.config.temperature,
412 max_output_tokens: self.config.max_output_tokens,
413 top_p: self.config.top_p,
414 top_k: self.config.top_k,
415 })
416 } else {
417 None
418 }
419 };
420
421 GeminiRequest {
422 contents,
423 system_instruction,
424 generation_config,
425 tools: self.config.tools.as_ref().map(|tools| {
427 vec![GeminiToolDeclaration {
428 function_declarations: tools
429 .iter()
430 .map(|td| GeminiFunctionDeclaration {
431 name: td.function.name.clone(),
432 description: td.function.description.clone(),
433 parameters: td.function.parameters.clone(),
434 })
435 .collect(),
436 }]
437 }),
438 tool_config: self.config.tool_choice.as_ref().map(|choice| {
440 let mode = match choice.as_str() {
441 "none" => "NONE",
442 "any" => "ANY",
443 _ => "AUTO",
444 };
445 GeminiToolConfig {
446 function_calling_config: GeminiFunctionCallingConfig {
447 mode: mode.to_string(),
448 },
449 }
450 }),
451 }
452 }
453
454 fn parse_response(
456 &self,
457 response: GeminiResponse,
458 model: &str,
459 ) -> Result<LLMResult, GeminiError> {
460 if let Some(feedback) = &response.prompt_feedback {
462 if let Some(block_reason) = feedback.get("blockReason").and_then(|v| v.as_str()) {
463 return Err(GeminiError::SafetyBlock(block_reason.to_string()));
464 }
465 }
466
467 let candidates = response.candidates.ok_or(GeminiError::NoResponse)?;
468 let candidate = candidates
469 .into_iter()
470 .next()
471 .ok_or(GeminiError::NoResponse)?;
472
473 let content = candidate.content.ok_or(GeminiError::NoResponse)?;
474
475 let mut text_parts = String::new();
476 let mut tool_calls: Vec<lc_core::tools::ToolCall> = Vec::new();
477
478 for part in content.parts {
479 if let Some(text) = part.text {
480 text_parts.push_str(&text);
481 }
482 if let Some(fc) = part.function_call {
484 let args_str = fc.args.unwrap_or(serde_json::json!({})).to_string();
485 tool_calls.push(
486 lc_core::tools::ToolCall::builder(format!("call_{}", fc.name))
487 .name(fc.name)
488 .arguments(args_str)
489 .build(),
490 );
491 }
492 }
493
494 let token_usage = response.usage_metadata.map(|u| TokenUsage {
495 prompt_tokens: u.prompt_token_count.unwrap_or(0) as usize,
496 completion_tokens: u.candidates_token_count.unwrap_or(0) as usize,
497 total_tokens: u.total_token_count.unwrap_or(0) as usize,
498 });
499
500 Ok(LLMResult {
501 content: text_parts,
502 model: model.to_string(),
503 token_usage,
504 tool_calls: if tool_calls.is_empty() {
505 None
506 } else {
507 Some(tool_calls)
508 },
509 thinking_content: None,
510 })
511 }
512
513 async fn chat_internal(&self, messages: Vec<Message>) -> Result<LLMResult, GeminiError> {
515 let url = format!(
516 "{}/models/{}:generateContent",
517 self.config.base_url, self.config.model
518 );
519
520 let mut messages = messages;
523 crate::media::resolve_message_media(&mut messages, crate::media::MediaPolicy::Gemini)
524 .await
525 .map_err(|e| GeminiError::ApiError(e.to_string()))?;
526 let request_body = self.build_request(messages);
527
528 let response = crate::retry::send_with_retry(
532 || {
533 self.client
534 .post(&url)
535 .header("x-goog-api-key", &self.config.api_key)
536 .header("Content-Type", "application/json")
537 .json(&request_body)
538 },
539 &crate::retry::DEFAULT_RETRY,
540 )
541 .await
542 .map_err(|e| GeminiError::HttpError(e.to_string()))?;
543
544 let status = response.status();
545 let body = response
546 .text()
547 .await
548 .map_err(|e| GeminiError::HttpError(e.to_string()))?;
549
550 if !status.is_success() {
551 let preview: String = body.chars().take(500).collect();
554 return Err(GeminiError::ApiError(format!(
555 "HTTP {}: {}",
556 status.as_u16(),
557 preview
558 )));
559 }
560
561 let gemini_response: GeminiResponse = serde_json::from_str(&body).map_err(|e| {
562 let preview: String = body.chars().take(200).collect();
564 GeminiError::ParseError(format!("{} - body: {}", e, preview))
565 })?;
566
567 self.parse_response(gemini_response, &self.config.model)
568 }
569
570 async fn stream_chat_internal(
572 &self,
573 messages: Vec<Message>,
574 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, GeminiError>> + Send>>, GeminiError>
575 {
576 let url = format!(
577 "{}/models/{}:streamGenerateContent?alt=event-stream",
578 self.config.base_url, self.config.model
579 );
580
581 let mut messages = messages;
583 crate::media::resolve_message_media(&mut messages, crate::media::MediaPolicy::Gemini)
584 .await
585 .map_err(|e| GeminiError::ApiError(e.to_string()))?;
586 let request_body = self.build_request(messages);
587
588 let response = self
589 .client
590 .post(&url)
591 .header("x-goog-api-key", &self.config.api_key)
592 .header("Content-Type", "application/json")
593 .json(&request_body)
594 .send()
595 .await
596 .map_err(|e| GeminiError::HttpError(e.to_string()))?;
597
598 let status = response.status();
599 if !status.is_success() {
600 let body = response.text().await.unwrap_or_default();
601 return Err(GeminiError::ApiError(format!(
602 "HTTP {}: {}",
603 status.as_u16(),
604 body
605 )));
606 }
607
608 let byte_stream = response.bytes_stream();
609 let sse_buffer = Arc::new(Mutex::new(SseByteFramer::new()));
612 let (tx, rx) = tokio::sync::mpsc::channel::<Result<StreamChunk, GeminiError>>(64);
613
614 let buffer_clone = sse_buffer.clone();
615 tokio::spawn(async move {
616 use futures_util::StreamExt;
617
618 let mut byte_stream = byte_stream;
619 while let Some(chunk_result) = byte_stream.next().await {
620 if let Ok(bytes) = chunk_result {
621 let events = {
623 let mut buffer_guard =
624 buffer_clone.lock().unwrap_or_else(|e| e.into_inner());
625 buffer_guard.push(&bytes)
626 };
627 for event_text in events {
630 for line in event_text.lines() {
631 let line = line.trim();
632 if !line.starts_with("data:") {
633 continue;
634 }
635
636 let data = line.trim_start_matches("data:").trim();
638 if data == "[DONE]" {
639 continue;
640 }
641
642 match serde_json::from_str::<GeminiResponse>(data) {
643 Ok(resp) => {
644 if let Some(candidates) = resp.candidates {
645 for candidate in candidates {
646 if let Some(content) = candidate.content {
647 for part in content.parts {
648 if let Some(text) = part.text {
649 if tx
650 .send(Ok(StreamChunk::new(text)))
651 .await
652 .is_err()
653 {
654 return;
655 }
656 }
657 }
658 }
659 }
660 }
661 if let Some(usage) = resp.usage_metadata {
664 let token_usage = TokenUsage {
665 prompt_tokens: usage.prompt_token_count.unwrap_or(0)
666 as usize,
667 completion_tokens: usage
668 .candidates_token_count
669 .unwrap_or(0)
670 as usize,
671 total_tokens: usage.total_token_count.unwrap_or(0)
672 as usize,
673 };
674 let usage_chunk = StreamChunk {
675 text: String::new(),
676 token_usage: Some(token_usage),
677 tool_calls: None,
678 };
679 if tx.send(Ok(usage_chunk)).await.is_err() {
680 return;
681 }
682 }
683 }
684 Err(e) => {
685 log::error!(
689 "Failed to parse Gemini streaming SSE event (skipping this token): {e}; data: {}",
690 &data[..data.len().min(200)]
691 );
692 }
693 }
694 }
695 }
696 } else if let Err(e) = chunk_result {
697 let _ = tx.send(Err(GeminiError::HttpError(e.to_string()))).await;
700 return;
701 }
702 }
703 });
704
705 let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
706 Ok(Box::pin(stream))
707 }
708}
709
710#[async_trait]
711impl Runnable<Vec<Message>, LLMResult> for GeminiChat {
712 type Error = GeminiError;
713
714 async fn invoke(
715 &self,
716 input: Vec<Message>,
717 config: Option<RunnableConfig>,
718 ) -> Result<LLMResult, Self::Error> {
719 self.chat(input, config).await
720 }
721
722 async fn stream(
723 &self,
724 input: Vec<Message>,
725 config: Option<RunnableConfig>,
726 ) -> Result<Pin<Box<dyn Stream<Item = Result<LLMResult, Self::Error>> + Send>>, Self::Error>
727 {
728 let model = self.config.model.clone();
729 let (temp, max) = crate::sampling::sampling_overrides(&config);
730 let mut effective = self.clone();
731 if let Some(t) = temp {
732 effective.config.temperature = Some(t);
733 }
734 if let Some(m) = max {
735 effective.config.max_output_tokens = Some(m);
736 }
737 let token_stream = effective.stream_chat_internal(input).await?;
738
739 let stream = token_stream.map(move |token_result| match token_result {
742 Ok(chunk) => Ok(LLMResult {
743 content: chunk.text,
744 model: model.clone(),
745 token_usage: chunk.token_usage,
746 tool_calls: None,
747 thinking_content: None,
748 }),
749 Err(e) => Err(e),
750 });
751
752 Ok(Box::pin(stream))
753 }
754}
755
756#[async_trait]
757impl BaseLanguageModel<Vec<Message>, LLMResult> for GeminiChat {
758 fn model_name(&self) -> &str {
759 &self.config.model
760 }
761
762 fn get_num_tokens(&self, text: &str) -> usize {
763 lc_core::token_counter::count_tokens(text).unwrap_or_else(|e| {
764 log::warn!("Token counting failed, falling back to byte-length estimation: {e}");
766 text.len()
767 })
768 }
769
770 fn temperature(&self) -> Option<f32> {
771 self.config.temperature
772 }
773
774 fn max_tokens(&self) -> Option<usize> {
775 self.config.max_output_tokens
776 }
777
778 fn with_temperature(mut self, temp: f32) -> Self {
779 self.config.temperature = Some(temp);
780 self
781 }
782
783 fn with_max_tokens(mut self, max: usize) -> Self {
784 self.config.max_output_tokens = Some(max);
785 self
786 }
787}
788
789#[async_trait]
790impl BaseChatModel for GeminiChat {
791 async fn chat(
792 &self,
793 messages: Vec<Message>,
794 config: Option<RunnableConfig>,
795 ) -> Result<LLMResult, Self::Error> {
796 let run_name = config
797 .as_ref()
798 .and_then(|c| c.run_name.clone())
799 .unwrap_or_else(|| format!("{}:chat", self.config.model));
800
801 let mut run = run_tree_from_config(
802 run_name,
803 RunType::Llm,
804 json!({
805 "messages": messages.iter().map(|m| m.content.clone()).collect::<Vec<_>>(),
806 "model": self.config.model,
807 }),
808 config.as_ref(),
809 );
810
811 if let Some(ref cfg) = config {
812 if let Some(ref callbacks) = cfg.callbacks {
813 for handler in callbacks.handlers() {
814 handler.on_llm_start(&run, &messages).await;
815 }
816 }
817 }
818
819 let (temp, max) = crate::sampling::sampling_overrides(&config);
820 let mut effective = self.clone();
821 if let Some(t) = temp {
822 effective.config.temperature = Some(t);
823 }
824 if let Some(m) = max {
825 effective.config.max_output_tokens = Some(m);
826 }
827 let result = effective.chat_internal(messages.clone()).await;
828
829 match result {
830 Ok(response) => {
831 run.end(json!({
832 "content": &response.content,
833 "model": &response.model,
834 "token_usage": &response.token_usage,
835 }));
836
837 if let Some(ref cfg) = config {
838 if let Some(ref callbacks) = cfg.callbacks {
839 for handler in callbacks.handlers() {
840 handler.on_llm_end(&run, &response.content).await;
841 }
842 }
843 }
844
845 Ok(response)
846 }
847 Err(e) => {
848 run.end_with_error(e.to_string());
849
850 if let Some(ref cfg) = config {
851 if let Some(ref callbacks) = cfg.callbacks {
852 for handler in callbacks.handlers() {
853 handler.on_llm_error(&run, &e.to_string()).await;
854 }
855 }
856 }
857
858 Err(e)
859 }
860 }
861 }
862
863 async fn stream_chat(
864 &self,
865 messages: Vec<Message>,
866 config: Option<RunnableConfig>,
867 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, Self::Error>> + Send>>, Self::Error>
868 {
869 let run_name = config
870 .as_ref()
871 .and_then(|c| c.run_name.clone())
872 .unwrap_or_else(|| format!("{}:stream", self.config.model));
873
874 let run = run_tree_from_config(
875 run_name,
876 RunType::Llm,
877 json!({
878 "messages": messages.len(),
879 "model": self.config.model,
880 }),
881 config.as_ref(),
882 );
883
884 if let Some(ref cfg) = config {
885 if let Some(ref callbacks) = cfg.callbacks {
886 for handler in callbacks.handlers() {
887 handler.on_llm_start(&run, &messages).await;
888 }
889 }
890 }
891
892 let (temp, max) = crate::sampling::sampling_overrides(&config);
893 let mut effective = self.clone();
894 if let Some(t) = temp {
895 effective.config.temperature = Some(t);
896 }
897 if let Some(m) = max {
898 effective.config.max_output_tokens = Some(m);
899 }
900 let stream = effective.stream_chat_internal(messages).await?;
901
902 let callbacks = config.and_then(|c| c.callbacks);
903 let stream = stream.then(move |token_result| {
904 let cbs = callbacks.clone();
905 let run = run.clone();
906 async move {
907 if let Some(ref cbs) = cbs {
908 if let Ok(ref token) = token_result {
909 for handler in cbs.handlers() {
910 handler.on_llm_new_token(&run, &token.text).await;
911 }
912 }
913 }
914 token_result
915 }
916 });
917
918 Ok(Box::pin(stream))
919 }
920
921 fn bind_tools(
922 &self,
923 tools: Vec<ToolDefinition>,
924 ) -> Option<Box<dyn BaseChatModel<Error = Self::Error> + Send + Sync>> {
925 Some(Box::new(self.bind_tools(tools)))
928 }
929}
930
931pub struct GeminiStructuredOutputMethod<T: DeserializeOwned + JsonSchema> {
933 config: GeminiConfig,
934 client: reqwest::Client,
935 _phantom: PhantomData<T>,
936}
937
938impl<T: DeserializeOwned + JsonSchema> GeminiStructuredOutputMethod<T> {
939 pub async fn invoke(&self, messages: Vec<Message>) -> Result<T, GeminiError> {
941 let chat = GeminiChat {
942 config: self.config.clone(),
943 client: self.client.clone(),
944 };
945
946 let result = chat.chat_internal(messages).await?;
947 let structured = StructuredOutput::<T>::new(result);
948 structured
949 .parse()
950 .map_err(|e| GeminiError::ParseError(e.to_string()))
951 }
952}