Skip to main content

zai_rs/model/chat/
data.rs

1//! # Chat Completion Data Models
2//!
3//! This module defines the core data structures for chat completion requests,
4//! implementing type-safe chat interactions with the Zhipu AI API.
5//!
6//! ## Type-State Pattern
7//!
8//! The implementation uses Rust's type system to enforce compile-time
9//! guarantees about streaming capabilities through phantom types
10//! (`StreamOn`/`StreamOff`).
11//!
12//! ## Features
13//!
14//! - **Type-safe model binding** - Compile-time verification of model-message
15//!   compatibility
16//! - **Builder pattern** - Fluent API for request construction
17//! - **Streaming support** - Type-state based streaming capability enforcement
18//! - **Tool integration** - Support for function calling and tool usage
19//! - **Parameter control** - Temperature, top-p, max tokens, and other
20//!   generation parameters
21
22use std::{marker::PhantomData, sync::Arc};
23
24use serde::Serialize;
25use validator::Validate;
26
27use super::super::{chat_base_request::*, tools::*, traits::*};
28use crate::client::{
29    endpoints::{ApiBase, EndpointConfig, paths},
30    http::{HttpClient, HttpClientConfig, parse_typed_response},
31};
32
33// Type-state is defined in model::traits::{StreamState, StreamOn, StreamOff}
34
35/// Type-safe chat completion request structure.
36///
37/// This struct represents a chat completion request with compile-time
38/// guarantees for model compatibility and streaming capabilities.
39///
40/// ## Type Parameters
41///
42/// - `N` - The AI model type (must implement `ModelName + Chat`)
43/// - `M` - The message type (must form a valid bound with the model)
44/// - `S` - Stream state (`StreamOn` or `StreamOff`, defaults to `StreamOff`)
45///
46/// ## Examples
47///
48/// ```rust,ignore
49/// let model = GLM4_5_flash {};
50/// let messages = TextMessage::user("Hello, how are you?");
51/// let request = ChatCompletion::new(model, messages, api_key);
52/// ```
53pub struct ChatCompletion<N, M, S = StreamOff>
54where
55    N: ModelName + Chat,
56    (N, M): Bounded,
57    ChatBody<N, M>: Serialize,
58    S: StreamState,
59{
60    /// API key for authentication with the Zhipu AI service.
61    pub key: String,
62
63    /// Final API endpoint URL for chat completions.
64    pub url: String,
65
66    endpoint_config: EndpointConfig,
67    api_base: ApiBase,
68    http_config: Arc<HttpClientConfig>,
69
70    /// The request body containing model, messages, and parameters.
71    body: ChatBody<N, M>,
72
73    /// Phantom data to track streaming capability at compile time.
74    _stream: PhantomData<S>,
75}
76
77impl<N, M> ChatCompletion<N, M, StreamOff>
78where
79    N: ModelName + Chat,
80    (N, M): Bounded,
81    ChatBody<N, M>: Serialize,
82{
83    /// Creates a new non-streaming chat completion request.
84    ///
85    /// ## Arguments
86    ///
87    /// * `model` - The AI model to use for completion
88    /// * `messages` - The conversation messages
89    /// * `key` - API key for authentication
90    ///
91    /// ## Returns
92    ///
93    /// A new `ChatCompletion` instance configured for non-streaming requests.
94    pub fn new(model: N, messages: M, key: String) -> ChatCompletion<N, M, StreamOff> {
95        let body = ChatBody::new(model, messages);
96        let endpoint_config = EndpointConfig::default();
97        let api_base = ApiBase::PaasV4;
98        let url = endpoint_config.url(&api_base, paths::CHAT_COMPLETIONS);
99        ChatCompletion {
100            body,
101            key,
102            url,
103            endpoint_config,
104            api_base,
105            http_config: Arc::new(HttpClientConfig::default()),
106            _stream: PhantomData,
107        }
108    }
109
110    /// Gets mutable access to the request body for further customization.
111    ///
112    /// This method allows modification of request parameters after initial
113    /// creation.
114    pub fn body_mut(&mut self) -> &mut ChatBody<N, M> {
115        &mut self.body
116    }
117
118    /// Adds additional messages to the conversation.
119    ///
120    /// This method provides a fluent interface for building conversation
121    /// context.
122    ///
123    /// ## Arguments
124    ///
125    /// * `messages` - Additional messages to append to the conversation
126    ///
127    /// ## Returns
128    ///
129    /// Self with the updated message collection, enabling method chaining.
130    pub fn add_messages(mut self, messages: M) -> Self {
131        self.body = self.body.add_messages(messages);
132        self
133    }
134    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
135        self.body = self.body.with_request_id(request_id);
136        self
137    }
138    pub fn with_do_sample(mut self, do_sample: bool) -> Self {
139        self.body = self.body.with_do_sample(do_sample);
140        self
141    }
142
143    pub fn with_temperature(mut self, temperature: f64) -> Self {
144        self.body = self.body.with_temperature(temperature);
145        self
146    }
147    pub fn with_top_p(mut self, top_p: f64) -> Self {
148        self.body = self.body.with_top_p(top_p);
149        self
150    }
151    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
152        self.body = self.body.with_max_tokens(max_tokens);
153        self
154    }
155    pub fn add_tool(mut self, tool: Tools) -> Self {
156        self.body = self.body.add_tools(tool);
157        self
158    }
159    pub fn add_tools(mut self, tools: Vec<Tools>) -> Self {
160        self.body = self.body.extend_tools(tools);
161        self
162    }
163    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
164        self.body = self.body.with_user_id(user_id);
165        self
166    }
167    pub fn with_stop(mut self, stop: String) -> Self {
168        self.body = self.body.with_stop(stop);
169        self
170    }
171
172    /// Sets a custom API endpoint URL for this chat completion request.
173    ///
174    /// This method allows overriding the default API endpoint with a custom
175    /// URL, enabling support for different deployment environments or proxy
176    /// configurations.
177    ///
178    /// ## Arguments
179    ///
180    /// * `url` - The custom API endpoint URL
181    ///
182    /// ## Returns
183    ///
184    /// Self with the updated URL, enabling method chaining.
185    ///
186    /// ## Examples
187    ///
188    /// ```rust,ignore
189    /// let request = ChatCompletion::new(model, messages, api_key)
190    ///     .with_url("https://custom-api.example.com/v1/chat/completions");
191    /// ```
192    pub fn with_url(mut self, url: impl Into<String>) -> Self {
193        self.url = url.into();
194        self
195    }
196
197    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
198        self.api_base = ApiBase::Custom(base_url.into());
199        self.url = self
200            .endpoint_config
201            .url(&self.api_base, paths::CHAT_COMPLETIONS);
202        self
203    }
204
205    pub fn with_endpoint_config(mut self, endpoint_config: EndpointConfig) -> Self {
206        self.endpoint_config = endpoint_config;
207        self.url = self
208            .endpoint_config
209            .url(&self.api_base, paths::CHAT_COMPLETIONS);
210        self
211    }
212
213    pub fn with_http_config(mut self, config: HttpClientConfig) -> Self {
214        self.http_config = Arc::new(config);
215        self
216    }
217
218    /// Sets the URL to the coding plan endpoint.
219    ///
220    /// This method configures the chat completion request to use the
221    /// coding-specific API endpoint "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions".
222    ///
223    /// ## Returns
224    ///
225    /// Self with the coding plan URL, enabling method chaining.
226    ///
227    /// ## Examples
228    ///
229    /// ```rust,ignore
230    /// let request = ChatCompletion::new(model, messages, api_key)
231    ///     .with_coding_plan();
232    /// ```
233    pub fn with_coding_plan(mut self) -> Self {
234        self.api_base = ApiBase::CodingPaasV4;
235        self.url = self
236            .endpoint_config
237            .url(&self.api_base, paths::CHAT_COMPLETIONS);
238        self
239    }
240
241    // Optional: only available when model supports thinking
242    pub fn with_thinking(mut self, thinking: ThinkingType) -> Self
243    where
244        N: ThinkEnable,
245    {
246        self.body = self.body.with_thinking(thinking);
247        self
248    }
249
250    // Optional: only available for GLM-5.2+ (reasoning_effort support)
251    pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self
252    where
253        N: ReasoningEffortEnable,
254    {
255        self.body = self.body.with_reasoning_effort(effort);
256        self
257    }
258
259    /// Enables streaming for this chat completion request.
260    ///
261    /// This method transitions the request to streaming mode, allowing
262    /// real-time response processing through Server-Sent Events (SSE).
263    ///
264    /// ## Returns
265    ///
266    /// A new `ChatCompletion` instance with streaming enabled (`StreamOn`).
267    pub fn enable_stream(mut self) -> ChatCompletion<N, M, StreamOn> {
268        self.body.stream = Some(true);
269        ChatCompletion {
270            key: self.key,
271            url: self.url,
272            body: self.body,
273            endpoint_config: self.endpoint_config,
274            api_base: self.api_base,
275            http_config: self.http_config,
276            _stream: PhantomData,
277        }
278    }
279
280    /// Validate request parameters for non-stream chat (StreamOff)
281    pub fn validate(&self) -> crate::ZaiResult<()> {
282        // Field-level validation from ChatBody
283        // (temperature/top_p/max_tokens/user_id/stop...)
284
285        self.body
286            .validate()
287            .map_err(crate::client::error::ZaiError::from)?;
288        // Ensure not accidentally enabling stream in StreamOff state
289
290        if matches!(self.body.stream, Some(true)) {
291            return Err(crate::client::error::ZaiError::ApiError {
292                code: 1200,
293                message: "stream=true detected; use enable_stream() and streaming APIs instead"
294                    .to_string(),
295            });
296        }
297
298        Ok(())
299    }
300
301    pub async fn send(
302        &self,
303    ) -> crate::ZaiResult<crate::model::chat_base_response::ChatCompletionResponse>
304    where
305        N: serde::Serialize,
306        M: serde::Serialize,
307    {
308        self.validate()?;
309
310        // post() handles non-2xx responses internally (returns Err), so here we
311        // only receive a successful response with valid HTTP status.
312        let resp: reqwest::Response = self.post().await?;
313
314        let parsed =
315            parse_typed_response::<crate::model::chat_base_response::ChatCompletionResponse>(resp)
316                .await?;
317
318        Ok(parsed)
319    }
320}
321
322impl<N, M> ChatCompletion<N, M, StreamOn>
323where
324    N: ModelName + Chat,
325    (N, M): Bounded,
326    ChatBody<N, M>: Serialize,
327{
328    pub fn with_tool_stream(mut self, tool_stream: bool) -> Self
329    where
330        N: ToolStreamEnable,
331    {
332        self.body = self.body.with_tool_stream(tool_stream);
333        self
334    }
335
336    /// Disables streaming for this chat completion request.
337    ///
338    /// This method ensures the request will receive a complete response
339    /// rather than streaming chunks.
340    ///
341    /// ## Returns
342    ///
343    /// A new `ChatCompletion` instance with streaming disabled (`StreamOff`).
344    pub fn disable_stream(mut self) -> ChatCompletion<N, M, StreamOff> {
345        self.body.stream = Some(false);
346        // Reset tool_stream when disabling streaming since tool_stream depends on
347        // stream
348        self.body.tool_stream = None;
349        ChatCompletion {
350            key: self.key,
351            url: self.url,
352            body: self.body,
353            endpoint_config: self.endpoint_config,
354            api_base: self.api_base,
355            http_config: self.http_config,
356            _stream: PhantomData,
357        }
358    }
359}
360
361impl<N, M, S> HttpClient for ChatCompletion<N, M, S>
362where
363    N: ModelName + Serialize + Chat,
364    M: Serialize,
365    (N, M): Bounded,
366    S: StreamState,
367{
368    type Body = ChatBody<N, M>;
369    type ApiUrl = String;
370    type ApiKey = String;
371
372    /// Returns the API endpoint URL for chat completions.
373    fn api_url(&self) -> &Self::ApiUrl {
374        &self.url
375    }
376    fn api_key(&self) -> &Self::ApiKey {
377        &self.key
378    }
379    fn body(&self) -> &Self::Body {
380        &self.body
381    }
382
383    fn http_config(&self) -> Arc<HttpClientConfig> {
384        self.http_config.clone()
385    }
386}
387
388/// Enables Server-Sent Events (SSE) streaming for streaming-enabled chat
389/// completions.
390///
391/// This implementation allows streaming chat completions to be processed
392/// incrementally as responses arrive from the API.
393impl<N, M> crate::model::traits::SseStreamable for ChatCompletion<N, M, StreamOn>
394where
395    N: ModelName + Serialize + Chat,
396    M: Serialize,
397    (N, M): Bounded,
398{
399}
400
401/// Provides streaming extension methods for streaming-enabled chat completions.
402///
403/// This implementation enables the use of streaming-specific methods
404/// for processing chat responses in real-time.
405impl<N, M> crate::model::stream_ext::StreamChatLikeExt for ChatCompletion<N, M, StreamOn>
406where
407    N: ModelName + Serialize + Chat,
408    M: Serialize,
409    (N, M): Bounded,
410{
411}