1use crate::OneOrMany;
5use crate::completion::{self, CompletionError, CompletionRequest, GetTokenUsage};
6use crate::http_client::HttpClientExt;
7use crate::message::{self, MimeType, Reasoning};
8use crate::telemetry::SpanCombinator;
9use serde_json::{Map, Value};
10use tracing::{Level, enabled, info_span};
11use tracing_futures::Instrument;
12use url::form_urlencoded;
13
14use super::client::InteractionsClient;
15
16pub mod streaming;
18pub use interactions_api_types::*;
19
20#[derive(Clone, Debug)]
26pub struct InteractionsCompletionModel<T = reqwest::Client> {
27 pub(crate) client: InteractionsClient<T>,
28 pub model: String,
29}
30
31impl<T> InteractionsCompletionModel<T> {
32 pub fn new(client: InteractionsClient<T>, model: impl Into<String>) -> Self {
34 Self {
35 client,
36 model: model.into(),
37 }
38 }
39
40 pub fn with_model(client: InteractionsClient<T>, model: &str) -> Self {
42 Self {
43 client,
44 model: model.to_string(),
45 }
46 }
47
48 pub fn generate_content_api(self) -> super::completion::CompletionModel<T> {
50 super::completion::CompletionModel::with_model(
51 self.client.generate_content_api(),
52 &self.model,
53 )
54 }
55
56 pub(crate) fn create_completion_request(
57 &self,
58 completion_request: CompletionRequest,
59 stream_override: Option<bool>,
60 ) -> Result<CreateInteractionRequest, CompletionError> {
61 create_request_body(self.model.clone(), completion_request, stream_override)
62 }
63}
64
65impl<T> InteractionsCompletionModel<T>
66where
67 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
68{
69 pub async fn create_interaction(
71 &self,
72 completion_request: CompletionRequest,
73 ) -> Result<Interaction, CompletionError> {
74 let request = self.create_completion_request(completion_request, Some(false))?;
75 self.client.create_interaction(request).await
76 }
77
78 pub async fn get_interaction(
80 &self,
81 interaction_id: impl AsRef<str>,
82 ) -> Result<Interaction, CompletionError> {
83 self.client.get_interaction(interaction_id).await
84 }
85
86 pub async fn stream_interaction_events(
88 &self,
89 completion_request: CompletionRequest,
90 ) -> Result<streaming::InteractionEventStream, CompletionError> {
91 let request = self.create_completion_request(completion_request, Some(true))?;
92 self.client.stream_interaction_events(request).await
93 }
94
95 pub async fn stream_interaction_events_by_id(
97 &self,
98 interaction_id: impl AsRef<str>,
99 last_event_id: Option<&str>,
100 ) -> Result<streaming::InteractionEventStream, CompletionError> {
101 self.client
102 .stream_interaction_events_by_id(interaction_id, last_event_id)
103 .await
104 }
105}
106
107impl<T> completion::CompletionModel for InteractionsCompletionModel<T>
108where
109 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
110{
111 type Response = Interaction;
112 type StreamingResponse = streaming::StreamingCompletionResponse;
113 type Client = InteractionsClient<T>;
114
115 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
116 Self::new(client.clone(), model)
117 }
118
119 async fn completion(
120 &self,
121 completion_request: CompletionRequest,
122 ) -> Result<completion::CompletionResponse<Interaction>, CompletionError> {
123 let span = if tracing::Span::current().is_disabled() {
124 info_span!(
125 target: "rig::completions",
126 "interactions",
127 gen_ai.operation.name = "interactions",
128 gen_ai.provider.name = "gcp.gemini",
129 gen_ai.request.model = self.model,
130 gen_ai.system_instructions = &completion_request.preamble,
131 gen_ai.response.id = tracing::field::Empty,
132 gen_ai.response.model = tracing::field::Empty,
133 gen_ai.usage.output_tokens = tracing::field::Empty,
134 gen_ai.usage.input_tokens = tracing::field::Empty,
135 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
136 gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
137 gen_ai.usage.reasoning_tokens = tracing::field::Empty,
138 )
139 } else {
140 tracing::Span::current()
141 };
142
143 let request = self.create_completion_request(completion_request, Some(false))?;
144
145 if enabled!(Level::TRACE) {
146 tracing::trace!(
147 target: "rig::completions",
148 "Gemini interactions completion request: {}",
149 serde_json::to_string_pretty(&request)?
150 );
151 }
152
153 let body = serde_json::to_vec(&request)?;
154 let request = self
155 .client
156 .post("/v1beta/interactions")?
157 .body(body)
158 .map_err(|e| CompletionError::HttpError(e.into()))?;
159
160 async move {
161 let response = self.client.send::<_, Vec<u8>>(request).await?;
162
163 if response.status().is_success() {
164 let response_body = response
165 .into_body()
166 .await
167 .map_err(CompletionError::HttpError)?;
168
169 let response_text = String::from_utf8_lossy(&response_body).to_string();
170
171 let response: Interaction =
172 serde_json::from_slice(&response_body).map_err(|err| {
173 tracing::error!(
174 error = %err,
175 body = %response_text,
176 "Failed to deserialize Gemini interactions response"
177 );
178 CompletionError::JsonError(err)
179 })?;
180
181 let span = tracing::Span::current();
182 span.record_response_metadata(&response);
183 span.record_token_usage(&response);
184
185 if enabled!(Level::TRACE) {
186 tracing::trace!(
187 target: "rig::completions",
188 "Gemini interactions completion response: {}",
189 serde_json::to_string_pretty(&response)?
190 );
191 }
192
193 response.try_into()
194 } else {
195 let text = String::from_utf8_lossy(
196 &response
197 .into_body()
198 .await
199 .map_err(CompletionError::HttpError)?,
200 )
201 .into();
202
203 Err(CompletionError::ProviderError(text))
204 }
205 }
206 .instrument(span)
207 .await
208 }
209
210 async fn stream(
211 &self,
212 request: CompletionRequest,
213 ) -> Result<
214 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
215 CompletionError,
216 > {
217 InteractionsCompletionModel::stream(self, request).await
218 }
219}
220
221impl<T> InteractionsClient<T>
222where
223 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
224{
225 pub async fn create_interaction(
227 &self,
228 request: CreateInteractionRequest,
229 ) -> Result<Interaction, CompletionError> {
230 if request.stream == Some(true) {
231 return Err(CompletionError::RequestError(Box::new(
232 std::io::Error::new(
233 std::io::ErrorKind::InvalidInput,
234 "stream=true requires stream_interaction_events",
235 ),
236 )));
237 }
238
239 let body = serde_json::to_vec(&request)?;
240 let request = self
241 .post("/v1beta/interactions")?
242 .body(body)
243 .map_err(|e| CompletionError::HttpError(e.into()))?;
244
245 send_interaction_request(self, request).await
246 }
247
248 pub async fn get_interaction(
250 &self,
251 interaction_id: impl AsRef<str>,
252 ) -> Result<Interaction, CompletionError> {
253 let path = format!("/v1beta/interactions/{}", interaction_id.as_ref());
254 let request = self
255 .get(path)?
256 .body(Vec::new())
257 .map_err(|e| CompletionError::HttpError(e.into()))?;
258
259 send_interaction_request(self, request).await
260 }
261
262 pub async fn stream_interaction_events(
264 &self,
265 mut request: CreateInteractionRequest,
266 ) -> Result<streaming::InteractionEventStream, CompletionError> {
267 request.stream = Some(true);
268 let body = serde_json::to_vec(&request)?;
269 let request = self
270 .post_sse("/v1beta/interactions")?
271 .header("Content-Type", "application/json")
272 .body(body)
273 .map_err(|e| CompletionError::HttpError(e.into()))?;
274
275 Ok(streaming::stream_interaction_events(self.clone(), request))
276 }
277
278 pub async fn stream_interaction_events_by_id(
280 &self,
281 interaction_id: impl AsRef<str>,
282 last_event_id: Option<&str>,
283 ) -> Result<streaming::InteractionEventStream, CompletionError> {
284 let path = build_interaction_stream_path(interaction_id.as_ref(), last_event_id);
285 let request = self
286 .get_sse(path)?
287 .body(Vec::new())
288 .map_err(|e| CompletionError::HttpError(e.into()))?;
289
290 Ok(streaming::stream_interaction_events(self.clone(), request))
291 }
292}
293
294pub(crate) fn create_request_body(
295 model: String,
296 completion_request: CompletionRequest,
297 stream_override: Option<bool>,
298) -> Result<CreateInteractionRequest, CompletionError> {
299 let mut history = Vec::new();
300 if let Some(docs) = completion_request.normalized_documents() {
301 history.push(docs);
302 }
303 history.extend(completion_request.chat_history);
304 let (history_system, history) = split_system_messages_from_history(history);
305
306 let turns = history
307 .into_iter()
308 .map(Turn::try_from)
309 .collect::<Result<Vec<_>, _>>()
310 .map_err(|err| CompletionError::RequestError(Box::new(err)))?;
311
312 let input = InteractionInput::Turns(turns);
313
314 let raw_params = completion_request
315 .additional_params
316 .unwrap_or_else(|| Value::Object(Map::new()));
317
318 let mut params: AdditionalParameters = serde_json::from_value(raw_params)?;
319
320 let mut generation_config = params.generation_config.take().unwrap_or_default();
321 if let Some(temp) = completion_request.temperature {
322 generation_config.temperature = Some(temp);
323 }
324 if let Some(max_tokens) = completion_request.max_tokens {
325 generation_config.max_output_tokens = Some(max_tokens);
326 }
327 if let Some(tool_choice) = completion_request.tool_choice {
328 generation_config.tool_choice = Some(tool_choice.try_into()?);
329 }
330 let generation_config = if generation_config.is_empty() {
331 None
332 } else {
333 Some(generation_config)
334 };
335
336 let system_instruction = completion_request
337 .preamble
338 .or_else(|| {
339 if history_system.is_empty() {
340 None
341 } else {
342 Some(history_system.join("\n\n"))
343 }
344 })
345 .or(params.system_instruction.take());
346
347 let mut tools = Vec::new();
348 if !completion_request.tools.is_empty() {
349 tools.extend(
350 completion_request
351 .tools
352 .into_iter()
353 .map(Tool::try_from)
354 .collect::<Result<Vec<_>, _>>()?,
355 );
356 }
357 if let Some(mut extra_tools) = params.tools.take() {
358 tools.append(&mut extra_tools);
359 }
360 let tools = if tools.is_empty() { None } else { Some(tools) };
361
362 let stream = stream_override.or(params.stream.take());
363
364 let (agent, agent_config) = if params.agent.is_some() {
365 (params.agent.take(), params.agent_config.take())
366 } else {
367 (None, None)
368 };
369
370 let response_format = params.response_format.take();
371 let response_mime_type = params.response_mime_type.take();
372
373 if response_format.is_some() && response_mime_type.is_none() {
374 return Err(CompletionError::RequestError(Box::new(
375 std::io::Error::new(
376 std::io::ErrorKind::InvalidInput,
377 "response_mime_type is required when response_format is set",
378 ),
379 )));
380 }
381
382 Ok(CreateInteractionRequest {
383 model: if agent.is_some() { None } else { Some(model) },
384 agent,
385 input,
386 system_instruction,
387 tools,
388 response_format,
389 response_mime_type,
390 stream,
391 store: params.store.take(),
392 background: params.background.take(),
393 generation_config,
394 agent_config,
395 response_modalities: params.response_modalities.take(),
396 previous_interaction_id: params.previous_interaction_id.take(),
397 additional_params: params.additional_params.take(),
398 })
399}
400
401fn split_system_messages_from_history(
402 history: Vec<completion::Message>,
403) -> (Vec<String>, Vec<completion::Message>) {
404 let mut system = Vec::new();
405 let mut remaining = Vec::new();
406
407 for message in history {
408 match message {
409 completion::Message::System { content } => system.push(content),
410 other => remaining.push(other),
411 }
412 }
413
414 (system, remaining)
415}
416
417async fn send_interaction_request<T>(
418 client: &InteractionsClient<T>,
419 request: crate::http_client::Request<Vec<u8>>,
420) -> Result<Interaction, CompletionError>
421where
422 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
423{
424 let response = client.send::<_, Vec<u8>>(request).await?;
425
426 if response.status().is_success() {
427 let response_body = response
428 .into_body()
429 .await
430 .map_err(CompletionError::HttpError)?;
431
432 let response_text = String::from_utf8_lossy(&response_body).to_string();
433
434 let response: Interaction = serde_json::from_slice(&response_body).map_err(|err| {
435 tracing::error!(
436 error = %err,
437 body = %response_text,
438 "Failed to deserialize Gemini interactions response"
439 );
440 CompletionError::JsonError(err)
441 })?;
442
443 Ok(response)
444 } else {
445 let text = String::from_utf8_lossy(
446 &response
447 .into_body()
448 .await
449 .map_err(CompletionError::HttpError)?,
450 )
451 .into();
452
453 Err(CompletionError::ProviderError(text))
454 }
455}
456
457fn build_interaction_stream_path(interaction_id: &str, last_event_id: Option<&str>) -> String {
458 let mut serializer = form_urlencoded::Serializer::new(String::new());
459 serializer.append_pair("stream", "true");
460 if let Some(last_event_id) = last_event_id {
461 serializer.append_pair("last_event_id", last_event_id);
462 }
463 format!(
464 "/v1beta/interactions/{}?{}",
465 interaction_id,
466 serializer.finish()
467 )
468}
469
470impl TryFrom<Interaction> for completion::CompletionResponse<Interaction> {
471 type Error = CompletionError;
472
473 fn try_from(response: Interaction) -> Result<Self, Self::Error> {
474 if response.outputs.is_empty() {
475 let status = response.status.as_ref().map(|status| format!("{status:?}"));
476 let message = match status {
477 Some(status) => format!(
478 "Interaction contained no outputs (status: {status}). Use get_interaction for background tasks."
479 ),
480 None => "Interaction contained no outputs".to_string(),
481 };
482 return Err(CompletionError::ResponseError(message));
483 }
484
485 let content = response
486 .outputs
487 .iter()
488 .cloned()
489 .filter_map(|output| match assistant_content_from_output(output) {
490 Ok(Some(content)) => Some(Ok(content)),
491 Ok(None) => None,
492 Err(err) => Some(Err(err)),
493 })
494 .collect::<Result<Vec<_>, _>>()?;
495
496 let choice = OneOrMany::many(content).map_err(|_| {
497 CompletionError::ResponseError(
498 "Response contained no message or tool call (empty)".to_owned(),
499 )
500 })?;
501
502 let usage = response
503 .usage
504 .as_ref()
505 .and_then(|usage| usage.token_usage())
506 .unwrap_or_default();
507
508 Ok(completion::CompletionResponse {
509 choice,
510 usage,
511 raw_response: response,
512 message_id: None,
513 })
514 }
515}
516
517fn assistant_content_from_output(
518 output: Content,
519) -> Result<Option<completion::AssistantContent>, CompletionError> {
520 match output {
521 Content::Text(TextContent { text, .. }) => {
522 Ok(Some(completion::AssistantContent::text(text)))
523 }
524 Content::FunctionCall(FunctionCallContent {
525 name,
526 arguments,
527 id,
528 ..
529 }) => {
530 let Some(name) = name else {
531 return Ok(None);
532 };
533 let call_id = id.unwrap_or_else(|| name.clone());
534 Ok(Some(completion::AssistantContent::tool_call_with_call_id(
535 name.clone(),
536 call_id,
537 name,
538 arguments.unwrap_or(Value::Object(Map::new())),
539 )))
540 }
541 Content::Thought(ThoughtContent {
542 summary, signature, ..
543 }) => {
544 let mut reasoning_content = summary
545 .unwrap_or_default()
546 .into_iter()
547 .filter_map(|content| match content {
548 ThoughtSummaryContent::Text(text) => Some(message::ReasoningContent::Text {
549 text: text.text,
550 signature: None,
551 }),
552 _ => None,
553 })
554 .collect::<Vec<_>>();
555
556 if reasoning_content.is_empty() {
557 return Ok(None);
558 }
559
560 if let Some(signature) = signature
561 && let Some(message::ReasoningContent::Text {
562 signature: first_signature,
563 ..
564 }) = reasoning_content
565 .iter_mut()
566 .find(|content| matches!(content, message::ReasoningContent::Text { .. }))
567 {
568 *first_signature = Some(signature);
569 }
570
571 Ok(Some(completion::AssistantContent::Reasoning(Reasoning {
572 id: None,
573 content: reasoning_content,
574 })))
575 }
576 Content::Image(ImageContent {
577 data,
578 uri,
579 mime_type,
580 ..
581 }) => {
582 let Some(mime_type) = mime_type else {
583 return Err(CompletionError::ResponseError(
584 "Image output missing mime_type".to_owned(),
585 ));
586 };
587
588 let media_type =
589 message::ImageMediaType::from_mime_type(&mime_type).ok_or_else(|| {
590 CompletionError::ResponseError(format!(
591 "Unsupported image output mime type {mime_type}"
592 ))
593 })?;
594
595 let image = if let Some(data) = data {
596 message::AssistantContent::image_base64(
597 data,
598 Some(media_type),
599 Some(message::ImageDetail::default()),
600 )
601 } else if let Some(uri) = uri {
602 completion::AssistantContent::Image(message::Image {
603 data: message::DocumentSourceKind::Url(uri),
604 media_type: Some(media_type),
605 detail: Some(message::ImageDetail::default()),
606 additional_params: None,
607 })
608 } else {
609 return Err(CompletionError::ResponseError(
610 "Image output missing data or uri".to_owned(),
611 ));
612 };
613
614 Ok(Some(image))
615 }
616 _ => Ok(None),
617 }
618}
619
620fn split_data_uri(
621 src: message::DocumentSourceKind,
622) -> Result<(Option<String>, Option<String>), message::MessageError> {
623 match src {
624 message::DocumentSourceKind::Url(uri) => Ok((None, Some(uri))),
625 message::DocumentSourceKind::Base64(data) | message::DocumentSourceKind::String(data) => {
626 Ok((Some(data), None))
627 }
628 message::DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
629 "Raw content is not supported, encode as base64 first".to_string(),
630 )),
631 message::DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
632 "Provider file IDs are not supported for Gemini Interactions inputs".to_string(),
633 )),
634 message::DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
635 "Unknown content source".to_string(),
636 )),
637 }
638}
639
640pub mod interactions_api_types {
642 use super::split_data_uri;
643 use crate::completion::{CompletionError, GetTokenUsage, Usage};
644 use crate::message::{self, MimeType};
645 use crate::telemetry::ProviderResponseExt;
646 use serde::{Deserialize, Serialize};
647 use serde_json::{Value, json};
648
649 #[derive(Debug, Deserialize, Serialize, Default, Clone)]
655 #[serde(rename_all = "snake_case")]
656 pub struct AdditionalParameters {
657 pub agent: Option<String>,
658 pub agent_config: Option<AgentConfig>,
659 pub background: Option<bool>,
660 pub generation_config: Option<GenerationConfig>,
661 pub previous_interaction_id: Option<String>,
662 pub response_modalities: Option<Vec<ResponseModality>>,
663 pub response_format: Option<Value>,
664 pub response_mime_type: Option<String>,
665 pub store: Option<bool>,
666 pub stream: Option<bool>,
667 pub system_instruction: Option<String>,
668 pub tools: Option<Vec<Tool>>,
669 #[serde(flatten, skip_serializing_if = "Option::is_none")]
670 pub additional_params: Option<Value>,
671 }
672
673 #[derive(Debug, Deserialize, Serialize, Clone)]
675 #[serde(rename_all = "snake_case")]
676 pub struct CreateInteractionRequest {
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub model: Option<String>,
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub agent: Option<String>,
681 pub input: InteractionInput,
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub system_instruction: Option<String>,
684 #[serde(skip_serializing_if = "Option::is_none")]
685 pub tools: Option<Vec<Tool>>,
686 #[serde(skip_serializing_if = "Option::is_none")]
687 pub response_format: Option<Value>,
688 #[serde(skip_serializing_if = "Option::is_none")]
689 pub response_mime_type: Option<String>,
690 #[serde(skip_serializing_if = "Option::is_none")]
691 pub stream: Option<bool>,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub store: Option<bool>,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub background: Option<bool>,
696 #[serde(skip_serializing_if = "Option::is_none")]
697 pub generation_config: Option<GenerationConfig>,
698 #[serde(skip_serializing_if = "Option::is_none")]
699 pub agent_config: Option<AgentConfig>,
700 #[serde(skip_serializing_if = "Option::is_none")]
701 pub response_modalities: Option<Vec<ResponseModality>>,
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub previous_interaction_id: Option<String>,
704 #[serde(flatten, skip_serializing_if = "Option::is_none")]
705 pub additional_params: Option<Value>,
706 }
707
708 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
710 #[serde(rename_all = "snake_case")]
711 pub struct Interaction {
712 #[serde(default)]
713 pub id: String,
714 #[serde(skip_serializing_if = "Option::is_none")]
715 pub model: Option<String>,
716 #[serde(skip_serializing_if = "Option::is_none")]
717 pub agent: Option<String>,
718 #[serde(skip_serializing_if = "Option::is_none")]
719 pub status: Option<InteractionStatus>,
720 #[serde(skip_serializing_if = "Option::is_none")]
721 pub object: Option<String>,
722 #[serde(skip_serializing_if = "Option::is_none")]
723 pub created: Option<String>,
724 #[serde(skip_serializing_if = "Option::is_none")]
725 pub updated: Option<String>,
726 #[serde(skip_serializing_if = "Option::is_none")]
727 pub role: Option<String>,
728 #[serde(default)]
729 pub outputs: Vec<Content>,
730 #[serde(skip_serializing_if = "Option::is_none")]
731 pub usage: Option<InteractionUsage>,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub system_instruction: Option<String>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub tools: Option<Vec<Tool>>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub background: Option<bool>,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub response_modalities: Option<Vec<ResponseModality>>,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub response_format: Option<Value>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub response_mime_type: Option<String>,
744 #[serde(skip_serializing_if = "Option::is_none")]
745 pub previous_interaction_id: Option<String>,
746 #[serde(skip_serializing_if = "Option::is_none")]
747 pub input: Option<InteractionInput>,
748 }
749
750 impl GetTokenUsage for Interaction {
751 fn token_usage(&self) -> Option<Usage> {
752 self.usage.as_ref().and_then(|usage| usage.token_usage())
753 }
754 }
755
756 impl ProviderResponseExt for Interaction {
757 type OutputMessage = Content;
758 type Usage = InteractionUsage;
759
760 fn get_response_id(&self) -> Option<String> {
761 if self.id.is_empty() {
762 None
763 } else {
764 Some(self.id.clone())
765 }
766 }
767
768 fn get_response_model_name(&self) -> Option<String> {
769 self.model.clone()
770 }
771
772 fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
773 self.outputs.clone()
774 }
775
776 fn get_text_response(&self) -> Option<String> {
777 let text = self
778 .outputs
779 .iter()
780 .filter_map(|content| match content {
781 Content::Text(text) => Some(text.text.clone()),
782 _ => None,
783 })
784 .collect::<Vec<_>>()
785 .join("\n");
786
787 if text.is_empty() { None } else { Some(text) }
788 }
789
790 fn get_usage(&self) -> Option<Self::Usage> {
791 self.usage.clone()
792 }
793 }
794
795 #[derive(Clone, Debug, Default)]
797 pub struct GoogleSearchExchange {
798 pub call_id: Option<String>,
800 pub calls: Vec<GoogleSearchCallContent>,
802 pub results: Vec<GoogleSearchResultContent>,
804 }
805
806 impl GoogleSearchExchange {
807 pub fn queries(&self) -> Vec<String> {
809 let mut queries = Vec::new();
810 for call in &self.calls {
811 if let Some(args) = &call.arguments
812 && let Some(call_queries) = &args.queries
813 {
814 queries.extend(call_queries.clone());
815 }
816 }
817 queries
818 }
819
820 pub fn result_items(&self) -> Vec<GoogleSearchResult> {
822 let mut items = Vec::new();
823 for result in &self.results {
824 if let Some(entries) = &result.result {
825 items.extend(entries.clone());
826 }
827 }
828 items
829 }
830 }
831
832 #[derive(Clone, Debug, Default)]
834 pub struct UrlContextExchange {
835 pub call_id: Option<String>,
837 pub calls: Vec<UrlContextCallContent>,
839 pub results: Vec<UrlContextResultContent>,
841 }
842
843 impl UrlContextExchange {
844 pub fn urls(&self) -> Vec<String> {
846 let mut urls = Vec::new();
847 for call in &self.calls {
848 if let Some(args) = &call.arguments
849 && let Some(call_urls) = &args.urls
850 {
851 urls.extend(call_urls.clone());
852 }
853 }
854 urls
855 }
856
857 pub fn result_items(&self) -> Vec<UrlContextResult> {
859 let mut items = Vec::new();
860 for result in &self.results {
861 if let Some(entries) = &result.result {
862 items.extend(entries.clone());
863 }
864 }
865 items
866 }
867 }
868
869 #[derive(Clone, Debug, Default)]
871 pub struct CodeExecutionExchange {
872 pub call_id: Option<String>,
874 pub calls: Vec<CodeExecutionCallContent>,
876 pub results: Vec<CodeExecutionResultContent>,
878 }
879
880 impl CodeExecutionExchange {
881 pub fn code_snippets(&self) -> Vec<String> {
883 let mut snippets = Vec::new();
884 for call in &self.calls {
885 if let Some(args) = &call.arguments
886 && let Some(code) = &args.code
887 {
888 snippets.push(code.clone());
889 }
890 }
891 snippets
892 }
893
894 pub fn outputs(&self) -> Vec<String> {
896 let mut outputs = Vec::new();
897 for result in &self.results {
898 if let Some(output) = &result.result {
899 outputs.push(output.clone());
900 }
901 }
902 outputs
903 }
904 }
905
906 impl Interaction {
907 pub fn google_search_exchanges(&self) -> Vec<GoogleSearchExchange> {
912 let mut exchanges: Vec<GoogleSearchExchange> = Vec::new();
913 let mut last_call_index: Option<usize> = None;
914
915 for content in &self.outputs {
916 match content {
917 Content::GoogleSearchCall(call) => {
918 let index = if let Some(call_id) = call.id.as_ref() {
919 if let Some(index) = exchanges
920 .iter()
921 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
922 {
923 if let Some(exchange) = exchanges.get_mut(index) {
924 exchange.calls.push(call.clone());
925 }
926 index
927 } else {
928 exchanges.push(GoogleSearchExchange {
929 call_id: Some(call_id.clone()),
930 calls: vec![call.clone()],
931 results: Vec::new(),
932 });
933 exchanges.len() - 1
934 }
935 } else {
936 exchanges.push(GoogleSearchExchange {
937 call_id: None,
938 calls: vec![call.clone()],
939 results: Vec::new(),
940 });
941 exchanges.len() - 1
942 };
943 last_call_index = Some(index);
944 }
945 Content::GoogleSearchResult(result) => {
946 if let Some(call_id) = result.call_id.as_ref() {
947 if let Some(index) = exchanges
948 .iter()
949 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
950 {
951 if let Some(exchange) = exchanges.get_mut(index) {
952 exchange.results.push(result.clone());
953 }
954 } else {
955 exchanges.push(GoogleSearchExchange {
956 call_id: Some(call_id.clone()),
957 calls: Vec::new(),
958 results: vec![result.clone()],
959 });
960 }
961 } else if let Some(index) = last_call_index {
962 if let Some(exchange) = exchanges.get_mut(index) {
963 exchange.results.push(result.clone());
964 }
965 } else {
966 exchanges.push(GoogleSearchExchange {
967 call_id: None,
968 calls: Vec::new(),
969 results: vec![result.clone()],
970 });
971 last_call_index = Some(exchanges.len() - 1);
972 }
973 }
974 _ => {}
975 }
976 }
977
978 exchanges
979 }
980
981 pub fn google_search_call_contents(&self) -> Vec<GoogleSearchCallContent> {
983 self.google_search_exchanges()
984 .into_iter()
985 .flat_map(|exchange| exchange.calls)
986 .collect()
987 }
988
989 pub fn google_search_result_contents(&self) -> Vec<GoogleSearchResultContent> {
991 self.google_search_exchanges()
992 .into_iter()
993 .flat_map(|exchange| exchange.results)
994 .collect()
995 }
996
997 pub fn google_search_queries(&self) -> Vec<String> {
999 self.google_search_exchanges()
1000 .into_iter()
1001 .flat_map(|exchange| exchange.queries())
1002 .collect()
1003 }
1004
1005 pub fn google_search_results(&self) -> Vec<GoogleSearchResult> {
1007 self.google_search_exchanges()
1008 .into_iter()
1009 .flat_map(|exchange| exchange.result_items())
1010 .collect()
1011 }
1012
1013 pub fn url_context_exchanges(&self) -> Vec<UrlContextExchange> {
1018 let mut exchanges: Vec<UrlContextExchange> = Vec::new();
1019 let mut last_call_index: Option<usize> = None;
1020
1021 for content in &self.outputs {
1022 match content {
1023 Content::UrlContextCall(call) => {
1024 let index = if let Some(call_id) = call.id.as_ref() {
1025 if let Some(index) = exchanges
1026 .iter()
1027 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1028 {
1029 if let Some(exchange) = exchanges.get_mut(index) {
1030 exchange.calls.push(call.clone());
1031 }
1032 index
1033 } else {
1034 exchanges.push(UrlContextExchange {
1035 call_id: Some(call_id.clone()),
1036 calls: vec![call.clone()],
1037 results: Vec::new(),
1038 });
1039 exchanges.len() - 1
1040 }
1041 } else {
1042 exchanges.push(UrlContextExchange {
1043 call_id: None,
1044 calls: vec![call.clone()],
1045 results: Vec::new(),
1046 });
1047 exchanges.len() - 1
1048 };
1049 last_call_index = Some(index);
1050 }
1051 Content::UrlContextResult(result) => {
1052 if let Some(call_id) = result.call_id.as_ref() {
1053 if let Some(index) = exchanges
1054 .iter()
1055 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1056 {
1057 if let Some(exchange) = exchanges.get_mut(index) {
1058 exchange.results.push(result.clone());
1059 }
1060 } else {
1061 exchanges.push(UrlContextExchange {
1062 call_id: Some(call_id.clone()),
1063 calls: Vec::new(),
1064 results: vec![result.clone()],
1065 });
1066 }
1067 } else if let Some(index) = last_call_index {
1068 if let Some(exchange) = exchanges.get_mut(index) {
1069 exchange.results.push(result.clone());
1070 }
1071 } else {
1072 exchanges.push(UrlContextExchange {
1073 call_id: None,
1074 calls: Vec::new(),
1075 results: vec![result.clone()],
1076 });
1077 last_call_index = Some(exchanges.len() - 1);
1078 }
1079 }
1080 _ => {}
1081 }
1082 }
1083
1084 exchanges
1085 }
1086
1087 pub fn url_context_call_contents(&self) -> Vec<UrlContextCallContent> {
1089 self.url_context_exchanges()
1090 .into_iter()
1091 .flat_map(|exchange| exchange.calls)
1092 .collect()
1093 }
1094
1095 pub fn url_context_result_contents(&self) -> Vec<UrlContextResultContent> {
1097 self.url_context_exchanges()
1098 .into_iter()
1099 .flat_map(|exchange| exchange.results)
1100 .collect()
1101 }
1102
1103 pub fn url_context_urls(&self) -> Vec<String> {
1105 self.url_context_exchanges()
1106 .into_iter()
1107 .flat_map(|exchange| exchange.urls())
1108 .collect()
1109 }
1110
1111 pub fn url_context_results(&self) -> Vec<UrlContextResult> {
1113 self.url_context_exchanges()
1114 .into_iter()
1115 .flat_map(|exchange| exchange.result_items())
1116 .collect()
1117 }
1118
1119 pub fn code_execution_exchanges(&self) -> Vec<CodeExecutionExchange> {
1124 let mut exchanges: Vec<CodeExecutionExchange> = Vec::new();
1125 let mut last_call_index: Option<usize> = None;
1126
1127 for content in &self.outputs {
1128 match content {
1129 Content::CodeExecutionCall(call) => {
1130 let index = if let Some(call_id) = call.id.as_ref() {
1131 if let Some(index) = exchanges
1132 .iter()
1133 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1134 {
1135 if let Some(exchange) = exchanges.get_mut(index) {
1136 exchange.calls.push(call.clone());
1137 }
1138 index
1139 } else {
1140 exchanges.push(CodeExecutionExchange {
1141 call_id: Some(call_id.clone()),
1142 calls: vec![call.clone()],
1143 results: Vec::new(),
1144 });
1145 exchanges.len() - 1
1146 }
1147 } else {
1148 exchanges.push(CodeExecutionExchange {
1149 call_id: None,
1150 calls: vec![call.clone()],
1151 results: Vec::new(),
1152 });
1153 exchanges.len() - 1
1154 };
1155 last_call_index = Some(index);
1156 }
1157 Content::CodeExecutionResult(result) => {
1158 if let Some(call_id) = result.call_id.as_ref() {
1159 if let Some(index) = exchanges
1160 .iter()
1161 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1162 {
1163 if let Some(exchange) = exchanges.get_mut(index) {
1164 exchange.results.push(result.clone());
1165 }
1166 } else {
1167 exchanges.push(CodeExecutionExchange {
1168 call_id: Some(call_id.clone()),
1169 calls: Vec::new(),
1170 results: vec![result.clone()],
1171 });
1172 }
1173 } else if let Some(index) = last_call_index {
1174 if let Some(exchange) = exchanges.get_mut(index) {
1175 exchange.results.push(result.clone());
1176 }
1177 } else {
1178 exchanges.push(CodeExecutionExchange {
1179 call_id: None,
1180 calls: Vec::new(),
1181 results: vec![result.clone()],
1182 });
1183 last_call_index = Some(exchanges.len() - 1);
1184 }
1185 }
1186 _ => {}
1187 }
1188 }
1189
1190 exchanges
1191 }
1192
1193 pub fn code_execution_call_contents(&self) -> Vec<CodeExecutionCallContent> {
1195 self.code_execution_exchanges()
1196 .into_iter()
1197 .flat_map(|exchange| exchange.calls)
1198 .collect()
1199 }
1200
1201 pub fn code_execution_result_contents(&self) -> Vec<CodeExecutionResultContent> {
1203 self.code_execution_exchanges()
1204 .into_iter()
1205 .flat_map(|exchange| exchange.results)
1206 .collect()
1207 }
1208
1209 pub fn code_execution_snippets(&self) -> Vec<String> {
1211 self.code_execution_exchanges()
1212 .into_iter()
1213 .flat_map(|exchange| exchange.code_snippets())
1214 .collect()
1215 }
1216
1217 pub fn code_execution_outputs(&self) -> Vec<String> {
1219 self.code_execution_exchanges()
1220 .into_iter()
1221 .flat_map(|exchange| exchange.outputs())
1222 .collect()
1223 }
1224
1225 pub fn text_with_inline_citations(&self) -> Option<String> {
1227 let text = self
1228 .outputs
1229 .iter()
1230 .filter_map(|content| match content {
1231 Content::Text(text) => Some(text.with_inline_citations()),
1232 _ => None,
1233 })
1234 .collect::<Vec<_>>()
1235 .join("\n");
1236
1237 if text.is_empty() { None } else { Some(text) }
1238 }
1239
1240 pub fn is_terminal(&self) -> bool {
1242 self.status
1243 .as_ref()
1244 .is_some_and(InteractionStatus::is_terminal)
1245 }
1246
1247 pub fn is_completed(&self) -> bool {
1249 matches!(self.status, Some(InteractionStatus::Completed))
1250 }
1251 }
1252
1253 #[derive(Clone, Debug, Deserialize, Serialize)]
1255 #[serde(rename_all = "snake_case")]
1256 pub enum InteractionStatus {
1257 InProgress,
1258 RequiresAction,
1259 Completed,
1260 Failed,
1261 Cancelled,
1262 }
1263
1264 impl InteractionStatus {
1265 pub fn is_terminal(&self) -> bool {
1267 matches!(
1268 self,
1269 InteractionStatus::Completed
1270 | InteractionStatus::Failed
1271 | InteractionStatus::Cancelled
1272 )
1273 }
1274 }
1275
1276 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1278 #[serde(rename_all = "snake_case")]
1279 pub struct InteractionUsage {
1280 #[serde(skip_serializing_if = "Option::is_none")]
1281 pub total_input_tokens: Option<u64>,
1282 #[serde(skip_serializing_if = "Option::is_none")]
1283 pub total_output_tokens: Option<u64>,
1284 #[serde(skip_serializing_if = "Option::is_none")]
1285 pub total_tokens: Option<u64>,
1286 }
1287
1288 impl GetTokenUsage for InteractionUsage {
1289 fn token_usage(&self) -> Option<Usage> {
1290 let mut usage = Usage::new();
1291 usage.input_tokens = self.total_input_tokens.unwrap_or_default();
1292 usage.output_tokens = self.total_output_tokens.unwrap_or_default();
1293 usage.total_tokens = self
1294 .total_tokens
1295 .unwrap_or(usage.input_tokens + usage.output_tokens);
1296 Some(usage)
1297 }
1298 }
1299
1300 #[derive(Clone, Debug, Deserialize, Serialize)]
1302 #[serde(untagged)]
1303 pub enum InteractionInput {
1304 Text(String),
1305 Content(Content),
1306 Turns(Vec<Turn>),
1307 Contents(Vec<Content>),
1308 }
1309
1310 #[derive(Clone, Debug, Deserialize, Serialize)]
1312 #[serde(rename_all = "lowercase")]
1313 pub enum Role {
1314 User,
1315 Model,
1316 }
1317
1318 #[derive(Clone, Debug, Deserialize, Serialize)]
1320 pub struct Turn {
1321 pub role: Role,
1322 pub content: TurnContent,
1323 }
1324
1325 #[derive(Clone, Debug, Deserialize, Serialize)]
1327 #[serde(untagged)]
1328 pub enum TurnContent {
1329 Text(String),
1330 Contents(Vec<Content>),
1331 }
1332
1333 impl TryFrom<crate::completion::Message> for Turn {
1334 type Error = message::MessageError;
1335
1336 fn try_from(message: crate::completion::Message) -> Result<Self, Self::Error> {
1337 match message {
1338 crate::completion::Message::System { content } => Ok(Self {
1339 role: Role::User,
1340 content: TurnContent::Text(content),
1341 }),
1342 crate::completion::Message::User { content } => {
1343 let contents = content
1344 .into_iter()
1345 .map(Content::try_from)
1346 .collect::<Result<Vec<_>, _>>()?;
1347 Ok(Self {
1348 role: Role::User,
1349 content: TurnContent::Contents(contents),
1350 })
1351 }
1352 crate::completion::Message::Assistant { content, .. } => {
1353 let contents = content
1354 .into_iter()
1355 .map(Content::try_from)
1356 .collect::<Result<Vec<_>, _>>()?;
1357 Ok(Self {
1358 role: Role::Model,
1359 content: TurnContent::Contents(contents),
1360 })
1361 }
1362 }
1363 }
1364 }
1365
1366 #[derive(Clone, Debug, Deserialize, Serialize)]
1372 pub struct Annotation {
1373 #[serde(skip_serializing_if = "Option::is_none")]
1374 pub start_index: Option<i64>,
1375 #[serde(skip_serializing_if = "Option::is_none")]
1376 pub end_index: Option<i64>,
1377 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub source: Option<String>,
1379 }
1380
1381 #[derive(Clone, Debug)]
1383 pub struct Citation {
1384 pub start_index: usize,
1385 pub end_index: usize,
1386 pub source: String,
1387 }
1388
1389 #[derive(Clone, Debug, Deserialize, Serialize)]
1391 pub struct TextContent {
1392 pub text: String,
1393 #[serde(skip_serializing_if = "Option::is_none")]
1394 pub annotations: Option<Vec<Annotation>>,
1395 }
1396
1397 impl TextContent {
1398 pub fn citations(&self) -> Vec<Citation> {
1400 let mut citations = Vec::new();
1401 let Some(annotations) = self.annotations.as_ref() else {
1402 return citations;
1403 };
1404
1405 for annotation in annotations {
1406 let (Some(start), Some(end), Some(source)) = (
1407 annotation.start_index,
1408 annotation.end_index,
1409 annotation.source.as_ref(),
1410 ) else {
1411 continue;
1412 };
1413
1414 if start < 0 || end < 0 {
1415 continue;
1416 }
1417 let start = start as usize;
1418 let end = end as usize;
1419 if end <= start || end > self.text.len() {
1420 continue;
1421 }
1422 if !self.text.is_char_boundary(start) || !self.text.is_char_boundary(end) {
1423 continue;
1424 }
1425
1426 citations.push(Citation {
1427 start_index: start,
1428 end_index: end,
1429 source: source.clone(),
1430 });
1431 }
1432
1433 citations.sort_by(|a, b| {
1434 a.start_index
1435 .cmp(&b.start_index)
1436 .then_with(|| a.end_index.cmp(&b.end_index))
1437 });
1438
1439 citations
1440 }
1441
1442 pub fn with_inline_citations(&self) -> String {
1444 let citations = self.citations();
1445 if citations.is_empty() {
1446 return self.text.clone();
1447 }
1448
1449 let mut source_order = Vec::new();
1450 for citation in &citations {
1451 if !source_order.contains(&citation.source) {
1452 source_order.push(citation.source.clone());
1453 }
1454 }
1455
1456 let mut inserts = citations
1457 .iter()
1458 .map(|citation| {
1459 let index = source_order
1460 .iter()
1461 .position(|source| source == &citation.source)
1462 .map(|idx| idx + 1)
1463 .unwrap_or(0);
1464 (
1465 citation.start_index,
1466 citation.end_index,
1467 index,
1468 &citation.source,
1469 )
1470 })
1471 .collect::<Vec<_>>();
1472
1473 inserts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| b.0.cmp(&a.0)));
1474
1475 let mut text = self.text.clone();
1476 for (_, end, index, source) in inserts {
1477 if index == 0 {
1478 continue;
1479 }
1480 let citation = format!("[{}]({})", index, source);
1481 text.insert_str(end, &citation);
1482 }
1483
1484 text
1485 }
1486 }
1487
1488 #[derive(Clone, Debug, Deserialize, Serialize)]
1490 pub struct ImageContent {
1491 #[serde(skip_serializing_if = "Option::is_none")]
1492 pub data: Option<String>,
1493 #[serde(skip_serializing_if = "Option::is_none")]
1494 pub uri: Option<String>,
1495 #[serde(skip_serializing_if = "Option::is_none")]
1496 pub mime_type: Option<String>,
1497 #[serde(skip_serializing_if = "Option::is_none")]
1498 pub resolution: Option<MediaResolution>,
1499 }
1500
1501 #[derive(Clone, Debug, Deserialize, Serialize)]
1503 pub struct AudioContent {
1504 #[serde(skip_serializing_if = "Option::is_none")]
1505 pub data: Option<String>,
1506 #[serde(skip_serializing_if = "Option::is_none")]
1507 pub uri: Option<String>,
1508 #[serde(skip_serializing_if = "Option::is_none")]
1509 pub mime_type: Option<String>,
1510 }
1511
1512 #[derive(Clone, Debug, Deserialize, Serialize)]
1514 pub struct DocumentContent {
1515 #[serde(skip_serializing_if = "Option::is_none")]
1516 pub data: Option<String>,
1517 #[serde(skip_serializing_if = "Option::is_none")]
1518 pub uri: Option<String>,
1519 #[serde(skip_serializing_if = "Option::is_none")]
1520 pub mime_type: Option<String>,
1521 }
1522
1523 #[derive(Clone, Debug, Deserialize, Serialize)]
1525 pub struct VideoContent {
1526 #[serde(skip_serializing_if = "Option::is_none")]
1527 pub data: Option<String>,
1528 #[serde(skip_serializing_if = "Option::is_none")]
1529 pub uri: Option<String>,
1530 #[serde(skip_serializing_if = "Option::is_none")]
1531 pub mime_type: Option<String>,
1532 #[serde(skip_serializing_if = "Option::is_none")]
1533 pub resolution: Option<MediaResolution>,
1534 }
1535
1536 #[derive(Clone, Debug, Deserialize, Serialize)]
1538 pub struct ThoughtContent {
1539 #[serde(skip_serializing_if = "Option::is_none")]
1540 pub signature: Option<String>,
1541 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub summary: Option<Vec<ThoughtSummaryContent>>,
1543 }
1544
1545 #[derive(Clone, Debug, Deserialize, Serialize)]
1547 #[serde(untagged)]
1548 pub enum ThoughtSummaryContent {
1549 Text(TextContent),
1550 Image(ImageContent),
1551 }
1552
1553 #[derive(Clone, Debug, Deserialize, Serialize)]
1555 pub struct FunctionCallContent {
1556 #[serde(skip_serializing_if = "Option::is_none")]
1557 pub name: Option<String>,
1558 #[serde(skip_serializing_if = "Option::is_none")]
1559 pub arguments: Option<Value>,
1560 #[serde(skip_serializing_if = "Option::is_none")]
1561 pub id: Option<String>,
1562 }
1563
1564 #[derive(Clone, Debug, Deserialize, Serialize)]
1566 pub struct FunctionResultContent {
1567 #[serde(skip_serializing_if = "Option::is_none")]
1568 pub name: Option<String>,
1569 #[serde(skip_serializing_if = "Option::is_none")]
1570 pub is_error: Option<bool>,
1571 #[serde(skip_serializing_if = "Option::is_none")]
1572 pub result: Option<Value>,
1573 #[serde(skip_serializing_if = "Option::is_none")]
1574 pub call_id: Option<String>,
1575 }
1576
1577 #[derive(Clone, Debug, Deserialize, Serialize)]
1579 pub struct CodeExecutionCallArguments {
1580 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub language: Option<String>,
1582 #[serde(skip_serializing_if = "Option::is_none")]
1583 pub code: Option<String>,
1584 }
1585
1586 #[derive(Clone, Debug, Deserialize, Serialize)]
1588 pub struct CodeExecutionCallContent {
1589 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub arguments: Option<CodeExecutionCallArguments>,
1591 #[serde(skip_serializing_if = "Option::is_none")]
1592 pub id: Option<String>,
1593 }
1594
1595 #[derive(Clone, Debug, Deserialize, Serialize)]
1597 pub struct CodeExecutionResultContent {
1598 #[serde(skip_serializing_if = "Option::is_none")]
1599 pub result: Option<String>,
1600 #[serde(skip_serializing_if = "Option::is_none")]
1601 pub is_error: Option<bool>,
1602 #[serde(skip_serializing_if = "Option::is_none")]
1603 pub signature: Option<String>,
1604 #[serde(skip_serializing_if = "Option::is_none")]
1605 pub call_id: Option<String>,
1606 }
1607
1608 #[derive(Clone, Debug, Deserialize, Serialize)]
1610 pub struct UrlContextCallArguments {
1611 #[serde(skip_serializing_if = "Option::is_none")]
1612 pub urls: Option<Vec<String>>,
1613 }
1614
1615 #[derive(Clone, Debug, Deserialize, Serialize)]
1617 pub struct UrlContextCallContent {
1618 #[serde(skip_serializing_if = "Option::is_none")]
1619 pub arguments: Option<UrlContextCallArguments>,
1620 #[serde(skip_serializing_if = "Option::is_none")]
1621 pub id: Option<String>,
1622 }
1623
1624 #[derive(Clone, Debug, Deserialize, Serialize)]
1626 pub struct UrlContextResult {
1627 #[serde(skip_serializing_if = "Option::is_none")]
1628 pub url: Option<String>,
1629 #[serde(skip_serializing_if = "Option::is_none")]
1630 pub status: Option<String>,
1631 }
1632
1633 #[derive(Clone, Debug, Deserialize, Serialize)]
1635 pub struct UrlContextResultContent {
1636 #[serde(skip_serializing_if = "Option::is_none")]
1637 pub signature: Option<String>,
1638 #[serde(skip_serializing_if = "Option::is_none")]
1639 pub result: Option<Vec<UrlContextResult>>,
1640 #[serde(skip_serializing_if = "Option::is_none")]
1641 pub is_error: Option<bool>,
1642 #[serde(skip_serializing_if = "Option::is_none")]
1643 pub call_id: Option<String>,
1644 }
1645
1646 #[derive(Clone, Debug, Deserialize, Serialize)]
1648 pub struct GoogleSearchCallArguments {
1649 #[serde(skip_serializing_if = "Option::is_none")]
1650 pub queries: Option<Vec<String>>,
1651 }
1652
1653 #[derive(Clone, Debug, Deserialize, Serialize)]
1655 pub struct GoogleSearchCallContent {
1656 #[serde(skip_serializing_if = "Option::is_none")]
1657 pub arguments: Option<GoogleSearchCallArguments>,
1658 #[serde(skip_serializing_if = "Option::is_none")]
1659 pub id: Option<String>,
1660 }
1661
1662 #[derive(Clone, Debug, Deserialize, Serialize)]
1664 pub struct GoogleSearchResult {
1665 #[serde(skip_serializing_if = "Option::is_none")]
1666 pub url: Option<String>,
1667 #[serde(skip_serializing_if = "Option::is_none")]
1668 pub title: Option<String>,
1669 #[serde(skip_serializing_if = "Option::is_none")]
1670 pub rendered_content: Option<String>,
1671 }
1672
1673 #[derive(Clone, Debug, Deserialize, Serialize)]
1675 pub struct GoogleSearchResultContent {
1676 #[serde(skip_serializing_if = "Option::is_none")]
1677 pub signature: Option<String>,
1678 #[serde(skip_serializing_if = "Option::is_none")]
1679 pub result: Option<Vec<GoogleSearchResult>>,
1680 #[serde(skip_serializing_if = "Option::is_none")]
1681 pub is_error: Option<bool>,
1682 #[serde(skip_serializing_if = "Option::is_none")]
1683 pub call_id: Option<String>,
1684 }
1685
1686 #[derive(Clone, Debug, Deserialize, Serialize)]
1688 pub struct McpServerToolCallContent {
1689 #[serde(skip_serializing_if = "Option::is_none")]
1690 pub name: Option<String>,
1691 #[serde(skip_serializing_if = "Option::is_none")]
1692 pub server_name: Option<String>,
1693 #[serde(skip_serializing_if = "Option::is_none")]
1694 pub arguments: Option<Value>,
1695 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub id: Option<String>,
1697 }
1698
1699 #[derive(Clone, Debug, Deserialize, Serialize)]
1701 pub struct McpServerToolResultContent {
1702 #[serde(skip_serializing_if = "Option::is_none")]
1703 pub name: Option<String>,
1704 #[serde(skip_serializing_if = "Option::is_none")]
1705 pub server_name: Option<String>,
1706 #[serde(skip_serializing_if = "Option::is_none")]
1707 pub result: Option<Value>,
1708 #[serde(skip_serializing_if = "Option::is_none")]
1709 pub call_id: Option<String>,
1710 }
1711
1712 #[derive(Clone, Debug, Deserialize, Serialize)]
1714 pub struct FileSearchResult {
1715 pub title: String,
1716 pub text: String,
1717 pub file_search_store: String,
1718 }
1719
1720 #[derive(Clone, Debug, Deserialize, Serialize)]
1722 pub struct FileSearchResultContent {
1723 #[serde(skip_serializing_if = "Option::is_none")]
1724 pub result: Option<Vec<FileSearchResult>>,
1725 }
1726
1727 #[derive(Clone, Debug, Deserialize, Serialize)]
1729 #[serde(tag = "type", rename_all = "snake_case")]
1730 pub enum Content {
1731 Text(TextContent),
1732 Image(ImageContent),
1733 Audio(AudioContent),
1734 Document(DocumentContent),
1735 Video(VideoContent),
1736 Thought(ThoughtContent),
1737 FunctionCall(FunctionCallContent),
1738 FunctionResult(FunctionResultContent),
1739 CodeExecutionCall(CodeExecutionCallContent),
1740 CodeExecutionResult(CodeExecutionResultContent),
1741 UrlContextCall(UrlContextCallContent),
1742 UrlContextResult(UrlContextResultContent),
1743 GoogleSearchCall(GoogleSearchCallContent),
1744 GoogleSearchResult(GoogleSearchResultContent),
1745 McpServerToolCall(McpServerToolCallContent),
1746 McpServerToolResult(McpServerToolResultContent),
1747 FileSearchResult(FileSearchResultContent),
1748 }
1749
1750 impl TryFrom<message::UserContent> for Content {
1751 type Error = message::MessageError;
1752
1753 fn try_from(content: message::UserContent) -> Result<Self, Self::Error> {
1754 match content {
1755 message::UserContent::Text(message::Text { text }) => Ok(Self::Text(TextContent {
1756 text,
1757 annotations: None,
1758 })),
1759 message::UserContent::ToolResult(message::ToolResult {
1760 id,
1761 call_id,
1762 content,
1763 }) => {
1764 let Some(call_id) = call_id else {
1765 return Err(message::MessageError::ConversionError(
1766 "Tool results require call_id for Gemini Interactions API".to_string(),
1767 ));
1768 };
1769
1770 let content = content.first();
1771
1772 let message::ToolResultContent::Text(text) = content else {
1773 return Err(message::MessageError::ConversionError(
1774 "Tool result content must be text".to_string(),
1775 ));
1776 };
1777
1778 let result: Value = serde_json::from_str(&text.text).unwrap_or_else(|error| {
1779 tracing::trace!(?error, "Tool result is not valid JSON; sending as string");
1780 json!(text.text)
1781 });
1782
1783 Ok(Self::FunctionResult(FunctionResultContent {
1784 name: Some(id),
1785 is_error: None,
1786 result: Some(result),
1787 call_id: Some(call_id),
1788 }))
1789 }
1790 message::UserContent::Image(message::Image {
1791 data, media_type, ..
1792 }) => {
1793 let media_type = media_type.ok_or_else(|| {
1794 message::MessageError::ConversionError(
1795 "Media type for image is required for Gemini".to_string(),
1796 )
1797 })?;
1798 let mime_type = media_type.to_mime_type().to_string();
1799 let (data, uri) = split_data_uri(data)?;
1800 Ok(Self::Image(ImageContent {
1801 data,
1802 uri,
1803 mime_type: Some(mime_type),
1804 resolution: None,
1805 }))
1806 }
1807 message::UserContent::Audio(message::Audio {
1808 data, media_type, ..
1809 }) => {
1810 let media_type = media_type.ok_or_else(|| {
1811 message::MessageError::ConversionError(
1812 "Media type for audio is required for Gemini".to_string(),
1813 )
1814 })?;
1815 let mime_type = media_type.to_mime_type().to_string();
1816 let (data, uri) = split_data_uri(data)?;
1817 Ok(Self::Audio(AudioContent {
1818 data,
1819 uri,
1820 mime_type: Some(mime_type),
1821 }))
1822 }
1823 message::UserContent::Video(message::Video {
1824 data, media_type, ..
1825 }) => {
1826 let media_type = media_type.ok_or_else(|| {
1827 message::MessageError::ConversionError(
1828 "Media type for video is required for Gemini".to_string(),
1829 )
1830 })?;
1831 let mime_type = media_type.to_mime_type().to_string();
1832 let (data, uri) = split_data_uri(data)?;
1833 Ok(Self::Video(VideoContent {
1834 data,
1835 uri,
1836 mime_type: Some(mime_type),
1837 resolution: None,
1838 }))
1839 }
1840 message::UserContent::Document(message::Document {
1841 data, media_type, ..
1842 }) => {
1843 let media_type = media_type.ok_or_else(|| {
1844 message::MessageError::ConversionError(
1845 "Media type for document is required for Gemini".to_string(),
1846 )
1847 })?;
1848 let mime_type = media_type.to_mime_type().to_string();
1849 let (data, uri) = split_data_uri(data)?;
1850 Ok(Self::Document(DocumentContent {
1851 data,
1852 uri,
1853 mime_type: Some(mime_type),
1854 }))
1855 }
1856 }
1857 }
1858 }
1859
1860 impl TryFrom<message::AssistantContent> for Content {
1861 type Error = message::MessageError;
1862
1863 fn try_from(content: message::AssistantContent) -> Result<Self, Self::Error> {
1864 match content {
1865 message::AssistantContent::Text(message::Text { text }) => {
1866 Ok(Self::Text(TextContent {
1867 text,
1868 annotations: None,
1869 }))
1870 }
1871 message::AssistantContent::ToolCall(tool_call) => {
1872 let call_id = tool_call.call_id.unwrap_or_else(|| tool_call.id.clone());
1873 Ok(Self::FunctionCall(FunctionCallContent {
1874 name: Some(tool_call.function.name),
1875 arguments: Some(tool_call.function.arguments),
1876 id: Some(call_id),
1877 }))
1878 }
1879 message::AssistantContent::Reasoning(message::Reasoning { content, .. }) => {
1880 let mut signature = None;
1881 let summary = content
1882 .into_iter()
1883 .map(|reasoning_content| {
1884 let text = match reasoning_content {
1885 message::ReasoningContent::Text {
1886 text,
1887 signature: content_signature,
1888 } => {
1889 if signature.is_none() {
1890 signature = content_signature;
1891 }
1892 text
1893 }
1894 message::ReasoningContent::Summary(text)
1895 | message::ReasoningContent::Encrypted(text) => text,
1896 message::ReasoningContent::Redacted { data } => data,
1897 };
1898
1899 ThoughtSummaryContent::Text(TextContent {
1900 text,
1901 annotations: None,
1902 })
1903 })
1904 .collect();
1905
1906 Ok(Self::Thought(ThoughtContent {
1907 signature,
1908 summary: Some(summary),
1909 }))
1910 }
1911 message::AssistantContent::Image(message::Image {
1912 data, media_type, ..
1913 }) => {
1914 let media_type = media_type.ok_or_else(|| {
1915 message::MessageError::ConversionError(
1916 "Media type for image is required for Gemini".to_string(),
1917 )
1918 })?;
1919 let mime_type = media_type.to_mime_type().to_string();
1920 let (data, uri) = split_data_uri(data)?;
1921 Ok(Self::Image(ImageContent {
1922 data,
1923 uri,
1924 mime_type: Some(mime_type),
1925 resolution: None,
1926 }))
1927 }
1928 }
1929 }
1930 }
1931
1932 #[derive(Clone, Debug, Deserialize, Serialize)]
1938 #[serde(rename_all = "snake_case")]
1939 pub enum ResponseModality {
1940 Text,
1941 Image,
1942 Audio,
1943 }
1944
1945 #[derive(Clone, Debug, Deserialize, Serialize)]
1947 #[serde(rename_all = "snake_case")]
1948 pub enum ThinkingLevel {
1949 Minimal,
1950 Low,
1951 Medium,
1952 High,
1953 }
1954
1955 #[derive(Clone, Debug, Deserialize, Serialize)]
1957 #[serde(rename_all = "snake_case")]
1958 pub enum ThinkingSummaries {
1959 Auto,
1960 None,
1961 }
1962
1963 #[derive(Clone, Debug, Deserialize, Serialize)]
1965 #[serde(rename_all = "snake_case")]
1966 pub struct SpeechConfig {
1967 #[serde(skip_serializing_if = "Option::is_none")]
1968 pub voice: Option<String>,
1969 #[serde(skip_serializing_if = "Option::is_none")]
1970 pub language: Option<String>,
1971 #[serde(skip_serializing_if = "Option::is_none")]
1972 pub speaker: Option<String>,
1973 }
1974
1975 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1977 #[serde(rename_all = "snake_case")]
1978 pub struct GenerationConfig {
1979 #[serde(skip_serializing_if = "Option::is_none")]
1980 pub temperature: Option<f64>,
1981 #[serde(skip_serializing_if = "Option::is_none")]
1982 pub top_p: Option<f64>,
1983 #[serde(skip_serializing_if = "Option::is_none")]
1984 pub seed: Option<u64>,
1985 #[serde(skip_serializing_if = "Option::is_none")]
1986 pub stop_sequences: Option<Vec<String>>,
1987 #[serde(skip_serializing_if = "Option::is_none")]
1988 pub tool_choice: Option<ToolChoice>,
1989 #[serde(skip_serializing_if = "Option::is_none")]
1990 pub thinking_level: Option<ThinkingLevel>,
1991 #[serde(skip_serializing_if = "Option::is_none")]
1992 pub thinking_summaries: Option<ThinkingSummaries>,
1993 #[serde(skip_serializing_if = "Option::is_none")]
1994 pub max_output_tokens: Option<u64>,
1995 #[serde(skip_serializing_if = "Option::is_none")]
1996 pub speech_config: Option<Vec<SpeechConfig>>,
1997 }
1998
1999 impl GenerationConfig {
2000 pub fn is_empty(&self) -> bool {
2002 self.temperature.is_none()
2003 && self.top_p.is_none()
2004 && self.seed.is_none()
2005 && self.stop_sequences.is_none()
2006 && self.tool_choice.is_none()
2007 && self.thinking_level.is_none()
2008 && self.thinking_summaries.is_none()
2009 && self.max_output_tokens.is_none()
2010 && self.speech_config.is_none()
2011 }
2012 }
2013
2014 #[derive(Clone, Debug, Deserialize, Serialize)]
2016 #[serde(untagged)]
2017 pub enum ToolChoice {
2018 Type(ToolChoiceType),
2019 Config(ToolChoiceConfig),
2020 }
2021
2022 #[derive(Clone, Debug, Deserialize, Serialize)]
2024 #[serde(rename_all = "snake_case")]
2025 pub enum ToolChoiceType {
2026 Auto,
2027 Any,
2028 None,
2029 Validated,
2030 }
2031
2032 #[derive(Clone, Debug, Deserialize, Serialize)]
2034 pub struct ToolChoiceConfig {
2035 pub allowed_tools: AllowedTools,
2036 }
2037
2038 #[derive(Clone, Debug, Deserialize, Serialize)]
2040 pub struct AllowedTools {
2041 #[serde(skip_serializing_if = "Option::is_none")]
2042 pub mode: Option<ToolChoiceType>,
2043 #[serde(skip_serializing_if = "Option::is_none")]
2044 pub tools: Option<Vec<String>>,
2045 }
2046
2047 #[derive(Clone, Debug, Deserialize, Serialize)]
2049 #[serde(tag = "type", rename_all = "snake_case")]
2050 pub enum Tool {
2051 Function(FunctionTool),
2052 GoogleSearch,
2053 CodeExecution,
2054 UrlContext,
2055 ComputerUse(ComputerUseTool),
2056 McpServer(McpServerTool),
2057 FileSearch(FileSearchTool),
2058 }
2059
2060 #[derive(Clone, Debug, Deserialize, Serialize)]
2062 pub struct FunctionTool {
2063 #[serde(skip_serializing_if = "Option::is_none")]
2064 pub name: Option<String>,
2065 #[serde(skip_serializing_if = "Option::is_none")]
2066 pub description: Option<String>,
2067 #[serde(skip_serializing_if = "Option::is_none")]
2068 pub parameters: Option<Value>,
2069 }
2070
2071 #[derive(Clone, Debug, Deserialize, Serialize)]
2073 pub struct ComputerUseTool {
2074 #[serde(skip_serializing_if = "Option::is_none")]
2075 pub environment: Option<String>,
2076 #[serde(skip_serializing_if = "Option::is_none")]
2077 pub excluded_predefined_functions: Option<Vec<String>>,
2078 }
2079
2080 #[derive(Clone, Debug, Deserialize, Serialize)]
2082 pub struct McpServerTool {
2083 #[serde(skip_serializing_if = "Option::is_none")]
2084 pub name: Option<String>,
2085 #[serde(skip_serializing_if = "Option::is_none")]
2086 pub url: Option<String>,
2087 #[serde(skip_serializing_if = "Option::is_none")]
2088 pub headers: Option<Value>,
2089 #[serde(skip_serializing_if = "Option::is_none")]
2090 pub allowed_tools: Option<AllowedTools>,
2091 }
2092
2093 #[derive(Clone, Debug, Deserialize, Serialize)]
2095 pub struct FileSearchTool {
2096 #[serde(skip_serializing_if = "Option::is_none")]
2097 pub file_search_store_names: Option<Vec<String>>,
2098 #[serde(skip_serializing_if = "Option::is_none")]
2099 pub top_k: Option<u64>,
2100 #[serde(skip_serializing_if = "Option::is_none")]
2101 pub metadata_filter: Option<String>,
2102 }
2103
2104 impl TryFrom<crate::completion::ToolDefinition> for Tool {
2105 type Error = CompletionError;
2106
2107 fn try_from(tool: crate::completion::ToolDefinition) -> Result<Self, Self::Error> {
2108 Ok(Tool::Function(FunctionTool {
2109 name: Some(tool.name),
2110 description: Some(tool.description),
2111 parameters: Some(tool.parameters),
2112 }))
2113 }
2114 }
2115
2116 impl TryFrom<message::ToolChoice> for ToolChoice {
2117 type Error = CompletionError;
2118
2119 fn try_from(tool_choice: message::ToolChoice) -> Result<Self, Self::Error> {
2120 match tool_choice {
2121 message::ToolChoice::Auto => Ok(ToolChoice::Type(ToolChoiceType::Auto)),
2122 message::ToolChoice::None => Ok(ToolChoice::Type(ToolChoiceType::None)),
2123 message::ToolChoice::Required => Ok(ToolChoice::Type(ToolChoiceType::Any)),
2124 message::ToolChoice::Specific { function_names } => {
2125 Ok(ToolChoice::Config(ToolChoiceConfig {
2126 allowed_tools: AllowedTools {
2127 mode: Some(ToolChoiceType::Validated),
2128 tools: Some(function_names),
2129 },
2130 }))
2131 }
2132 }
2133 }
2134 }
2135
2136 #[derive(Clone, Debug, Deserialize, Serialize)]
2138 #[serde(tag = "type", rename_all = "kebab-case")]
2139 pub enum AgentConfig {
2140 Dynamic,
2141 DeepResearch {
2142 #[serde(skip_serializing_if = "Option::is_none")]
2143 thinking_summaries: Option<ThinkingSummaries>,
2144 },
2145 }
2146
2147 #[derive(Clone, Debug, Deserialize, Serialize)]
2149 #[serde(rename_all = "snake_case")]
2150 pub enum MediaResolution {
2151 Low,
2152 Medium,
2153 High,
2154 UltraHigh,
2155 }
2156
2157 #[derive(Clone, Debug, Deserialize, Serialize)]
2163 #[serde(tag = "event_type")]
2164 pub enum InteractionSseEvent {
2165 #[serde(rename = "interaction.start")]
2166 InteractionStart {
2167 interaction: Interaction,
2168 #[serde(skip_serializing_if = "Option::is_none")]
2169 event_id: Option<String>,
2170 },
2171 #[serde(rename = "interaction.complete")]
2172 InteractionComplete {
2173 interaction: Interaction,
2174 #[serde(skip_serializing_if = "Option::is_none")]
2175 event_id: Option<String>,
2176 },
2177 #[serde(rename = "interaction.status_update")]
2178 InteractionStatusUpdate {
2179 interaction_id: String,
2180 status: InteractionStatus,
2181 #[serde(skip_serializing_if = "Option::is_none")]
2182 event_id: Option<String>,
2183 },
2184 #[serde(rename = "content.start")]
2185 ContentStart {
2186 index: i32,
2187 content: Content,
2188 #[serde(skip_serializing_if = "Option::is_none")]
2189 event_id: Option<String>,
2190 },
2191 #[serde(rename = "content.delta")]
2192 ContentDelta {
2193 index: i32,
2194 delta: ContentDelta,
2195 #[serde(skip_serializing_if = "Option::is_none")]
2196 event_id: Option<String>,
2197 },
2198 #[serde(rename = "content.stop")]
2199 ContentStop {
2200 index: i32,
2201 #[serde(skip_serializing_if = "Option::is_none")]
2202 event_id: Option<String>,
2203 },
2204 #[serde(rename = "error")]
2205 Error {
2206 error: ErrorEvent,
2207 #[serde(skip_serializing_if = "Option::is_none")]
2208 event_id: Option<String>,
2209 },
2210 }
2211
2212 #[derive(Clone, Debug, Deserialize, Serialize)]
2214 pub struct ErrorEvent {
2215 pub code: String,
2216 pub message: String,
2217 }
2218
2219 #[derive(Clone, Debug, Deserialize, Serialize)]
2221 #[serde(tag = "type", rename_all = "snake_case")]
2222 pub enum ContentDelta {
2223 Text(TextDelta),
2224 Image(ImageDelta),
2225 Audio(AudioDelta),
2226 Document(DocumentDelta),
2227 Video(VideoDelta),
2228 ThoughtSummary(ThoughtSummaryDelta),
2229 ThoughtSignature(ThoughtSignatureDelta),
2230 FunctionCall(FunctionCallDelta),
2231 FunctionResult(FunctionResultDelta),
2232 CodeExecutionCall(CodeExecutionCallDelta),
2233 CodeExecutionResult(CodeExecutionResultDelta),
2234 UrlContextCall(UrlContextCallDelta),
2235 UrlContextResult(UrlContextResultDelta),
2236 GoogleSearchCall(GoogleSearchCallDelta),
2237 GoogleSearchResult(GoogleSearchResultDelta),
2238 McpServerToolCall(McpServerToolCallDelta),
2239 McpServerToolResult(McpServerToolResultDelta),
2240 FileSearchResult(FileSearchResultDelta),
2241 }
2242
2243 #[derive(Clone, Debug, Deserialize, Serialize)]
2245 pub struct TextDelta {
2246 #[serde(skip_serializing_if = "Option::is_none")]
2247 pub text: Option<String>,
2248 #[serde(skip_serializing_if = "Option::is_none")]
2249 pub annotations: Option<Vec<Annotation>>,
2250 }
2251
2252 #[derive(Clone, Debug, Deserialize, Serialize)]
2254 pub struct ImageDelta {
2255 #[serde(skip_serializing_if = "Option::is_none")]
2256 pub data: Option<String>,
2257 #[serde(skip_serializing_if = "Option::is_none")]
2258 pub uri: Option<String>,
2259 #[serde(skip_serializing_if = "Option::is_none")]
2260 pub mime_type: Option<String>,
2261 #[serde(skip_serializing_if = "Option::is_none")]
2262 pub resolution: Option<MediaResolution>,
2263 }
2264
2265 #[derive(Clone, Debug, Deserialize, Serialize)]
2267 pub struct AudioDelta {
2268 #[serde(skip_serializing_if = "Option::is_none")]
2269 pub data: Option<String>,
2270 #[serde(skip_serializing_if = "Option::is_none")]
2271 pub uri: Option<String>,
2272 #[serde(skip_serializing_if = "Option::is_none")]
2273 pub mime_type: Option<String>,
2274 }
2275
2276 #[derive(Clone, Debug, Deserialize, Serialize)]
2278 pub struct DocumentDelta {
2279 #[serde(skip_serializing_if = "Option::is_none")]
2280 pub data: Option<String>,
2281 #[serde(skip_serializing_if = "Option::is_none")]
2282 pub uri: Option<String>,
2283 #[serde(skip_serializing_if = "Option::is_none")]
2284 pub mime_type: Option<String>,
2285 }
2286
2287 #[derive(Clone, Debug, Deserialize, Serialize)]
2289 pub struct VideoDelta {
2290 #[serde(skip_serializing_if = "Option::is_none")]
2291 pub data: Option<String>,
2292 #[serde(skip_serializing_if = "Option::is_none")]
2293 pub uri: Option<String>,
2294 #[serde(skip_serializing_if = "Option::is_none")]
2295 pub mime_type: Option<String>,
2296 #[serde(skip_serializing_if = "Option::is_none")]
2297 pub resolution: Option<MediaResolution>,
2298 }
2299
2300 #[derive(Clone, Debug, Deserialize, Serialize)]
2302 pub struct ThoughtSummaryDelta {
2303 pub content: ThoughtSummaryContent,
2304 }
2305
2306 #[derive(Clone, Debug, Deserialize, Serialize)]
2308 pub struct ThoughtSignatureDelta {
2309 pub signature: String,
2310 }
2311
2312 #[derive(Clone, Debug, Deserialize, Serialize)]
2314 pub struct FunctionCallDelta {
2315 #[serde(skip_serializing_if = "Option::is_none")]
2316 pub name: Option<String>,
2317 #[serde(skip_serializing_if = "Option::is_none")]
2318 pub arguments: Option<Value>,
2319 #[serde(skip_serializing_if = "Option::is_none")]
2320 pub id: Option<String>,
2321 }
2322
2323 #[derive(Clone, Debug, Deserialize, Serialize)]
2325 pub struct FunctionResultDelta {
2326 #[serde(skip_serializing_if = "Option::is_none")]
2327 pub name: Option<String>,
2328 #[serde(skip_serializing_if = "Option::is_none")]
2329 pub result: Option<Value>,
2330 #[serde(skip_serializing_if = "Option::is_none")]
2331 pub call_id: Option<String>,
2332 #[serde(skip_serializing_if = "Option::is_none")]
2333 pub is_error: Option<bool>,
2334 }
2335
2336 #[derive(Clone, Debug, Deserialize, Serialize)]
2338 pub struct CodeExecutionCallDelta {
2339 #[serde(skip_serializing_if = "Option::is_none")]
2340 pub arguments: Option<CodeExecutionCallArguments>,
2341 #[serde(skip_serializing_if = "Option::is_none")]
2342 pub id: Option<String>,
2343 }
2344
2345 #[derive(Clone, Debug, Deserialize, Serialize)]
2347 pub struct CodeExecutionResultDelta {
2348 #[serde(skip_serializing_if = "Option::is_none")]
2349 pub result: Option<String>,
2350 #[serde(skip_serializing_if = "Option::is_none")]
2351 pub is_error: Option<bool>,
2352 #[serde(skip_serializing_if = "Option::is_none")]
2353 pub signature: Option<String>,
2354 #[serde(skip_serializing_if = "Option::is_none")]
2355 pub call_id: Option<String>,
2356 }
2357
2358 #[derive(Clone, Debug, Deserialize, Serialize)]
2360 pub struct UrlContextCallDelta {
2361 #[serde(skip_serializing_if = "Option::is_none")]
2362 pub arguments: Option<UrlContextCallArguments>,
2363 #[serde(skip_serializing_if = "Option::is_none")]
2364 pub id: Option<String>,
2365 }
2366
2367 #[derive(Clone, Debug, Deserialize, Serialize)]
2369 pub struct UrlContextResultDelta {
2370 #[serde(skip_serializing_if = "Option::is_none")]
2371 pub result: Option<Vec<UrlContextResult>>,
2372 #[serde(skip_serializing_if = "Option::is_none")]
2373 pub signature: Option<String>,
2374 #[serde(skip_serializing_if = "Option::is_none")]
2375 pub is_error: Option<bool>,
2376 #[serde(skip_serializing_if = "Option::is_none")]
2377 pub call_id: Option<String>,
2378 }
2379
2380 #[derive(Clone, Debug, Deserialize, Serialize)]
2382 pub struct GoogleSearchCallDelta {
2383 #[serde(skip_serializing_if = "Option::is_none")]
2384 pub arguments: Option<GoogleSearchCallArguments>,
2385 #[serde(skip_serializing_if = "Option::is_none")]
2386 pub id: Option<String>,
2387 }
2388
2389 #[derive(Clone, Debug, Deserialize, Serialize)]
2391 pub struct GoogleSearchResultDelta {
2392 #[serde(skip_serializing_if = "Option::is_none")]
2393 pub result: Option<Vec<GoogleSearchResult>>,
2394 #[serde(skip_serializing_if = "Option::is_none")]
2395 pub signature: Option<String>,
2396 #[serde(skip_serializing_if = "Option::is_none")]
2397 pub is_error: Option<bool>,
2398 #[serde(skip_serializing_if = "Option::is_none")]
2399 pub call_id: Option<String>,
2400 }
2401
2402 #[derive(Clone, Debug, Deserialize, Serialize)]
2404 pub struct McpServerToolCallDelta {
2405 #[serde(skip_serializing_if = "Option::is_none")]
2406 pub name: Option<String>,
2407 #[serde(skip_serializing_if = "Option::is_none")]
2408 pub server_name: Option<String>,
2409 #[serde(skip_serializing_if = "Option::is_none")]
2410 pub arguments: Option<Value>,
2411 #[serde(skip_serializing_if = "Option::is_none")]
2412 pub id: Option<String>,
2413 }
2414
2415 #[derive(Clone, Debug, Deserialize, Serialize)]
2417 pub struct McpServerToolResultDelta {
2418 #[serde(skip_serializing_if = "Option::is_none")]
2419 pub name: Option<String>,
2420 #[serde(skip_serializing_if = "Option::is_none")]
2421 pub server_name: Option<String>,
2422 #[serde(skip_serializing_if = "Option::is_none")]
2423 pub result: Option<Value>,
2424 #[serde(skip_serializing_if = "Option::is_none")]
2425 pub call_id: Option<String>,
2426 }
2427
2428 #[derive(Clone, Debug, Deserialize, Serialize)]
2430 pub struct FileSearchResultDelta {
2431 #[serde(skip_serializing_if = "Option::is_none")]
2432 pub result: Option<Vec<FileSearchResult>>,
2433 }
2434}
2435
2436#[cfg(test)]
2437mod tests {
2438 use super::*;
2439 use crate::OneOrMany;
2440 use crate::completion::{CompletionRequest, Message};
2441 use crate::message::{self, ToolChoice as MessageToolChoice};
2442 use serde_json::json;
2443
2444 #[test]
2445 fn test_create_request_body_simple() {
2446 let prompt = Message::User {
2447 content: OneOrMany::one(message::UserContent::text("Hello")),
2448 };
2449
2450 let request = CompletionRequest {
2451 model: None,
2452 preamble: Some("Be precise.".to_string()),
2453 chat_history: OneOrMany::one(prompt),
2454 documents: vec![],
2455 tools: vec![],
2456 temperature: Some(0.7),
2457 max_tokens: Some(128),
2458 tool_choice: Some(MessageToolChoice::Required),
2459 additional_params: None,
2460 output_schema: None,
2461 };
2462
2463 let result = create_request_body("gemini-2.5-flash".to_string(), request, Some(false))
2464 .expect("request should build");
2465
2466 assert_eq!(result.model.as_deref(), Some("gemini-2.5-flash"));
2467 assert!(result.agent.is_none());
2468 assert_eq!(result.stream, Some(false));
2469 assert_eq!(result.system_instruction.as_deref(), Some("Be precise."));
2470
2471 let config = result.generation_config.expect("generation config missing");
2472 assert_eq!(config.temperature, Some(0.7));
2473 assert_eq!(config.max_output_tokens, Some(128));
2474 assert!(matches!(
2475 config.tool_choice,
2476 Some(ToolChoice::Type(ToolChoiceType::Any))
2477 ));
2478
2479 let InteractionInput::Turns(turns) = result.input else {
2480 panic!("expected turns input");
2481 };
2482 assert_eq!(turns.len(), 1);
2483 let turn = &turns[0];
2484 assert!(matches!(turn.role, Role::User));
2485 let TurnContent::Contents(contents) = &turn.content else {
2486 panic!("expected content array");
2487 };
2488 assert_eq!(contents.len(), 1);
2489 match &contents[0] {
2490 Content::Text(TextContent { text, .. }) => assert_eq!(text, "Hello"),
2491 other => panic!("unexpected content: {other:?}"),
2492 }
2493 }
2494
2495 #[test]
2496 fn test_tool_result_requires_call_id() {
2497 let content = message::UserContent::ToolResult(message::ToolResult {
2498 id: "get_weather".to_string(),
2499 call_id: None,
2500 content: OneOrMany::one(message::ToolResultContent::text("ok")),
2501 });
2502
2503 let err = Content::try_from(content).expect_err("should require call_id");
2504 assert!(format!("{err}").contains("call_id"));
2505 }
2506
2507 #[test]
2508 fn test_response_function_call_mapping() {
2509 let interaction = Interaction {
2510 id: "interaction-1".to_string(),
2511 outputs: vec![Content::FunctionCall(FunctionCallContent {
2512 name: Some("get_weather".to_string()),
2513 arguments: Some(json!({"location": "Paris"})),
2514 id: Some("call-123".to_string()),
2515 })],
2516 usage: Some(InteractionUsage {
2517 total_input_tokens: Some(5),
2518 total_output_tokens: Some(7),
2519 total_tokens: Some(12),
2520 }),
2521 ..Default::default()
2522 };
2523
2524 let response: completion::CompletionResponse<Interaction> =
2525 interaction.try_into().expect("conversion should succeed");
2526
2527 let choice = response.choice.first();
2528 match choice {
2529 completion::AssistantContent::ToolCall(tool_call) => {
2530 assert_eq!(tool_call.function.name, "get_weather");
2531 assert_eq!(tool_call.call_id.as_deref(), Some("call-123"));
2532 }
2533 other => panic!("unexpected content: {other:?}"),
2534 }
2535
2536 assert_eq!(response.usage.input_tokens, 5);
2537 assert_eq!(response.usage.output_tokens, 7);
2538 assert_eq!(response.usage.total_tokens, 12);
2539 }
2540
2541 #[test]
2542 fn test_google_search_tool_serialization() {
2543 let tool = Tool::GoogleSearch;
2544 let value = serde_json::to_value(tool).expect("tool should serialize");
2545 assert_eq!(value, json!({ "type": "google_search" }));
2546 }
2547
2548 #[test]
2549 fn test_url_context_tool_serialization() {
2550 let tool = Tool::UrlContext;
2551 let value = serde_json::to_value(tool).expect("tool should serialize");
2552 assert_eq!(value, json!({ "type": "url_context" }));
2553 }
2554
2555 #[test]
2556 fn test_code_execution_tool_serialization() {
2557 let tool = Tool::CodeExecution;
2558 let value = serde_json::to_value(tool).expect("tool should serialize");
2559 assert_eq!(value, json!({ "type": "code_execution" }));
2560 }
2561
2562 #[test]
2563 fn test_google_search_helpers() {
2564 let interaction = Interaction {
2565 outputs: vec![
2566 Content::GoogleSearchCall(GoogleSearchCallContent {
2567 arguments: Some(GoogleSearchCallArguments {
2568 queries: Some(vec!["query-one".to_string(), "query-two".to_string()]),
2569 }),
2570 id: Some("call-1".to_string()),
2571 }),
2572 Content::GoogleSearchResult(GoogleSearchResultContent {
2573 result: Some(vec![GoogleSearchResult {
2574 url: Some("https://example.com".to_string()),
2575 title: Some("Example One".to_string()),
2576 rendered_content: None,
2577 }]),
2578 signature: None,
2579 is_error: None,
2580 call_id: Some("call-1".to_string()),
2581 }),
2582 Content::GoogleSearchCall(GoogleSearchCallContent {
2583 arguments: Some(GoogleSearchCallArguments {
2584 queries: Some(vec!["query-three".to_string()]),
2585 }),
2586 id: Some("call-2".to_string()),
2587 }),
2588 Content::GoogleSearchResult(GoogleSearchResultContent {
2589 result: Some(vec![GoogleSearchResult {
2590 url: Some("https://example.org".to_string()),
2591 title: Some("Example Two".to_string()),
2592 rendered_content: None,
2593 }]),
2594 signature: None,
2595 is_error: None,
2596 call_id: Some("call-2".to_string()),
2597 }),
2598 ],
2599 ..Default::default()
2600 };
2601
2602 let exchanges = interaction.google_search_exchanges();
2603 assert_eq!(exchanges.len(), 2);
2604 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2605 assert_eq!(
2606 exchanges[0].queries(),
2607 vec!["query-one".to_string(), "query-two".to_string()]
2608 );
2609 let exchange_results = exchanges[0].result_items();
2610 assert_eq!(exchange_results.len(), 1);
2611 assert_eq!(exchange_results[0].title.as_deref(), Some("Example One"));
2612
2613 assert_eq!(exchanges[1].call_id.as_deref(), Some("call-2"));
2614 assert_eq!(exchanges[1].queries(), vec!["query-three".to_string()]);
2615 let exchange_results = exchanges[1].result_items();
2616 assert_eq!(exchange_results.len(), 1);
2617 assert_eq!(exchange_results[0].title.as_deref(), Some("Example Two"));
2618
2619 let queries = interaction.google_search_queries();
2620 assert_eq!(queries, vec!["query-one", "query-two", "query-three"]);
2621
2622 let results = interaction.google_search_results();
2623 assert_eq!(results.len(), 2);
2624 assert_eq!(results[0].title.as_deref(), Some("Example One"));
2625 assert_eq!(results[1].title.as_deref(), Some("Example Two"));
2626
2627 let call_contents = interaction.google_search_call_contents();
2628 assert_eq!(call_contents.len(), 2);
2629 assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
2630 assert_eq!(call_contents[1].id.as_deref(), Some("call-2"));
2631
2632 let result_contents = interaction.google_search_result_contents();
2633 assert_eq!(result_contents.len(), 2);
2634 assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
2635 assert_eq!(result_contents[1].call_id.as_deref(), Some("call-2"));
2636 }
2637
2638 #[test]
2639 fn test_google_search_helpers_without_call_id() {
2640 let interaction = Interaction {
2641 outputs: vec![
2642 Content::GoogleSearchCall(GoogleSearchCallContent {
2643 arguments: Some(GoogleSearchCallArguments {
2644 queries: Some(vec!["query-one".to_string()]),
2645 }),
2646 id: None,
2647 }),
2648 Content::GoogleSearchResult(GoogleSearchResultContent {
2649 result: Some(vec![GoogleSearchResult {
2650 url: Some("https://example.com".to_string()),
2651 title: Some("Example One".to_string()),
2652 rendered_content: None,
2653 }]),
2654 signature: None,
2655 is_error: None,
2656 call_id: None,
2657 }),
2658 Content::GoogleSearchCall(GoogleSearchCallContent {
2659 arguments: Some(GoogleSearchCallArguments {
2660 queries: Some(vec!["query-two".to_string()]),
2661 }),
2662 id: Some("call-2".to_string()),
2663 }),
2664 Content::GoogleSearchResult(GoogleSearchResultContent {
2665 result: Some(vec![GoogleSearchResult {
2666 url: Some("https://example.org".to_string()),
2667 title: Some("Example Two".to_string()),
2668 rendered_content: None,
2669 }]),
2670 signature: None,
2671 is_error: None,
2672 call_id: None,
2673 }),
2674 ],
2675 ..Default::default()
2676 };
2677
2678 let exchanges = interaction.google_search_exchanges();
2679 assert_eq!(exchanges.len(), 2);
2680
2681 let no_id = exchanges
2682 .iter()
2683 .find(|exchange| exchange.call_id.is_none())
2684 .expect("expected no-id exchange");
2685 assert_eq!(no_id.calls.len(), 1);
2686 assert_eq!(no_id.results.len(), 1);
2687
2688 let with_id = exchanges
2689 .iter()
2690 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2691 .expect("expected call-2 exchange");
2692 assert_eq!(with_id.calls.len(), 1);
2693 assert_eq!(with_id.results.len(), 1);
2694 }
2695
2696 #[test]
2697 fn test_url_context_helpers() {
2698 let interaction = Interaction {
2699 outputs: vec![
2700 Content::UrlContextCall(UrlContextCallContent {
2701 arguments: Some(UrlContextCallArguments {
2702 urls: Some(vec![
2703 "https://example.com".to_string(),
2704 "https://example.org".to_string(),
2705 ]),
2706 }),
2707 id: Some("call-1".to_string()),
2708 }),
2709 Content::UrlContextResult(UrlContextResultContent {
2710 result: Some(vec![UrlContextResult {
2711 url: Some("https://example.com".to_string()),
2712 status: Some("success".to_string()),
2713 }]),
2714 signature: None,
2715 is_error: None,
2716 call_id: Some("call-1".to_string()),
2717 }),
2718 ],
2719 ..Default::default()
2720 };
2721
2722 let exchanges = interaction.url_context_exchanges();
2723 assert_eq!(exchanges.len(), 1);
2724 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2725 assert_eq!(
2726 exchanges[0].urls(),
2727 vec!["https://example.com", "https://example.org"]
2728 );
2729 let results = exchanges[0].result_items();
2730 assert_eq!(results.len(), 1);
2731 assert_eq!(results[0].status.as_deref(), Some("success"));
2732
2733 let urls = interaction.url_context_urls();
2734 assert_eq!(urls, vec!["https://example.com", "https://example.org"]);
2735
2736 let results = interaction.url_context_results();
2737 assert_eq!(results.len(), 1);
2738 assert_eq!(results[0].url.as_deref(), Some("https://example.com"));
2739
2740 let call_contents = interaction.url_context_call_contents();
2741 assert_eq!(call_contents.len(), 1);
2742 assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
2743
2744 let result_contents = interaction.url_context_result_contents();
2745 assert_eq!(result_contents.len(), 1);
2746 assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
2747 }
2748
2749 #[test]
2750 fn test_url_context_helpers_without_call_id() {
2751 let interaction = Interaction {
2752 outputs: vec![
2753 Content::UrlContextCall(UrlContextCallContent {
2754 arguments: Some(UrlContextCallArguments {
2755 urls: Some(vec!["https://example.com".to_string()]),
2756 }),
2757 id: None,
2758 }),
2759 Content::UrlContextResult(UrlContextResultContent {
2760 result: Some(vec![UrlContextResult {
2761 url: Some("https://example.com".to_string()),
2762 status: Some("success".to_string()),
2763 }]),
2764 signature: None,
2765 is_error: None,
2766 call_id: None,
2767 }),
2768 Content::UrlContextCall(UrlContextCallContent {
2769 arguments: Some(UrlContextCallArguments {
2770 urls: Some(vec!["https://example.org".to_string()]),
2771 }),
2772 id: Some("call-2".to_string()),
2773 }),
2774 Content::UrlContextResult(UrlContextResultContent {
2775 result: Some(vec![UrlContextResult {
2776 url: Some("https://example.org".to_string()),
2777 status: Some("success".to_string()),
2778 }]),
2779 signature: None,
2780 is_error: None,
2781 call_id: None,
2782 }),
2783 ],
2784 ..Default::default()
2785 };
2786
2787 let exchanges = interaction.url_context_exchanges();
2788 assert_eq!(exchanges.len(), 2);
2789
2790 let no_id = exchanges
2791 .iter()
2792 .find(|exchange| exchange.call_id.is_none())
2793 .expect("expected no-id exchange");
2794 assert_eq!(no_id.calls.len(), 1);
2795 assert_eq!(no_id.results.len(), 1);
2796
2797 let with_id = exchanges
2798 .iter()
2799 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2800 .expect("expected call-2 exchange");
2801 assert_eq!(with_id.calls.len(), 1);
2802 assert_eq!(with_id.results.len(), 1);
2803 }
2804
2805 #[test]
2806 fn test_code_execution_helpers() {
2807 let interaction = Interaction {
2808 outputs: vec![
2809 Content::CodeExecutionCall(CodeExecutionCallContent {
2810 arguments: Some(CodeExecutionCallArguments {
2811 language: Some("python".to_string()),
2812 code: Some("print(2 + 2)".to_string()),
2813 }),
2814 id: Some("call-1".to_string()),
2815 }),
2816 Content::CodeExecutionResult(CodeExecutionResultContent {
2817 result: Some("4\n".to_string()),
2818 signature: None,
2819 is_error: None,
2820 call_id: Some("call-1".to_string()),
2821 }),
2822 ],
2823 ..Default::default()
2824 };
2825
2826 let exchanges = interaction.code_execution_exchanges();
2827 assert_eq!(exchanges.len(), 1);
2828 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2829 assert_eq!(exchanges[0].code_snippets(), vec!["print(2 + 2)"]);
2830 assert_eq!(exchanges[0].outputs(), vec!["4\n"]);
2831
2832 let calls = interaction.code_execution_call_contents();
2833 assert_eq!(calls.len(), 1);
2834 assert_eq!(calls[0].id.as_deref(), Some("call-1"));
2835
2836 let results = interaction.code_execution_result_contents();
2837 assert_eq!(results.len(), 1);
2838 assert_eq!(results[0].call_id.as_deref(), Some("call-1"));
2839
2840 let snippets = interaction.code_execution_snippets();
2841 assert_eq!(snippets, vec!["print(2 + 2)"]);
2842
2843 let outputs = interaction.code_execution_outputs();
2844 assert_eq!(outputs, vec!["4\n"]);
2845 }
2846
2847 #[test]
2848 fn test_code_execution_helpers_without_call_id() {
2849 let interaction = Interaction {
2850 outputs: vec![
2851 Content::CodeExecutionCall(CodeExecutionCallContent {
2852 arguments: Some(CodeExecutionCallArguments {
2853 language: Some("python".to_string()),
2854 code: Some("print(1 + 1)".to_string()),
2855 }),
2856 id: None,
2857 }),
2858 Content::CodeExecutionResult(CodeExecutionResultContent {
2859 result: Some("2\n".to_string()),
2860 signature: None,
2861 is_error: None,
2862 call_id: None,
2863 }),
2864 Content::CodeExecutionCall(CodeExecutionCallContent {
2865 arguments: Some(CodeExecutionCallArguments {
2866 language: Some("python".to_string()),
2867 code: Some("print(2 + 2)".to_string()),
2868 }),
2869 id: Some("call-2".to_string()),
2870 }),
2871 Content::CodeExecutionResult(CodeExecutionResultContent {
2872 result: Some("4\n".to_string()),
2873 signature: None,
2874 is_error: None,
2875 call_id: None,
2876 }),
2877 ],
2878 ..Default::default()
2879 };
2880
2881 let exchanges = interaction.code_execution_exchanges();
2882 assert_eq!(exchanges.len(), 2);
2883
2884 let no_id = exchanges
2885 .iter()
2886 .find(|exchange| exchange.call_id.is_none())
2887 .expect("expected no-id exchange");
2888 assert_eq!(no_id.calls.len(), 1);
2889 assert_eq!(no_id.results.len(), 1);
2890
2891 let with_id = exchanges
2892 .iter()
2893 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2894 .expect("expected call-2 exchange");
2895 assert_eq!(with_id.calls.len(), 1);
2896 assert_eq!(with_id.results.len(), 1);
2897 }
2898
2899 #[test]
2900 fn test_interaction_status_helpers() {
2901 let mut interaction = Interaction {
2902 status: Some(InteractionStatus::InProgress),
2903 ..Default::default()
2904 };
2905 assert!(!interaction.is_terminal());
2906 assert!(!interaction.is_completed());
2907
2908 interaction.status = Some(InteractionStatus::Completed);
2909 assert!(interaction.is_terminal());
2910 assert!(interaction.is_completed());
2911
2912 interaction.status = Some(InteractionStatus::Failed);
2913 assert!(interaction.is_terminal());
2914 assert!(!interaction.is_completed());
2915 }
2916
2917 #[test]
2918 fn test_build_interaction_stream_path() {
2919 let path = build_interaction_stream_path("interaction-123", None);
2920 assert_eq!(path, "/v1beta/interactions/interaction-123?stream=true");
2921
2922 let path = build_interaction_stream_path("interaction-123", Some("event-456"));
2923 assert_eq!(
2924 path,
2925 "/v1beta/interactions/interaction-123?stream=true&last_event_id=event-456"
2926 );
2927 }
2928
2929 #[test]
2930 fn test_inline_citations_from_annotations() {
2931 let text_content = TextContent {
2932 text: "Hello world".to_string(),
2933 annotations: Some(vec![
2934 Annotation {
2935 start_index: Some(6),
2936 end_index: Some(11),
2937 source: Some("https://example.com".to_string()),
2938 },
2939 Annotation {
2940 start_index: Some(0),
2941 end_index: Some(5),
2942 source: Some("https://hello.example".to_string()),
2943 },
2944 ]),
2945 };
2946
2947 let cited = text_content.with_inline_citations();
2948 assert_eq!(
2949 cited,
2950 "Hello[1](https://hello.example) world[2](https://example.com)"
2951 );
2952
2953 let interaction = Interaction {
2954 outputs: vec![Content::Text(text_content)],
2955 ..Default::default()
2956 };
2957
2958 let cited_text = interaction.text_with_inline_citations();
2959 assert_eq!(
2960 cited_text.as_deref(),
2961 Some("Hello[1](https://hello.example) world[2](https://example.com)")
2962 );
2963 }
2964}