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 guarantees
9//! about streaming capabilities through phantom types (`StreamOn`/`StreamOff`).
10//!
11//! ## Features
12//!
13//! - **Type-safe model binding** - Compile-time verification of model-message compatibility
14//! - **Builder pattern** - Fluent API for request construction
15//! - **Streaming support** - Type-state based streaming capability enforcement
16//! - **Tool integration** - Support for function calling and tool usage
17//! - **Parameter control** - Temperature, top-p, max tokens, and other generation parameters
18
19use super::super::chat_base_request::*;
20use super::super::tools::*;
21use super::super::traits::*;
22use crate::client::http::HttpClient;
23use serde::Serialize;
24use std::marker::PhantomData;
25use validator::Validate;
26
27// Type-state is defined in model::traits::{StreamState, StreamOn, StreamOff}
28
29/// Type-safe chat completion request structure.
30///
31/// This struct represents a chat completion request with compile-time guarantees
32/// for model compatibility and streaming capabilities.
33///
34/// ## Type Parameters
35///
36/// - `N` - The AI model type (must implement `ModelName + Chat`)
37/// - `M` - The message type (must form a valid bound with the model)
38/// - `S` - Stream state (`StreamOn` or `StreamOff`, defaults to `StreamOff`)
39///
40/// ## Examples
41///
42/// ```rust,ignore
43/// let model = GLM4_5_flash {};
44/// let messages = TextMessage::user("Hello, how are you?");
45/// let request = ChatCompletion::new(model, messages, api_key);
46/// ```
47pub struct ChatCompletion<N, M, S = StreamOff>
48where
49    N: ModelName + Chat,
50    (N, M): Bounded,
51    ChatBody<N, M>: Serialize,
52    S: StreamState,
53{
54    /// API key for authentication with the Zhipu AI service.
55    pub key: String,
56
57    /// API endpoint URL for chat completions.
58    /// Defaults to "https://open.bigmodel.cn/api/paas/v4/chat/completions"
59    /// but can be customized using the `with_url()` method.
60    pub url: String,
61
62    /// The request body containing model, messages, and parameters.
63    body: ChatBody<N, M>,
64
65    /// Phantom data to track streaming capability at compile time.
66    _stream: PhantomData<S>,
67}
68
69impl<N, M> ChatCompletion<N, M, StreamOff>
70where
71    N: ModelName + Chat,
72    (N, M): Bounded,
73    ChatBody<N, M>: Serialize,
74{
75    /// Creates a new non-streaming chat completion request.
76    ///
77    /// ## Arguments
78    ///
79    /// * `model` - The AI model to use for completion
80    /// * `messages` - The conversation messages
81    /// * `key` - API key for authentication
82    ///
83    /// ## Returns
84    ///
85    /// A new `ChatCompletion` instance configured for non-streaming requests.
86    pub fn new(model: N, messages: M, key: String) -> ChatCompletion<N, M, StreamOff> {
87        let body = ChatBody::new(model, messages);
88        ChatCompletion {
89            body,
90            key,
91            url: "https://open.bigmodel.cn/api/paas/v4/chat/completions".to_string(),
92            _stream: PhantomData,
93        }
94    }
95
96    /// Gets mutable access to the request body for further customization.
97    ///
98    /// This method allows modification of request parameters after initial creation.
99    pub fn body_mut(&mut self) -> &mut ChatBody<N, M> {
100        &mut self.body
101    }
102
103    /// Adds additional messages to the conversation.
104    ///
105    /// This method provides a fluent interface for building conversation context.
106    ///
107    /// ## Arguments
108    ///
109    /// * `messages` - Additional messages to append to the conversation
110    ///
111    /// ## Returns
112    ///
113    /// Self with the updated message collection, enabling method chaining.
114    pub fn add_messages(mut self, messages: M) -> Self {
115        self.body = self.body.add_messages(messages);
116        self
117    }
118    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
119        self.body = self.body.with_request_id(request_id);
120        self
121    }
122    pub fn with_do_sample(mut self, do_sample: bool) -> Self {
123        self.body = self.body.with_do_sample(do_sample);
124        self
125    }
126
127    pub fn with_temperature(mut self, temperature: f32) -> Self {
128        self.body = self.body.with_temperature(temperature);
129        self
130    }
131    pub fn with_top_p(mut self, top_p: f32) -> Self {
132        self.body = self.body.with_top_p(top_p);
133        self
134    }
135    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
136        self.body = self.body.with_max_tokens(max_tokens);
137        self
138    }
139    pub fn add_tool(mut self, tool: Tools) -> Self {
140        self.body = self.body.add_tools(tool);
141        self
142    }
143    pub fn add_tools(mut self, tools: Vec<Tools>) -> Self {
144        self.body = self.body.extend_tools(tools);
145        self
146    }
147    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
148        self.body = self.body.with_user_id(user_id);
149        self
150    }
151    pub fn with_stop(mut self, stop: String) -> Self {
152        self.body = self.body.with_stop(stop);
153        self
154    }
155
156    /// Sets a custom API endpoint URL for this chat completion request.
157    ///
158    /// This method allows overriding the default API endpoint with a custom URL,
159    /// enabling support for different deployment environments or proxy configurations.
160    ///
161    /// ## Arguments
162    ///
163    /// * `url` - The custom API endpoint URL
164    ///
165    /// ## Returns
166    ///
167    /// Self with the updated URL, enabling method chaining.
168    ///
169    /// ## Examples
170    ///
171    /// ```rust,ignore
172    /// let request = ChatCompletion::new(model, messages, api_key)
173    ///     .with_url("https://custom-api.example.com/v1/chat/completions");
174    /// ```
175    pub fn with_url(mut self, url: impl Into<String>) -> Self {
176        self.url = url.into();
177        self
178    }
179
180    /// Sets the URL to the coding plan endpoint.
181    ///
182    /// This method configures the chat completion request to use the coding-specific
183    /// API endpoint "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions".
184    ///
185    /// ## Returns
186    ///
187    /// Self with the coding plan URL, enabling method chaining.
188    ///
189    /// ## Examples
190    ///
191    /// ```rust,ignore
192    /// let request = ChatCompletion::new(model, messages, api_key)
193    ///     .with_coding_plan();
194    /// ```
195    pub fn with_coding_plan(mut self) -> Self {
196        self.url = "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions".to_string();
197        self
198    }
199
200    // Optional: only available when model supports thinking
201    pub fn with_thinking(mut self, thinking: ThinkingType) -> Self
202    where
203        N: ThinkEnable,
204    {
205        self.body = self.body.with_thinking(thinking);
206        self
207    }
208
209    /// Enables streaming for this chat completion request.
210    ///
211    /// This method transitions the request to streaming mode, allowing
212    /// real-time response processing through Server-Sent Events (SSE).
213    ///
214    /// ## Returns
215    ///
216    /// A new `ChatCompletion` instance with streaming enabled (`StreamOn`).
217    pub fn enable_stream(mut self) -> ChatCompletion<N, M, StreamOn> {
218        self.body.stream = Some(true);
219        ChatCompletion {
220            key: self.key,
221            url: self.url,
222            body: self.body,
223            _stream: PhantomData,
224        }
225    }
226
227    /// Disables streaming for this chat completion request.
228    ///
229    /// This method ensures the request will receive a complete response
230    /// rather than streaming chunks.
231    ///
232    /// ## Returns
233    ///
234    /// A new `ChatCompletion` instance with streaming disabled (`StreamOff`).
235    pub fn disable_stream(mut self) -> ChatCompletion<N, M, StreamOff> {
236        self.body.stream = Some(false);
237        ChatCompletion {
238            key: self.key,
239            url: self.url,
240            body: self.body,
241            _stream: PhantomData,
242        }
243    }
244    /// Validate request parameters for non-stream chat (StreamOff)
245    pub fn validate(&self) -> anyhow::Result<()> {
246        // Field-level validation from ChatBody (temperature/top_p/max_tokens/user_id/stop...)
247        self.body.validate().map_err(|e| anyhow::anyhow!(e))?;
248        // Ensure not accidentally enabling stream in StreamOff state
249        if matches!(self.body.stream, Some(true)) {
250
251            return Err(anyhow::anyhow!(
252                "stream=true detected; use enable_stream() and streaming APIs instead"
253            ));
254        }
255        Ok(())
256    }
257
258    /// Send the request and parse typed response.
259    /// Automatically runs `validate()` before sending.
260    pub async fn send(
261        &self,
262    ) -> anyhow::Result<crate::model::chat_base_response::ChatCompletionResponse>
263    where
264        N: serde::Serialize,
265        M: serde::Serialize,
266    {
267        self.validate()?;
268        let resp: reqwest::Response = self.post().await?;
269        let parsed = resp
270            .json::<crate::model::chat_base_response::ChatCompletionResponse>()
271            .await?;
272        Ok(parsed)
273    }
274}
275
276impl<N, M> ChatCompletion<N, M, StreamOn>
277where
278    N: ModelName + Chat,
279    (N, M): Bounded,
280    ChatBody<N, M>: Serialize,
281{
282    pub fn with_tool_stream(mut self, tool_stream: bool) -> Self
283    where
284        N: ToolStreamEnable,
285    {
286        self.body = self.body.with_tool_stream(tool_stream);
287        self
288    }
289}
290
291impl<N, M, S> HttpClient for ChatCompletion<N, M, S>
292where
293    N: ModelName + Serialize + Chat,
294    M: Serialize,
295    (N, M): Bounded,
296    S: StreamState,
297{
298    type Body = ChatBody<N, M>;
299    type ApiUrl = String;
300    type ApiKey = String;
301
302    /// Returns the API endpoint URL for chat completions.
303    fn api_url(&self) -> &Self::ApiUrl {
304        &self.url
305    }
306    fn api_key(&self) -> &Self::ApiKey {
307        &self.key
308    }
309    fn body(&self) -> &Self::Body {
310        &self.body
311    }
312}
313
314/// Enables Server-Sent Events (SSE) streaming for streaming-enabled chat completions.
315///
316/// This implementation allows streaming chat completions to be processed
317/// incrementally as responses arrive from the API.
318impl<N, M> crate::model::traits::SseStreamable for ChatCompletion<N, M, StreamOn>
319where
320    N: ModelName + Serialize + Chat,
321    M: Serialize,
322    (N, M): Bounded,
323{
324}
325
326/// Provides streaming extension methods for streaming-enabled chat completions.
327///
328/// This implementation enables the use of streaming-specific methods
329/// for processing chat responses in real-time.
330impl<N, M> crate::model::stream_ext::StreamChatLikeExt for ChatCompletion<N, M, StreamOn>
331where
332    N: ModelName + Serialize + Chat,
333    M: Serialize,
334    (N, M): Bounded,
335{
336}