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