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