Skip to main content

zai_rs/services/assistants/
response.rs

1//! Typed response models for assistant invocation and discovery endpoints.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    ZaiResult,
7    client::error::{ZaiError, codes},
8};
9
10fn empty_response(operation: &str) -> ZaiError {
11    ZaiError::ApiError {
12        code: codes::SDK_VALIDATION,
13        message: format!("{operation} response contained no documented fields"),
14    }
15}
16
17/// Text content inside a multimodal assistant response.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct AssistantResponseContentPart {
20    /// Content type. The current response schema permits only `text`.
21    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
22    pub type_: Option<AssistantResponseContentType>,
23    /// Text content.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub text: Option<String>,
26}
27
28/// Content type used by a multimodal assistant response part.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum AssistantResponseContentType {
31    /// A text response part.
32    #[serde(rename = "text")]
33    Text,
34}
35
36/// Text or multimodal content returned by an assistant.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum AssistantResponseContent {
40    /// Plain text content.
41    Text(String),
42    /// Multimodal response parts.
43    Parts(Vec<AssistantResponseContentPart>),
44}
45
46/// Audio content returned by a voice-capable assistant.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct AssistantResponseAudio {
49    /// Audio identifier used for follow-up turns.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub id: Option<String>,
52    /// Base64-encoded audio data.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub data: Option<String>,
55    /// Audio expiry timestamp as returned by the API.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub expires_at: Option<String>,
58}
59
60/// Function call returned by an assistant.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct AssistantResponseFunctionCall {
63    /// Function name.
64    pub name: String,
65    /// JSON-formatted function arguments.
66    pub arguments: String,
67}
68
69/// MCP call type returned by an assistant.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum AssistantMcpCallType {
72    /// List the server's available tools.
73    #[serde(rename = "mcp_list_tools")]
74    ListTools,
75    /// Invoke one MCP tool.
76    #[serde(rename = "mcp_call")]
77    Call,
78}
79
80/// JSON-schema type used by an MCP input schema.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum AssistantMcpSchemaType {
83    /// JSON object schema.
84    #[serde(rename = "object")]
85    Object,
86}
87
88/// Input schema advertised by an MCP tool.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct AssistantMcpInputSchema {
91    /// Schema type (`object`).
92    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
93    pub type_: Option<AssistantMcpSchemaType>,
94    /// Open map of property definitions.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub properties: Option<serde_json::Map<String, serde_json::Value>>,
97    /// Required property names.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub required: Option<Vec<String>>,
100    /// Whether undeclared properties are accepted.
101    #[serde(
102        rename = "additionalProperties",
103        skip_serializing_if = "Option::is_none"
104    )]
105    pub additional_properties: Option<bool>,
106}
107
108/// Tool descriptor returned by an MCP list-tools call.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct AssistantMcpTool {
111    /// Tool name.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub name: Option<String>,
114    /// Tool description.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub description: Option<String>,
117    /// Open annotation object.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub annotations: Option<serde_json::Map<String, serde_json::Value>>,
120    /// Tool input schema.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub input_schema: Option<AssistantMcpInputSchema>,
123}
124
125/// MCP payload attached to an assistant tool call.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct AssistantMcpCall {
128    /// MCP call identifier.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub id: Option<String>,
131    /// MCP operation type.
132    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
133    pub type_: Option<AssistantMcpCallType>,
134    /// MCP server label.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub server_label: Option<String>,
137    /// Error returned by the MCP server.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub error: Option<String>,
140    /// Tools returned by a list-tools operation.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub tools: Option<Vec<AssistantMcpTool>>,
143    /// JSON-formatted call arguments.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub arguments: Option<String>,
146    /// Called tool name.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub name: Option<String>,
149    /// Open object returned by the MCP tool.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub output: Option<serde_json::Map<String, serde_json::Value>>,
152}
153
154/// Tool call returned in an assistant response message.
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct AssistantResponseToolCall {
157    /// Function-call payload.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub function: Option<AssistantResponseFunctionCall>,
160    /// MCP-call payload.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub mcp: Option<AssistantMcpCall>,
163    /// Tool-call identifier.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub id: Option<String>,
166    /// Tool type such as `function` or `mcp`.
167    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
168    pub type_: Option<String>,
169}
170
171/// Message returned in an assistant invocation choice.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct AssistantResponseMessage {
174    /// Response role.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub role: Option<String>,
177    /// Text or multimodal response content. JSON `null` maps to `None`.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub content: Option<AssistantResponseContent>,
180    /// Model reasoning content, when returned.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub reasoning_content: Option<String>,
183    /// Voice-model audio payload.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub audio: Option<AssistantResponseAudio>,
186    /// Function or MCP tool calls.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub tool_calls: Option<Vec<AssistantResponseToolCall>>,
189}
190
191/// One assistant invocation choice.
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct AssistantInvokeChoice {
194    /// Choice index.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub index: Option<i64>,
197    /// Assistant message.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub message: Option<AssistantResponseMessage>,
200    /// Completion reason.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub finish_reason: Option<String>,
203}
204
205/// Token usage returned by an assistant invocation.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct AssistantUsage {
208    /// Prompt token count.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub prompt_tokens: Option<i64>,
211    /// Completion token count.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub completion_tokens: Option<i64>,
214    /// Total token count.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub total_tokens: Option<i64>,
217}
218
219/// Response from invoking an assistant.
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221pub struct AssistantInvokeResponse {
222    /// Invocation identifier.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub id: Option<String>,
225    /// Request identifier.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub request_id: Option<String>,
228    /// Creation timestamp.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub created: Option<i64>,
231    /// Model used by the assistant.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub model: Option<String>,
234    /// Assistant choices.
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub choices: Option<Vec<AssistantInvokeChoice>>,
237    /// Token usage.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub usage: Option<AssistantUsage>,
240}
241
242impl AssistantInvokeResponse {
243    /// Enforce the operation contract's non-empty success-response invariant.
244    pub fn validate(&self) -> ZaiResult<()> {
245        if self.id.is_some()
246            || self.request_id.is_some()
247            || self.created.is_some()
248            || self.model.is_some()
249            || self.choices.is_some()
250            || self.usage.is_some()
251        {
252            Ok(())
253        } else {
254            Err(empty_response("assistant invoke"))
255        }
256    }
257}
258
259/// One key/value tag attached to an assistant.
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub struct AssistantTag {
262    /// Tag key.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub key: Option<String>,
265    /// Human-readable tag label.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub label: Option<String>,
268}
269
270/// Assistant record returned by the list endpoint.
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub struct AssistantInfo {
273    /// Assistant identifier.
274    pub assistant_id: String,
275    /// Assistant name.
276    pub name: String,
277    /// Avatar URL.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub avatar: Option<String>,
280    /// Assistant description.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub description: Option<String>,
283    /// Supported tool names.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub tools: Option<Vec<String>>,
286    /// Assistant tags.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub tags: Option<Vec<AssistantTag>>,
289    /// Assistant status.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub status: Option<String>,
292    /// Open starter-prompt objects.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub starter_prompts: Option<Vec<serde_json::Map<String, serde_json::Value>>>,
295    /// Creation time.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub created_at: Option<String>,
298    /// Last update time.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub updated_at: Option<String>,
301}
302
303/// Response containing assistants available to the current account.
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct AssistantListResponse {
306    /// Whether the operation succeeded.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub success: Option<bool>,
309    /// Business status code.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub code: Option<i64>,
312    /// Business status message.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub msg: Option<String>,
315    /// Matching assistants.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub data: Option<Vec<AssistantInfo>>,
318}
319
320impl AssistantListResponse {
321    /// Enforce the operation contract's non-empty success-response invariant.
322    pub fn validate(&self) -> ZaiResult<()> {
323        if self.success.is_some()
324            || self.code.is_some()
325            || self.msg.is_some()
326            || self.data.is_some()
327        {
328            Ok(())
329        } else {
330            Err(empty_response("assistant list"))
331        }
332    }
333}
334
335/// Token usage recorded for one assistant conversation.
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
337pub struct AssistantConversationUsage {
338    /// Prompt token count.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub prompt_tokens: Option<i64>,
341    /// Completion token count.
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub completion_tokens: Option<i64>,
344    /// Total token count.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub total_tokens: Option<i64>,
347}
348
349/// One assistant conversation record.
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351pub struct AssistantConversation {
352    /// Conversation identifier.
353    pub id: String,
354    /// Assistant identifier.
355    pub assistant_id: String,
356    /// Creation time.
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub create_time: Option<String>,
359    /// Last update time.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub update_time: Option<String>,
362    /// Conversation token usage.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub usage: Option<AssistantConversationUsage>,
365}
366
367/// Paginated assistant-conversation payload.
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
369pub struct AssistantConversationPage {
370    /// Assistant identifier.
371    pub assistant_id: String,
372    /// Conversations on this page.
373    pub conversation_list: Vec<AssistantConversation>,
374    /// Whether another page is available.
375    pub has_more: bool,
376}
377
378/// Response containing an assistant's conversations.
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct AssistantConversationListResponse {
381    /// Whether the operation succeeded.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub success: Option<bool>,
384    /// Business status code.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub code: Option<i64>,
387    /// Business status message.
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub msg: Option<String>,
390    /// Paginated conversation data.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub data: Option<AssistantConversationPage>,
393}
394
395impl AssistantConversationListResponse {
396    /// Enforce the operation contract's non-empty success-response invariant.
397    pub fn validate(&self) -> ZaiResult<()> {
398        if self.success.is_some()
399            || self.code.is_some()
400            || self.msg.is_some()
401            || self.data.is_some()
402        {
403            Ok(())
404        } else {
405            Err(empty_response("assistant conversation list"))
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn list_items_preserve_required_fields() {
416        let response: AssistantListResponse = serde_json::from_value(serde_json::json!({
417            "success": true,
418            "code": 200,
419            "msg": "ok",
420            "data": [{"assistant_id": "assistant-1", "name": "Helper"}]
421        }))
422        .unwrap();
423        assert_eq!(
424            response.data.as_ref().unwrap()[0].assistant_id,
425            "assistant-1"
426        );
427        assert!(response.validate().is_ok());
428
429        let missing_name = serde_json::from_value::<AssistantListResponse>(serde_json::json!({
430            "data": [{"assistant_id": "assistant-1"}]
431        }));
432        assert!(missing_name.is_err());
433    }
434
435    #[test]
436    fn empty_top_level_responses_violate_the_operations_contract() {
437        assert!(
438            serde_json::from_value::<AssistantInvokeResponse>(serde_json::json!({}))
439                .unwrap()
440                .validate()
441                .is_err()
442        );
443        assert!(
444            serde_json::from_value::<AssistantListResponse>(serde_json::json!({}))
445                .unwrap()
446                .validate()
447                .is_err()
448        );
449        assert!(
450            serde_json::from_value::<AssistantConversationListResponse>(serde_json::json!({}))
451                .unwrap()
452                .validate()
453                .is_err()
454        );
455    }
456}