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