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