Skip to main content

rust_genai_types/
interactions.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5use crate::http::HttpOptions;
6
7/// Interactions 输入(支持文本、内容列表或完整 turns)。
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(untagged)]
10#[allow(clippy::large_enum_variant)]
11pub enum InteractionInput {
12    Text(String),
13    Content(InteractionContent),
14    Contents(Vec<InteractionContent>),
15    Turns(Vec<InteractionTurn>),
16}
17
18impl InteractionInput {
19    #[must_use]
20    pub fn text(text: impl Into<String>) -> Self {
21        Self::Text(text.into())
22    }
23
24    #[must_use]
25    pub const fn content(content: InteractionContent) -> Self {
26        Self::Content(content)
27    }
28
29    #[must_use]
30    pub const fn contents(contents: Vec<InteractionContent>) -> Self {
31        Self::Contents(contents)
32    }
33
34    #[must_use]
35    pub const fn turns(turns: Vec<InteractionTurn>) -> Self {
36        Self::Turns(turns)
37    }
38}
39
40impl From<String> for InteractionInput {
41    fn from(value: String) -> Self {
42        Self::Text(value)
43    }
44}
45
46impl From<&str> for InteractionInput {
47    fn from(value: &str) -> Self {
48        Self::Text(value.to_string())
49    }
50}
51
52/// Interactions 内容(支持多种 type,未知字段保留到 extra)。
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54#[serde(rename_all = "snake_case")]
55pub struct InteractionContent {
56    #[serde(rename = "type")]
57    pub content_type: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub text: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub annotations: Option<Vec<Value>>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub data: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub uri: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub mime_type: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub id: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub arguments: Option<Value>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub result: Option<Value>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub call_id: Option<String>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub is_error: Option<bool>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub code: Option<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub language: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub summary: Option<Value>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub urls: Option<Vec<String>>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub queries: Option<Vec<String>>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub rendered_content: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub title: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub url: Option<String>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub resolution: Option<String>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub signature: Option<String>,
100    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
101    #[serde(flatten)]
102    pub extra: HashMap<String, Value>,
103}
104
105impl InteractionContent {
106    pub fn text(text: impl Into<String>) -> Self {
107        Self {
108            content_type: "text".into(),
109            text: Some(text.into()),
110            ..Default::default()
111        }
112    }
113
114    pub fn image_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
115        Self {
116            content_type: "image".into(),
117            data: Some(data.into()),
118            mime_type: Some(mime_type.into()),
119            ..Default::default()
120        }
121    }
122
123    pub fn audio_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
124        Self {
125            content_type: "audio".into(),
126            data: Some(data.into()),
127            mime_type: Some(mime_type.into()),
128            ..Default::default()
129        }
130    }
131}
132
133/// Interactions Turn。
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub struct InteractionTurn {
137    pub role: String,
138    pub content: InteractionTurnContent,
139    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
140    #[serde(flatten)]
141    pub extra: HashMap<String, Value>,
142}
143
144/// Turn 的 content 字段:可以是 string 或 content 数组。
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(untagged)]
147pub enum InteractionTurnContent {
148    Text(String),
149    Contents(Vec<InteractionContent>),
150}
151
152impl InteractionTurnContent {
153    #[must_use]
154    pub fn text(text: impl Into<String>) -> Self {
155        Self::Text(text.into())
156    }
157
158    #[must_use]
159    pub const fn contents(contents: Vec<InteractionContent>) -> Self {
160        Self::Contents(contents)
161    }
162}
163
164/// Interactions 生成响应模态。
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "lowercase")]
167pub enum InteractionResponseModality {
168    Text,
169    Image,
170    Audio,
171    Video,
172    Document,
173}
174
175/// Thinking level(Interactions 版本,lowercase)。
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(rename_all = "lowercase")]
178pub enum InteractionThinkingLevel {
179    Minimal,
180    Low,
181    Medium,
182    High,
183}
184
185/// Thinking summaries 输出策略。
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "lowercase")]
188pub enum InteractionThinkingSummaries {
189    Auto,
190    #[serde(rename = "none")]
191    NoneValue,
192}
193
194/// Tool choice mode(Interactions)。
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "lowercase")]
197pub enum ToolChoiceType {
198    Auto,
199    Any,
200    #[serde(rename = "none")]
201    NoneValue,
202    Validated,
203}
204
205/// The configuration for allowed tools.
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
207#[serde(rename_all = "snake_case")]
208pub struct AllowedTools {
209    /// The mode of the tool choice.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub mode: Option<ToolChoiceType>,
212    /// The names of the allowed tools.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub tools: Option<Vec<String>>,
215    /// Forward-compatible extension fields.
216    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
217    #[serde(flatten)]
218    pub extra: HashMap<String, Value>,
219}
220
221/// Tool choice config wrapper.
222#[derive(Debug, Clone, Serialize, Deserialize, Default)]
223#[serde(rename_all = "snake_case")]
224pub struct ToolChoiceConfig {
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub allowed_tools: Option<AllowedTools>,
227    /// Forward-compatible extension fields.
228    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
229    #[serde(flatten)]
230    pub extra: HashMap<String, Value>,
231}
232
233/// ToolChoice: a string mode or a config object.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(untagged)]
236pub enum ToolChoice {
237    Type(ToolChoiceType),
238    Config(ToolChoiceConfig),
239}
240
241/// Computer-use tool configuration.
242#[derive(Debug, Clone, Serialize, Deserialize, Default)]
243pub struct ComputerUseTool {
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub environment: Option<String>,
246    #[serde(rename = "excludedPredefinedFunctions")]
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub excluded_predefined_functions: Option<Vec<String>>,
249    /// Forward-compatible extension fields.
250    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
251    #[serde(flatten)]
252    pub extra: HashMap<String, Value>,
253}
254
255/// MCP server tool configuration.
256#[derive(Debug, Clone, Serialize, Deserialize, Default)]
257#[serde(rename_all = "snake_case")]
258pub struct McpServerTool {
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub allowed_tools: Option<Vec<AllowedTools>>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub headers: Option<HashMap<String, String>>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub name: Option<String>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub url: Option<String>,
267    /// Forward-compatible extension fields.
268    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
269    #[serde(flatten)]
270    pub extra: HashMap<String, Value>,
271}
272
273/// File search tool configuration.
274#[derive(Debug, Clone, Serialize, Deserialize, Default)]
275#[serde(rename_all = "snake_case")]
276pub struct FileSearchTool {
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub file_search_store_names: Option<Vec<String>>,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub metadata_filter: Option<String>,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub top_k: Option<i32>,
283    /// Forward-compatible extension fields.
284    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
285    #[serde(flatten)]
286    pub extra: HashMap<String, Value>,
287}
288
289/// Function tool configuration.
290#[derive(Debug, Clone, Serialize, Deserialize, Default)]
291pub struct FunctionTool {
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub description: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub name: Option<String>,
296    /// JSON schema for the function parameters.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub parameters: Option<Value>,
299    /// Forward-compatible extension fields.
300    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
301    #[serde(flatten)]
302    pub extra: HashMap<String, Value>,
303}
304
305/// Interactions tool declarations.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(tag = "type")]
308pub enum Tool {
309    #[serde(rename = "function")]
310    Function(FunctionTool),
311    #[serde(rename = "google_search")]
312    GoogleSearch,
313    #[serde(rename = "code_execution")]
314    CodeExecution,
315    #[serde(rename = "url_context")]
316    UrlContext,
317    #[serde(rename = "computer_use")]
318    ComputerUse(ComputerUseTool),
319    #[serde(rename = "mcp_server")]
320    McpServer(McpServerTool),
321    #[serde(rename = "file_search")]
322    FileSearch(FileSearchTool),
323}
324
325/// The configuration for speech interaction.
326#[derive(Debug, Clone, Serialize, Deserialize, Default)]
327#[serde(rename_all = "snake_case")]
328pub struct SpeechConfig {
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub voice: Option<String>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub language: Option<String>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub speaker: Option<String>,
335    /// Forward-compatible extension fields.
336    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
337    #[serde(flatten)]
338    pub extra: HashMap<String, Value>,
339}
340
341/// The configuration for image interaction.
342#[derive(Debug, Clone, Serialize, Deserialize, Default)]
343#[serde(rename_all = "snake_case")]
344pub struct ImageConfig {
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub aspect_ratio: Option<String>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub image_size: Option<String>,
349    /// Forward-compatible extension fields.
350    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
351    #[serde(flatten)]
352    pub extra: HashMap<String, Value>,
353}
354
355/// Configuration parameters for model interactions.
356#[derive(Debug, Clone, Serialize, Deserialize, Default)]
357#[serde(rename_all = "snake_case")]
358pub struct GenerationConfig {
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub temperature: Option<f32>,
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub top_p: Option<f32>,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub seed: Option<i32>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub stop_sequences: Option<Vec<String>>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub tool_choice: Option<ToolChoice>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub thinking_level: Option<InteractionThinkingLevel>,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub thinking_summaries: Option<InteractionThinkingSummaries>,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub max_output_tokens: Option<i32>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub speech_config: Option<Vec<SpeechConfig>>,
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub image_config: Option<ImageConfig>,
379    /// Forward-compatible extension fields.
380    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
381    #[serde(flatten)]
382    pub extra: HashMap<String, Value>,
383}
384
385/// Configuration for dynamic agents.
386#[derive(Debug, Clone, Serialize, Deserialize, Default)]
387pub struct DynamicAgentConfig {
388    /// Forward-compatible extension fields.
389    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
390    #[serde(flatten)]
391    pub extra: HashMap<String, Value>,
392}
393
394/// Configuration for the Deep Research agent.
395#[derive(Debug, Clone, Serialize, Deserialize, Default)]
396#[serde(rename_all = "snake_case")]
397pub struct DeepResearchAgentConfig {
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub thinking_summaries: Option<InteractionThinkingSummaries>,
400    /// Forward-compatible extension fields.
401    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
402    #[serde(flatten)]
403    pub extra: HashMap<String, Value>,
404}
405
406/// Agent configuration union (discriminator: `type`).
407#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(tag = "type")]
409pub enum AgentConfig {
410    #[serde(rename = "dynamic")]
411    Dynamic(DynamicAgentConfig),
412    #[serde(rename = "deep-research")]
413    DeepResearch(DeepResearchAgentConfig),
414}
415
416/// Create Interaction 配置(请求体)。
417#[derive(Debug, Clone, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub struct CreateInteractionConfig {
420    /// Optional. HTTP request overrides (SDK only).
421    #[serde(skip_serializing, skip_deserializing)]
422    pub http_options: Option<HttpOptions>,
423    /// The name of the `Model` used for generating the interaction.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub model: Option<String>,
426    /// The name of the `Agent` used for generating the interaction.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub agent: Option<String>,
429    pub input: InteractionInput,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub tools: Option<Vec<Tool>>,
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub generation_config: Option<GenerationConfig>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub agent_config: Option<AgentConfig>,
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub background: Option<bool>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub store: Option<bool>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub previous_interaction_id: Option<String>,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub response_format: Option<Value>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub response_mime_type: Option<String>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub response_modalities: Option<Vec<InteractionResponseModality>>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub system_instruction: Option<String>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub stream: Option<bool>,
452}
453
454impl CreateInteractionConfig {
455    pub fn new(model: impl Into<String>, input: impl Into<InteractionInput>) -> Self {
456        Self {
457            http_options: None,
458            model: Some(model.into()),
459            agent: None,
460            input: input.into(),
461            tools: None,
462            generation_config: None,
463            agent_config: None,
464            background: None,
465            store: None,
466            previous_interaction_id: None,
467            response_format: None,
468            response_mime_type: None,
469            response_modalities: None,
470            system_instruction: None,
471            stream: None,
472        }
473    }
474
475    pub fn new_agent(agent: impl Into<String>, input: impl Into<InteractionInput>) -> Self {
476        Self {
477            http_options: None,
478            model: None,
479            agent: Some(agent.into()),
480            input: input.into(),
481            tools: None,
482            generation_config: None,
483            agent_config: None,
484            background: None,
485            store: None,
486            previous_interaction_id: None,
487            response_format: None,
488            response_mime_type: None,
489            response_modalities: None,
490            system_instruction: None,
491            stream: None,
492        }
493    }
494}
495
496/// Get Interaction 配置。
497#[derive(Debug, Clone, Serialize, Deserialize, Default)]
498#[serde(rename_all = "snake_case")]
499pub struct GetInteractionConfig {
500    #[serde(skip_serializing, skip_deserializing)]
501    pub http_options: Option<HttpOptions>,
502    /// If set to true, includes the input in the response.
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub include_input: Option<bool>,
505    /// If set to true, the generated content will be streamed incrementally.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub stream: Option<bool>,
508    /// Optional. Resumes the interaction stream from the next chunk after the event id.
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub last_event_id: Option<String>,
511}
512
513/// Delete Interaction 配置。
514#[derive(Debug, Clone, Serialize, Deserialize, Default)]
515#[serde(rename_all = "snake_case")]
516pub struct DeleteInteractionConfig {
517    #[serde(skip_serializing, skip_deserializing)]
518    pub http_options: Option<HttpOptions>,
519}
520
521/// Cancel Interaction 配置。
522#[derive(Debug, Clone, Serialize, Deserialize, Default)]
523#[serde(rename_all = "snake_case")]
524pub struct CancelInteractionConfig {
525    #[serde(skip_serializing, skip_deserializing)]
526    pub http_options: Option<HttpOptions>,
527}
528
529/// Interaction usage by modality.
530#[derive(Debug, Clone, Serialize, Deserialize, Default)]
531#[serde(rename_all = "snake_case")]
532pub struct InteractionUsageBucket {
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub text: Option<i32>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub image: Option<i32>,
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub audio: Option<i32>,
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub video: Option<i32>,
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub document: Option<i32>,
543    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
544    #[serde(flatten)]
545    pub extra: HashMap<String, Value>,
546}
547
548/// Interaction usage summary.
549#[derive(Debug, Clone, Serialize, Deserialize, Default)]
550#[serde(rename_all = "snake_case")]
551pub struct InteractionUsage {
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub total_input_tokens: Option<i32>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub total_output_tokens: Option<i32>,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub total_thought_tokens: Option<i32>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub total_cached_tokens: Option<i32>,
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub total_tool_use_tokens: Option<i32>,
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub total_tokens: Option<i32>,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub input_tokens_by_modality: Option<Vec<InteractionTokensByModality>>,
566    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
567    #[serde(flatten)]
568    pub extra: HashMap<String, Value>,
569}
570
571/// Token usage by modality.
572#[derive(Debug, Clone, Serialize, Deserialize, Default)]
573#[serde(rename_all = "snake_case")]
574pub struct InteractionTokensByModality {
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub modality: Option<String>,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub tokens: Option<i32>,
579    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
580    #[serde(flatten)]
581    pub extra: HashMap<String, Value>,
582}
583
584/// Interaction resource.
585#[derive(Debug, Clone, Serialize, Deserialize, Default)]
586#[serde(rename_all = "snake_case")]
587pub struct Interaction {
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub id: Option<String>,
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub object: Option<String>,
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub model: Option<String>,
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub agent: Option<String>,
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub role: Option<String>,
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub status: Option<String>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub created: Option<String>,
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub updated: Option<String>,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub input: Option<InteractionInput>,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub outputs: Option<Vec<InteractionContent>>,
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub usage: Option<InteractionUsage>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub previous_interaction_id: Option<String>,
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub background: Option<bool>,
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub store: Option<bool>,
616    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
617    #[serde(flatten)]
618    pub extra: HashMap<String, Value>,
619}
620
621/// SSE 事件载荷。
622#[derive(Debug, Clone, Serialize, Deserialize, Default)]
623#[serde(rename_all = "snake_case")]
624pub struct InteractionSseEvent {
625    #[serde(rename = "event_type", alias = "eventType")]
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub event_type: Option<String>,
628    #[serde(rename = "event_id", alias = "eventId")]
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub event_id: Option<String>,
631    /// The Interaction resource (present for start/complete events).
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub interaction: Option<Interaction>,
634    /// Present for interaction.status_update events.
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub interaction_id: Option<String>,
637    /// Present for interaction.status_update events.
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub status: Option<String>,
640    /// Present for content.* events.
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub index: Option<i32>,
643    /// Present for content.start events.
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub content: Option<InteractionContent>,
646    /// Present for content.delta events.
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub delta: Option<InteractionContent>,
649    /// Present for error events.
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub error: Option<InteractionError>,
652    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
653    #[serde(flatten)]
654    pub extra: HashMap<String, Value>,
655}
656
657/// Backward-compatible alias.
658#[deprecated(note = "Renamed to InteractionSseEvent")]
659pub type InteractionEvent = InteractionSseEvent;
660
661#[derive(Debug, Clone, Serialize, Deserialize, Default)]
662#[serde(rename_all = "snake_case")]
663pub struct InteractionError {
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub code: Option<String>,
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub message: Option<String>,
668    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
669    #[serde(flatten)]
670    pub extra: HashMap<String, Value>,
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    #[test]
678    fn input_text_serializes_as_string() {
679        let input = InteractionInput::text("hello");
680        let json = serde_json::to_string(&input).unwrap();
681        assert_eq!(json, "\"hello\"");
682    }
683
684    #[test]
685    fn input_contents_serializes_as_array() {
686        let input = InteractionInput::contents(vec![InteractionContent::text("hi")]);
687        let value = serde_json::to_value(&input).unwrap();
688        assert!(value.is_array());
689    }
690
691    #[test]
692    fn interaction_event_start_roundtrip() {
693        let json = r#"{
694            "event_type": "interaction.start",
695            "event_id": "evt_1",
696            "interaction": {
697                "id": "int_123",
698                "status": "in_progress"
699            }
700        }"#;
701        let event: InteractionSseEvent = serde_json::from_str(json).unwrap();
702        assert_eq!(event.event_type.as_deref(), Some("interaction.start"));
703        assert_eq!(event.event_id.as_deref(), Some("evt_1"));
704        assert_eq!(
705            event.interaction.as_ref().and_then(|d| d.id.as_deref()),
706            Some("int_123")
707        );
708    }
709
710    #[test]
711    fn interaction_content_helpers() {
712        let image = InteractionContent::image_data("AAAA", "image/png");
713        assert_eq!(image.content_type, "image");
714        assert_eq!(image.mime_type.as_deref(), Some("image/png"));
715
716        let audio = InteractionContent::audio_data("BBBB", "audio/wav");
717        assert_eq!(audio.content_type, "audio");
718        assert_eq!(audio.mime_type.as_deref(), Some("audio/wav"));
719    }
720
721    #[test]
722    fn interaction_input_from_str() {
723        let input: InteractionInput = "hi".into();
724        let json = serde_json::to_string(&input).unwrap();
725        assert_eq!(json, "\"hi\"");
726    }
727
728    #[test]
729    fn create_interaction_config_new_sets_fields() {
730        let config = CreateInteractionConfig::new("model-1", "hello");
731        assert_eq!(config.model.as_deref(), Some("model-1"));
732        match config.input {
733            InteractionInput::Text(value) => assert_eq!(value, "hello"),
734            _ => panic!("expected text input"),
735        }
736    }
737
738    #[test]
739    fn interaction_event_alias_event_type() {
740        let json = r#"{
741            "eventType": "interaction.complete",
742            "eventId": "evt_9",
743            "interaction": { "id": "int_456", "status": "completed" }
744        }"#;
745        let event: InteractionSseEvent = serde_json::from_str(json).unwrap();
746        assert_eq!(event.event_type.as_deref(), Some("interaction.complete"));
747        assert_eq!(event.event_id.as_deref(), Some("evt_9"));
748        assert_eq!(
749            event.interaction.as_ref().and_then(|d| d.id.as_deref()),
750            Some("int_456")
751        );
752    }
753}