1use base64::{Engine, prelude::BASE64_STANDARD};
5
6use crate::OneOrMany;
7use crate::completion::{self, CompletionError, CompletionRequest, GetTokenUsage};
8use crate::http_client::HttpClientExt;
9use crate::message::{self, MimeType, Reasoning};
10use crate::telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator};
11use serde_json::{Map, Value};
12use tracing::{Level, enabled};
13use tracing_futures::Instrument;
14use url::form_urlencoded;
15
16use super::client::InteractionsClient;
17
18pub mod streaming;
20pub use interactions_api_types::*;
21
22#[derive(Clone, Debug)]
28pub struct InteractionsCompletionModel<T = reqwest::Client> {
29 pub(crate) client: InteractionsClient<T>,
30 pub model: String,
31}
32
33impl<T> InteractionsCompletionModel<T> {
34 pub fn new(client: InteractionsClient<T>, model: impl Into<String>) -> Self {
36 Self {
37 client,
38 model: model.into(),
39 }
40 }
41
42 pub fn with_model(client: InteractionsClient<T>, model: &str) -> Self {
44 Self {
45 client,
46 model: model.to_string(),
47 }
48 }
49
50 pub fn generate_content_api(self) -> super::completion::CompletionModel<T> {
52 super::completion::CompletionModel::with_model(
53 self.client.generate_content_api(),
54 &self.model,
55 )
56 }
57
58 pub(crate) fn create_completion_request(
59 &self,
60 completion_request: CompletionRequest,
61 stream_override: Option<bool>,
62 ) -> Result<CreateInteractionRequest, CompletionError> {
63 create_request_body(self.model.clone(), completion_request, stream_override)
64 }
65}
66
67impl<T> InteractionsCompletionModel<T>
68where
69 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
70{
71 pub async fn create_interaction(
73 &self,
74 completion_request: CompletionRequest,
75 ) -> Result<Interaction, CompletionError> {
76 let request = self.create_completion_request(completion_request, Some(false))?;
77 self.client.create_interaction(request).await
78 }
79
80 pub async fn get_interaction(
82 &self,
83 interaction_id: impl AsRef<str>,
84 ) -> Result<Interaction, CompletionError> {
85 self.client.get_interaction(interaction_id).await
86 }
87
88 pub async fn stream_interaction_events(
90 &self,
91 completion_request: CompletionRequest,
92 ) -> Result<streaming::InteractionEventStream, CompletionError> {
93 let request = self.create_completion_request(completion_request, Some(true))?;
94 self.client.stream_interaction_events(request).await
95 }
96
97 pub async fn stream_interaction_events_by_id(
99 &self,
100 interaction_id: impl AsRef<str>,
101 last_event_id: Option<&str>,
102 ) -> Result<streaming::InteractionEventStream, CompletionError> {
103 self.client
104 .stream_interaction_events_by_id(interaction_id, last_event_id)
105 .await
106 }
107}
108
109impl<T> completion::CompletionModel for InteractionsCompletionModel<T>
110where
111 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
112{
113 type Response = Interaction;
114 type StreamingResponse = streaming::StreamingCompletionResponse;
115 type Client = InteractionsClient<T>;
116
117 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
118 Self::new(client.clone(), model)
119 }
120
121 async fn completion(
122 &self,
123 completion_request: CompletionRequest,
124 ) -> Result<completion::CompletionResponse<Interaction>, CompletionError> {
125 let span = CompletionSpanBuilder::new(
126 "gcp.gemini",
127 &self.model,
128 CompletionOperation::Interactions,
129 )
130 .system_instructions(
131 completion_request.preamble.as_deref(),
132 completion_request.record_telemetry_content,
133 )
134 .build();
135
136 let request = self.create_completion_request(completion_request, Some(false))?;
137
138 if enabled!(Level::TRACE) {
139 tracing::trace!(
140 target: "rig::completions",
141 "Gemini interactions completion request: {}",
142 serde_json::to_string_pretty(&request)?
143 );
144 }
145
146 let body = serde_json::to_vec(&request)?;
147 let request = self
148 .client
149 .post("/v1beta/interactions")?
150 .body(body)
151 .map_err(|e| CompletionError::HttpError(e.into()))?;
152
153 async move {
154 let response = self.client.send::<_, Vec<u8>>(request).await?;
155
156 if response.status().is_success() {
157 let response_body = response
158 .into_body()
159 .await
160 .map_err(CompletionError::HttpError)?;
161
162 let response_text = String::from_utf8_lossy(&response_body).to_string();
163
164 let response: Interaction =
165 serde_json::from_slice(&response_body).map_err(|err| {
166 tracing::error!(
167 error = %err,
168 body = %response_text,
169 "Failed to deserialize Gemini interactions response"
170 );
171 CompletionError::JsonError(err)
172 })?;
173
174 let span = tracing::Span::current();
175 span.record_response_metadata(&response);
176 span.record_token_usage(&response);
177
178 if enabled!(Level::TRACE) {
179 tracing::trace!(
180 target: "rig::completions",
181 "Gemini interactions completion response: {}",
182 serde_json::to_string_pretty(&response)?
183 );
184 }
185
186 response.try_into()
187 } else {
188 let status = response.status();
189 let body = response
190 .into_body()
191 .await
192 .map_err(CompletionError::HttpError)?;
193
194 Err(CompletionError::from_http_response(
195 status,
196 String::from_utf8_lossy(&body),
197 ))
198 }
199 }
200 .instrument(span)
201 .await
202 }
203
204 async fn stream(
205 &self,
206 request: CompletionRequest,
207 ) -> Result<
208 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
209 CompletionError,
210 > {
211 InteractionsCompletionModel::stream(self, request).await
212 }
213}
214
215impl<T> InteractionsClient<T>
216where
217 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
218{
219 pub async fn create_interaction(
221 &self,
222 request: CreateInteractionRequest,
223 ) -> Result<Interaction, CompletionError> {
224 if request.stream == Some(true) {
225 return Err(CompletionError::RequestError(Box::new(
226 std::io::Error::new(
227 std::io::ErrorKind::InvalidInput,
228 "stream=true requires stream_interaction_events",
229 ),
230 )));
231 }
232
233 let body = serde_json::to_vec(&request)?;
234 let request = self
235 .post("/v1beta/interactions")?
236 .body(body)
237 .map_err(|e| CompletionError::HttpError(e.into()))?;
238
239 send_interaction_request(self, request).await
240 }
241
242 pub async fn get_interaction(
244 &self,
245 interaction_id: impl AsRef<str>,
246 ) -> Result<Interaction, CompletionError> {
247 let path = format!("/v1beta/interactions/{}", interaction_id.as_ref());
248 let request = self
249 .get(path)?
250 .body(Vec::new())
251 .map_err(|e| CompletionError::HttpError(e.into()))?;
252
253 send_interaction_request(self, request).await
254 }
255
256 pub async fn stream_interaction_events(
258 &self,
259 mut request: CreateInteractionRequest,
260 ) -> Result<streaming::InteractionEventStream, CompletionError> {
261 request.stream = Some(true);
262 let body = serde_json::to_vec(&request)?;
263 let request = self
264 .post_sse("/v1beta/interactions")?
265 .header("Content-Type", "application/json")
266 .body(body)
267 .map_err(|e| CompletionError::HttpError(e.into()))?;
268
269 Ok(streaming::stream_interaction_events(self.clone(), request))
270 }
271
272 pub async fn stream_interaction_events_by_id(
274 &self,
275 interaction_id: impl AsRef<str>,
276 last_event_id: Option<&str>,
277 ) -> Result<streaming::InteractionEventStream, CompletionError> {
278 let path = build_interaction_stream_path(interaction_id.as_ref(), last_event_id);
279 let request = self
280 .get_sse(path)?
281 .body(Vec::new())
282 .map_err(|e| CompletionError::HttpError(e.into()))?;
283
284 Ok(streaming::stream_interaction_events(self.clone(), request))
285 }
286}
287
288pub(crate) fn create_request_body(
289 model: String,
290 completion_request: CompletionRequest,
291 stream_override: Option<bool>,
292) -> Result<CreateInteractionRequest, CompletionError> {
293 let chat_history = completion_request.chat_history_with_documents();
294
295 let mut history = Vec::new();
296 history.extend(chat_history);
297 let (history_system, history) = split_system_messages_from_history(history);
298
299 let steps = history
300 .into_iter()
301 .map(Step::try_from)
302 .collect::<Result<Vec<_>, _>>()
303 .map_err(|err| CompletionError::RequestError(Box::new(err)))?;
304
305 let input = InteractionInput::Steps(steps);
306
307 let raw_params = completion_request
308 .additional_params
309 .unwrap_or_else(|| Value::Object(Map::new()));
310
311 let mut params: AdditionalParameters = serde_json::from_value(raw_params)?;
312
313 let mut generation_config = params.generation_config.take().unwrap_or_default();
314 if let Some(temp) = completion_request.temperature {
315 generation_config.temperature = Some(temp);
316 }
317 if let Some(max_tokens) = completion_request.max_tokens {
318 generation_config.max_output_tokens = Some(max_tokens);
319 }
320 if let Some(tool_choice) = completion_request.tool_choice {
321 generation_config.tool_choice = Some(tool_choice.try_into()?);
322 }
323 let generation_config = if generation_config.is_empty() {
324 None
325 } else {
326 Some(generation_config)
327 };
328
329 let system_instruction = completion_request
330 .preamble
331 .or_else(|| {
332 if history_system.is_empty() {
333 None
334 } else {
335 Some(history_system.join("\n\n"))
336 }
337 })
338 .or(params.system_instruction.take());
339
340 let mut tools = Vec::new();
341 if !completion_request.tools.is_empty() {
342 tools.extend(
343 completion_request
344 .tools
345 .into_iter()
346 .map(Tool::try_from)
347 .collect::<Result<Vec<_>, _>>()?,
348 );
349 }
350 if let Some(mut extra_tools) = params.tools.take() {
351 tools.append(&mut extra_tools);
352 }
353 let tools = if tools.is_empty() { None } else { Some(tools) };
354
355 let stream = stream_override.or(params.stream.take());
356
357 let (agent, agent_config) = if params.agent.is_some() {
358 (params.agent.take(), params.agent_config.take())
359 } else {
360 (None, None)
361 };
362
363 let response_format = params.response_format.take();
364 let response_mime_type = params.response_mime_type.take();
365
366 if response_format.is_some() && response_mime_type.is_none() {
367 return Err(CompletionError::RequestError(Box::new(
368 std::io::Error::new(
369 std::io::ErrorKind::InvalidInput,
370 "response_mime_type is required when response_format is set",
371 ),
372 )));
373 }
374
375 Ok(CreateInteractionRequest {
376 model: if agent.is_some() { None } else { Some(model) },
377 agent,
378 input,
379 system_instruction,
380 tools,
381 response_format,
382 response_mime_type,
383 stream,
384 store: params.store.take(),
385 background: params.background.take(),
386 generation_config,
387 agent_config,
388 response_modalities: params.response_modalities.take(),
389 previous_interaction_id: params.previous_interaction_id.take(),
390 additional_params: params.additional_params.take(),
391 })
392}
393
394fn split_system_messages_from_history(
395 history: Vec<completion::Message>,
396) -> (Vec<String>, Vec<completion::Message>) {
397 let mut system = Vec::new();
398 let mut remaining = Vec::new();
399
400 for message in history {
401 match message {
402 completion::Message::System { content } => system.push(content),
403 other => remaining.push(other),
404 }
405 }
406
407 (system, remaining)
408}
409
410async fn send_interaction_request<T>(
411 client: &InteractionsClient<T>,
412 request: crate::http_client::Request<Vec<u8>>,
413) -> Result<Interaction, CompletionError>
414where
415 T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
416{
417 let response = client.send::<_, Vec<u8>>(request).await?;
418
419 if response.status().is_success() {
420 let response_body = response
421 .into_body()
422 .await
423 .map_err(CompletionError::HttpError)?;
424
425 let response_text = String::from_utf8_lossy(&response_body).to_string();
426
427 let response: Interaction = serde_json::from_slice(&response_body).map_err(|err| {
428 tracing::error!(
429 error = %err,
430 body = %response_text,
431 "Failed to deserialize Gemini interactions response"
432 );
433 CompletionError::JsonError(err)
434 })?;
435
436 Ok(response)
437 } else {
438 let status = response.status();
439 let body = response
440 .into_body()
441 .await
442 .map_err(CompletionError::HttpError)?;
443
444 Err(CompletionError::from_http_response(
445 status,
446 String::from_utf8_lossy(&body),
447 ))
448 }
449}
450
451fn build_interaction_stream_path(interaction_id: &str, last_event_id: Option<&str>) -> String {
452 let mut serializer = form_urlencoded::Serializer::new(String::new());
453 serializer.append_pair("stream", "true");
454 if let Some(last_event_id) = last_event_id {
455 serializer.append_pair("last_event_id", last_event_id);
456 }
457 format!(
458 "/v1beta/interactions/{}?{}",
459 interaction_id,
460 serializer.finish()
461 )
462}
463
464impl TryFrom<Interaction> for completion::CompletionResponse<Interaction> {
465 type Error = CompletionError;
466
467 fn try_from(response: Interaction) -> Result<Self, Self::Error> {
468 let output_contents = response.output_contents();
469 if output_contents.is_empty() {
470 let message = match response.status.as_ref() {
471 Some(InteractionStatus::InProgress) => {
472 "Interaction contained no outputs yet (status: InProgress). Use get_interaction for background tasks.".to_string()
473 }
474 Some(status) => format!("Interaction contained no outputs (status: {status:?})."),
475 None => "Interaction contained no outputs".to_string(),
476 };
477 return Err(CompletionError::ResponseError(message));
478 }
479
480 let content = output_contents
481 .into_iter()
482 .filter_map(|output| match assistant_content_from_output(output) {
483 Ok(Some(content)) => Some(Ok(content)),
484 Ok(None) => None,
485 Err(err) => Some(Err(err)),
486 })
487 .collect::<Result<Vec<_>, _>>()?;
488
489 let choice = OneOrMany::many(content).map_err(|_| {
490 CompletionError::ResponseError(
491 "Response contained no message or tool call (empty)".to_owned(),
492 )
493 })?;
494
495 let usage = response
496 .usage
497 .as_ref()
498 .map(|usage| usage.token_usage())
499 .unwrap_or_default();
500
501 Ok(completion::CompletionResponse {
502 choice,
503 usage,
504 raw_response: response,
505 message_id: None,
506 })
507 }
508}
509
510fn assistant_content_from_output(
511 output: Content,
512) -> Result<Option<completion::AssistantContent>, CompletionError> {
513 match output {
514 Content::Text(TextContent { text, .. }) => {
515 Ok(Some(completion::AssistantContent::text(text)))
516 }
517 Content::FunctionCall(FunctionCallContent {
518 name,
519 arguments,
520 id,
521 ..
522 }) => {
523 let Some(name) = name else {
524 return Ok(None);
525 };
526 let call_id = id.unwrap_or_else(|| name.clone());
527 Ok(Some(completion::AssistantContent::tool_call_with_call_id(
528 name.clone(),
529 call_id,
530 name,
531 arguments.unwrap_or(Value::Object(Map::new())),
532 )))
533 }
534 Content::Thought(ThoughtContent {
535 summary, signature, ..
536 }) => {
537 let mut reasoning_content = summary
538 .unwrap_or_default()
539 .into_iter()
540 .filter_map(|content| match content {
541 ThoughtSummaryContent::Text(text) => Some(message::ReasoningContent::Text {
542 text: text.text,
543 signature: None,
544 }),
545 _ => None,
546 })
547 .collect::<Vec<_>>();
548
549 if reasoning_content.is_empty() {
550 return Ok(None);
551 }
552
553 if let Some(signature) = signature
554 && let Some(message::ReasoningContent::Text {
555 signature: first_signature,
556 ..
557 }) = reasoning_content
558 .iter_mut()
559 .find(|content| matches!(content, message::ReasoningContent::Text { .. }))
560 {
561 *first_signature = Some(signature);
562 }
563
564 Ok(Some(completion::AssistantContent::Reasoning(Reasoning {
565 id: None,
566 content: reasoning_content,
567 })))
568 }
569 Content::Image(ImageContent {
570 data,
571 uri,
572 mime_type,
573 ..
574 }) => {
575 let Some(mime_type) = mime_type else {
576 return Err(CompletionError::ResponseError(
577 "Image output missing mime_type".to_owned(),
578 ));
579 };
580
581 let media_type =
582 message::ImageMediaType::from_mime_type(&mime_type).ok_or_else(|| {
583 CompletionError::ResponseError(format!(
584 "Unsupported image output mime type {mime_type}"
585 ))
586 })?;
587
588 let image = if let Some(data) = data {
589 message::AssistantContent::image_base64(
590 data,
591 Some(media_type),
592 Some(message::ImageDetail::default()),
593 )
594 } else if let Some(uri) = uri {
595 completion::AssistantContent::Image(message::Image {
596 data: message::DocumentSourceKind::Url(uri),
597 media_type: Some(media_type),
598 detail: Some(message::ImageDetail::default()),
599 additional_params: None,
600 })
601 } else {
602 return Err(CompletionError::ResponseError(
603 "Image output missing data or uri".to_owned(),
604 ));
605 };
606
607 Ok(Some(image))
608 }
609 _ => Ok(None),
610 }
611}
612
613fn split_data_uri(
614 src: message::DocumentSourceKind,
615) -> Result<(Option<String>, Option<String>), message::MessageError> {
616 match src {
617 message::DocumentSourceKind::Url(uri) => Ok((None, Some(uri))),
618 message::DocumentSourceKind::Base64(data) => Ok((Some(data), None)),
619 message::DocumentSourceKind::String(data) => {
620 Ok((Some(BASE64_STANDARD.encode(data.as_bytes())), None))
621 }
622 message::DocumentSourceKind::Raw(data) => Ok((Some(BASE64_STANDARD.encode(data)), None)),
623 message::DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
624 "Provider file IDs are not supported for Gemini Interactions inputs".to_string(),
625 )),
626 message::DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
627 "Unknown content source".to_string(),
628 )),
629 }
630}
631
632pub mod interactions_api_types {
634 use super::split_data_uri;
635 use crate::completion::{CompletionError, GetTokenUsage, Usage};
636 use crate::message::{self, MimeType};
637 use crate::telemetry::ProviderResponseExt;
638 use base64::{Engine, prelude::BASE64_STANDARD};
639 use serde::{Deserialize, Serialize};
640 use serde_json::Value;
641
642 #[derive(Debug, Deserialize, Serialize, Default, Clone)]
648 #[serde(rename_all = "snake_case")]
649 pub struct AdditionalParameters {
650 pub agent: Option<String>,
651 pub agent_config: Option<AgentConfig>,
652 pub background: Option<bool>,
653 pub generation_config: Option<GenerationConfig>,
654 pub previous_interaction_id: Option<String>,
655 pub response_modalities: Option<Vec<ResponseModality>>,
656 pub response_format: Option<Value>,
657 pub response_mime_type: Option<String>,
658 pub store: Option<bool>,
659 pub stream: Option<bool>,
660 pub system_instruction: Option<String>,
661 pub tools: Option<Vec<Tool>>,
662 #[serde(flatten, skip_serializing_if = "Option::is_none")]
663 pub additional_params: Option<Value>,
664 }
665
666 #[derive(Debug, Deserialize, Serialize, Clone)]
668 #[serde(rename_all = "snake_case")]
669 pub struct CreateInteractionRequest {
670 #[serde(skip_serializing_if = "Option::is_none")]
671 pub model: Option<String>,
672 #[serde(skip_serializing_if = "Option::is_none")]
673 pub agent: Option<String>,
674 pub input: InteractionInput,
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub system_instruction: Option<String>,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub tools: Option<Vec<Tool>>,
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub response_format: Option<Value>,
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub response_mime_type: Option<String>,
683 #[serde(skip_serializing_if = "Option::is_none")]
684 pub stream: Option<bool>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub store: Option<bool>,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub background: Option<bool>,
689 #[serde(skip_serializing_if = "Option::is_none")]
690 pub generation_config: Option<GenerationConfig>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 pub agent_config: Option<AgentConfig>,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 pub response_modalities: Option<Vec<ResponseModality>>,
695 #[serde(skip_serializing_if = "Option::is_none")]
696 pub previous_interaction_id: Option<String>,
697 #[serde(flatten, skip_serializing_if = "Option::is_none")]
698 pub additional_params: Option<Value>,
699 }
700
701 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
703 #[serde(rename_all = "snake_case")]
704 pub struct Interaction {
705 #[serde(default)]
706 pub id: String,
707 #[serde(skip_serializing_if = "Option::is_none")]
708 pub model: Option<String>,
709 #[serde(skip_serializing_if = "Option::is_none")]
710 pub agent: Option<String>,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 pub status: Option<InteractionStatus>,
713 #[serde(skip_serializing_if = "Option::is_none")]
714 pub object: Option<String>,
715 #[serde(skip_serializing_if = "Option::is_none")]
716 pub created: Option<String>,
717 #[serde(skip_serializing_if = "Option::is_none")]
718 pub updated: Option<String>,
719 #[serde(skip_serializing_if = "Option::is_none")]
720 pub role: Option<String>,
721 #[serde(default)]
722 pub steps: Vec<Step>,
723 #[serde(skip_serializing_if = "Option::is_none")]
724 pub usage: Option<InteractionUsage>,
725 #[serde(skip_serializing_if = "Option::is_none")]
726 pub system_instruction: Option<String>,
727 #[serde(skip_serializing_if = "Option::is_none")]
728 pub tools: Option<Vec<Tool>>,
729 #[serde(skip_serializing_if = "Option::is_none")]
730 pub background: Option<bool>,
731 #[serde(skip_serializing_if = "Option::is_none")]
732 pub response_modalities: Option<Vec<ResponseModality>>,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub response_format: Option<Value>,
735 #[serde(skip_serializing_if = "Option::is_none")]
736 pub response_mime_type: Option<String>,
737 #[serde(skip_serializing_if = "Option::is_none")]
738 pub previous_interaction_id: Option<String>,
739 #[serde(skip_serializing_if = "Option::is_none")]
740 pub input: Option<InteractionInput>,
741 }
742
743 impl GetTokenUsage for Interaction {
744 fn token_usage(&self) -> Usage {
745 self.usage
746 .as_ref()
747 .map(|usage| usage.token_usage())
748 .unwrap_or_default()
749 }
750 }
751
752 impl ProviderResponseExt for Interaction {
753 type OutputMessage = Content;
754 type Usage = InteractionUsage;
755
756 fn get_response_id(&self) -> Option<String> {
757 if self.id.is_empty() {
758 None
759 } else {
760 Some(self.id.clone())
761 }
762 }
763
764 fn get_response_model_name(&self) -> Option<String> {
765 self.model.clone()
766 }
767
768 fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
769 self.output_contents()
770 }
771
772 fn get_text_response(&self) -> Option<String> {
773 let text = self
774 .output_contents()
775 .iter()
776 .filter_map(|content| match content {
777 Content::Text(text) => Some(text.text.clone()),
778 _ => None,
779 })
780 .collect::<Vec<_>>()
781 .join("\n");
782
783 if text.is_empty() { None } else { Some(text) }
784 }
785
786 fn get_usage(&self) -> Option<Self::Usage> {
787 self.usage.clone()
788 }
789 }
790
791 #[derive(Clone, Debug, Default)]
793 pub struct GoogleSearchExchange {
794 pub call_id: Option<String>,
796 pub calls: Vec<GoogleSearchCallContent>,
798 pub results: Vec<GoogleSearchResultContent>,
800 }
801
802 impl GoogleSearchExchange {
803 pub fn queries(&self) -> Vec<String> {
805 let mut queries = Vec::new();
806 for call in &self.calls {
807 if let Some(args) = &call.arguments
808 && let Some(call_queries) = &args.queries
809 {
810 queries.extend(call_queries.clone());
811 }
812 }
813 queries
814 }
815
816 pub fn result_items(&self) -> Vec<GoogleSearchResult> {
818 let mut items = Vec::new();
819 for result in &self.results {
820 if let Some(entries) = &result.result {
821 items.extend(entries.clone());
822 }
823 }
824 items
825 }
826 }
827
828 #[derive(Clone, Debug, Default)]
830 pub struct UrlContextExchange {
831 pub call_id: Option<String>,
833 pub calls: Vec<UrlContextCallContent>,
835 pub results: Vec<UrlContextResultContent>,
837 }
838
839 impl UrlContextExchange {
840 pub fn urls(&self) -> Vec<String> {
842 let mut urls = Vec::new();
843 for call in &self.calls {
844 if let Some(args) = &call.arguments
845 && let Some(call_urls) = &args.urls
846 {
847 urls.extend(call_urls.clone());
848 }
849 }
850 urls
851 }
852
853 pub fn result_items(&self) -> Vec<UrlContextResult> {
855 let mut items = Vec::new();
856 for result in &self.results {
857 if let Some(entries) = &result.result {
858 items.extend(entries.clone());
859 }
860 }
861 items
862 }
863 }
864
865 #[derive(Clone, Debug, Default)]
867 pub struct CodeExecutionExchange {
868 pub call_id: Option<String>,
870 pub calls: Vec<CodeExecutionCallContent>,
872 pub results: Vec<CodeExecutionResultContent>,
874 }
875
876 impl CodeExecutionExchange {
877 pub fn code_snippets(&self) -> Vec<String> {
879 let mut snippets = Vec::new();
880 for call in &self.calls {
881 if let Some(args) = &call.arguments
882 && let Some(code) = &args.code
883 {
884 snippets.push(code.clone());
885 }
886 }
887 snippets
888 }
889
890 pub fn outputs(&self) -> Vec<String> {
892 let mut outputs = Vec::new();
893 for result in &self.results {
894 if let Some(output) = &result.result {
895 outputs.push(output.clone());
896 }
897 }
898 outputs
899 }
900 }
901
902 impl Interaction {
903 pub(crate) fn output_contents(&self) -> Vec<Content> {
904 self.steps.iter().flat_map(Step::output_contents).collect()
905 }
906
907 pub fn google_search_exchanges(&self) -> Vec<GoogleSearchExchange> {
912 let mut exchanges: Vec<GoogleSearchExchange> = Vec::new();
913 let mut last_call_index: Option<usize> = None;
914 let output_contents = self.output_contents();
915
916 for content in &output_contents {
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 let output_contents = self.output_contents();
1022
1023 for content in &output_contents {
1024 match content {
1025 Content::UrlContextCall(call) => {
1026 let index = if let Some(call_id) = call.id.as_ref() {
1027 if let Some(index) = exchanges
1028 .iter()
1029 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1030 {
1031 if let Some(exchange) = exchanges.get_mut(index) {
1032 exchange.calls.push(call.clone());
1033 }
1034 index
1035 } else {
1036 exchanges.push(UrlContextExchange {
1037 call_id: Some(call_id.clone()),
1038 calls: vec![call.clone()],
1039 results: Vec::new(),
1040 });
1041 exchanges.len() - 1
1042 }
1043 } else {
1044 exchanges.push(UrlContextExchange {
1045 call_id: None,
1046 calls: vec![call.clone()],
1047 results: Vec::new(),
1048 });
1049 exchanges.len() - 1
1050 };
1051 last_call_index = Some(index);
1052 }
1053 Content::UrlContextResult(result) => {
1054 if let Some(call_id) = result.call_id.as_ref() {
1055 if let Some(index) = exchanges
1056 .iter()
1057 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1058 {
1059 if let Some(exchange) = exchanges.get_mut(index) {
1060 exchange.results.push(result.clone());
1061 }
1062 } else {
1063 exchanges.push(UrlContextExchange {
1064 call_id: Some(call_id.clone()),
1065 calls: Vec::new(),
1066 results: vec![result.clone()],
1067 });
1068 }
1069 } else if let Some(index) = last_call_index {
1070 if let Some(exchange) = exchanges.get_mut(index) {
1071 exchange.results.push(result.clone());
1072 }
1073 } else {
1074 exchanges.push(UrlContextExchange {
1075 call_id: None,
1076 calls: Vec::new(),
1077 results: vec![result.clone()],
1078 });
1079 last_call_index = Some(exchanges.len() - 1);
1080 }
1081 }
1082 _ => {}
1083 }
1084 }
1085
1086 exchanges
1087 }
1088
1089 pub fn url_context_call_contents(&self) -> Vec<UrlContextCallContent> {
1091 self.url_context_exchanges()
1092 .into_iter()
1093 .flat_map(|exchange| exchange.calls)
1094 .collect()
1095 }
1096
1097 pub fn url_context_result_contents(&self) -> Vec<UrlContextResultContent> {
1099 self.url_context_exchanges()
1100 .into_iter()
1101 .flat_map(|exchange| exchange.results)
1102 .collect()
1103 }
1104
1105 pub fn url_context_urls(&self) -> Vec<String> {
1107 self.url_context_exchanges()
1108 .into_iter()
1109 .flat_map(|exchange| exchange.urls())
1110 .collect()
1111 }
1112
1113 pub fn url_context_results(&self) -> Vec<UrlContextResult> {
1115 self.url_context_exchanges()
1116 .into_iter()
1117 .flat_map(|exchange| exchange.result_items())
1118 .collect()
1119 }
1120
1121 pub fn code_execution_exchanges(&self) -> Vec<CodeExecutionExchange> {
1126 let mut exchanges: Vec<CodeExecutionExchange> = Vec::new();
1127 let mut last_call_index: Option<usize> = None;
1128 let output_contents = self.output_contents();
1129
1130 for content in &output_contents {
1131 match content {
1132 Content::CodeExecutionCall(call) => {
1133 let index = if let Some(call_id) = call.id.as_ref() {
1134 if let Some(index) = exchanges
1135 .iter()
1136 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1137 {
1138 if let Some(exchange) = exchanges.get_mut(index) {
1139 exchange.calls.push(call.clone());
1140 }
1141 index
1142 } else {
1143 exchanges.push(CodeExecutionExchange {
1144 call_id: Some(call_id.clone()),
1145 calls: vec![call.clone()],
1146 results: Vec::new(),
1147 });
1148 exchanges.len() - 1
1149 }
1150 } else {
1151 exchanges.push(CodeExecutionExchange {
1152 call_id: None,
1153 calls: vec![call.clone()],
1154 results: Vec::new(),
1155 });
1156 exchanges.len() - 1
1157 };
1158 last_call_index = Some(index);
1159 }
1160 Content::CodeExecutionResult(result) => {
1161 if let Some(call_id) = result.call_id.as_ref() {
1162 if let Some(index) = exchanges
1163 .iter()
1164 .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1165 {
1166 if let Some(exchange) = exchanges.get_mut(index) {
1167 exchange.results.push(result.clone());
1168 }
1169 } else {
1170 exchanges.push(CodeExecutionExchange {
1171 call_id: Some(call_id.clone()),
1172 calls: Vec::new(),
1173 results: vec![result.clone()],
1174 });
1175 }
1176 } else if let Some(index) = last_call_index {
1177 if let Some(exchange) = exchanges.get_mut(index) {
1178 exchange.results.push(result.clone());
1179 }
1180 } else {
1181 exchanges.push(CodeExecutionExchange {
1182 call_id: None,
1183 calls: Vec::new(),
1184 results: vec![result.clone()],
1185 });
1186 last_call_index = Some(exchanges.len() - 1);
1187 }
1188 }
1189 _ => {}
1190 }
1191 }
1192
1193 exchanges
1194 }
1195
1196 pub fn code_execution_call_contents(&self) -> Vec<CodeExecutionCallContent> {
1198 self.code_execution_exchanges()
1199 .into_iter()
1200 .flat_map(|exchange| exchange.calls)
1201 .collect()
1202 }
1203
1204 pub fn code_execution_result_contents(&self) -> Vec<CodeExecutionResultContent> {
1206 self.code_execution_exchanges()
1207 .into_iter()
1208 .flat_map(|exchange| exchange.results)
1209 .collect()
1210 }
1211
1212 pub fn code_execution_snippets(&self) -> Vec<String> {
1214 self.code_execution_exchanges()
1215 .into_iter()
1216 .flat_map(|exchange| exchange.code_snippets())
1217 .collect()
1218 }
1219
1220 pub fn code_execution_outputs(&self) -> Vec<String> {
1222 self.code_execution_exchanges()
1223 .into_iter()
1224 .flat_map(|exchange| exchange.outputs())
1225 .collect()
1226 }
1227
1228 pub fn text_with_inline_citations(&self) -> Option<String> {
1230 let text = self
1231 .output_contents()
1232 .iter()
1233 .filter_map(|content| match content {
1234 Content::Text(text) => Some(text.with_inline_citations()),
1235 _ => None,
1236 })
1237 .collect::<Vec<_>>()
1238 .join("\n");
1239
1240 if text.is_empty() { None } else { Some(text) }
1241 }
1242
1243 pub fn is_terminal(&self) -> bool {
1245 self.status
1246 .as_ref()
1247 .is_some_and(InteractionStatus::is_terminal)
1248 }
1249
1250 pub fn is_completed(&self) -> bool {
1252 matches!(self.status, Some(InteractionStatus::Completed))
1253 }
1254 }
1255
1256 #[derive(Clone, Debug, Deserialize, Serialize)]
1258 #[serde(rename_all = "snake_case")]
1259 pub enum InteractionStatus {
1260 InProgress,
1261 RequiresAction,
1262 Incomplete,
1263 BudgetExceeded,
1264 Completed,
1265 Failed,
1266 Cancelled,
1267 }
1268
1269 impl InteractionStatus {
1270 pub fn is_terminal(&self) -> bool {
1272 matches!(
1273 self,
1274 InteractionStatus::Completed
1275 | InteractionStatus::Incomplete
1276 | InteractionStatus::BudgetExceeded
1277 | InteractionStatus::Failed
1278 | InteractionStatus::Cancelled
1279 )
1280 }
1281 }
1282
1283 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1285 #[serde(rename_all = "snake_case")]
1286 pub struct InteractionUsage {
1287 #[serde(skip_serializing_if = "Option::is_none")]
1288 pub total_input_tokens: Option<u64>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1290 pub total_output_tokens: Option<u64>,
1291 #[serde(skip_serializing_if = "Option::is_none")]
1292 pub total_tokens: Option<u64>,
1293 }
1294
1295 impl GetTokenUsage for InteractionUsage {
1296 fn token_usage(&self) -> Usage {
1297 let mut usage = Usage::new();
1298 usage.input_tokens = self.total_input_tokens.unwrap_or_default();
1299 usage.output_tokens = self.total_output_tokens.unwrap_or_default();
1300 usage.total_tokens = self
1301 .total_tokens
1302 .unwrap_or(usage.input_tokens + usage.output_tokens);
1303 usage
1304 }
1305 }
1306
1307 #[derive(Clone, Debug, Deserialize, Serialize)]
1309 #[serde(untagged)]
1310 pub enum InteractionInput {
1311 Text(String),
1312 Content(Content),
1313 Steps(Vec<Step>),
1314 Contents(Vec<Content>),
1315 }
1316
1317 #[derive(Clone, Debug, Deserialize, Serialize)]
1319 #[serde(tag = "type", rename_all = "snake_case")]
1320 pub enum Step {
1321 UserInput { content: Vec<Content> },
1322 ModelOutput { content: Vec<Content> },
1323 Thought(ThoughtContent),
1324 FunctionCall(FunctionCallContent),
1325 FunctionResult(FunctionResultContent),
1326 CodeExecutionCall(CodeExecutionCallContent),
1327 CodeExecutionResult(CodeExecutionResultContent),
1328 UrlContextCall(UrlContextCallContent),
1329 UrlContextResult(UrlContextResultContent),
1330 GoogleSearchCall(GoogleSearchCallContent),
1331 GoogleSearchResult(GoogleSearchResultContent),
1332 McpServerToolCall(McpServerToolCallContent),
1333 McpServerToolResult(McpServerToolResultContent),
1334 FileSearchResult(FileSearchResultContent),
1335 }
1336
1337 impl Step {
1338 fn output_contents(&self) -> Vec<Content> {
1339 match self {
1340 Step::UserInput { .. } => Vec::new(),
1341 Step::ModelOutput { content } => content.clone(),
1342 Step::Thought(content) => vec![Content::Thought(content.clone())],
1343 Step::FunctionCall(content) => vec![Content::FunctionCall(content.clone())],
1344 Step::FunctionResult(content) => vec![Content::FunctionResult(content.clone())],
1345 Step::CodeExecutionCall(content) => {
1346 vec![Content::CodeExecutionCall(content.clone())]
1347 }
1348 Step::CodeExecutionResult(content) => {
1349 vec![Content::CodeExecutionResult(content.clone())]
1350 }
1351 Step::UrlContextCall(content) => vec![Content::UrlContextCall(content.clone())],
1352 Step::UrlContextResult(content) => {
1353 vec![Content::UrlContextResult(content.clone())]
1354 }
1355 Step::GoogleSearchCall(content) => {
1356 vec![Content::GoogleSearchCall(content.clone())]
1357 }
1358 Step::GoogleSearchResult(content) => {
1359 vec![Content::GoogleSearchResult(content.clone())]
1360 }
1361 Step::McpServerToolCall(content) => {
1362 vec![Content::McpServerToolCall(content.clone())]
1363 }
1364 Step::McpServerToolResult(content) => {
1365 vec![Content::McpServerToolResult(content.clone())]
1366 }
1367 Step::FileSearchResult(content) => {
1368 vec![Content::FileSearchResult(content.clone())]
1369 }
1370 }
1371 }
1372 }
1373
1374 impl TryFrom<crate::completion::Message> for Step {
1375 type Error = message::MessageError;
1376
1377 fn try_from(message: crate::completion::Message) -> Result<Self, Self::Error> {
1378 match message {
1379 crate::completion::Message::System { content } => Ok(Self::UserInput {
1380 content: vec![Content::Text(TextContent {
1381 text: content,
1382 annotations: None,
1383 })],
1384 }),
1385 crate::completion::Message::User { content } => {
1386 let content = content
1387 .into_iter()
1388 .map(Content::try_from)
1389 .collect::<Result<Vec<_>, _>>()?;
1390 Ok(Self::UserInput { content })
1391 }
1392 crate::completion::Message::Assistant { content, .. } => {
1393 let content = content
1394 .into_iter()
1395 .map(Content::try_from)
1396 .collect::<Result<Vec<_>, _>>()?;
1397 Ok(Self::ModelOutput { content })
1398 }
1399 }
1400 }
1401 }
1402
1403 #[derive(Clone, Debug, Deserialize, Serialize)]
1409 pub struct Annotation {
1410 #[serde(skip_serializing_if = "Option::is_none")]
1411 pub start_index: Option<i64>,
1412 #[serde(skip_serializing_if = "Option::is_none")]
1413 pub end_index: Option<i64>,
1414 #[serde(skip_serializing_if = "Option::is_none")]
1415 pub source: Option<String>,
1416 }
1417
1418 #[derive(Clone, Debug)]
1420 pub struct Citation {
1421 pub start_index: usize,
1422 pub end_index: usize,
1423 pub source: String,
1424 }
1425
1426 #[derive(Clone, Debug, Deserialize, Serialize)]
1428 pub struct TextContent {
1429 pub text: String,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1431 pub annotations: Option<Vec<Annotation>>,
1432 }
1433
1434 impl TextContent {
1435 pub fn citations(&self) -> Vec<Citation> {
1437 let mut citations = Vec::new();
1438 let Some(annotations) = self.annotations.as_ref() else {
1439 return citations;
1440 };
1441
1442 for annotation in annotations {
1443 let (Some(start), Some(end), Some(source)) = (
1444 annotation.start_index,
1445 annotation.end_index,
1446 annotation.source.as_ref(),
1447 ) else {
1448 continue;
1449 };
1450
1451 if start < 0 || end < 0 {
1452 continue;
1453 }
1454 let start = start as usize;
1455 let end = end as usize;
1456 if end <= start || end > self.text.len() {
1457 continue;
1458 }
1459 if !self.text.is_char_boundary(start) || !self.text.is_char_boundary(end) {
1460 continue;
1461 }
1462
1463 citations.push(Citation {
1464 start_index: start,
1465 end_index: end,
1466 source: source.clone(),
1467 });
1468 }
1469
1470 citations.sort_by(|a, b| {
1471 a.start_index
1472 .cmp(&b.start_index)
1473 .then_with(|| a.end_index.cmp(&b.end_index))
1474 });
1475
1476 citations
1477 }
1478
1479 pub fn with_inline_citations(&self) -> String {
1481 let citations = self.citations();
1482 if citations.is_empty() {
1483 return self.text.clone();
1484 }
1485
1486 let mut source_order = Vec::new();
1487 for citation in &citations {
1488 if !source_order.contains(&citation.source) {
1489 source_order.push(citation.source.clone());
1490 }
1491 }
1492
1493 let mut inserts = citations
1494 .iter()
1495 .map(|citation| {
1496 let index = source_order
1497 .iter()
1498 .position(|source| source == &citation.source)
1499 .map(|idx| idx + 1)
1500 .unwrap_or(0);
1501 (
1502 citation.start_index,
1503 citation.end_index,
1504 index,
1505 &citation.source,
1506 )
1507 })
1508 .collect::<Vec<_>>();
1509
1510 inserts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| b.0.cmp(&a.0)));
1511
1512 let mut text = self.text.clone();
1513 for (_, end, index, source) in inserts {
1514 if index == 0 {
1515 continue;
1516 }
1517 let citation = format!("[{}]({})", index, source);
1518 text.insert_str(end, &citation);
1519 }
1520
1521 text
1522 }
1523 }
1524
1525 #[derive(Clone, Debug, Deserialize, Serialize)]
1527 pub struct ImageContent {
1528 #[serde(skip_serializing_if = "Option::is_none")]
1529 pub data: Option<String>,
1530 #[serde(skip_serializing_if = "Option::is_none")]
1531 pub uri: Option<String>,
1532 #[serde(skip_serializing_if = "Option::is_none")]
1533 pub mime_type: Option<String>,
1534 #[serde(skip_serializing_if = "Option::is_none")]
1535 pub resolution: Option<MediaResolution>,
1536 }
1537
1538 #[derive(Clone, Debug, Deserialize, Serialize)]
1540 pub struct AudioContent {
1541 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub data: Option<String>,
1543 #[serde(skip_serializing_if = "Option::is_none")]
1544 pub uri: Option<String>,
1545 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub mime_type: Option<String>,
1547 }
1548
1549 #[derive(Clone, Debug, Deserialize, Serialize)]
1551 pub struct DocumentContent {
1552 #[serde(skip_serializing_if = "Option::is_none")]
1553 pub data: Option<String>,
1554 #[serde(skip_serializing_if = "Option::is_none")]
1555 pub uri: Option<String>,
1556 #[serde(skip_serializing_if = "Option::is_none")]
1557 pub mime_type: Option<String>,
1558 }
1559
1560 #[derive(Clone, Debug, Deserialize, Serialize)]
1562 pub struct VideoContent {
1563 #[serde(skip_serializing_if = "Option::is_none")]
1564 pub data: Option<String>,
1565 #[serde(skip_serializing_if = "Option::is_none")]
1566 pub uri: Option<String>,
1567 #[serde(skip_serializing_if = "Option::is_none")]
1568 pub mime_type: Option<String>,
1569 #[serde(skip_serializing_if = "Option::is_none")]
1570 pub resolution: Option<MediaResolution>,
1571 }
1572
1573 #[derive(Clone, Debug, Deserialize, Serialize)]
1575 pub struct ThoughtContent {
1576 #[serde(skip_serializing_if = "Option::is_none")]
1577 pub signature: Option<String>,
1578 #[serde(skip_serializing_if = "Option::is_none")]
1579 pub summary: Option<Vec<ThoughtSummaryContent>>,
1580 }
1581
1582 #[derive(Clone, Debug, Deserialize, Serialize)]
1584 #[serde(untagged)]
1585 pub enum ThoughtSummaryContent {
1586 Text(TextContent),
1587 Image(ImageContent),
1588 }
1589
1590 #[derive(Clone, Debug, Deserialize, Serialize)]
1592 pub struct FunctionCallContent {
1593 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub name: Option<String>,
1595 #[serde(skip_serializing_if = "Option::is_none")]
1596 pub arguments: Option<Value>,
1597 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub id: Option<String>,
1599 }
1600
1601 #[derive(Clone, Debug, Deserialize, Serialize)]
1603 pub struct FunctionResultContent {
1604 #[serde(skip_serializing_if = "Option::is_none")]
1605 pub name: Option<String>,
1606 #[serde(skip_serializing_if = "Option::is_none")]
1607 pub is_error: Option<bool>,
1608 #[serde(skip_serializing_if = "Option::is_none")]
1609 pub result: Option<Value>,
1610 #[serde(skip_serializing_if = "Option::is_none")]
1611 pub call_id: Option<String>,
1612 }
1613
1614 #[derive(Clone, Debug, Deserialize, Serialize)]
1616 pub struct CodeExecutionCallArguments {
1617 #[serde(skip_serializing_if = "Option::is_none")]
1618 pub language: Option<String>,
1619 #[serde(skip_serializing_if = "Option::is_none")]
1620 pub code: Option<String>,
1621 }
1622
1623 #[derive(Clone, Debug, Deserialize, Serialize)]
1625 pub struct CodeExecutionCallContent {
1626 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub arguments: Option<CodeExecutionCallArguments>,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1629 pub id: Option<String>,
1630 }
1631
1632 #[derive(Clone, Debug, Deserialize, Serialize)]
1634 pub struct CodeExecutionResultContent {
1635 #[serde(skip_serializing_if = "Option::is_none")]
1636 pub result: Option<String>,
1637 #[serde(skip_serializing_if = "Option::is_none")]
1638 pub is_error: Option<bool>,
1639 #[serde(skip_serializing_if = "Option::is_none")]
1640 pub signature: Option<String>,
1641 #[serde(skip_serializing_if = "Option::is_none")]
1642 pub call_id: Option<String>,
1643 }
1644
1645 #[derive(Clone, Debug, Deserialize, Serialize)]
1647 pub struct UrlContextCallArguments {
1648 #[serde(skip_serializing_if = "Option::is_none")]
1649 pub urls: Option<Vec<String>>,
1650 }
1651
1652 #[derive(Clone, Debug, Deserialize, Serialize)]
1654 pub struct UrlContextCallContent {
1655 #[serde(skip_serializing_if = "Option::is_none")]
1656 pub arguments: Option<UrlContextCallArguments>,
1657 #[serde(skip_serializing_if = "Option::is_none")]
1658 pub id: Option<String>,
1659 }
1660
1661 #[derive(Clone, Debug, Deserialize, Serialize)]
1663 pub struct UrlContextResult {
1664 #[serde(skip_serializing_if = "Option::is_none")]
1665 pub url: Option<String>,
1666 #[serde(skip_serializing_if = "Option::is_none")]
1667 pub status: Option<String>,
1668 }
1669
1670 #[derive(Clone, Debug, Deserialize, Serialize)]
1672 pub struct UrlContextResultContent {
1673 #[serde(skip_serializing_if = "Option::is_none")]
1674 pub signature: Option<String>,
1675 #[serde(skip_serializing_if = "Option::is_none")]
1676 pub result: Option<Vec<UrlContextResult>>,
1677 #[serde(skip_serializing_if = "Option::is_none")]
1678 pub is_error: Option<bool>,
1679 #[serde(skip_serializing_if = "Option::is_none")]
1680 pub call_id: Option<String>,
1681 }
1682
1683 #[derive(Clone, Debug, Deserialize, Serialize)]
1685 pub struct GoogleSearchCallArguments {
1686 #[serde(skip_serializing_if = "Option::is_none")]
1687 pub queries: Option<Vec<String>>,
1688 }
1689
1690 #[derive(Clone, Debug, Deserialize, Serialize)]
1692 pub struct GoogleSearchCallContent {
1693 #[serde(skip_serializing_if = "Option::is_none")]
1694 pub arguments: Option<GoogleSearchCallArguments>,
1695 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub id: Option<String>,
1697 }
1698
1699 #[derive(Clone, Debug, Deserialize, Serialize)]
1701 pub struct GoogleSearchResult {
1702 #[serde(skip_serializing_if = "Option::is_none")]
1703 pub url: Option<String>,
1704 #[serde(skip_serializing_if = "Option::is_none")]
1705 pub title: Option<String>,
1706 #[serde(skip_serializing_if = "Option::is_none")]
1707 pub rendered_content: Option<String>,
1708 }
1709
1710 #[derive(Clone, Debug, Deserialize, Serialize)]
1712 pub struct GoogleSearchResultContent {
1713 #[serde(skip_serializing_if = "Option::is_none")]
1714 pub signature: Option<String>,
1715 #[serde(skip_serializing_if = "Option::is_none")]
1716 pub result: Option<Vec<GoogleSearchResult>>,
1717 #[serde(skip_serializing_if = "Option::is_none")]
1718 pub is_error: Option<bool>,
1719 #[serde(skip_serializing_if = "Option::is_none")]
1720 pub call_id: Option<String>,
1721 }
1722
1723 #[derive(Clone, Debug, Deserialize, Serialize)]
1725 pub struct McpServerToolCallContent {
1726 #[serde(skip_serializing_if = "Option::is_none")]
1727 pub name: Option<String>,
1728 #[serde(skip_serializing_if = "Option::is_none")]
1729 pub server_name: Option<String>,
1730 #[serde(skip_serializing_if = "Option::is_none")]
1731 pub arguments: Option<Value>,
1732 #[serde(skip_serializing_if = "Option::is_none")]
1733 pub id: Option<String>,
1734 }
1735
1736 #[derive(Clone, Debug, Deserialize, Serialize)]
1738 pub struct McpServerToolResultContent {
1739 #[serde(skip_serializing_if = "Option::is_none")]
1740 pub name: Option<String>,
1741 #[serde(skip_serializing_if = "Option::is_none")]
1742 pub server_name: Option<String>,
1743 #[serde(skip_serializing_if = "Option::is_none")]
1744 pub result: Option<Value>,
1745 #[serde(skip_serializing_if = "Option::is_none")]
1746 pub call_id: Option<String>,
1747 }
1748
1749 #[derive(Clone, Debug, Deserialize, Serialize)]
1751 pub struct FileSearchResult {
1752 pub title: String,
1753 pub text: String,
1754 pub file_search_store: String,
1755 }
1756
1757 #[derive(Clone, Debug, Deserialize, Serialize)]
1759 pub struct FileSearchResultContent {
1760 #[serde(skip_serializing_if = "Option::is_none")]
1761 pub result: Option<Vec<FileSearchResult>>,
1762 }
1763
1764 #[derive(Clone, Debug, Deserialize, Serialize)]
1766 #[serde(tag = "type", rename_all = "snake_case")]
1767 pub enum Content {
1768 Text(TextContent),
1769 Image(ImageContent),
1770 Audio(AudioContent),
1771 Document(DocumentContent),
1772 Video(VideoContent),
1773 Thought(ThoughtContent),
1774 FunctionCall(FunctionCallContent),
1775 FunctionResult(FunctionResultContent),
1776 CodeExecutionCall(CodeExecutionCallContent),
1777 CodeExecutionResult(CodeExecutionResultContent),
1778 UrlContextCall(UrlContextCallContent),
1779 UrlContextResult(UrlContextResultContent),
1780 GoogleSearchCall(GoogleSearchCallContent),
1781 GoogleSearchResult(GoogleSearchResultContent),
1782 McpServerToolCall(McpServerToolCallContent),
1783 McpServerToolResult(McpServerToolResultContent),
1784 FileSearchResult(FileSearchResultContent),
1785 }
1786
1787 fn rich_function_result_block(
1788 content: message::ToolResultContent,
1789 ) -> Result<Value, message::MessageError> {
1790 let content = match content {
1791 message::ToolResultContent::Text(text) => Content::Text(TextContent {
1792 text: text.text,
1793 annotations: None,
1794 }),
1795 message::ToolResultContent::Json { value } => Content::Text(TextContent {
1796 text: value.to_string(),
1797 annotations: None,
1798 }),
1799 message::ToolResultContent::Image(message::Image {
1800 data, media_type, ..
1801 }) => {
1802 let media_type = media_type.ok_or_else(|| {
1803 message::MessageError::ConversionError(
1804 "Image media type is required for Gemini Interactions tool results"
1805 .to_string(),
1806 )
1807 })?;
1808 let (data, uri) = split_data_uri(data)?;
1809
1810 Content::Image(ImageContent {
1811 data,
1812 uri,
1813 mime_type: Some(media_type.to_mime_type().to_string()),
1814 resolution: None,
1815 })
1816 }
1817 };
1818
1819 serde_json::to_value(content).map_err(|err| {
1820 message::MessageError::ConversionError(format!(
1821 "Failed to serialize Gemini Interactions tool result content: {err}"
1822 ))
1823 })
1824 }
1825
1826 impl TryFrom<message::UserContent> for Content {
1827 type Error = message::MessageError;
1828
1829 fn try_from(content: message::UserContent) -> Result<Self, Self::Error> {
1830 match content {
1831 message::UserContent::Text(message::Text { text, .. }) => {
1832 Ok(Self::Text(TextContent {
1833 text,
1834 annotations: None,
1835 }))
1836 }
1837 message::UserContent::ToolResult(message::ToolResult {
1838 id,
1839 call_id,
1840 content,
1841 }) => {
1842 let Some(call_id) = call_id else {
1843 return Err(message::MessageError::ConversionError(
1844 "Tool results require call_id for Gemini Interactions API".to_string(),
1845 ));
1846 };
1847
1848 let mut contents = content.into_iter().collect::<Vec<_>>();
1849 let result = if contents.len() == 1 {
1850 let content = contents.pop().ok_or_else(|| {
1851 message::MessageError::ConversionError(
1852 "Tool result content must not be empty".to_string(),
1853 )
1854 })?;
1855
1856 match content {
1857 message::ToolResultContent::Text(text) => Value::String(text.text),
1858 message::ToolResultContent::Json { value } => match value {
1859 value @ (Value::String(_) | Value::Object(_)) => value,
1860 value => Value::Array(vec![rich_function_result_block(
1861 message::ToolResultContent::Json { value },
1862 )?]),
1863 },
1864 rich_content => {
1865 Value::Array(vec![rich_function_result_block(rich_content)?])
1866 }
1867 }
1868 } else {
1869 Value::Array(
1870 contents
1871 .into_iter()
1872 .map(rich_function_result_block)
1873 .collect::<Result<Vec<_>, _>>()?,
1874 )
1875 };
1876
1877 Ok(Self::FunctionResult(FunctionResultContent {
1878 name: Some(id),
1879 is_error: None,
1880 result: Some(result),
1881 call_id: Some(call_id),
1882 }))
1883 }
1884 message::UserContent::Image(message::Image {
1885 data, media_type, ..
1886 }) => {
1887 let media_type = media_type.ok_or_else(|| {
1888 message::MessageError::ConversionError(
1889 "Media type for image is required for Gemini".to_string(),
1890 )
1891 })?;
1892 let mime_type = media_type.to_mime_type().to_string();
1893 let (data, uri) = split_data_uri(data)?;
1894 Ok(Self::Image(ImageContent {
1895 data,
1896 uri,
1897 mime_type: Some(mime_type),
1898 resolution: None,
1899 }))
1900 }
1901 message::UserContent::Audio(message::Audio {
1902 data, media_type, ..
1903 }) => {
1904 let media_type = media_type.ok_or_else(|| {
1905 message::MessageError::ConversionError(
1906 "Media type for audio is required for Gemini".to_string(),
1907 )
1908 })?;
1909 let mime_type = media_type.to_mime_type().to_string();
1910 let (data, uri) = split_data_uri(data)?;
1911 Ok(Self::Audio(AudioContent {
1912 data,
1913 uri,
1914 mime_type: Some(mime_type),
1915 }))
1916 }
1917 message::UserContent::Video(message::Video {
1918 data, media_type, ..
1919 }) => {
1920 let media_type = media_type.ok_or_else(|| {
1921 message::MessageError::ConversionError(
1922 "Media type for video is required for Gemini".to_string(),
1923 )
1924 })?;
1925 let mime_type = media_type.to_mime_type().to_string();
1926 let (data, uri) = split_data_uri(data)?;
1927 Ok(Self::Video(VideoContent {
1928 data,
1929 uri,
1930 mime_type: Some(mime_type),
1931 resolution: None,
1932 }))
1933 }
1934 message::UserContent::Document(message::Document {
1935 data, media_type, ..
1936 }) => {
1937 let media_type = media_type.ok_or_else(|| {
1938 message::MessageError::ConversionError(
1939 "Media type for document is required for Gemini".to_string(),
1940 )
1941 })?;
1942 if matches!(media_type, message::DocumentMediaType::TXT) {
1943 let text = match data {
1944 message::DocumentSourceKind::String(text) => text,
1945 message::DocumentSourceKind::Base64(data) => {
1946 let decoded = BASE64_STANDARD.decode(data).map_err(|error| {
1947 message::MessageError::ConversionError(format!(
1948 "Failed to decode text document base64 data: {error}"
1949 ))
1950 })?;
1951 String::from_utf8(decoded).map_err(|error| {
1952 message::MessageError::ConversionError(format!(
1953 "Text document data must be UTF-8: {error}"
1954 ))
1955 })?
1956 }
1957 message::DocumentSourceKind::Raw(data) => String::from_utf8(data)
1958 .map_err(|error| {
1959 message::MessageError::ConversionError(format!(
1960 "Text document data must be UTF-8: {error}"
1961 ))
1962 })?,
1963 message::DocumentSourceKind::Url(_) => {
1964 return Err(message::MessageError::ConversionError(
1965 "Text document URLs are not supported for Gemini Interactions inputs"
1966 .to_string(),
1967 ));
1968 }
1969 message::DocumentSourceKind::FileId(_) => {
1970 return Err(message::MessageError::ConversionError(
1971 "Provider file IDs are not supported for Gemini Interactions inputs"
1972 .to_string(),
1973 ));
1974 }
1975 message::DocumentSourceKind::Unknown => {
1976 return Err(message::MessageError::ConversionError(
1977 "Unknown content source".to_string(),
1978 ));
1979 }
1980 };
1981 return Ok(Self::Text(TextContent {
1982 text,
1983 annotations: None,
1984 }));
1985 }
1986 let mime_type = media_type.to_mime_type().to_string();
1987 let (data, uri) = split_data_uri(data)?;
1988 Ok(Self::Document(DocumentContent {
1989 data,
1990 uri,
1991 mime_type: Some(mime_type),
1992 }))
1993 }
1994 }
1995 }
1996 }
1997
1998 impl TryFrom<message::AssistantContent> for Content {
1999 type Error = message::MessageError;
2000
2001 fn try_from(content: message::AssistantContent) -> Result<Self, Self::Error> {
2002 match content {
2003 message::AssistantContent::Text(message::Text { text, .. }) => {
2004 Ok(Self::Text(TextContent {
2005 text,
2006 annotations: None,
2007 }))
2008 }
2009 message::AssistantContent::ToolCall(tool_call) => {
2010 let call_id = tool_call.call_id.unwrap_or_else(|| tool_call.id.clone());
2011 Ok(Self::FunctionCall(FunctionCallContent {
2012 name: Some(tool_call.function.name),
2013 arguments: Some(tool_call.function.arguments),
2014 id: Some(call_id),
2015 }))
2016 }
2017 message::AssistantContent::Reasoning(message::Reasoning { content, .. }) => {
2018 let mut signature = None;
2019 let summary = content
2020 .into_iter()
2021 .map(|reasoning_content| {
2022 let text = match reasoning_content {
2023 message::ReasoningContent::Text {
2024 text,
2025 signature: content_signature,
2026 } => {
2027 if signature.is_none() {
2028 signature = content_signature;
2029 }
2030 text
2031 }
2032 message::ReasoningContent::Summary(text)
2033 | message::ReasoningContent::Encrypted(text) => text,
2034 message::ReasoningContent::Redacted { data } => data,
2035 };
2036
2037 ThoughtSummaryContent::Text(TextContent {
2038 text,
2039 annotations: None,
2040 })
2041 })
2042 .collect();
2043
2044 Ok(Self::Thought(ThoughtContent {
2045 signature,
2046 summary: Some(summary),
2047 }))
2048 }
2049 message::AssistantContent::Image(message::Image {
2050 data, media_type, ..
2051 }) => {
2052 let media_type = media_type.ok_or_else(|| {
2053 message::MessageError::ConversionError(
2054 "Media type for image is required for Gemini".to_string(),
2055 )
2056 })?;
2057 let mime_type = media_type.to_mime_type().to_string();
2058 let (data, uri) = split_data_uri(data)?;
2059 Ok(Self::Image(ImageContent {
2060 data,
2061 uri,
2062 mime_type: Some(mime_type),
2063 resolution: None,
2064 }))
2065 }
2066 }
2067 }
2068 }
2069
2070 #[derive(Clone, Debug, Deserialize, Serialize)]
2076 #[serde(rename_all = "snake_case")]
2077 pub enum ResponseModality {
2078 Text,
2079 Image,
2080 Audio,
2081 }
2082
2083 #[derive(Clone, Debug, Deserialize, Serialize)]
2085 #[serde(rename_all = "snake_case")]
2086 pub enum ThinkingLevel {
2087 Minimal,
2088 Low,
2089 Medium,
2090 High,
2091 }
2092
2093 #[derive(Clone, Debug, Deserialize, Serialize)]
2095 #[serde(rename_all = "snake_case")]
2096 pub enum ThinkingSummaries {
2097 Auto,
2098 None,
2099 }
2100
2101 #[derive(Clone, Debug, Deserialize, Serialize)]
2103 #[serde(rename_all = "snake_case")]
2104 pub struct SpeechConfig {
2105 #[serde(skip_serializing_if = "Option::is_none")]
2106 pub voice: Option<String>,
2107 #[serde(skip_serializing_if = "Option::is_none")]
2108 pub language: Option<String>,
2109 #[serde(skip_serializing_if = "Option::is_none")]
2110 pub speaker: Option<String>,
2111 }
2112
2113 #[derive(Clone, Debug, Deserialize, Serialize, Default)]
2115 #[serde(rename_all = "snake_case")]
2116 pub struct GenerationConfig {
2117 #[serde(skip_serializing_if = "Option::is_none")]
2118 pub temperature: Option<f64>,
2119 #[serde(skip_serializing_if = "Option::is_none")]
2120 pub top_p: Option<f64>,
2121 #[serde(skip_serializing_if = "Option::is_none")]
2122 pub seed: Option<u64>,
2123 #[serde(skip_serializing_if = "Option::is_none")]
2124 pub stop_sequences: Option<Vec<String>>,
2125 #[serde(skip_serializing_if = "Option::is_none")]
2126 pub tool_choice: Option<ToolChoice>,
2127 #[serde(skip_serializing_if = "Option::is_none")]
2128 pub thinking_level: Option<ThinkingLevel>,
2129 #[serde(skip_serializing_if = "Option::is_none")]
2130 pub thinking_summaries: Option<ThinkingSummaries>,
2131 #[serde(skip_serializing_if = "Option::is_none")]
2132 pub max_output_tokens: Option<u64>,
2133 #[serde(skip_serializing_if = "Option::is_none")]
2134 pub speech_config: Option<Vec<SpeechConfig>>,
2135 }
2136
2137 impl GenerationConfig {
2138 pub fn is_empty(&self) -> bool {
2140 self.temperature.is_none()
2141 && self.top_p.is_none()
2142 && self.seed.is_none()
2143 && self.stop_sequences.is_none()
2144 && self.tool_choice.is_none()
2145 && self.thinking_level.is_none()
2146 && self.thinking_summaries.is_none()
2147 && self.max_output_tokens.is_none()
2148 && self.speech_config.is_none()
2149 }
2150 }
2151
2152 #[derive(Clone, Debug, Deserialize, Serialize)]
2154 #[serde(untagged)]
2155 pub enum ToolChoice {
2156 Type(ToolChoiceType),
2157 Config(ToolChoiceConfig),
2158 }
2159
2160 #[derive(Clone, Debug, Deserialize, Serialize)]
2162 #[serde(rename_all = "snake_case")]
2163 pub enum ToolChoiceType {
2164 Auto,
2165 Any,
2166 None,
2167 Validated,
2168 }
2169
2170 #[derive(Clone, Debug, Deserialize, Serialize)]
2172 pub struct ToolChoiceConfig {
2173 pub allowed_tools: AllowedTools,
2174 }
2175
2176 #[derive(Clone, Debug, Deserialize, Serialize)]
2178 pub struct AllowedTools {
2179 #[serde(skip_serializing_if = "Option::is_none")]
2180 pub mode: Option<ToolChoiceType>,
2181 #[serde(skip_serializing_if = "Option::is_none")]
2182 pub tools: Option<Vec<String>>,
2183 }
2184
2185 #[derive(Clone, Debug, Deserialize, Serialize)]
2187 #[serde(tag = "type", rename_all = "snake_case")]
2188 pub enum Tool {
2189 Function(FunctionTool),
2190 GoogleSearch,
2191 CodeExecution,
2192 UrlContext,
2193 ComputerUse(ComputerUseTool),
2194 McpServer(McpServerTool),
2195 FileSearch(FileSearchTool),
2196 }
2197
2198 #[derive(Clone, Debug, Deserialize, Serialize)]
2200 pub struct FunctionTool {
2201 #[serde(skip_serializing_if = "Option::is_none")]
2202 pub name: Option<String>,
2203 #[serde(skip_serializing_if = "Option::is_none")]
2204 pub description: Option<String>,
2205 #[serde(skip_serializing_if = "Option::is_none")]
2206 pub parameters: Option<Value>,
2207 }
2208
2209 #[derive(Clone, Debug, Deserialize, Serialize)]
2211 pub struct ComputerUseTool {
2212 #[serde(skip_serializing_if = "Option::is_none")]
2213 pub environment: Option<String>,
2214 #[serde(skip_serializing_if = "Option::is_none")]
2215 pub excluded_predefined_functions: Option<Vec<String>>,
2216 }
2217
2218 #[derive(Clone, Debug, Deserialize, Serialize)]
2220 pub struct McpServerTool {
2221 #[serde(skip_serializing_if = "Option::is_none")]
2222 pub name: Option<String>,
2223 #[serde(skip_serializing_if = "Option::is_none")]
2224 pub url: Option<String>,
2225 #[serde(skip_serializing_if = "Option::is_none")]
2226 pub headers: Option<Value>,
2227 #[serde(skip_serializing_if = "Option::is_none")]
2228 pub allowed_tools: Option<AllowedTools>,
2229 }
2230
2231 #[derive(Clone, Debug, Deserialize, Serialize)]
2233 pub struct FileSearchTool {
2234 #[serde(skip_serializing_if = "Option::is_none")]
2235 pub file_search_store_names: Option<Vec<String>>,
2236 #[serde(skip_serializing_if = "Option::is_none")]
2237 pub top_k: Option<u64>,
2238 #[serde(skip_serializing_if = "Option::is_none")]
2239 pub metadata_filter: Option<String>,
2240 }
2241
2242 impl TryFrom<crate::completion::ToolDefinition> for Tool {
2243 type Error = CompletionError;
2244
2245 fn try_from(tool: crate::completion::ToolDefinition) -> Result<Self, Self::Error> {
2246 Ok(Tool::Function(FunctionTool {
2247 name: Some(tool.name),
2248 description: Some(tool.description),
2249 parameters: Some(tool.parameters),
2250 }))
2251 }
2252 }
2253
2254 impl TryFrom<message::ToolChoice> for ToolChoice {
2255 type Error = CompletionError;
2256
2257 fn try_from(tool_choice: message::ToolChoice) -> Result<Self, Self::Error> {
2258 match tool_choice {
2259 message::ToolChoice::Auto => Ok(ToolChoice::Type(ToolChoiceType::Auto)),
2260 message::ToolChoice::None => Ok(ToolChoice::Type(ToolChoiceType::None)),
2261 message::ToolChoice::Required => Ok(ToolChoice::Type(ToolChoiceType::Any)),
2262 message::ToolChoice::Specific { function_names } => {
2263 Ok(ToolChoice::Config(ToolChoiceConfig {
2264 allowed_tools: AllowedTools {
2265 mode: Some(ToolChoiceType::Validated),
2266 tools: Some(function_names),
2267 },
2268 }))
2269 }
2270 }
2271 }
2272 }
2273
2274 #[derive(Clone, Debug, Deserialize, Serialize)]
2276 #[serde(tag = "type", rename_all = "kebab-case")]
2277 pub enum AgentConfig {
2278 Dynamic,
2279 DeepResearch {
2280 #[serde(skip_serializing_if = "Option::is_none")]
2281 thinking_summaries: Option<ThinkingSummaries>,
2282 },
2283 }
2284
2285 #[derive(Clone, Debug, Deserialize, Serialize)]
2287 #[serde(rename_all = "snake_case")]
2288 pub enum MediaResolution {
2289 Low,
2290 Medium,
2291 High,
2292 UltraHigh,
2293 }
2294
2295 #[derive(Clone, Debug, Deserialize, Serialize)]
2301 #[serde(tag = "event_type")]
2302 pub enum InteractionSseEvent {
2303 #[serde(rename = "interaction.created")]
2304 InteractionCreated {
2305 interaction: Interaction,
2306 #[serde(skip_serializing_if = "Option::is_none")]
2307 event_id: Option<String>,
2308 },
2309 #[serde(rename = "interaction.completed")]
2310 InteractionCompleted {
2311 interaction: Interaction,
2312 #[serde(skip_serializing_if = "Option::is_none")]
2313 event_id: Option<String>,
2314 },
2315 #[serde(rename = "interaction.status_update")]
2316 InteractionStatusUpdate {
2317 interaction_id: String,
2318 status: InteractionStatus,
2319 #[serde(skip_serializing_if = "Option::is_none")]
2320 event_id: Option<String>,
2321 },
2322 #[serde(rename = "step.start")]
2323 StepStart {
2324 index: i32,
2325 step: Step,
2326 #[serde(skip_serializing_if = "Option::is_none")]
2327 event_id: Option<String>,
2328 },
2329 #[serde(rename = "step.delta")]
2330 StepDelta {
2331 index: i32,
2332 delta: ContentDelta,
2333 #[serde(skip_serializing_if = "Option::is_none")]
2334 event_id: Option<String>,
2335 },
2336 #[serde(rename = "step.stop")]
2337 StepStop {
2338 index: i32,
2339 #[serde(skip_serializing_if = "Option::is_none")]
2340 event_id: Option<String>,
2341 },
2342 #[serde(rename = "error")]
2343 Error {
2344 error: ErrorEvent,
2345 #[serde(skip_serializing_if = "Option::is_none")]
2346 event_id: Option<String>,
2347 },
2348 }
2349
2350 #[derive(Clone, Debug, Deserialize, Serialize)]
2352 pub struct ErrorEvent {
2353 pub code: String,
2354 pub message: String,
2355 }
2356
2357 #[derive(Clone, Debug, Deserialize, Serialize)]
2359 #[serde(tag = "type", rename_all = "snake_case")]
2360 pub enum ContentDelta {
2361 Text(TextDelta),
2362 Image(ImageDelta),
2363 Audio(AudioDelta),
2364 Document(DocumentDelta),
2365 Video(VideoDelta),
2366 ThoughtSummary(ThoughtSummaryDelta),
2367 ThoughtSignature(ThoughtSignatureDelta),
2368 FunctionCall(FunctionCallDelta),
2369 FunctionResult(FunctionResultDelta),
2370 CodeExecutionCall(CodeExecutionCallDelta),
2371 CodeExecutionResult(CodeExecutionResultDelta),
2372 UrlContextCall(UrlContextCallDelta),
2373 UrlContextResult(UrlContextResultDelta),
2374 GoogleSearchCall(GoogleSearchCallDelta),
2375 GoogleSearchResult(GoogleSearchResultDelta),
2376 McpServerToolCall(McpServerToolCallDelta),
2377 McpServerToolResult(McpServerToolResultDelta),
2378 FileSearchResult(FileSearchResultDelta),
2379 }
2380
2381 #[derive(Clone, Debug, Deserialize, Serialize)]
2383 pub struct TextDelta {
2384 #[serde(skip_serializing_if = "Option::is_none")]
2385 pub text: Option<String>,
2386 #[serde(skip_serializing_if = "Option::is_none")]
2387 pub annotations: Option<Vec<Annotation>>,
2388 }
2389
2390 #[derive(Clone, Debug, Deserialize, Serialize)]
2392 pub struct ImageDelta {
2393 #[serde(skip_serializing_if = "Option::is_none")]
2394 pub data: Option<String>,
2395 #[serde(skip_serializing_if = "Option::is_none")]
2396 pub uri: Option<String>,
2397 #[serde(skip_serializing_if = "Option::is_none")]
2398 pub mime_type: Option<String>,
2399 #[serde(skip_serializing_if = "Option::is_none")]
2400 pub resolution: Option<MediaResolution>,
2401 }
2402
2403 #[derive(Clone, Debug, Deserialize, Serialize)]
2405 pub struct AudioDelta {
2406 #[serde(skip_serializing_if = "Option::is_none")]
2407 pub data: Option<String>,
2408 #[serde(skip_serializing_if = "Option::is_none")]
2409 pub uri: Option<String>,
2410 #[serde(skip_serializing_if = "Option::is_none")]
2411 pub mime_type: Option<String>,
2412 }
2413
2414 #[derive(Clone, Debug, Deserialize, Serialize)]
2416 pub struct DocumentDelta {
2417 #[serde(skip_serializing_if = "Option::is_none")]
2418 pub data: Option<String>,
2419 #[serde(skip_serializing_if = "Option::is_none")]
2420 pub uri: Option<String>,
2421 #[serde(skip_serializing_if = "Option::is_none")]
2422 pub mime_type: Option<String>,
2423 }
2424
2425 #[derive(Clone, Debug, Deserialize, Serialize)]
2427 pub struct VideoDelta {
2428 #[serde(skip_serializing_if = "Option::is_none")]
2429 pub data: Option<String>,
2430 #[serde(skip_serializing_if = "Option::is_none")]
2431 pub uri: Option<String>,
2432 #[serde(skip_serializing_if = "Option::is_none")]
2433 pub mime_type: Option<String>,
2434 #[serde(skip_serializing_if = "Option::is_none")]
2435 pub resolution: Option<MediaResolution>,
2436 }
2437
2438 #[derive(Clone, Debug, Deserialize, Serialize)]
2440 pub struct ThoughtSummaryDelta {
2441 pub content: ThoughtSummaryContent,
2442 }
2443
2444 #[derive(Clone, Debug, Deserialize, Serialize)]
2446 pub struct ThoughtSignatureDelta {
2447 pub signature: String,
2448 }
2449
2450 #[derive(Clone, Debug, Deserialize, Serialize)]
2452 pub struct FunctionCallDelta {
2453 #[serde(skip_serializing_if = "Option::is_none")]
2454 pub name: Option<String>,
2455 #[serde(skip_serializing_if = "Option::is_none")]
2456 pub arguments: Option<Value>,
2457 #[serde(skip_serializing_if = "Option::is_none")]
2458 pub id: Option<String>,
2459 }
2460
2461 #[derive(Clone, Debug, Deserialize, Serialize)]
2463 pub struct FunctionResultDelta {
2464 #[serde(skip_serializing_if = "Option::is_none")]
2465 pub name: Option<String>,
2466 #[serde(skip_serializing_if = "Option::is_none")]
2467 pub result: Option<Value>,
2468 #[serde(skip_serializing_if = "Option::is_none")]
2469 pub call_id: Option<String>,
2470 #[serde(skip_serializing_if = "Option::is_none")]
2471 pub is_error: Option<bool>,
2472 }
2473
2474 #[derive(Clone, Debug, Deserialize, Serialize)]
2476 pub struct CodeExecutionCallDelta {
2477 #[serde(skip_serializing_if = "Option::is_none")]
2478 pub arguments: Option<CodeExecutionCallArguments>,
2479 #[serde(skip_serializing_if = "Option::is_none")]
2480 pub id: Option<String>,
2481 }
2482
2483 #[derive(Clone, Debug, Deserialize, Serialize)]
2485 pub struct CodeExecutionResultDelta {
2486 #[serde(skip_serializing_if = "Option::is_none")]
2487 pub result: Option<String>,
2488 #[serde(skip_serializing_if = "Option::is_none")]
2489 pub is_error: Option<bool>,
2490 #[serde(skip_serializing_if = "Option::is_none")]
2491 pub signature: Option<String>,
2492 #[serde(skip_serializing_if = "Option::is_none")]
2493 pub call_id: Option<String>,
2494 }
2495
2496 #[derive(Clone, Debug, Deserialize, Serialize)]
2498 pub struct UrlContextCallDelta {
2499 #[serde(skip_serializing_if = "Option::is_none")]
2500 pub arguments: Option<UrlContextCallArguments>,
2501 #[serde(skip_serializing_if = "Option::is_none")]
2502 pub id: Option<String>,
2503 }
2504
2505 #[derive(Clone, Debug, Deserialize, Serialize)]
2507 pub struct UrlContextResultDelta {
2508 #[serde(skip_serializing_if = "Option::is_none")]
2509 pub result: Option<Vec<UrlContextResult>>,
2510 #[serde(skip_serializing_if = "Option::is_none")]
2511 pub signature: Option<String>,
2512 #[serde(skip_serializing_if = "Option::is_none")]
2513 pub is_error: Option<bool>,
2514 #[serde(skip_serializing_if = "Option::is_none")]
2515 pub call_id: Option<String>,
2516 }
2517
2518 #[derive(Clone, Debug, Deserialize, Serialize)]
2520 pub struct GoogleSearchCallDelta {
2521 #[serde(skip_serializing_if = "Option::is_none")]
2522 pub arguments: Option<GoogleSearchCallArguments>,
2523 #[serde(skip_serializing_if = "Option::is_none")]
2524 pub id: Option<String>,
2525 }
2526
2527 #[derive(Clone, Debug, Deserialize, Serialize)]
2529 pub struct GoogleSearchResultDelta {
2530 #[serde(skip_serializing_if = "Option::is_none")]
2531 pub result: Option<Vec<GoogleSearchResult>>,
2532 #[serde(skip_serializing_if = "Option::is_none")]
2533 pub signature: Option<String>,
2534 #[serde(skip_serializing_if = "Option::is_none")]
2535 pub is_error: Option<bool>,
2536 #[serde(skip_serializing_if = "Option::is_none")]
2537 pub call_id: Option<String>,
2538 }
2539
2540 #[derive(Clone, Debug, Deserialize, Serialize)]
2542 pub struct McpServerToolCallDelta {
2543 #[serde(skip_serializing_if = "Option::is_none")]
2544 pub name: Option<String>,
2545 #[serde(skip_serializing_if = "Option::is_none")]
2546 pub server_name: Option<String>,
2547 #[serde(skip_serializing_if = "Option::is_none")]
2548 pub arguments: Option<Value>,
2549 #[serde(skip_serializing_if = "Option::is_none")]
2550 pub id: Option<String>,
2551 }
2552
2553 #[derive(Clone, Debug, Deserialize, Serialize)]
2555 pub struct McpServerToolResultDelta {
2556 #[serde(skip_serializing_if = "Option::is_none")]
2557 pub name: Option<String>,
2558 #[serde(skip_serializing_if = "Option::is_none")]
2559 pub server_name: Option<String>,
2560 #[serde(skip_serializing_if = "Option::is_none")]
2561 pub result: Option<Value>,
2562 #[serde(skip_serializing_if = "Option::is_none")]
2563 pub call_id: Option<String>,
2564 }
2565
2566 #[derive(Clone, Debug, Deserialize, Serialize)]
2568 pub struct FileSearchResultDelta {
2569 #[serde(skip_serializing_if = "Option::is_none")]
2570 pub result: Option<Vec<FileSearchResult>>,
2571 }
2572}
2573
2574#[cfg(test)]
2575mod tests {
2576 use super::*;
2577 use crate::OneOrMany;
2578 use crate::completion::{CompletionRequest, Message};
2579 use crate::message::{self, ToolChoice as MessageToolChoice};
2580 use serde_json::json;
2581
2582 #[test]
2583 fn test_create_request_body_simple() {
2584 let prompt = Message::User {
2585 content: OneOrMany::one(message::UserContent::text("Hello")),
2586 };
2587
2588 let request = CompletionRequest {
2589 record_telemetry_content: false,
2590 model: None,
2591 preamble: Some("Be precise.".to_string()),
2592 chat_history: OneOrMany::one(prompt),
2593 documents: vec![],
2594 tools: vec![],
2595 temperature: Some(0.7),
2596 max_tokens: Some(128),
2597 tool_choice: Some(MessageToolChoice::Required),
2598 additional_params: None,
2599 output_schema: None,
2600 };
2601
2602 let result = create_request_body("gemini-2.5-flash".to_string(), request, Some(false))
2603 .expect("request should build");
2604
2605 assert_eq!(result.model.as_deref(), Some("gemini-2.5-flash"));
2606 assert!(result.agent.is_none());
2607 assert_eq!(result.stream, Some(false));
2608 assert_eq!(result.system_instruction.as_deref(), Some("Be precise."));
2609
2610 let config = result.generation_config.expect("generation config missing");
2611 assert_eq!(config.temperature, Some(0.7));
2612 assert_eq!(config.max_output_tokens, Some(128));
2613 assert!(matches!(
2614 config.tool_choice,
2615 Some(ToolChoice::Type(ToolChoiceType::Any))
2616 ));
2617
2618 let InteractionInput::Steps(steps) = result.input else {
2619 panic!("expected steps input");
2620 };
2621 assert_eq!(steps.len(), 1);
2622 let Step::UserInput { content: contents } = &steps[0] else {
2623 panic!("expected user input step");
2624 };
2625 assert_eq!(contents.len(), 1);
2626 match &contents[0] {
2627 Content::Text(TextContent { text, .. }) => assert_eq!(text, "Hello"),
2628 other => panic!("unexpected content: {other:?}"),
2629 }
2630 }
2631
2632 #[test]
2633 fn test_tool_result_requires_call_id() {
2634 let content = message::UserContent::ToolResult(message::ToolResult {
2635 id: "get_weather".to_string(),
2636 call_id: None,
2637 content: OneOrMany::one(message::ToolResultContent::text("ok")),
2638 });
2639
2640 let err = Content::try_from(content).expect_err("should require call_id");
2641 assert!(format!("{err}").contains("call_id"));
2642 }
2643
2644 #[test]
2645 fn test_tool_result_preserves_text_and_json_types() {
2646 let content = message::UserContent::ToolResult(message::ToolResult {
2647 id: "get_weather".to_string(),
2648 call_id: Some("call-123".to_string()),
2649 content: OneOrMany::many(vec![
2650 message::ToolResultContent::text(r#"{"status":"literal"}"#),
2651 message::ToolResultContent::json(json!({ "status": "structured" })),
2652 ])
2653 .expect("tool result content is non-empty"),
2654 });
2655
2656 let converted = Content::try_from(content).expect("tool result should convert");
2657 let Content::FunctionResult(result) = converted else {
2658 panic!("expected function result");
2659 };
2660 let expected_result = json!([
2661 {
2662 "type": "text",
2663 "text": "{\"status\":\"literal\"}"
2664 },
2665 {
2666 "type": "text",
2667 "text": "{\"status\":\"structured\"}"
2668 }
2669 ]);
2670 assert_eq!(result.result, Some(expected_result.clone()));
2671 assert_eq!(
2672 serde_json::to_value(Content::FunctionResult(result))
2673 .expect("function result should serialize"),
2674 json!({
2675 "type": "function_result",
2676 "name": "get_weather",
2677 "result": expected_result,
2678 "call_id": "call-123"
2679 })
2680 );
2681 }
2682
2683 #[test]
2684 fn test_tool_result_text_and_json_singletons_remain_scalar() {
2685 let cases = [
2686 (
2687 message::ToolResultContent::text(r#"{"status":"literal"}"#),
2688 json!("{\"status\":\"literal\"}"),
2689 ),
2690 (
2691 message::ToolResultContent::json(json!({ "status": "structured" })),
2692 json!({ "status": "structured" }),
2693 ),
2694 (
2695 message::ToolResultContent::json(json!("structured string")),
2696 json!("structured string"),
2697 ),
2698 ];
2699
2700 for (tool_content, expected) in cases {
2701 let content = message::UserContent::ToolResult(message::ToolResult {
2702 id: "get_weather".to_string(),
2703 call_id: Some("call-123".to_string()),
2704 content: OneOrMany::one(tool_content),
2705 });
2706
2707 let Content::FunctionResult(result) =
2708 Content::try_from(content).expect("tool result should convert")
2709 else {
2710 panic!("expected function result");
2711 };
2712 assert_eq!(result.result, Some(expected));
2713 }
2714 }
2715
2716 #[test]
2717 fn test_tool_result_rich_singletons_use_tagged_content() {
2718 let cases = [
2719 (
2720 message::ToolResultContent::json(json!(["sunny", 72])),
2721 json!([{
2722 "type": "text",
2723 "text": "[\"sunny\",72]"
2724 }]),
2725 ),
2726 (
2727 message::ToolResultContent::image_base64(
2728 "image-data",
2729 Some(message::ImageMediaType::PNG),
2730 None,
2731 ),
2732 json!([{
2733 "type": "image",
2734 "data": "image-data",
2735 "mime_type": "image/png"
2736 }]),
2737 ),
2738 ];
2739
2740 for (tool_content, expected) in cases {
2741 let content = message::UserContent::ToolResult(message::ToolResult {
2742 id: "get_weather".to_string(),
2743 call_id: Some("call-123".to_string()),
2744 content: OneOrMany::one(tool_content),
2745 });
2746
2747 let Content::FunctionResult(result) =
2748 Content::try_from(content).expect("tool result should convert")
2749 else {
2750 panic!("expected function result");
2751 };
2752 assert_eq!(result.result, Some(expected));
2753 }
2754 }
2755
2756 #[test]
2757 fn test_tool_result_images_and_text_serialize_as_ordered_tagged_content() {
2758 let tool_result = message::UserContent::ToolResult(message::ToolResult {
2759 id: "render".to_string(),
2760 call_id: Some("call-image".to_string()),
2761 content: OneOrMany::many(vec![
2762 message::ToolResultContent::image_base64(
2763 "first-image",
2764 Some(message::ImageMediaType::PNG),
2765 None,
2766 ),
2767 message::ToolResultContent::text("between-images"),
2768 message::ToolResultContent::Image(message::Image {
2769 data: message::DocumentSourceKind::Url(
2770 "https://example.com/second.jpg".to_string(),
2771 ),
2772 media_type: Some(message::ImageMediaType::JPEG),
2773 detail: None,
2774 additional_params: None,
2775 }),
2776 ])
2777 .expect("tool result content should be non-empty"),
2778 });
2779 let request = CompletionRequest {
2780 record_telemetry_content: false,
2781 model: None,
2782 preamble: None,
2783 chat_history: OneOrMany::one(Message::User {
2784 content: OneOrMany::one(tool_result),
2785 }),
2786 documents: vec![],
2787 tools: vec![],
2788 temperature: None,
2789 max_tokens: None,
2790 tool_choice: None,
2791 additional_params: None,
2792 output_schema: None,
2793 };
2794
2795 let request = create_request_body("gemini-2.5-flash".to_string(), request, None)
2796 .expect("request should build");
2797 let serialized = serde_json::to_value(request).expect("request should serialize");
2798
2799 assert_eq!(
2800 serialized.pointer("/input/0/content/0"),
2801 Some(&json!({
2802 "type": "function_result",
2803 "name": "render",
2804 "result": [
2805 {
2806 "type": "image",
2807 "data": "first-image",
2808 "mime_type": "image/png"
2809 },
2810 {
2811 "type": "text",
2812 "text": "between-images"
2813 },
2814 {
2815 "type": "image",
2816 "uri": "https://example.com/second.jpg",
2817 "mime_type": "image/jpeg"
2818 }
2819 ],
2820 "call_id": "call-image"
2821 }))
2822 );
2823 }
2824
2825 #[test]
2826 fn test_response_function_call_mapping() {
2827 let interaction = Interaction {
2828 id: "interaction-1".to_string(),
2829 steps: vec![Step::FunctionCall(FunctionCallContent {
2830 name: Some("get_weather".to_string()),
2831 arguments: Some(json!({"location": "Paris"})),
2832 id: Some("call-123".to_string()),
2833 })],
2834 usage: Some(InteractionUsage {
2835 total_input_tokens: Some(5),
2836 total_output_tokens: Some(7),
2837 total_tokens: Some(12),
2838 }),
2839 ..Default::default()
2840 };
2841
2842 let response: completion::CompletionResponse<Interaction> =
2843 interaction.try_into().expect("conversion should succeed");
2844
2845 let choice = response.choice.first();
2846 match choice {
2847 completion::AssistantContent::ToolCall(tool_call) => {
2848 assert_eq!(tool_call.function.name, "get_weather");
2849 assert_eq!(tool_call.call_id.as_deref(), Some("call-123"));
2850 }
2851 other => panic!("unexpected content: {other:?}"),
2852 }
2853
2854 assert_eq!(response.usage.input_tokens, 5);
2855 assert_eq!(response.usage.output_tokens, 7);
2856 assert_eq!(response.usage.total_tokens, 12);
2857 }
2858
2859 #[test]
2860 fn test_google_search_tool_serialization() {
2861 let tool = Tool::GoogleSearch;
2862 let value = serde_json::to_value(tool).expect("tool should serialize");
2863 assert_eq!(value, json!({ "type": "google_search" }));
2864 }
2865
2866 #[test]
2867 fn test_url_context_tool_serialization() {
2868 let tool = Tool::UrlContext;
2869 let value = serde_json::to_value(tool).expect("tool should serialize");
2870 assert_eq!(value, json!({ "type": "url_context" }));
2871 }
2872
2873 #[test]
2874 fn test_code_execution_tool_serialization() {
2875 let tool = Tool::CodeExecution;
2876 let value = serde_json::to_value(tool).expect("tool should serialize");
2877 assert_eq!(value, json!({ "type": "code_execution" }));
2878 }
2879
2880 #[test]
2881 fn test_google_search_helpers() {
2882 let interaction = Interaction {
2883 steps: vec![
2884 Step::GoogleSearchCall(GoogleSearchCallContent {
2885 arguments: Some(GoogleSearchCallArguments {
2886 queries: Some(vec!["query-one".to_string(), "query-two".to_string()]),
2887 }),
2888 id: Some("call-1".to_string()),
2889 }),
2890 Step::GoogleSearchResult(GoogleSearchResultContent {
2891 result: Some(vec![GoogleSearchResult {
2892 url: Some("https://example.com".to_string()),
2893 title: Some("Example One".to_string()),
2894 rendered_content: None,
2895 }]),
2896 signature: None,
2897 is_error: None,
2898 call_id: Some("call-1".to_string()),
2899 }),
2900 Step::GoogleSearchCall(GoogleSearchCallContent {
2901 arguments: Some(GoogleSearchCallArguments {
2902 queries: Some(vec!["query-three".to_string()]),
2903 }),
2904 id: Some("call-2".to_string()),
2905 }),
2906 Step::GoogleSearchResult(GoogleSearchResultContent {
2907 result: Some(vec![GoogleSearchResult {
2908 url: Some("https://example.org".to_string()),
2909 title: Some("Example Two".to_string()),
2910 rendered_content: None,
2911 }]),
2912 signature: None,
2913 is_error: None,
2914 call_id: Some("call-2".to_string()),
2915 }),
2916 ],
2917 ..Default::default()
2918 };
2919
2920 let exchanges = interaction.google_search_exchanges();
2921 assert_eq!(exchanges.len(), 2);
2922 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2923 assert_eq!(
2924 exchanges[0].queries(),
2925 vec!["query-one".to_string(), "query-two".to_string()]
2926 );
2927 let exchange_results = exchanges[0].result_items();
2928 assert_eq!(exchange_results.len(), 1);
2929 assert_eq!(exchange_results[0].title.as_deref(), Some("Example One"));
2930
2931 assert_eq!(exchanges[1].call_id.as_deref(), Some("call-2"));
2932 assert_eq!(exchanges[1].queries(), vec!["query-three".to_string()]);
2933 let exchange_results = exchanges[1].result_items();
2934 assert_eq!(exchange_results.len(), 1);
2935 assert_eq!(exchange_results[0].title.as_deref(), Some("Example Two"));
2936
2937 let queries = interaction.google_search_queries();
2938 assert_eq!(queries, vec!["query-one", "query-two", "query-three"]);
2939
2940 let results = interaction.google_search_results();
2941 assert_eq!(results.len(), 2);
2942 assert_eq!(results[0].title.as_deref(), Some("Example One"));
2943 assert_eq!(results[1].title.as_deref(), Some("Example Two"));
2944
2945 let call_contents = interaction.google_search_call_contents();
2946 assert_eq!(call_contents.len(), 2);
2947 assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
2948 assert_eq!(call_contents[1].id.as_deref(), Some("call-2"));
2949
2950 let result_contents = interaction.google_search_result_contents();
2951 assert_eq!(result_contents.len(), 2);
2952 assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
2953 assert_eq!(result_contents[1].call_id.as_deref(), Some("call-2"));
2954 }
2955
2956 #[test]
2957 fn test_google_search_helpers_without_call_id() {
2958 let interaction = Interaction {
2959 steps: vec![
2960 Step::GoogleSearchCall(GoogleSearchCallContent {
2961 arguments: Some(GoogleSearchCallArguments {
2962 queries: Some(vec!["query-one".to_string()]),
2963 }),
2964 id: None,
2965 }),
2966 Step::GoogleSearchResult(GoogleSearchResultContent {
2967 result: Some(vec![GoogleSearchResult {
2968 url: Some("https://example.com".to_string()),
2969 title: Some("Example One".to_string()),
2970 rendered_content: None,
2971 }]),
2972 signature: None,
2973 is_error: None,
2974 call_id: None,
2975 }),
2976 Step::GoogleSearchCall(GoogleSearchCallContent {
2977 arguments: Some(GoogleSearchCallArguments {
2978 queries: Some(vec!["query-two".to_string()]),
2979 }),
2980 id: Some("call-2".to_string()),
2981 }),
2982 Step::GoogleSearchResult(GoogleSearchResultContent {
2983 result: Some(vec![GoogleSearchResult {
2984 url: Some("https://example.org".to_string()),
2985 title: Some("Example Two".to_string()),
2986 rendered_content: None,
2987 }]),
2988 signature: None,
2989 is_error: None,
2990 call_id: None,
2991 }),
2992 ],
2993 ..Default::default()
2994 };
2995
2996 let exchanges = interaction.google_search_exchanges();
2997 assert_eq!(exchanges.len(), 2);
2998
2999 let no_id = exchanges
3000 .iter()
3001 .find(|exchange| exchange.call_id.is_none())
3002 .expect("expected no-id exchange");
3003 assert_eq!(no_id.calls.len(), 1);
3004 assert_eq!(no_id.results.len(), 1);
3005
3006 let with_id = exchanges
3007 .iter()
3008 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
3009 .expect("expected call-2 exchange");
3010 assert_eq!(with_id.calls.len(), 1);
3011 assert_eq!(with_id.results.len(), 1);
3012 }
3013
3014 #[test]
3015 fn test_url_context_helpers() {
3016 let interaction = Interaction {
3017 steps: vec![
3018 Step::UrlContextCall(UrlContextCallContent {
3019 arguments: Some(UrlContextCallArguments {
3020 urls: Some(vec![
3021 "https://example.com".to_string(),
3022 "https://example.org".to_string(),
3023 ]),
3024 }),
3025 id: Some("call-1".to_string()),
3026 }),
3027 Step::UrlContextResult(UrlContextResultContent {
3028 result: Some(vec![UrlContextResult {
3029 url: Some("https://example.com".to_string()),
3030 status: Some("success".to_string()),
3031 }]),
3032 signature: None,
3033 is_error: None,
3034 call_id: Some("call-1".to_string()),
3035 }),
3036 ],
3037 ..Default::default()
3038 };
3039
3040 let exchanges = interaction.url_context_exchanges();
3041 assert_eq!(exchanges.len(), 1);
3042 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
3043 assert_eq!(
3044 exchanges[0].urls(),
3045 vec!["https://example.com", "https://example.org"]
3046 );
3047 let results = exchanges[0].result_items();
3048 assert_eq!(results.len(), 1);
3049 assert_eq!(results[0].status.as_deref(), Some("success"));
3050
3051 let urls = interaction.url_context_urls();
3052 assert_eq!(urls, vec!["https://example.com", "https://example.org"]);
3053
3054 let results = interaction.url_context_results();
3055 assert_eq!(results.len(), 1);
3056 assert_eq!(results[0].url.as_deref(), Some("https://example.com"));
3057
3058 let call_contents = interaction.url_context_call_contents();
3059 assert_eq!(call_contents.len(), 1);
3060 assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
3061
3062 let result_contents = interaction.url_context_result_contents();
3063 assert_eq!(result_contents.len(), 1);
3064 assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
3065 }
3066
3067 #[test]
3068 fn test_url_context_helpers_without_call_id() {
3069 let interaction = Interaction {
3070 steps: vec![
3071 Step::UrlContextCall(UrlContextCallContent {
3072 arguments: Some(UrlContextCallArguments {
3073 urls: Some(vec!["https://example.com".to_string()]),
3074 }),
3075 id: None,
3076 }),
3077 Step::UrlContextResult(UrlContextResultContent {
3078 result: Some(vec![UrlContextResult {
3079 url: Some("https://example.com".to_string()),
3080 status: Some("success".to_string()),
3081 }]),
3082 signature: None,
3083 is_error: None,
3084 call_id: None,
3085 }),
3086 Step::UrlContextCall(UrlContextCallContent {
3087 arguments: Some(UrlContextCallArguments {
3088 urls: Some(vec!["https://example.org".to_string()]),
3089 }),
3090 id: Some("call-2".to_string()),
3091 }),
3092 Step::UrlContextResult(UrlContextResultContent {
3093 result: Some(vec![UrlContextResult {
3094 url: Some("https://example.org".to_string()),
3095 status: Some("success".to_string()),
3096 }]),
3097 signature: None,
3098 is_error: None,
3099 call_id: None,
3100 }),
3101 ],
3102 ..Default::default()
3103 };
3104
3105 let exchanges = interaction.url_context_exchanges();
3106 assert_eq!(exchanges.len(), 2);
3107
3108 let no_id = exchanges
3109 .iter()
3110 .find(|exchange| exchange.call_id.is_none())
3111 .expect("expected no-id exchange");
3112 assert_eq!(no_id.calls.len(), 1);
3113 assert_eq!(no_id.results.len(), 1);
3114
3115 let with_id = exchanges
3116 .iter()
3117 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
3118 .expect("expected call-2 exchange");
3119 assert_eq!(with_id.calls.len(), 1);
3120 assert_eq!(with_id.results.len(), 1);
3121 }
3122
3123 #[test]
3124 fn test_code_execution_helpers() {
3125 let interaction = Interaction {
3126 steps: vec![
3127 Step::CodeExecutionCall(CodeExecutionCallContent {
3128 arguments: Some(CodeExecutionCallArguments {
3129 language: Some("python".to_string()),
3130 code: Some("print(2 + 2)".to_string()),
3131 }),
3132 id: Some("call-1".to_string()),
3133 }),
3134 Step::CodeExecutionResult(CodeExecutionResultContent {
3135 result: Some("4\n".to_string()),
3136 signature: None,
3137 is_error: None,
3138 call_id: Some("call-1".to_string()),
3139 }),
3140 ],
3141 ..Default::default()
3142 };
3143
3144 let exchanges = interaction.code_execution_exchanges();
3145 assert_eq!(exchanges.len(), 1);
3146 assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
3147 assert_eq!(exchanges[0].code_snippets(), vec!["print(2 + 2)"]);
3148 assert_eq!(exchanges[0].outputs(), vec!["4\n"]);
3149
3150 let calls = interaction.code_execution_call_contents();
3151 assert_eq!(calls.len(), 1);
3152 assert_eq!(calls[0].id.as_deref(), Some("call-1"));
3153
3154 let results = interaction.code_execution_result_contents();
3155 assert_eq!(results.len(), 1);
3156 assert_eq!(results[0].call_id.as_deref(), Some("call-1"));
3157
3158 let snippets = interaction.code_execution_snippets();
3159 assert_eq!(snippets, vec!["print(2 + 2)"]);
3160
3161 let outputs = interaction.code_execution_outputs();
3162 assert_eq!(outputs, vec!["4\n"]);
3163 }
3164
3165 #[test]
3166 fn test_code_execution_helpers_without_call_id() {
3167 let interaction = Interaction {
3168 steps: vec![
3169 Step::CodeExecutionCall(CodeExecutionCallContent {
3170 arguments: Some(CodeExecutionCallArguments {
3171 language: Some("python".to_string()),
3172 code: Some("print(1 + 1)".to_string()),
3173 }),
3174 id: None,
3175 }),
3176 Step::CodeExecutionResult(CodeExecutionResultContent {
3177 result: Some("2\n".to_string()),
3178 signature: None,
3179 is_error: None,
3180 call_id: None,
3181 }),
3182 Step::CodeExecutionCall(CodeExecutionCallContent {
3183 arguments: Some(CodeExecutionCallArguments {
3184 language: Some("python".to_string()),
3185 code: Some("print(2 + 2)".to_string()),
3186 }),
3187 id: Some("call-2".to_string()),
3188 }),
3189 Step::CodeExecutionResult(CodeExecutionResultContent {
3190 result: Some("4\n".to_string()),
3191 signature: None,
3192 is_error: None,
3193 call_id: None,
3194 }),
3195 ],
3196 ..Default::default()
3197 };
3198
3199 let exchanges = interaction.code_execution_exchanges();
3200 assert_eq!(exchanges.len(), 2);
3201
3202 let no_id = exchanges
3203 .iter()
3204 .find(|exchange| exchange.call_id.is_none())
3205 .expect("expected no-id exchange");
3206 assert_eq!(no_id.calls.len(), 1);
3207 assert_eq!(no_id.results.len(), 1);
3208
3209 let with_id = exchanges
3210 .iter()
3211 .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
3212 .expect("expected call-2 exchange");
3213 assert_eq!(with_id.calls.len(), 1);
3214 assert_eq!(with_id.results.len(), 1);
3215 }
3216
3217 #[test]
3218 fn test_interaction_status_helpers() {
3219 let mut interaction = Interaction {
3220 status: Some(InteractionStatus::InProgress),
3221 ..Default::default()
3222 };
3223 assert!(!interaction.is_terminal());
3224 assert!(!interaction.is_completed());
3225
3226 interaction.status = Some(InteractionStatus::Completed);
3227 assert!(interaction.is_terminal());
3228 assert!(interaction.is_completed());
3229
3230 interaction.status = Some(InteractionStatus::Failed);
3231 assert!(interaction.is_terminal());
3232 assert!(!interaction.is_completed());
3233
3234 interaction.status = Some(InteractionStatus::BudgetExceeded);
3235 assert!(interaction.is_terminal());
3236 assert!(!interaction.is_completed());
3237 }
3238
3239 #[test]
3240 fn test_budget_exceeded_status_deserializes() {
3241 let status: InteractionStatus = serde_json::from_value(json!("budget_exceeded"))
3242 .expect("budget_exceeded should deserialize");
3243
3244 assert!(matches!(status, InteractionStatus::BudgetExceeded));
3245 assert!(status.is_terminal());
3246 }
3247
3248 #[test]
3249 fn test_budget_exceeded_status_update_deserializes() {
3250 let event: InteractionSseEvent = serde_json::from_value(json!({
3251 "event_type": "interaction.status_update",
3252 "interaction_id": "interaction-123",
3253 "status": "budget_exceeded",
3254 "event_id": "event-456"
3255 }))
3256 .expect("budget_exceeded status update should deserialize");
3257
3258 match event {
3259 InteractionSseEvent::InteractionStatusUpdate {
3260 interaction_id,
3261 status,
3262 event_id,
3263 } => {
3264 assert_eq!(interaction_id, "interaction-123");
3265 assert!(matches!(status, InteractionStatus::BudgetExceeded));
3266 assert!(status.is_terminal());
3267 assert_eq!(event_id.as_deref(), Some("event-456"));
3268 }
3269 other => panic!("expected status update event, got {other:?}"),
3270 }
3271 }
3272
3273 #[test]
3274 fn test_build_interaction_stream_path() {
3275 let path = build_interaction_stream_path("interaction-123", None);
3276 assert_eq!(path, "/v1beta/interactions/interaction-123?stream=true");
3277
3278 let path = build_interaction_stream_path("interaction-123", Some("event-456"));
3279 assert_eq!(
3280 path,
3281 "/v1beta/interactions/interaction-123?stream=true&last_event_id=event-456"
3282 );
3283 }
3284
3285 #[test]
3286 fn test_inline_citations_from_annotations() {
3287 let text_content = TextContent {
3288 text: "Hello world".to_string(),
3289 annotations: Some(vec![
3290 Annotation {
3291 start_index: Some(6),
3292 end_index: Some(11),
3293 source: Some("https://example.com".to_string()),
3294 },
3295 Annotation {
3296 start_index: Some(0),
3297 end_index: Some(5),
3298 source: Some("https://hello.example".to_string()),
3299 },
3300 ]),
3301 };
3302
3303 let cited = text_content.with_inline_citations();
3304 assert_eq!(
3305 cited,
3306 "Hello[1](https://hello.example) world[2](https://example.com)"
3307 );
3308
3309 let interaction = Interaction {
3310 steps: vec![Step::ModelOutput {
3311 content: vec![Content::Text(text_content)],
3312 }],
3313 ..Default::default()
3314 };
3315
3316 let cited_text = interaction.text_with_inline_citations();
3317 assert_eq!(
3318 cited_text.as_deref(),
3319 Some("Hello[1](https://hello.example) world[2](https://example.com)")
3320 );
3321 }
3322}