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