1use serde_json::{Map, Value, json};
15use url::form_urlencoded;
16
17use crate::completion::{CompletionRequest, Media, Replay, ReplayTarget};
18use crate::error::EncodeError;
19use crate::message::{
20 AssistantContent, DocumentData, DocumentMediaType, DocumentSourceKind as Source, Message,
21 MimeType, ToolChoice as Choice, ToolResultContent, UserContent,
22};
23use crate::providers::internal::wire_ids::WireIds;
24use crate::telemetry::GenAiOperation;
25use crate::wire::{Descriptor, Mode};
26
27pub mod streaming;
29
30use super::completion::PROVIDER_NAME;
31
32pub const DEEP_RESEARCH_PREVIEW_04_2026: &str = "deep-research-preview-04-2026";
36pub const DEEP_RESEARCH_MAX_PREVIEW_04_2026: &str = "deep-research-max-preview-04-2026";
39
40const API: crate::message::Api = crate::message::Api::from_static("gemini.interactions");
42
43const BLOB_SLOTS: &[&str] = &["/signature"];
45
46#[non_exhaustive]
49#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
50#[serde(untagged)]
51pub enum InteractionSpeechConfig {
52 Voice([InteractionSpeech; 1]),
54 Speakers {
57 speakers: Vec<InteractionSpeech>,
59 },
60}
61
62impl InteractionSpeechConfig {
63 pub fn voice(voice: InteractionSpeech) -> Self {
65 Self::Voice([voice])
66 }
67
68 pub fn speakers<S: Into<String>, V: Into<String>>(
70 speakers: impl IntoIterator<Item = (S, V)>,
71 ) -> Self {
72 Self::Speakers {
73 speakers: speakers
74 .into_iter()
75 .map(|(speaker, voice)| InteractionSpeech::voice(voice).speaker(speaker))
76 .collect(),
77 }
78 }
79}
80
81#[non_exhaustive]
83#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
84pub struct InteractionSpeech {
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub voice: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub language: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub speaker: Option<String>,
94}
95
96impl InteractionSpeech {
97 pub fn voice(voice: impl Into<String>) -> Self {
99 Self {
100 voice: Some(voice.into()),
101 ..Self::default()
102 }
103 }
104
105 pub fn language(mut self, language: impl Into<String>) -> Self {
107 self.language = Some(language.into());
108 self
109 }
110
111 pub fn speaker(mut self, speaker: impl Into<String>) -> Self {
113 self.speaker = Some(speaker.into());
114 self
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
121pub struct Interactions {
122 pub provider: crate::providers::gemini::GeminiConfig,
124 pub model: String,
126 #[serde(skip)]
128 pub facts: crate::catalog::ModelFacts,
129}
130
131impl Interactions {
132 pub fn new(provider: crate::providers::gemini::GeminiConfig, model: impl Into<String>) -> Self {
134 Self {
135 provider,
136 model: model.into(),
137 facts: crate::catalog::ModelFacts::default(),
138 }
139 }
140
141 pub fn with_facts(mut self, facts: crate::catalog::ModelFacts) -> Self {
144 self.facts = facts;
145 self
146 }
147}
148
149impl crate::wire::Wire for Interactions {
150 type Op = crate::operation::Completion;
151 type Payload = crate::wire::Encoded;
152 type Frame = crate::wire::WireFrame;
153 type Event = streaming::InteractionsEvent;
154 type Decoder<'id> = streaming::InteractionsDecoder;
155 type Reassembler = streaming::document::Interaction;
156
157 fn describe(&self) -> Descriptor<'_> {
158 Descriptor::new(PROVIDER_NAME)
159 .model(self.model.as_str())
160 .facts(&self.facts)
161 .telemetry(|_| GenAiOperation::Chat)
162 .replay(self)
163 }
164
165 fn encode(
166 &self,
167 request: CompletionRequest,
168 mode: Mode,
169 ) -> Result<crate::wire::Encoded, EncodeError> {
170 use crate::providers::internal::LogTarget;
171 let streaming = matches!(mode, Mode::Streaming);
174 let body = create_request_body(self, &request, Some(streaming))?;
175 let (path, framing, target) = match streaming {
176 true => (
177 "/v1beta/interactions?alt=sse",
178 crate::wire::Framing::Sse,
179 LogTarget::Streaming,
180 ),
181 false => (
182 "/v1beta/interactions",
183 crate::wire::Framing::Whole,
184 LogTarget::Completions,
185 ),
186 };
187 crate::providers::internal::trace_json(
188 target,
189 "Gemini interactions completion request",
190 &body,
191 );
192 let request = http::Request::post(self.provider.interactions_uri(path))
193 .header("Content-Type", "application/json")
194 .header(
195 crate::providers::gemini::GeminiConfig::INTERACTIONS_KEY_HEADER,
196 self.provider.api_key.expose(),
197 )
198 .body(body.into_body())?;
199 Ok(crate::wire::Encoded::new(request, framing))
201 }
202
203 fn decoder<'id>(&self) -> Self::Decoder<'id> {
204 streaming::InteractionsDecoder::default()
205 }
206}
207
208impl ReplayTarget for Interactions {
209 fn map_options(
211 &self,
212 request: &CompletionRequest,
213 fields: crate::completion::options::OptionFields<'_>,
214 ) -> crate::completion::options::OptionMap {
215 let model = request.model.as_deref().unwrap_or(&self.model);
216 super::options::interactions(&self.facts, model, fields)
217 }
218
219 fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
220 Some(&self.facts)
221 }
222
223 fn api(&self) -> crate::message::Api {
224 API
225 }
226
227 fn provider(&self) -> &str {
228 PROVIDER_NAME
229 }
230
231 fn model(&self) -> &str {
232 &self.model
233 }
234
235 fn accepts(&self, model: &str) -> crate::completion::Accepts {
237 super::completion::accepts(model)
238 }
239
240 fn encodes(&self, _model: &str, media: Media<'_>) -> bool {
241 encodes(media)
242 }
243
244 fn continues_stored(&self, request: &CompletionRequest) -> bool {
247 crate::completion::options::param(self, request, "previous_interaction_id")
248 .is_some_and(|id| !id.is_null())
249 }
250
251 fn normalize_tool_call_id(
252 &self,
253 id: &str,
254 _model: &str,
255 _: Option<&crate::message::Origin>,
256 ) -> String {
257 crate::providers::internal::wire_ids::legal_call_id(id, 64)
258 }
259
260 fn later_system(&self, _model: &str) -> crate::completion::LaterSystem {
263 crate::completion::LaterSystem::Leading
264 }
265
266 fn call_id_slot(&self) -> Option<&'static str> {
267 Some("/id")
268 }
269
270 fn blob_slots(&self) -> &'static [&'static str] {
271 BLOB_SLOTS
272 }
273
274 fn identity(&self, item: &Value) -> Map<String, Value> {
276 item.get("id")
277 .map(|id| Map::from_iter([("id".to_owned(), id.clone())]))
278 .unwrap_or_default()
279 }
280}
281
282fn encodes(media: Media<'_>) -> bool {
286 let carried = |source: &Source| matches!(source, Source::Url(_) | Source::Base64(_));
287 match media {
288 Media::Image(image, place) => {
289 super::completion::reads_image(image.media_type.as_ref(), place) && carried(&image.data)
290 }
291 Media::Audio(audio) => audio.media_type.is_some() && carried(&audio.data),
292 Media::Video(video) => video.media_type.is_some() && carried(&video.data),
293 Media::Document(document) => match (&document.media_type, &document.data) {
294 (_, DocumentData::Text(_)) => true,
295 (Some(DocumentMediaType::PDF), DocumentData::File(data)) => carried(data),
296 _ => false,
297 },
298 }
299}
300
301#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
305pub struct InteractionResume {
306 pub provider: crate::providers::gemini::GeminiConfig,
308 pub interaction_id: String,
310 pub last_event_id: Option<String>,
313 #[serde(skip)]
316 pub facts: crate::catalog::ModelFacts,
317}
318
319impl InteractionResume {
320 pub fn new(
322 provider: crate::providers::gemini::GeminiConfig,
323 interaction_id: impl Into<String>,
324 ) -> Self {
325 Self {
326 provider,
327 interaction_id: interaction_id.into(),
328 last_event_id: None,
329 facts: crate::catalog::ModelFacts::default(),
330 }
331 }
332
333 pub fn with_facts(mut self, facts: crate::catalog::ModelFacts) -> Self {
335 self.facts = facts;
336 self
337 }
338
339 pub fn after_event(mut self, last_event_id: impl Into<String>) -> Self {
341 self.last_event_id = Some(last_event_id.into());
342 self
343 }
344}
345
346impl crate::wire::Wire for InteractionResume {
347 type Op = crate::operation::Completion;
348 type Payload = crate::wire::Encoded;
349 type Frame = crate::wire::WireFrame;
350 type Event = streaming::InteractionsEvent;
351 type Decoder<'id> = streaming::InteractionsDecoder;
352 type Reassembler = streaming::document::Interaction;
353
354 fn describe(&self) -> Descriptor<'_> {
358 Descriptor::new(PROVIDER_NAME)
359 .facts(&self.facts)
360 .telemetry(|_| GenAiOperation::Chat)
361 .replay(self)
362 }
363
364 fn encode(
369 &self,
370 request: CompletionRequest,
371 mode: Mode,
372 ) -> Result<crate::wire::Encoded, EncodeError> {
373 let params = crate::completion::options::request_params(
374 self,
375 &request,
376 |input| {
380 if input.raw_tools()?.is_empty() {
381 Ok(Map::new())
382 } else {
383 Err(EncodeError::request(
384 "a resumed interaction takes no `additional_params.tools` or provider \
385 tools: it is read, not created",
386 ))
387 }
388 },
389 crate::completion::options::RawAt::Top,
390 &[],
391 )?;
392 if !params.is_empty() {
393 return Err(EncodeError::request(
394 "a resumed interaction takes no provider options or `additional_params`: it is \
395 read, not created",
396 ));
397 }
398 let id = &self.interaction_id;
399 let (path, framing) = match mode {
400 Mode::Unary => (
401 format!("/v1beta/interactions/{id}"),
402 crate::wire::Framing::Whole,
403 ),
404 Mode::Streaming => {
405 let mut query = form_urlencoded::Serializer::new(String::new());
406 query.append_pair("stream", "true");
407 if let Some(last_event_id) = &self.last_event_id {
408 query.append_pair("last_event_id", last_event_id);
409 }
410 let path = format!("/v1beta/interactions/{id}?{}&alt=sse", query.finish());
411 (path, crate::wire::Framing::Sse)
412 }
413 };
414 let request = http::Request::get(self.provider.interactions_uri(&path))
415 .header(
416 crate::providers::gemini::GeminiConfig::INTERACTIONS_KEY_HEADER,
417 self.provider.api_key.expose(),
418 )
419 .body(crate::wire::Body::empty())?;
420 Ok(crate::wire::Encoded::new(request, framing))
421 }
422
423 fn decoder<'id>(&self) -> Self::Decoder<'id> {
424 streaming::InteractionsDecoder::resuming(self.interaction_id.as_str())
425 }
426}
427
428impl ReplayTarget for InteractionResume {
429 fn map_options(
432 &self,
433 _request: &CompletionRequest,
434 fields: crate::completion::options::OptionFields<'_>,
435 ) -> crate::completion::options::OptionMap {
436 crate::providers::internal::refuse_all(
437 fields,
438 "a resumed interaction is read, not created; its options were fixed when it was \
439 created",
440 )
441 }
442
443 fn facts(&self) -> Option<&crate::catalog::ModelFacts> {
444 Some(&self.facts)
445 }
446
447 fn api(&self) -> crate::message::Api {
448 API
449 }
450
451 fn provider(&self) -> &str {
452 PROVIDER_NAME
453 }
454
455 fn model(&self) -> &str {
456 ""
457 }
458
459 fn accepts(&self, model: &str) -> crate::completion::Accepts {
460 super::completion::accepts(model)
461 }
462
463 fn normalize_tool_call_id(
464 &self,
465 id: &str,
466 _model: &str,
467 _: Option<&crate::message::Origin>,
468 ) -> String {
469 crate::providers::internal::wire_ids::legal_call_id(id, 64)
470 }
471
472 fn later_system(&self, _model: &str) -> crate::completion::LaterSystem {
475 crate::completion::LaterSystem::Leading
476 }
477
478 fn call_id_slot(&self) -> Option<&'static str> {
479 Some("/id")
480 }
481
482 fn blob_slots(&self) -> &'static [&'static str] {
483 BLOB_SLOTS
484 }
485}
486
487pub(crate) fn create_request_body(
501 wire: &Interactions,
502 request: &CompletionRequest,
503 stream: Option<bool>,
504) -> Result<crate::completion::options::FinalBody, EncodeError> {
505 let rewrites: Vec<_> = stream
506 .map(crate::completion::options::Rewrite::Stream)
507 .into_iter()
508 .collect();
509 crate::completion::options::request_params(
510 wire,
511 request,
512 |input| create_base(wire, request, input),
513 crate::completion::options::RawAt::Top,
514 &rewrites,
515 )
516}
517
518fn create_base(
521 wire: &Interactions,
522 request: &CompletionRequest,
523 input: &mut crate::completion::options::BaseInput<'_>,
524) -> Result<Map<String, Value>, EncodeError> {
525 let model = request.model.clone().unwrap_or_else(|| wire.model.clone());
526 let set = |key: &str| input.param(key).is_some_and(|value| !value.is_null());
527 let agent = set("agent");
528 if set("response_format") && !set("response_mime_type") {
529 return Err(EncodeError::request(
530 "response_mime_type is required when response_format is set",
531 ));
532 }
533 let choice = request.tool_choice.clone().map(|choice| match choice {
534 Choice::Auto => json!("auto"),
535 Choice::None => json!("none"),
536 Choice::Required => json!("any"),
537 Choice::Specific { function_names } => {
538 json!({ "allowed_tools": { "mode": "validated", "tools": function_names } })
539 }
540 });
541 let config = crate::providers::internal::set_fields([
542 ("temperature", request.temperature.map(Value::from)),
543 ("max_output_tokens", request.max_tokens.map(Value::from)),
544 ("tool_choice", choice),
545 ]);
546 let mut tools: Vec<Value> = request
547 .tools
548 .iter()
549 .map(|tool| json!({ "type": "function", "name": tool.name, "description": tool.description, "parameters": tool.parameters }))
550 .collect();
551 tools.extend(input.raw_tools()?);
552 let (mut system, mut history) = (Vec::new(), Vec::new());
553 for message in &request.chat_history {
554 match &**message {
555 Message::System { content } => system.push(content.as_str()),
556 message => history.push(message),
557 }
558 }
559 let mut body = Map::new();
560 if !config.is_empty() {
561 body.insert("generation_config".to_owned(), Value::Object(config));
562 }
563 if !tools.is_empty() {
564 body.insert("tools".to_owned(), Value::Array(tools));
565 }
566 if !system.is_empty() {
567 body.insert("system_instruction".to_owned(), json!(system.join("\n\n")));
568 }
569 if !agent {
570 body.insert("model".to_owned(), json!(model));
571 }
572 body.insert(
573 "input".to_owned(),
574 Value::Array(steps(&history, wire, &model)?),
575 );
576 Ok(body)
577}
578
579fn steps(
583 history: &[&Message],
584 target: &dyn ReplayTarget,
585 model: &str,
586) -> Result<Vec<Value>, EncodeError> {
587 let ids = WireIds::for_target(history, target, model);
588 let mut steps = Vec::new();
589 for message in history {
590 match message {
591 Message::System { content } => {
592 steps.push(json!({ "type": "user_input", "content": [{ "type": "text", "text": content }] }));
593 }
594 Message::User { content } => {
595 let mut run = Vec::new();
596 for part in content {
597 let UserContent::ToolResult(result) = part else {
598 run.push(user_content(part)?);
599 continue;
600 };
601 if !run.is_empty() {
602 steps.push(user_input(std::mem::take(&mut run)));
603 }
604 let value = match result.content.as_slice() {
605 [ToolResultContent::Text(text)] => Value::from(text.text.as_str()),
606 [
610 ToolResultContent::Json {
611 value: value @ (Value::String(_) | Value::Object(_)),
612 },
613 ] => value.clone(),
614 [ToolResultContent::Json { value }] => json!({ "result": value }),
615 contents => Value::Array(
616 contents
617 .iter()
618 .map(result_content)
619 .collect::<Result<_, _>>()?,
620 ),
621 };
622 let mut step = Map::from_iter([
623 ("type".to_owned(), Value::from("function_result")),
624 ("name".to_owned(), Value::from(result.name.as_str())),
625 ("call_id".to_owned(), Value::from(ids.of(&result.call))),
626 ("result".to_owned(), value),
627 ]);
628 if result.is_error {
629 step.insert("is_error".to_owned(), Value::Bool(true));
630 }
631 steps.push(Value::Object(step));
632 }
633 if !run.is_empty() {
634 steps.push(user_input(run));
635 }
636 }
637 Message::Assistant(turn) => {
638 for block in &turn.content {
639 steps.extend(assistant_step(block, target, &ids)?);
640 }
641 }
642 }
643 }
644 Ok(steps)
645}
646
647fn user_input(content: Vec<Value>) -> Value {
649 Value::Object(Map::from_iter([
650 ("type".to_owned(), Value::from("user_input")),
651 ("content".to_owned(), Value::Array(content)),
652 ]))
653}
654
655fn result_content(content: &ToolResultContent) -> Result<Value, EncodeError> {
657 Ok(match content {
658 ToolResultContent::Text(text) => json!({ "type": "text", "text": text.text }),
659 ToolResultContent::Json { value } => json!({ "type": "text", "text": value.to_string() }),
660 ToolResultContent::Image(image) => media("image", image.media_type.clone(), &image.data)?,
661 })
662}
663
664fn user_content(part: &UserContent) -> Result<Value, EncodeError> {
667 match part {
668 UserContent::Text(text) => Ok(json!({ "type": "text", "text": text.text })),
669 UserContent::Image(image) => media("image", image.media_type.clone(), &image.data),
670 UserContent::Audio(audio) => media("audio", audio.media_type.clone(), &audio.data),
671 UserContent::Video(video) => media("video", video.media_type.clone(), &video.data),
672 UserContent::Document(document) => match &document.data {
673 DocumentData::Text(text) => Ok(json!({ "type": "text", "text": text })),
674 DocumentData::File(data) => media("document", document.media_type.clone(), data),
675 },
676 UserContent::ToolResult(_) => {
677 Err(EncodeError::request("a tool result is a step of its own"))
678 }
679 }
680}
681
682fn media<M: MimeType>(
685 kind: &str,
686 media_type: Option<M>,
687 source: &Source,
688) -> Result<Value, EncodeError> {
689 let unsendable = || {
690 EncodeError::request(format!(
691 "Gemini Interactions cannot receive this {kind} in its form"
692 ))
693 };
694 let mime_type = media_type.ok_or_else(unsendable)?.to_mime_type().to_owned();
695 let key = match super::completion::carried(source)? {
696 (true, uri) => ("uri", uri),
697 (false, data) => ("data", data),
698 };
699 Ok(json!({ "type": kind, key.0: key.1, "mime_type": mime_type }))
700}
701
702fn assistant_step(
706 block: &AssistantContent,
707 target: &dyn ReplayTarget,
708 ids: &WireIds,
709) -> Result<Option<Value>, EncodeError> {
710 let identity = match block.replay(target, ids) {
711 Replay::Item(item) => return Ok(Some(item.into_owned())),
712 Replay::Identity(identity) => identity,
713 Replay::Rebuild => Map::new(),
714 };
715 let output = |content: Value| {
716 Value::Object(Map::from_iter([
717 ("type".to_owned(), Value::from("model_output")),
718 ("content".to_owned(), Value::Array(vec![content])),
719 ]))
720 };
721 let mut step = match block {
722 AssistantContent::Text(text) => output(json!({ "type": "text", "text": text.text })),
723 AssistantContent::Reasoning(reasoning)
724 if reasoning.redacted || reasoning.text.trim().is_empty() =>
725 {
726 return Ok(None);
727 }
728 AssistantContent::Reasoning(reasoning) => {
729 json!({ "type": "thought", "summary": [{ "type": "text", "text": reasoning.text }] })
730 }
731 AssistantContent::ToolCall(call) => json!({
732 "type": "function_call",
733 "name": call.function.name,
734 "arguments": call.function.arguments,
735 }),
736 AssistantContent::Image(image) => {
737 output(media("image", image.media_type.clone(), &image.data)?)
738 }
739 AssistantContent::Opaque(opaque) => return Ok(Some(opaque.item.clone())),
740 };
741 if let Some(fields) = step.as_object_mut() {
742 fields.extend(identity);
743 if let AssistantContent::ToolCall(call) = block {
744 fields.insert("id".to_owned(), json!(ids.of(&call.id)));
745 }
746 }
747 Ok(Some(step))
748}
749
750#[cfg(test)]
751mod tests;
752
753#[cfg(test)]
754mod history_tests;