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