Skip to main content

zai_rs/model/async_chat/
data.rs

1use serde::Serialize;
2use validator::Validate;
3
4use super::super::{chat_base_request::*, tools::*, traits::*};
5use crate::{client::ZaiClient, model::async_chat_get::AsyncResponse};
6
7/// Builder for submitting a queued chat-completion task through a [`ZaiClient`].
8///
9/// Posts to the `async/chat/completions` task-submission endpoint.
10pub struct AsyncChatCompletion<N, M>
11where
12    N: ChatRequestModel + AsyncChat,
13    M: Serialize,
14    (N, M): Bounded,
15    ChatBody<N, M>: Serialize,
16{
17    body: ChatBody<N, M>,
18}
19
20impl<N, M> AsyncChatCompletion<N, M>
21where
22    N: ChatRequestModel + AsyncChat,
23    M: Serialize,
24    (N, M): Bounded,
25    ChatBody<N, M>: Serialize,
26{
27    /// Create a new async chat request from a model and the first message batch.
28    pub fn new(model: N, messages: M) -> Self {
29        Self {
30            body: ChatBody::new(model, messages),
31        }
32    }
33
34    /// Borrow the frozen request body.
35    pub const fn body(&self) -> &ChatBody<N, M> {
36        &self.body
37    }
38
39    /// Append one message to the conversation.
40    pub fn add_message(mut self, message: M) -> Self {
41        self.body = self.body.add_message(message);
42        self
43    }
44    /// Append multiple messages to the conversation.
45    pub fn extend_messages(mut self, messages: impl IntoIterator<Item = M>) -> Self {
46        self.body = self.body.extend_messages(messages);
47        self
48    }
49    /// Set the client-provided request identifier.
50    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
51        self.body = self.body.with_request_id(request_id);
52        self
53    }
54    /// Enable or disable probabilistic sampling.
55    pub fn with_do_sample(mut self, do_sample: bool) -> Self {
56        self.body = self.body.with_do_sample(do_sample);
57        self
58    }
59    /// Set the sampling temperature.
60    pub fn with_temperature(mut self, temperature: f64) -> Self {
61        self.body = self.body.with_temperature(temperature);
62        self
63    }
64    /// Set the nucleus-sampling probability.
65    pub fn with_top_p(mut self, top_p: f64) -> Self {
66        self.body = self.body.with_top_p(top_p);
67        self
68    }
69    /// Set the maximum number of generated tokens.
70    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
71        self.body = self.body.with_max_tokens(max_tokens);
72        self
73    }
74    /// Add one tool the model may call.
75    pub fn add_tool(mut self, tool: N::Tool) -> Self
76    where
77        N: ChatToolSupport,
78    {
79        self.body = self.body.add_tool(tool);
80        self
81    }
82    /// Add multiple tools the model may call.
83    pub fn add_tools(mut self, tools: impl IntoIterator<Item = N::Tool>) -> Self
84    where
85        N: ChatToolSupport,
86    {
87        self.body = self.body.add_tools(tools);
88        self
89    }
90    /// Set automatic tool selection.
91    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self
92    where
93        N: ChatToolSupport,
94    {
95        self.body = self.body.with_tool_choice(tool_choice);
96        self
97    }
98    /// Remove all tools and their selection policy.
99    pub fn clear_tools(mut self) -> Self
100    where
101        N: ChatToolSupport,
102    {
103        self.body = self.body.clear_tools();
104        self
105    }
106    /// Set the response format for a text model.
107    pub fn with_response_format(mut self, format: ResponseFormat) -> Self
108    where
109        N: ResponseFormatEnable,
110    {
111        self.body = self.body.with_response_format(format);
112        self
113    }
114    /// Enable or disable audio-output watermarking.
115    pub fn with_watermark_enabled(mut self, enabled: bool) -> Self
116    where
117        N: WatermarkEnable,
118    {
119        self.body = self.body.with_watermark_enabled(enabled);
120        self
121    }
122    /// Set the end-user identifier used for abuse monitoring.
123    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
124        self.body = self.body.with_user_id(user_id);
125        self
126    }
127    /// Add the stop sequence for this request.
128    pub fn with_stop(mut self, stop: impl Into<String>) -> Self {
129        self.body = self.body.with_stop(stop);
130        self
131    }
132    /// Configure thinking mode for a model that supports it.
133    pub fn with_thinking(mut self, thinking: ThinkingType) -> Self
134    where
135        N: ThinkEnable,
136    {
137        self.body = self.body.with_thinking(thinking);
138        self
139    }
140    /// Set reasoning depth for a model that supports `reasoning_effort`.
141    pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self
142    where
143        N: ReasoningEffortEnable,
144    {
145        self.body = self.body.with_reasoning_effort(effort);
146        self
147    }
148
149    /// Validate field constraints for the queued endpoint.
150    pub fn validate(&self) -> crate::ZaiResult<()> {
151        self.body
152            .validate()
153            .map_err(crate::client::error::ZaiError::from)?;
154        Ok(())
155    }
156
157    /// Submit via a [`ZaiClient`] and await the task-creation response.
158    pub async fn send_via(&self, client: &ZaiClient) -> crate::ZaiResult<AsyncResponse>
159    where
160        N: serde::Serialize,
161        M: serde::Serialize,
162    {
163        self.validate()?;
164        let route = crate::client::routes::CHAT_COMPLETE_ASYNC;
165        let url = client.endpoints().resolve_route(route, &[])?;
166        let response = client
167            .send_json::<_, AsyncResponse>(route.method(), url, &self.body)
168            .await?;
169        response.validate()?;
170        Ok(response)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::model::{
178        chat_message_types::{TextMessage, VoiceMessage},
179        chat_models::{GLM4_voice, GLM5_2},
180    };
181
182    #[test]
183    fn async_text_schema_never_serializes_stream_fields() {
184        let request = AsyncChatCompletion::new(GLM5_2 {}, TextMessage::user("hello"))
185            .add_tool(Tools::Function {
186                function: Function::new("lookup", "Lookup", serde_json::json!({"type": "object"})),
187            })
188            .with_tool_choice(ToolChoice::auto())
189            .with_response_format(ResponseFormat::JsonObject);
190        let json = serde_json::to_value(request.body()).unwrap();
191
192        assert_eq!(json["tool_choice"], "auto");
193        assert_eq!(json["response_format"]["type"], "json_object");
194        assert!(json.get("stream").is_none());
195        assert!(json.get("tool_stream").is_none());
196        assert!(json.get("watermark_enabled").is_none());
197    }
198
199    #[test]
200    fn async_audio_schema_serializes_only_its_specialized_field() {
201        let request = AsyncChatCompletion::new(GLM4_voice {}, VoiceMessage::new_user())
202            .with_watermark_enabled(true);
203        let json = serde_json::to_value(request.body()).unwrap();
204
205        assert_eq!(json["watermark_enabled"], true);
206        for field in [
207            "stream",
208            "tool_stream",
209            "tools",
210            "tool_choice",
211            "response_format",
212        ] {
213            assert!(
214                json.get(field).is_none(),
215                "unexpected async audio field: {field}"
216            );
217        }
218    }
219}