Skip to main content

zai_rs/model/
chat_base_response.rs

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