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.
14use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
15use validator::Validate;
16
17/// Successful business response (HTTP 200, application/json).
18/// Notes:
19/// - `choices` is often a single element in non-stream mode unless explicitly
20///   requested otherwise.
21/// - `id`/`request_id` are normalized to `String` even if the server returns
22///   numbers.
23/// - `usage` is typically present only after completion (not during streaming).
24
25#[derive(Clone, Serialize, Validate)]
26pub struct ChatCompletionResponse {
27    /// Task ID
28    #[serde(
29        skip_serializing_if = "Option::is_none",
30        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
31    )]
32    pub id: Option<String>,
33
34    /// Request ID
35    #[serde(
36        skip_serializing_if = "Option::is_none",
37        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
38    )]
39    pub request_id: Option<String>,
40
41    /// Request created time, Unix timestamp (seconds)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub created: Option<u64>,
44
45    /// Model name
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub model: Option<String>,
48
49    /// Model response list
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub choices: Option<Vec<Choice>>,
52
53    /// Token usage statistics at the end of the call
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub usage: Option<Usage>,
56
57    /// Video generation results
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub video_result: Option<Vec<VideoResultItem>>,
60
61    /// Information related to web search, returned when using
62    /// WebSearchToolSchema
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub web_search: Option<Vec<WebSearchInfo>>,
65
66    /// Content safety related information
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub content_filter: Option<Vec<ContentFilterInfo>>,
69    /// Processing status of the task. One of: PROCESSING (处理中), SUCCESS
70    /// (成功), FAIL (失败). Note: When PROCESSING, the final result needs
71    /// to be retrieved via a subsequent query.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub task_status: Option<TaskStatus>,
74}
75
76#[derive(Deserialize)]
77struct ChatCompletionResponseWire {
78    #[serde(
79        default,
80        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
81    )]
82    id: Option<String>,
83    #[serde(
84        default,
85        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
86    )]
87    request_id: Option<String>,
88    created: Option<u64>,
89    model: Option<String>,
90    choices: Option<Vec<Choice>>,
91    usage: Option<Usage>,
92    video_result: Option<Vec<VideoResultItem>>,
93    web_search: Option<Vec<WebSearchInfo>>,
94    content_filter: Option<Vec<ContentFilterInfo>>,
95    task_status: Option<TaskStatus>,
96}
97
98impl<'de> Deserialize<'de> for ChatCompletionResponse {
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        let wire = ChatCompletionResponseWire::deserialize(deserializer)?;
104        let response = Self {
105            id: wire.id,
106            request_id: wire.request_id,
107            created: wire.created,
108            model: wire.model,
109            choices: wire.choices,
110            usage: wire.usage,
111            video_result: wire.video_result,
112            web_search: wire.web_search,
113            content_filter: wire.content_filter,
114            task_status: wire.task_status,
115        };
116        if response.id.is_none()
117            && response.request_id.is_none()
118            && response.created.is_none()
119            && response.model.is_none()
120            && response.choices.is_none()
121            && response.usage.is_none()
122            && response.video_result.is_none()
123            && response.web_search.is_none()
124            && response.content_filter.is_none()
125            && response.task_status.is_none()
126        {
127            return Err(D::Error::custom(
128                "chat completion response contained no documented fields",
129            ));
130        }
131        Ok(response)
132    }
133}
134
135impl std::fmt::Debug for ChatCompletionResponse {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match serde_json::to_string_pretty(self) {
138            Ok(s) => f.write_str(&s),
139            Err(_) => f.debug_struct("ChatCompletionResponse").finish(),
140        }
141    }
142}
143/// Task processing status.
144/// Values correspond to upstream payload strings.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[non_exhaustive]
147pub enum TaskStatus {
148    /// Task is still running; poll again to retrieve the final result.
149    #[serde(rename = "PROCESSING", alias = "processing")]
150    Processing,
151    /// Task completed successfully.
152    #[serde(rename = "SUCCESS", alias = "success")]
153    Success,
154    /// Task failed.
155    #[serde(rename = "FAIL", alias = "fail")]
156    Fail,
157    /// An unrecognized status returned by a newer API version. The catch-all
158    /// (`#[serde(other)]`) keeps a single unknown value from failing the whole
159    /// response deserialization; callers should treat it as not-yet-complete
160    /// (keep polling) or surface it, rather than aborting.
161    #[serde(other)]
162    Unknown,
163}
164impl TaskStatus {
165    /// Return the canonical upstream string for this status
166    /// (`"PROCESSING"` / `"SUCCESS"` / `"FAIL"` / `"UNKNOWN"`).
167    pub fn as_str(&self) -> &'static str {
168        match self {
169            TaskStatus::Processing => "PROCESSING",
170            TaskStatus::Success => "SUCCESS",
171            TaskStatus::Fail => "FAIL",
172            TaskStatus::Unknown => "UNKNOWN",
173        }
174    }
175}
176
177impl std::fmt::Display for TaskStatus {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.write_str(self.as_str())
180    }
181}
182
183/// One choice item in the response.
184#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
185pub struct Choice {
186    /// Index of this result
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub index: Option<i32>,
189
190    /// Message content
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub message: Option<Message>,
193
194    /// Why generation finished
195
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub finish_reason: Option<String>,
198}
199
200/// Notes:
201/// - Depending on the model/mode, only one of `content`, `audio`, or
202///   `tool_calls` may be set.
203/// - Prefer `content` for final text; `reasoning_content` may contain internal
204///   traces (when available).
205///
206/// Assistant message payload
207#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
208pub struct Message {
209    /// Role of the message, defaults to "assistant"
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub role: Option<String>,
212
213    /// Current dialog content.
214    /// If function/tool calling is used, this may be null; otherwise contains
215    /// the inference result. For some models, content may include thinking
216    /// traces within `<think>` tags, with final output outside.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub content: Option<MessageContent>,
219
220    /// Reasoning chain content (only for specific models)
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub reasoning_content: Option<String>,
223
224    /// Audio payload for voice models (glm-4-voice)
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub audio: Option<AudioContent>,
227
228    /// Generated tool/function calls
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub tool_calls: Option<Vec<ToolCallMessage>>,
231}
232
233/// Assistant response content in either plain-text or multimodal-parts form.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(untagged)]
236pub enum MessageContent {
237    /// Plain assistant text.
238    Text(String),
239    /// Multimodal response parts.
240    Parts(Vec<MessageContentPart>),
241}
242
243impl MessageContent {
244    /// Borrow plain text when this is the text form.
245    pub fn as_str(&self) -> Option<&str> {
246        match self {
247            Self::Text(text) => Some(text),
248            Self::Parts(_) => None,
249        }
250    }
251
252    /// Borrow multimodal parts when this is the parts form.
253    pub fn as_parts(&self) -> Option<&[MessageContentPart]> {
254        match self {
255            Self::Text(_) => None,
256            Self::Parts(parts) => Some(parts),
257        }
258    }
259}
260
261/// One item in a multimodal assistant response.
262#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
263pub struct MessageContentPart {
264    /// Part kind. The current response schema only defines `text`.
265    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
266    pub type_: Option<MessageContentPartType>,
267    /// Text carried by this part.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub text: Option<String>,
270}
271
272/// Supported multimodal assistant response part kinds.
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "lowercase")]
275pub enum MessageContentPartType {
276    /// Text response part.
277    Text,
278}
279
280/// Tool/function call description inside message
281/// Notes:
282/// - When `function` is present, `type` is typically "function"; `mcp` is used
283///   for MCP calls.
284/// - `id` is normalized to `String` (server may return numbers).
285
286#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
287pub struct ToolCallMessage {
288    /// Unique id of this tool/function call (server may return numbers).
289    #[serde(
290        default,
291        skip_serializing_if = "Option::is_none",
292        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
293    )]
294    pub id: Option<String>,
295    /// Tool call type — typically `"function"` for function calls.
296    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
297    pub type_: Option<String>,
298    /// Function-call payload (name + arguments) when `type` is `"function"`.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub function: Option<ToolFunction>,
301    /// MCP tool call payload (when type indicates MCP)
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub mcp: Option<MCPMessage>,
304}
305
306/// Function-call payload inside a [`ToolCallMessage`].
307#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
308pub struct ToolFunction {
309    /// Name of the function/tool to invoke.
310    pub name: String,
311    /// JSON-encoded arguments to pass to the function.
312    pub arguments: String,
313}
314
315/// MCP tool call payload
316#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
317pub struct MCPMessage {
318    /// Unique id of this MCP tool call
319    #[serde(
320        default,
321        skip_serializing_if = "Option::is_none",
322        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
323    )]
324    pub id: Option<String>,
325    /// Tool call type: mcp_list_tools, mcp_call
326    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
327    pub type_: Option<MCPCallType>,
328    /// MCP server label
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub server_label: Option<String>,
331    /// Error message if any
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub error: Option<String>,
334
335    /// Tool list when type = mcp_list_tools
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub tools: Option<Vec<MCPTool>>,
338
339    /// Tool call arguments (JSON string) when type = mcp_call. A directly
340    /// encoded JSON value is normalized to its compact string representation.
341    #[serde(
342        default,
343        skip_serializing_if = "Option::is_none",
344        deserialize_with = "super::serde_helpers::optional_json_string"
345    )]
346    pub arguments: Option<String>,
347    /// Tool name when type = mcp_call
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub name: Option<String>,
350    /// Tool returned output when type = mcp_call
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub output: Option<serde_json::Value>,
353}
354
355/// MCP tool call type — either a tool-list request or an actual tool
356/// invocation.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358#[non_exhaustive]
359#[serde(rename_all = "snake_case")]
360pub enum MCPCallType {
361    /// Request the server to list its available MCP tools.
362    McpListTools,
363    /// Invoke a specific MCP tool.
364    McpCall,
365    /// An unrecognized `type` returned by a newer API or a different MCP
366    /// transport. Deserialized via `#[serde(other)]` so a novel value no longer
367    /// fails the whole response.
368    #[serde(other)]
369    Unknown,
370}
371
372/// Tool descriptor reported by an MCP server.
373#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
374pub struct MCPTool {
375    /// Tool name
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub name: Option<String>,
378    /// Tool description
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub description: Option<String>,
381    /// Tool annotations
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub annotations: Option<serde_json::Value>,
384    /// Tool input schema
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub input_schema: Option<MCPInputSchema>,
387}
388/// JSON-schema-like input descriptor for an MCP tool.
389#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
390pub struct MCPInputSchema {
391    /// Fixed value 'object'
392    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
393    pub type_: Option<MCPInputType>,
394    /// Parameter properties definition
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub properties: Option<serde_json::Value>,
397    /// Required property list
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub required: Option<Vec<String>>,
400    /// Whether additional properties are allowed
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub additional_properties: Option<bool>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
406/// Input schema type for MCP tools.
407/// Currently only `object` is observed; kept as an enum for forward
408/// compatibility.
409#[non_exhaustive]
410#[serde(rename_all = "lowercase")]
411pub enum MCPInputType {
412    /// JSON-schema `object` type (the only value observed today).
413    Object,
414    /// An unrecognized schema `type` from a newer MCP version. Deserialized via
415    /// `#[serde(other)]` so a novel value no longer fails the whole response.
416    #[serde(other)]
417    Unknown,
418}
419
420/// Audio content returned for voice models.
421/// Notes:
422/// - `data` is base64-encoded audio bytes (e.g., WAV/MP3) — decode before
423///   saving/playing.
424/// - `id` and `expires_at` are normalized to `String` and may be numeric on the
425///   wire.
426
427#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
428pub struct AudioContent {
429    /// Audio content id, can be used for multi-turn inputs
430    #[serde(
431        default,
432        skip_serializing_if = "Option::is_none",
433        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
434    )]
435    pub id: Option<String>,
436    /// Base64 encoded audio data
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub data: Option<String>,
439    /// Expiration time for the audio content
440    #[serde(
441        default,
442        skip_serializing_if = "Option::is_none",
443        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
444    )]
445    pub expires_at: Option<String>,
446}
447
448/// Token usage statistics.
449/// Notes:
450/// - `total_tokens` ≈ `prompt_tokens` + `completion_tokens`.
451/// - Some providers omit `usage` in streaming chunks; expect it mainly in the
452///   final response.
453/// - `prompt_tokens_details.cached_tokens` often indicates KV-cache hits or
454///   reused tokens.
455
456#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
457pub struct Usage {
458    /// Number of tokens in the prompt.
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub prompt_tokens: Option<u32>,
461    /// Number of tokens in the completion.
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub completion_tokens: Option<u32>,
464    /// Total tokens for this request (`prompt` + `completion`).
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub total_tokens: Option<u32>,
467    /// Details for prompt tokens (e.g., cached tokens count)
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub prompt_tokens_details: Option<PromptTokensDetails>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
473/// Details for how prompt tokens were accounted.
474/// Fields here are provider-specific and may expand in the future.
475pub struct PromptTokensDetails {
476    /// Number of tokens hit by cache
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub cached_tokens: Option<u32>,
479}
480
481/// Web search item returned by the service.
482/// Notes:
483/// - `link` and media URLs may be temporary; consider downloading or caching if
484///   needed.
485/// - Fields are optional and may vary by search provider/source.
486
487#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
488pub struct WebSearchInfo {
489    /// Source website icon
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub icon: Option<String>,
492    /// Search result title
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub title: Option<String>,
495    /// Search result page link
496    #[serde(skip_serializing_if = "Option::is_none")]
497    #[validate(url)]
498    pub link: Option<String>,
499    /// Media source name of the page
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub media: Option<String>,
502    /// Publish date on the website
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub publish_date: Option<String>,
505    /// Quoted text content from the search result page
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub content: Option<String>,
508    /// Corner mark sequence number
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub refer: Option<String>,
511}
512
513/// Video generation result item.
514/// Notes:
515/// - URLs may be temporary; fetch/save promptly if you need persistence.
516/// - Some providers deliver video asynchronously; this URL may point to a
517///   job/result resource.
518
519#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
520pub struct VideoResultItem {
521    /// Video link
522    #[serde(skip_serializing_if = "Option::is_none")]
523    #[validate(url)]
524    pub url: Option<String>,
525    /// Cover image link
526    #[serde(skip_serializing_if = "Option::is_none")]
527    #[validate(url)]
528    pub cover_image_url: Option<String>,
529}
530
531/// Content safety information item.
532/// Notes:
533/// - Use `role` + `level` to decide block/warn/allow strategies.
534/// - Providers may add categories or additional fields in the future.
535
536#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
537pub struct ContentFilterInfo {
538    /// Stage where the safety check applies: assistant (model inference), user
539    /// (user input), history (context)
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub role: Option<String>,
542
543    /// Severity level 0-3 (0 most severe, 3 minor)
544    #[serde(skip_serializing_if = "Option::is_none")]
545    #[validate(range(min = 0, max = 3))]
546    pub level: Option<i32>,
547}
548
549// Getter implementations
550impl ChatCompletionResponse {
551    /// Task id (normalized to `&str`).
552    pub fn id(&self) -> Option<&str> {
553        self.id.as_deref()
554    }
555    /// Request id (normalized to `&str`).
556    pub fn request_id(&self) -> Option<&str> {
557        self.request_id.as_deref()
558    }
559    /// Unix timestamp (seconds) at which the request was created.
560    pub fn created(&self) -> Option<u64> {
561        self.created
562    }
563    /// Model name that produced the response.
564    pub fn model(&self) -> Option<&str> {
565        self.model.as_deref()
566    }
567    /// Generated choices (typically one in non-stream mode).
568    pub fn choices(&self) -> Option<&[Choice]> {
569        self.choices.as_deref()
570    }
571    /// Token usage statistics (mainly on the final response).
572    pub fn usage(&self) -> Option<&Usage> {
573        self.usage.as_ref()
574    }
575    /// Video generation result items, if any.
576    pub fn video_result(&self) -> Option<&[VideoResultItem]> {
577        self.video_result.as_deref()
578    }
579    /// Web-search citations, if `web_search` was used.
580    pub fn web_search(&self) -> Option<&[WebSearchInfo]> {
581        self.web_search.as_deref()
582    }
583    /// Content-safety filter results, if any.
584    pub fn content_filter(&self) -> Option<&[ContentFilterInfo]> {
585        self.content_filter.as_deref()
586    }
587    /// Async task status, if this is an async response.
588    pub fn task_status(&self) -> Option<&TaskStatus> {
589        self.task_status.as_ref()
590    }
591}
592
593impl Choice {
594    /// Index of this choice within the `choices` array.
595    pub fn index(&self) -> Option<i32> {
596        self.index
597    }
598    /// The assistant message payload, when returned.
599    pub fn message(&self) -> Option<&Message> {
600        self.message.as_ref()
601    }
602    /// Reason generation finished (e.g. `"stop"`, `"length"`).
603    pub fn finish_reason(&self) -> Option<&str> {
604        self.finish_reason.as_deref()
605    }
606}
607
608impl Message {
609    /// Role of the message (typically `"assistant"`).
610    pub fn role(&self) -> Option<&str> {
611        self.role.as_deref()
612    }
613    /// Dialog content (may include `<think>` traces for some models).
614    pub fn content(&self) -> Option<&MessageContent> {
615        self.content.as_ref()
616    }
617    /// Return the assistant text when `content` is a JSON string; `None`
618    /// otherwise (absent, null, or the array-of-parts form). Convenience over
619    /// [`content`](Self::content) for the common case where the model returns a
620    /// plain string.
621    pub fn content_str(&self) -> Option<&str> {
622        self.content.as_ref().and_then(MessageContent::as_str)
623    }
624    /// Reasoning-chain content, when the model exposes it.
625    pub fn reasoning_content(&self) -> Option<&str> {
626        self.reasoning_content.as_deref()
627    }
628    /// Audio payload, for voice models.
629    pub fn audio(&self) -> Option<&AudioContent> {
630        self.audio.as_ref()
631    }
632    /// Tool/function calls the model wants the caller to execute.
633    pub fn tool_calls(&self) -> Option<&[ToolCallMessage]> {
634        self.tool_calls.as_deref()
635    }
636}
637
638impl ToolCallMessage {
639    /// Unique id of this tool/function call.
640    pub fn id(&self) -> Option<&str> {
641        self.id.as_deref()
642    }
643    /// Tool call type (typically `"function"`).
644    pub fn type_(&self) -> Option<&str> {
645        self.type_.as_deref()
646    }
647    /// Function-call payload (name + arguments).
648    pub fn function(&self) -> Option<&ToolFunction> {
649        self.function.as_ref()
650    }
651    /// MCP tool call payload, when applicable.
652    pub fn mcp(&self) -> Option<&MCPMessage> {
653        self.mcp.as_ref()
654    }
655}
656
657impl ToolFunction {
658    /// Name of the function/tool to invoke.
659    pub fn name(&self) -> &str {
660        &self.name
661    }
662    /// JSON-encoded arguments for the function call.
663    pub fn arguments(&self) -> &str {
664        &self.arguments
665    }
666}
667
668impl MCPMessage {
669    /// Unique id of this MCP tool call.
670    pub fn id(&self) -> Option<&str> {
671        self.id.as_deref()
672    }
673    /// MCP call type (`mcp_list_tools` / `mcp_call`).
674    pub fn type_(&self) -> Option<&MCPCallType> {
675        self.type_.as_ref()
676    }
677    /// MCP server label.
678    pub fn server_label(&self) -> Option<&str> {
679        self.server_label.as_deref()
680    }
681    /// Error message reported by the MCP server, if any.
682    pub fn error(&self) -> Option<&str> {
683        self.error.as_deref()
684    }
685    /// Tools advertised by the server (when `type` is `mcp_list_tools`).
686    pub fn tools(&self) -> Option<&[MCPTool]> {
687        self.tools.as_deref()
688    }
689    /// JSON-encoded call arguments (when `type` is `mcp_call`).
690    pub fn arguments(&self) -> Option<&str> {
691        self.arguments.as_deref()
692    }
693    /// Tool name invoked (when `type` is `mcp_call`).
694    pub fn name(&self) -> Option<&str> {
695        self.name.as_deref()
696    }
697    /// Raw tool output returned by the server (when `type` is `mcp_call`).
698    pub fn output(&self) -> Option<&serde_json::Value> {
699        self.output.as_ref()
700    }
701}
702
703impl MCPTool {
704    /// Tool name.
705    pub fn name(&self) -> Option<&str> {
706        self.name.as_deref()
707    }
708    /// Human-readable tool description.
709    pub fn description(&self) -> Option<&str> {
710        self.description.as_deref()
711    }
712    /// Tool annotations (provider-specific).
713    pub fn annotations(&self) -> Option<&serde_json::Value> {
714        self.annotations.as_ref()
715    }
716    /// Input schema describing the tool's parameters.
717    pub fn input_schema(&self) -> Option<&MCPInputSchema> {
718        self.input_schema.as_ref()
719    }
720}
721
722impl MCPInputSchema {
723    /// Schema type (currently always `object`).
724    pub fn type_(&self) -> Option<&MCPInputType> {
725        self.type_.as_ref()
726    }
727    /// Property definitions of the schema.
728    pub fn properties(&self) -> Option<&serde_json::Value> {
729        self.properties.as_ref()
730    }
731    /// List of required property names.
732    pub fn required(&self) -> Option<&[String]> {
733        self.required.as_deref()
734    }
735    /// Whether properties beyond `properties` are permitted.
736    pub fn additional_properties(&self) -> Option<bool> {
737        self.additional_properties
738    }
739}
740
741impl AudioContent {
742    /// Audio content id (usable for multi-turn inputs).
743    pub fn id(&self) -> Option<&str> {
744        self.id.as_deref()
745    }
746    /// Base64-encoded audio data.
747    pub fn data(&self) -> Option<&str> {
748        self.data.as_deref()
749    }
750    /// Expiration timestamp of the audio content.
751    pub fn expires_at(&self) -> Option<&str> {
752        self.expires_at.as_deref()
753    }
754}
755
756impl Usage {
757    /// Number of prompt tokens.
758    pub fn prompt_tokens(&self) -> Option<u32> {
759        self.prompt_tokens
760    }
761    /// Number of completion tokens.
762    pub fn completion_tokens(&self) -> Option<u32> {
763        self.completion_tokens
764    }
765    /// Total tokens for this request.
766    pub fn total_tokens(&self) -> Option<u32> {
767        self.total_tokens
768    }
769    /// Breakdown of prompt-token accounting.
770    pub fn prompt_tokens_details(&self) -> Option<&PromptTokensDetails> {
771        self.prompt_tokens_details.as_ref()
772    }
773}
774
775impl PromptTokensDetails {
776    /// Number of prompt tokens served from cache.
777    pub fn cached_tokens(&self) -> Option<u32> {
778        self.cached_tokens
779    }
780}
781
782impl WebSearchInfo {
783    /// Source website icon URL.
784    pub fn icon(&self) -> Option<&str> {
785        self.icon.as_deref()
786    }
787    /// Search-result title.
788    pub fn title(&self) -> Option<&str> {
789        self.title.as_deref()
790    }
791    /// Search-result page URL.
792    pub fn link(&self) -> Option<&str> {
793        self.link.as_deref()
794    }
795    /// Media/source name of the page.
796    pub fn media(&self) -> Option<&str> {
797        self.media.as_deref()
798    }
799    /// Publish date of the page.
800    pub fn publish_date(&self) -> Option<&str> {
801        self.publish_date.as_deref()
802    }
803    /// Quoted snippet from the result page.
804    pub fn content(&self) -> Option<&str> {
805        self.content.as_deref()
806    }
807    /// Reference marker (corner number) for the citation.
808    pub fn refer(&self) -> Option<&str> {
809        self.refer.as_deref()
810    }
811}
812
813impl VideoResultItem {
814    /// Generated video URL.
815    pub fn url(&self) -> Option<&str> {
816        self.url.as_deref()
817    }
818    /// Cover-image URL for the video.
819    pub fn cover_image_url(&self) -> Option<&str> {
820        self.cover_image_url.as_deref()
821    }
822}
823
824impl ContentFilterInfo {
825    /// Safety-check stage (`assistant`, `user`, or `history`).
826    pub fn role(&self) -> Option<&str> {
827        self.role.as_deref()
828    }
829    /// Severity level (`0` most severe … `3` minor).
830    pub fn level(&self) -> Option<i32> {
831        self.level
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    #[test]
840    fn tool_function_requires_the_documented_string_fields() {
841        let tf: ToolFunction =
842            serde_json::from_str(r#"{"name":"f","arguments":"{\"a\":1}"}"#).unwrap();
843        assert_eq!(tf.name, "f");
844        assert_eq!(tf.arguments, r#"{"a":1}"#);
845        assert!(serde_json::from_str::<ToolFunction>(r#"{"arguments":"{}"}"#).is_err());
846        assert!(serde_json::from_str::<ToolFunction>(r#"{"name":"f"}"#).is_err());
847        assert!(serde_json::from_str::<ToolFunction>(r#"{"name":"f","arguments":{}}"#).is_err());
848    }
849
850    #[test]
851    fn mcp_message_arguments_lenient() {
852        let m: MCPMessage = serde_json::from_str(r#"{"id":"x","arguments":{}}"#).unwrap();
853        assert_eq!(m.arguments.as_deref(), Some("{}"));
854        let m: MCPMessage = serde_json::from_str(r#"{"id":"x","arguments":null}"#).unwrap();
855        assert!(m.arguments.is_none());
856    }
857
858    #[test]
859    fn optional_custom_deserialized_fields_may_be_omitted() {
860        let tool_call: ToolCallMessage = serde_json::from_str("{}").unwrap();
861        assert!(tool_call.id.is_none());
862        let mcp: MCPMessage = serde_json::from_str("{}").unwrap();
863        assert!(mcp.id.is_none());
864        assert!(mcp.arguments.is_none());
865        let audio: AudioContent = serde_json::from_str("{}").unwrap();
866        assert!(audio.id.is_none());
867        assert!(audio.expires_at.is_none());
868    }
869
870    #[test]
871    fn completion_response_rejects_empty_success_bodies() {
872        assert!(serde_json::from_str::<ChatCompletionResponse>("{}").is_err());
873        assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"id":null}"#).is_err());
874        assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"id":"task-1"}"#).is_ok());
875        assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"choices":[]}"#).is_ok());
876    }
877
878    #[test]
879    fn choice_fields_follow_their_optional_openapi_shape() {
880        let choice: Choice = serde_json::from_str("{}").unwrap();
881        assert_eq!(choice.index(), None);
882        assert!(choice.message().is_none());
883    }
884
885    #[test]
886    fn assistant_content_accepts_only_the_documented_union() {
887        let text: Message = serde_json::from_value(serde_json::json!({
888            "content": "hello"
889        }))
890        .unwrap();
891        assert_eq!(text.content_str(), Some("hello"));
892
893        let parts: Message = serde_json::from_value(serde_json::json!({
894            "content": [{"type": "text", "text": "hello"}]
895        }))
896        .unwrap();
897        assert_eq!(
898            parts
899                .content()
900                .and_then(MessageContent::as_parts)
901                .and_then(|parts| parts.first())
902                .and_then(|part| part.text.as_deref()),
903            Some("hello")
904        );
905
906        assert!(serde_json::from_value::<Message>(serde_json::json!({"content": 42})).is_err());
907        assert!(serde_json::from_value::<Message>(serde_json::json!({"content": {}})).is_err());
908    }
909
910    #[test]
911    fn mcp_call_type_known_values_round_trip() {
912        let m: MCPMessage = serde_json::from_str(r#"{"id":"x","type":"mcp_call"}"#).unwrap();
913        assert!(matches!(m.type_, Some(MCPCallType::McpCall)));
914        let m: MCPMessage = serde_json::from_str(r#"{"id":"x","type":"mcp_list_tools"}"#).unwrap();
915        assert!(matches!(m.type_, Some(MCPCallType::McpListTools)));
916    }
917
918    #[test]
919    fn mcp_call_type_unknown_value_falls_back_to_unknown() {
920        // A novel `type` from a newer API/MCP transport must not fail the whole
921        // response — it maps to the `#[serde(other)]` catch-all.
922        let m: MCPMessage =
923            serde_json::from_str(r#"{"id":"x","type":"mcp_future_transport"}"#).unwrap();
924        assert!(matches!(m.type_, Some(MCPCallType::Unknown)));
925    }
926
927    #[test]
928    fn mcp_input_type_unknown_value_falls_back_to_unknown() {
929        let s: MCPInputSchema = serde_json::from_str(r#"{"type":"array"}"#).unwrap();
930        assert!(matches!(s.type_, Some(MCPInputType::Unknown)));
931        let s: MCPInputSchema = serde_json::from_str(r#"{"type":"object"}"#).unwrap();
932        assert!(matches!(s.type_, Some(MCPInputType::Object)));
933    }
934}