Skip to main content

zai_rs/model/
chat_base_response.rs

1//! # Chat Response Types
2//!
3//! Defines the standard response structures returned by chat-completion
4//! endpoints, including choices, usage statistics, and task-status tracking
5//! for async operations.
6//!
7//! Notes:
8//! - All fields are optional unless documented otherwise; servers may omit
9//!   fields or return null.
10//! - Some IDs may be numbers on the wire; we normalize them to `String` via
11//!   custom deserializers.
12//! - In non-stream responses, `choices` typically has length 1 unless the API
13//!   supports multi-candidate responses.
14/// Internal helper: Accepts string or number and deserializes into
15/// `Option<String>`.
16///
17/// Why: Some upstream fields (e.g., various `id`/`request_id`) may occasionally
18/// be returned as numbers. This keeps the public structs strongly typed while
19/// maximizing compatibility with heterogeneous payloads.
20use serde::{Deserialize, Deserializer, Serialize};
21use validator::Validate;
22
23// Helper: accept string or number and always deserialize into Option<String>
24fn de_opt_string_from_number_or_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
25where
26    D: Deserializer<'de>,
27{
28    let v = serde_json::Value::deserialize(deserializer)?;
29    match v {
30        serde_json::Value::Null => Ok(None),
31        serde_json::Value::String(s) => Ok(Some(s)),
32        serde_json::Value::Number(n) => Ok(Some(n.to_string())),
33        other => Err(serde::de::Error::custom(format!(
34            "expected string or number, got {}",
35            other
36        ))),
37    }
38}
39
40/// Successful business response (HTTP 200, application/json).
41/// Notes:
42/// - `choices` is often a single element in non-stream mode unless explicitly
43///   requested otherwise.
44/// - `id`/`request_id` are normalized to `String` even if the server returns
45///   numbers.
46/// - `usage` is typically present only after completion (not during streaming).
47
48#[derive(Clone, Serialize, Deserialize, Validate, Default)]
49#[serde(default)]
50pub struct ChatCompletionResponse {
51    /// Task ID
52    #[serde(
53        skip_serializing_if = "Option::is_none",
54        deserialize_with = "de_opt_string_from_number_or_string"
55    )]
56    pub id: Option<String>,
57
58    /// Request ID
59    #[serde(
60        skip_serializing_if = "Option::is_none",
61        deserialize_with = "de_opt_string_from_number_or_string"
62    )]
63    pub request_id: Option<String>,
64
65    /// Request created time, Unix timestamp (seconds)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub created: Option<u64>,
68
69    /// Model name
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub model: Option<String>,
72
73    /// Model response list
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub choices: Option<Vec<Choice>>,
76
77    /// Token usage statistics at the end of the call
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub usage: Option<Usage>,
80
81    /// Video generation results
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub video_result: Option<Vec<VideoResultItem>>,
84
85    /// Information related to web search, returned when using
86    /// WebSearchToolSchema
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub web_search: Option<Vec<WebSearchInfo>>,
89
90    /// Content safety related information
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub content_filter: Option<Vec<ContentFilterInfo>>,
93    /// Processing status of the task. One of: PROCESSING (处理中), SUCCESS
94    /// (成功), FAIL (失败). Note: When PROCESSING, the final result needs
95    /// to be retrieved via a subsequent query.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub task_status: Option<TaskStatus>,
98}
99
100impl std::fmt::Debug for ChatCompletionResponse {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match serde_json::to_string_pretty(self) {
103            Ok(s) => f.write_str(&s),
104            Err(_) => f.debug_struct("ChatCompletionResponse").finish(),
105        }
106    }
107}
108/// Task processing status.
109/// Values correspond to upstream payload strings.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum TaskStatus {
112    #[serde(rename = "PROCESSING", alias = "processing")]
113    Processing,
114    #[serde(rename = "SUCCESS", alias = "success")]
115    Success,
116    #[serde(rename = "FAIL", alias = "fail")]
117    Fail,
118}
119impl TaskStatus {
120    pub fn as_str(&self) -> &'static str {
121        match self {
122            TaskStatus::Processing => "PROCESSING",
123            TaskStatus::Success => "SUCCESS",
124            TaskStatus::Fail => "FAIL",
125        }
126    }
127}
128
129impl std::fmt::Display for TaskStatus {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.write_str(self.as_str())
132    }
133}
134
135/// One choice item in the response.
136#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
137pub struct Choice {
138    /// Index of this result
139    pub index: i32,
140
141    /// Message content
142    pub message: Message,
143
144    /// Why generation finished
145
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub finish_reason: Option<String>,
148}
149
150/// Notes:
151/// - Depending on the model/mode, only one of `content`, `audio`, or
152///   `tool_calls` may be set.
153/// - Prefer `content` for final text; `reasoning_content` may contain internal
154///   traces (when available).
155///
156/// Assistant message payload
157#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
158pub struct Message {
159    /// Role of the message, defaults to "assistant"
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub role: Option<String>,
162
163    /// Current dialog content.
164    /// If function/tool calling is used, this may be null; otherwise contains
165    /// the inference result. For some models, content may include thinking
166    /// traces within `<think>` tags, with final output outside.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub content: Option<serde_json::Value>,
169
170    /// Reasoning chain content (only for specific models)
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub reasoning_content: Option<String>,
173
174    /// Audio payload for voice models (glm-4-voice)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub audio: Option<AudioContent>,
177
178    /// Generated tool/function calls
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub tool_calls: Option<Vec<ToolCallMessage>>,
181}
182
183/// Tool/function call description inside message
184/// Notes:
185/// - When `function` is present, `type` is typically "function"; `mcp` is used
186///   for MCP calls.
187/// - `id` is normalized to `String` (server may return numbers).
188
189#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
190pub struct ToolCallMessage {
191    #[serde(
192        skip_serializing_if = "Option::is_none",
193        deserialize_with = "de_opt_string_from_number_or_string"
194    )]
195    pub id: Option<String>,
196    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
197    pub type_: Option<String>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub function: Option<ToolFunction>,
200    /// MCP tool call payload (when type indicates MCP)
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub mcp: Option<MCPMessage>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
206pub struct ToolFunction {
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub name: Option<String>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub arguments: Option<String>,
211}
212
213/// MCP tool call payload
214#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
215pub struct MCPMessage {
216    /// Unique id of this MCP tool call
217    #[serde(
218        skip_serializing_if = "Option::is_none",
219        deserialize_with = "de_opt_string_from_number_or_string"
220    )]
221    pub id: Option<String>,
222    /// Tool call type: mcp_list_tools, mcp_call
223    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
224    pub type_: Option<MCPCallType>,
225    /// MCP server label
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub server_label: Option<String>,
228    /// Error message if any
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub error: Option<String>,
231
232    /// Tool list when type = mcp_list_tools
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub tools: Option<Vec<MCPTool>>,
235
236    /// Tool call arguments (JSON string) when type = mcp_call
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub arguments: Option<String>,
239    /// Tool name when type = mcp_call
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub name: Option<String>,
242    /// Tool returned output when type = mcp_call
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub output: Option<serde_json::Value>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "snake_case")]
249pub enum MCPCallType {
250    McpListTools,
251    McpCall,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
255pub struct MCPTool {
256    /// Tool name
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub name: Option<String>,
259    /// Tool description
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub description: Option<String>,
262    /// Tool annotations
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub annotations: Option<serde_json::Value>,
265    /// Tool input schema
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub input_schema: Option<MCPInputSchema>,
268}
269#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
270pub struct MCPInputSchema {
271    /// Fixed value 'object'
272    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
273    pub type_: Option<MCPInputType>,
274    /// Parameter properties definition
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub properties: Option<serde_json::Value>,
277    /// Required property list
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub required: Option<Vec<String>>,
280    /// Whether additional properties are allowed
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub additional_properties: Option<bool>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286/// Input schema type for MCP tools.
287/// Currently only `object` is observed; kept as an enum for forward
288/// compatibility.
289#[serde(rename_all = "lowercase")]
290pub enum MCPInputType {
291    Object,
292}
293
294/// Audio content returned for voice models.
295/// Notes:
296/// - `data` is base64-encoded audio bytes (e.g., WAV/MP3) — decode before
297///   saving/playing.
298/// - `id` and `expires_at` are normalized to `String` and may be numeric on the
299///   wire.
300
301#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
302pub struct AudioContent {
303    /// Audio content id, can be used for multi-turn inputs
304    #[serde(
305        skip_serializing_if = "Option::is_none",
306        deserialize_with = "de_opt_string_from_number_or_string"
307    )]
308    pub id: Option<String>,
309    /// Base64 encoded audio data
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub data: Option<String>,
312    /// Expiration time for the audio content
313    #[serde(
314        skip_serializing_if = "Option::is_none",
315        deserialize_with = "de_opt_string_from_number_or_string"
316    )]
317    pub expires_at: Option<String>,
318}
319
320/// Token usage statistics.
321/// Notes:
322/// - `total_tokens` ≈ `prompt_tokens` + `completion_tokens`.
323/// - Some providers omit `usage` in streaming chunks; expect it mainly in the
324///   final response.
325/// - `prompt_tokens_details.cached_tokens` often indicates KV-cache hits or
326///   reused tokens.
327
328#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
329pub struct Usage {
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub prompt_tokens: Option<u32>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub completion_tokens: Option<u32>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub total_tokens: Option<u32>,
336    /// Details for prompt tokens (e.g., cached tokens count)
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub prompt_tokens_details: Option<PromptTokensDetails>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
342/// Details for how prompt tokens were accounted.
343/// Fields here are provider-specific and may expand in the future.
344pub struct PromptTokensDetails {
345    /// Number of tokens hit by cache
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub cached_tokens: Option<u32>,
348}
349
350/// Web search item returned by the service.
351/// Notes:
352/// - `link` and media URLs may be temporary; consider downloading or caching if
353///   needed.
354/// - Fields are optional and may vary by search provider/source.
355
356#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
357pub struct WebSearchInfo {
358    /// Source website icon
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub icon: Option<String>,
361    /// Search result title
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub title: Option<String>,
364    /// Search result page link
365    #[serde(skip_serializing_if = "Option::is_none")]
366    #[validate(url)]
367    pub link: Option<String>,
368    /// Media source name of the page
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub media: Option<String>,
371    /// Publish date on the website
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub publish_date: Option<String>,
374    /// Quoted text content from the search result page
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub content: Option<String>,
377    /// Corner mark sequence number
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub refer: Option<String>,
380}
381
382/// Video generation result item.
383/// Notes:
384/// - URLs may be temporary; fetch/save promptly if you need persistence.
385/// - Some providers deliver video asynchronously; this URL may point to a
386///   job/result resource.
387
388#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
389pub struct VideoResultItem {
390    /// Video link
391    #[serde(skip_serializing_if = "Option::is_none")]
392    #[validate(url)]
393    pub url: Option<String>,
394    /// Cover image link
395    #[serde(skip_serializing_if = "Option::is_none")]
396    #[validate(url)]
397    pub cover_image_url: Option<String>,
398}
399
400/// Content safety information item.
401/// Notes:
402/// - Use `role` + `level` to decide block/warn/allow strategies.
403/// - Providers may add categories or additional fields in the future.
404
405#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
406pub struct ContentFilterInfo {
407    /// Stage where the safety check applies: assistant (model inference), user
408    /// (user input), history (context)
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub role: Option<String>,
411
412    /// Severity level 0-3 (0 most severe, 3 minor)
413    #[serde(skip_serializing_if = "Option::is_none")]
414    #[validate(range(min = 0, max = 3))]
415    pub level: Option<i32>,
416}
417
418// Getter implementations
419impl ChatCompletionResponse {
420    pub fn id(&self) -> Option<&str> {
421        self.id.as_deref()
422    }
423    pub fn request_id(&self) -> Option<&str> {
424        self.request_id.as_deref()
425    }
426    pub fn created(&self) -> Option<u64> {
427        self.created
428    }
429    pub fn model(&self) -> Option<&str> {
430        self.model.as_deref()
431    }
432    pub fn choices(&self) -> Option<&[Choice]> {
433        self.choices.as_deref()
434    }
435    pub fn usage(&self) -> Option<&Usage> {
436        self.usage.as_ref()
437    }
438    pub fn video_result(&self) -> Option<&[VideoResultItem]> {
439        self.video_result.as_deref()
440    }
441    pub fn web_search(&self) -> Option<&[WebSearchInfo]> {
442        self.web_search.as_deref()
443    }
444    pub fn content_filter(&self) -> Option<&[ContentFilterInfo]> {
445        self.content_filter.as_deref()
446    }
447    pub fn task_status(&self) -> Option<&TaskStatus> {
448        self.task_status.as_ref()
449    }
450}
451
452impl Choice {
453    pub fn index(&self) -> i32 {
454        self.index
455    }
456    pub fn message(&self) -> &Message {
457        &self.message
458    }
459    pub fn finish_reason(&self) -> Option<&str> {
460        self.finish_reason.as_deref()
461    }
462}
463
464impl Message {
465    pub fn role(&self) -> Option<&str> {
466        self.role.as_deref()
467    }
468    pub fn content(&self) -> Option<&serde_json::Value> {
469        self.content.as_ref()
470    }
471    pub fn reasoning_content(&self) -> Option<&str> {
472        self.reasoning_content.as_deref()
473    }
474    pub fn audio(&self) -> Option<&AudioContent> {
475        self.audio.as_ref()
476    }
477    pub fn tool_calls(&self) -> Option<&[ToolCallMessage]> {
478        self.tool_calls.as_deref()
479    }
480}
481
482impl ToolCallMessage {
483    pub fn id(&self) -> Option<&str> {
484        self.id.as_deref()
485    }
486    pub fn type_(&self) -> Option<&str> {
487        self.type_.as_deref()
488    }
489    pub fn function(&self) -> Option<&ToolFunction> {
490        self.function.as_ref()
491    }
492    pub fn mcp(&self) -> Option<&MCPMessage> {
493        self.mcp.as_ref()
494    }
495}
496
497impl ToolFunction {
498    pub fn name(&self) -> Option<&str> {
499        self.name.as_deref()
500    }
501    pub fn arguments(&self) -> Option<&str> {
502        self.arguments.as_deref()
503    }
504}
505
506impl MCPMessage {
507    pub fn id(&self) -> Option<&str> {
508        self.id.as_deref()
509    }
510    pub fn type_(&self) -> Option<&MCPCallType> {
511        self.type_.as_ref()
512    }
513    pub fn server_label(&self) -> Option<&str> {
514        self.server_label.as_deref()
515    }
516    pub fn error(&self) -> Option<&str> {
517        self.error.as_deref()
518    }
519    pub fn tools(&self) -> Option<&[MCPTool]> {
520        self.tools.as_deref()
521    }
522    pub fn arguments(&self) -> Option<&str> {
523        self.arguments.as_deref()
524    }
525    pub fn name(&self) -> Option<&str> {
526        self.name.as_deref()
527    }
528    pub fn output(&self) -> Option<&serde_json::Value> {
529        self.output.as_ref()
530    }
531}
532
533impl MCPTool {
534    pub fn name(&self) -> Option<&str> {
535        self.name.as_deref()
536    }
537    pub fn description(&self) -> Option<&str> {
538        self.description.as_deref()
539    }
540    pub fn annotations(&self) -> Option<&serde_json::Value> {
541        self.annotations.as_ref()
542    }
543    pub fn input_schema(&self) -> Option<&MCPInputSchema> {
544        self.input_schema.as_ref()
545    }
546}
547
548impl MCPInputSchema {
549    pub fn type_(&self) -> Option<&MCPInputType> {
550        self.type_.as_ref()
551    }
552    pub fn properties(&self) -> Option<&serde_json::Value> {
553        self.properties.as_ref()
554    }
555    pub fn required(&self) -> Option<&[String]> {
556        self.required.as_deref()
557    }
558    pub fn additional_properties(&self) -> Option<bool> {
559        self.additional_properties
560    }
561}
562
563impl AudioContent {
564    pub fn id(&self) -> Option<&str> {
565        self.id.as_deref()
566    }
567    pub fn data(&self) -> Option<&str> {
568        self.data.as_deref()
569    }
570    pub fn expires_at(&self) -> Option<&str> {
571        self.expires_at.as_deref()
572    }
573}
574
575impl Usage {
576    pub fn prompt_tokens(&self) -> Option<u32> {
577        self.prompt_tokens
578    }
579    pub fn completion_tokens(&self) -> Option<u32> {
580        self.completion_tokens
581    }
582    pub fn total_tokens(&self) -> Option<u32> {
583        self.total_tokens
584    }
585    pub fn prompt_tokens_details(&self) -> Option<&PromptTokensDetails> {
586        self.prompt_tokens_details.as_ref()
587    }
588}
589
590impl PromptTokensDetails {
591    pub fn cached_tokens(&self) -> Option<u32> {
592        self.cached_tokens
593    }
594}
595
596impl WebSearchInfo {
597    pub fn icon(&self) -> Option<&str> {
598        self.icon.as_deref()
599    }
600    pub fn title(&self) -> Option<&str> {
601        self.title.as_deref()
602    }
603    pub fn link(&self) -> Option<&str> {
604        self.link.as_deref()
605    }
606    pub fn media(&self) -> Option<&str> {
607        self.media.as_deref()
608    }
609    pub fn publish_date(&self) -> Option<&str> {
610        self.publish_date.as_deref()
611    }
612    pub fn content(&self) -> Option<&str> {
613        self.content.as_deref()
614    }
615    pub fn refer(&self) -> Option<&str> {
616        self.refer.as_deref()
617    }
618}
619
620impl VideoResultItem {
621    pub fn url(&self) -> Option<&str> {
622        self.url.as_deref()
623    }
624    pub fn cover_image_url(&self) -> Option<&str> {
625        self.cover_image_url.as_deref()
626    }
627}
628
629impl ContentFilterInfo {
630    pub fn role(&self) -> Option<&str> {
631        self.role.as_deref()
632    }
633    pub fn level(&self) -> Option<i32> {
634        self.level
635    }
636}