1use serde::Serialize;
15use validator::*;
16
17use super::{tools::*, traits::*};
18
19#[derive(Debug, Clone, Validate, Serialize)]
48pub struct ChatBody<N, M>
49where
50 N: ModelName,
51 (N, M): Bounded,
52{
53 pub model: N,
55
56 pub messages: Vec<M>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
62 pub request_id: Option<String>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
68 pub thinking: Option<ThinkingType>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
75 pub reasoning_effort: Option<ReasoningEffort>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
81 pub do_sample: Option<bool>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
86 pub stream: Option<bool>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
92 pub tool_stream: Option<bool>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
98 #[validate(range(min = 0.0, max = 1.0))]
99 pub temperature: Option<f64>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
105 #[validate(range(min = 0.0, max = 1.0))]
106 pub top_p: Option<f64>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
111 #[validate(range(min = 1, max = 98304))]
112 pub max_tokens: Option<u32>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
118 pub tools: Option<Vec<Tools>>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
124 #[validate(length(min = 6, max = 128))]
125 pub user_id: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 #[validate(length(min = 1, max = 1))]
130 pub stop: Option<Vec<String>>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
135 pub response_format: Option<ResponseFormat>,
136}
137
138impl<N, M> ChatBody<N, M>
139where
140 N: ModelName,
141 (N, M): Bounded,
142{
143 pub fn new(model: N, messages: M) -> Self {
144 Self {
145 model,
146 messages: vec![messages],
147 request_id: None,
148 thinking: None,
149 reasoning_effort: None,
150 do_sample: None,
151 stream: None,
152 tool_stream: None,
153 temperature: None,
154 top_p: None,
155 max_tokens: None,
156 tools: None,
157 user_id: None,
158 stop: None,
159 response_format: None,
160 }
161 }
162
163 pub fn add_messages(mut self, messages: M) -> Self {
164 self.messages.push(messages);
165 self
166 }
167 pub fn add_message(mut self, message: M) -> Self {
170 self.messages.push(message);
171 self
172 }
173 pub fn extend_messages(mut self, messages: impl IntoIterator<Item = M>) -> Self {
175 self.messages.extend(messages);
176 self
177 }
178 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
179 self.request_id = Some(request_id.into());
180 self
181 }
182 pub fn with_do_sample(mut self, do_sample: bool) -> Self {
183 self.do_sample = Some(do_sample);
184 self
185 }
186 pub fn with_stream(mut self, stream: bool) -> Self {
187 self.stream = Some(stream);
188 self
189 }
190 pub fn with_temperature(mut self, temperature: f64) -> Self {
191 self.temperature = Some(temperature);
192 self
193 }
194 pub fn with_top_p(mut self, top_p: f64) -> Self {
195 self.top_p = Some(top_p);
196 self
197 }
198 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
199 self.max_tokens = Some(max_tokens);
200 self
201 }
202 #[deprecated(note = "with_tools is deprecated; use add_tool/add_tools instead")]
206 pub fn with_tools(mut self, tools: impl Into<Vec<Tools>>) -> Self {
207 self.tools = Some(tools.into());
208 self
209 }
210 pub fn add_tools(mut self, tools: Tools) -> Self {
211 self.tools.get_or_insert(Vec::new()).push(tools);
212 self
213 }
214 pub fn extend_tools(mut self, tools: Vec<Tools>) -> Self {
215 self.tools.get_or_insert(Vec::new()).extend(tools);
216 self
217 }
218 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
219 self.user_id = Some(user_id.into());
220 self
221 }
222 pub fn with_stop(mut self, stop: String) -> Self {
223 self.stop.get_or_insert_with(Vec::new).push(stop);
224 self
225 }
226}
227
228impl<N, M> ChatBody<N, M>
229where
230 N: ModelName + ThinkEnable,
231 (N, M): Bounded,
232{
233 pub fn with_thinking(mut self, thinking: ThinkingType) -> Self {
256 self.thinking = Some(thinking);
257 self
258 }
259}
260
261impl<N, M> ChatBody<N, M>
264where
265 N: ModelName + ReasoningEffortEnable,
266 (N, M): Bounded,
267{
268 pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
285 self.reasoning_effort = Some(effort);
286 self
287 }
288}
289
290impl<N, M> ChatBody<N, M>
292where
293 N: ModelName + ToolStreamEnable,
294 (N, M): Bounded,
295{
296 pub fn with_tool_stream(mut self, tool_stream: bool) -> Self {
299 self.tool_stream = Some(tool_stream);
300 if tool_stream {
301 self.stream = Some(true);
303 }
304 self
305 }
306}
307
308impl From<Tools> for Vec<Tools> {
310 fn from(tool: Tools) -> Self {
311 vec![tool]
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::model::{
319 chat_message_types::TextMessage,
320 chat_models::{GLM4_5, GLM4_6, GLM5_2},
321 };
322
323 #[test]
324 fn test_with_tool_stream_sets_both_fields() {
325 let body: ChatBody<GLM4_6, TextMessage> =
326 ChatBody::new(GLM4_6 {}, TextMessage::user("test"));
327 let body = body.with_tool_stream(true);
328 assert_eq!(body.tool_stream, Some(true));
329 assert_eq!(body.stream, Some(true));
330 }
331
332 #[test]
333 fn test_with_tool_stream_false_does_not_force_stream() {
334 let body: ChatBody<GLM4_6, TextMessage> =
335 ChatBody::new(GLM4_6 {}, TextMessage::user("test"));
336 let body = body.with_tool_stream(false);
337 assert_eq!(body.tool_stream, Some(false));
338 assert_ne!(body.stream, Some(true));
340 }
341
342 #[test]
343 fn test_add_tools_accumulates() {
344 let body: ChatBody<GLM4_6, TextMessage> =
345 ChatBody::new(GLM4_6 {}, TextMessage::user("test"));
346 let tool = crate::model::tools::Function::new(
347 "test_fn",
348 "A test function",
349 serde_json::json!({"type": "object"}),
350 );
351 let body = body.add_tools(crate::model::tools::Tools::Function { function: tool });
352 assert!(body.tools.is_some());
353 assert_eq!(body.tools.unwrap().len(), 1);
354 }
355
356 #[test]
357 fn test_extend_messages() {
358 let body: ChatBody<GLM4_6, TextMessage> =
359 ChatBody::new(GLM4_6 {}, TextMessage::user("first"));
360 let body = body.extend_messages(vec![
361 TextMessage::assistant("second"),
362 TextMessage::user("third"),
363 ]);
364 assert_eq!(body.messages.len(), 3);
365 match &body.messages[0] {
366 TextMessage::User { content } => assert_eq!(content, "first"),
367 _ => panic!("Expected User message"),
368 }
369 }
370
371 #[test]
372 fn test_add_message() {
373 let body: ChatBody<GLM4_6, TextMessage> =
374 ChatBody::new(GLM4_6 {}, TextMessage::user("first"));
375 let body = body.add_message(TextMessage::assistant("second"));
376 assert_eq!(body.messages.len(), 2);
377 }
378
379 #[test]
380 fn test_glm52_reasoning_effort_builder() {
381 let body: ChatBody<GLM5_2, TextMessage> = ChatBody::new(GLM5_2 {}, TextMessage::user("hi"))
383 .with_thinking(ThinkingType::enabled())
384 .with_reasoning_effort(ReasoningEffort::Max);
385 assert_eq!(body.reasoning_effort, Some(ReasoningEffort::Max));
386 assert!(body.thinking.is_some());
387 }
388
389 #[test]
390 fn test_glm52_serializes_model_name() {
391 let body: ChatBody<GLM5_2, TextMessage> = ChatBody::new(GLM5_2 {}, TextMessage::user("hi"));
392 let json = serde_json::to_value(&body).unwrap();
393 assert_eq!(json["model"], "glm-5.2");
394 assert!(json.get("reasoning_effort").is_none());
396 }
397
398 #[test]
399 fn test_sampling_parameters_serialize_without_f32_expansion() {
400 let body: ChatBody<GLM4_5, TextMessage> = ChatBody::new(GLM4_5 {}, TextMessage::user("hi"))
401 .with_temperature(0.7)
402 .with_top_p(0.9);
403
404 let json = serde_json::to_value(&body).unwrap();
405 assert_eq!(json["temperature"], serde_json::json!(0.7));
406 assert_eq!(json["top_p"], serde_json::json!(0.9));
407
408 let serialized = serde_json::to_string(&body).unwrap();
409 assert!(serialized.contains("\"temperature\":0.7"));
410 assert!(serialized.contains("\"top_p\":0.9"));
411 assert!(!serialized.contains("0.699999"));
412 assert!(!serialized.contains("0.899999"));
413 }
414
415 #[test]
416 fn test_reasoning_effort_serializes_level() {
417 let body: ChatBody<GLM5_2, TextMessage> = ChatBody::new(GLM5_2 {}, TextMessage::user("hi"))
418 .with_reasoning_effort(ReasoningEffort::Xhigh);
419 let json = serde_json::to_value(&body).unwrap();
420 assert_eq!(json["reasoning_effort"], "xhigh");
421 }
422}