1use std::marker::PhantomData;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use futures_util::{Stream, StreamExt};
6use serde::Serialize;
7use validator::Validate;
8
9use super::super::{
10 chat_base_request::*, chat_base_response::ChatCompletionResponse,
11 chat_stream_response::ChatStreamResponse, tools::*, traits::*,
12};
13use crate::client::ZaiClient;
14
15pub struct ChatStream {
20 inner: Pin<Box<dyn Stream<Item = crate::ZaiResult<ChatStreamResponse>> + Send + 'static>>,
21}
22
23impl ChatStream {
24 pub async fn next(&mut self) -> Option<crate::ZaiResult<ChatStreamResponse>> {
28 self.inner.next().await
29 }
30}
31
32impl Stream for ChatStream {
33 type Item = crate::ZaiResult<ChatStreamResponse>;
34
35 fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
36 self.inner.as_mut().poll_next(context)
37 }
38}
39
40pub struct ChatCompletion<N, M, S = StreamOff>
115where
116 N: ChatRequestModel + Chat,
117 M: Serialize,
118 (N, M): Bounded,
119 ChatBody<N, M>: Serialize,
120 S: StreamState,
121{
122 body: ChatBody<N, M>,
123 _stream: PhantomData<S>,
124}
125
126impl<N, M, S> ChatCompletion<N, M, S>
127where
128 N: ChatRequestModel + Chat,
129 M: Serialize,
130 (N, M): Bounded,
131 ChatBody<N, M>: Serialize,
132 S: StreamState,
133{
134 pub const fn body(&self) -> &ChatBody<N, M> {
136 &self.body
137 }
138}
139
140impl<N, M> ChatCompletion<N, M, StreamOff>
141where
142 N: ChatRequestModel + Chat,
143 M: Serialize,
144 (N, M): Bounded,
145 ChatBody<N, M>: Serialize,
146{
147 pub fn new(model: N, messages: M) -> ChatCompletion<N, M, StreamOff> {
149 let body = ChatBody::new(model, messages);
150 ChatCompletion {
151 body,
152 _stream: PhantomData,
153 }
154 }
155
156 pub fn add_message(mut self, message: M) -> Self {
158 self.body = self.body.add_message(message);
159 self
160 }
161 pub fn extend_messages(mut self, messages: impl IntoIterator<Item = M>) -> Self {
163 self.body = self.body.extend_messages(messages);
164 self
165 }
166 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
168 self.body = self.body.with_request_id(request_id);
169 self
170 }
171 pub fn with_do_sample(mut self, do_sample: bool) -> Self {
173 self.body = self.body.with_do_sample(do_sample);
174 self
175 }
176 pub fn with_temperature(mut self, temperature: f64) -> Self {
178 self.body = self.body.with_temperature(temperature);
179 self
180 }
181 pub fn with_top_p(mut self, top_p: f64) -> Self {
183 self.body = self.body.with_top_p(top_p);
184 self
185 }
186 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
188 self.body = self.body.with_max_tokens(max_tokens);
189 self
190 }
191 pub fn add_tool(mut self, tool: N::Tool) -> Self
193 where
194 N: ChatToolSupport,
195 {
196 self.body = self.body.add_tool(tool);
197 self
198 }
199 pub fn add_tools(mut self, tools: impl IntoIterator<Item = N::Tool>) -> Self
201 where
202 N: ChatToolSupport,
203 {
204 self.body = self.body.add_tools(tools);
205 self
206 }
207 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self
209 where
210 N: ChatToolSupport,
211 {
212 self.body = self.body.with_tool_choice(tool_choice);
213 self
214 }
215 pub fn clear_tools(mut self) -> Self
217 where
218 N: ChatToolSupport,
219 {
220 self.body = self.body.clear_tools();
221 self
222 }
223 pub fn with_response_format(mut self, format: ResponseFormat) -> Self
225 where
226 N: ResponseFormatEnable,
227 {
228 self.body = self.body.with_response_format(format);
229 self
230 }
231 pub fn with_watermark_enabled(mut self, enabled: bool) -> Self
233 where
234 N: WatermarkEnable,
235 {
236 self.body = self.body.with_watermark_enabled(enabled);
237 self
238 }
239 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
241 self.body = self.body.with_user_id(user_id);
242 self
243 }
244 pub fn with_stop(mut self, stop: impl Into<String>) -> Self {
246 self.body = self.body.with_stop(stop);
247 self
248 }
249
250 pub fn with_thinking(mut self, thinking: ThinkingType) -> Self
252 where
253 N: ThinkEnable,
254 {
255 self.body = self.body.with_thinking(thinking);
256 self
257 }
258
259 pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self
261 where
262 N: ReasoningEffortEnable,
263 {
264 self.body = self.body.with_reasoning_effort(effort);
265 self
266 }
267
268 pub fn enable_stream(mut self) -> ChatCompletion<N, M, StreamOn> {
270 self.body.set_stream(Some(true));
271 ChatCompletion {
272 body: self.body,
273 _stream: PhantomData,
274 }
275 }
276
277 pub fn validate(&self) -> crate::ZaiResult<()> {
279 self.body
280 .validate()
281 .map_err(crate::client::error::ZaiError::from)?;
282 if matches!(self.body.stream(), Some(true)) {
283 return Err(crate::client::error::ZaiError::ApiError {
284 code: crate::client::error::codes::SDK_VALIDATION,
285 message: "stream=true detected; use enable_stream() and streaming APIs instead"
286 .to_string(),
287 });
288 }
289 Ok(())
290 }
291
292 pub async fn send_via(&self, client: &ZaiClient) -> crate::ZaiResult<ChatCompletionResponse>
295 where
296 N: serde::Serialize,
297 M: serde::Serialize,
298 {
299 self.validate()?;
300 let route = crate::client::routes::CHAT_COMPLETE;
301 let url = client.endpoints().resolve_route(route, &[])?;
302 client
303 .send_json::<_, ChatCompletionResponse>(route.method(), url, &self.body)
304 .await
305 }
306
307 pub async fn send_via_coding_plan(
309 &self,
310 client: &ZaiClient,
311 ) -> crate::ZaiResult<ChatCompletionResponse>
312 where
313 N: serde::Serialize,
314 M: serde::Serialize,
315 {
316 self.validate()?;
317 let route = crate::client::routes::CHAT_COMPLETE_CODING;
318 let url = client.endpoints().resolve_route(route, &[])?;
319 client
320 .send_json::<_, ChatCompletionResponse>(route.method(), url, &self.body)
321 .await
322 }
323}
324
325impl<N, M> ChatCompletion<N, M, StreamOn>
326where
327 N: ChatRequestModel + Chat,
328 M: Serialize,
329 (N, M): Bounded,
330 ChatBody<N, M>: Serialize,
331{
332 pub fn with_tool_stream(mut self, tool_stream: bool) -> Self
334 where
335 N: ToolStreamEnable,
336 {
337 self.body = self.body.with_tool_stream(tool_stream);
338 self
339 }
340
341 pub fn disable_stream(mut self) -> ChatCompletion<N, M, StreamOff> {
343 self.body.set_stream(Some(false));
344 self.body.clear_tool_stream();
345 ChatCompletion {
346 body: self.body,
347 _stream: PhantomData,
348 }
349 }
350
351 pub async fn stream_via(&self, client: &ZaiClient) -> crate::ZaiResult<ChatStream>
357 where
358 N: serde::Serialize,
359 M: serde::Serialize,
360 {
361 self.body
362 .validate()
363 .map_err(crate::client::error::ZaiError::from)?;
364 let url = client
365 .endpoints()
366 .resolve_route(crate::client::routes::CHAT_COMPLETE, &[])?;
367 let raw = client
368 .send_sse_json(
369 crate::client::routes::CHAT_COMPLETE.method(),
370 url,
371 &self.body,
372 )
373 .await?;
374 let stream = crate::model::sse_parser::decode_required_done_stream(raw, |payload| {
375 serde_json::from_slice::<ChatStreamResponse>(payload)
376 .map_err(crate::ZaiError::from)
377 .and_then(validate_stream_finish_reason)
378 });
379 Ok(ChatStream { inner: stream })
380 }
381}
382
383fn validate_stream_finish_reason(
384 chunk: ChatStreamResponse,
385) -> crate::ZaiResult<ChatStreamResponse> {
386 let failure = chunk
387 .choices
388 .iter()
389 .filter_map(|choice| choice.finish_reason.as_deref())
390 .find_map(|reason| match reason {
391 "sensitive" => Some((1301, "stream stopped by content policy")),
392 "network_error" => Some((1234, "stream stopped by an upstream inference error")),
393 "model_context_window_exceeded" => {
394 Some((1261, "stream exceeded the model context window"))
395 },
396 _ => None,
397 });
398
399 match failure {
400 Some((code, message)) => Err(crate::ZaiError::from_api_response(
401 200,
402 code,
403 message.to_string(),
404 )),
405 None => Ok(chunk),
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use crate::model::{chat_message_types::TextMessage, chat_models::GLM5_2};
413
414 #[test]
415 fn stream_field_is_owned_by_the_type_state() {
416 let request = ChatCompletion::new(GLM5_2 {}, TextMessage::user("hello"));
417 let json = serde_json::to_value(request.body()).unwrap();
418 assert!(json.get("stream").is_none());
419 assert!(json.get("tool_stream").is_none());
420
421 let request = request.enable_stream().with_tool_stream(true);
422 let json = serde_json::to_value(request.body()).unwrap();
423 assert_eq!(json["stream"], true);
424 assert_eq!(json["tool_stream"], true);
425 }
426}