1use std::borrow::Cow;
6
7use crate::{
8 chat::*,
9 common::{FunctionCallDelta, ToolCallDelta, Usage},
10};
11
12#[must_use = "Builder does nothing until .build() is called"]
16#[derive(Clone, Debug)]
17pub struct ChatCompletionStreamResponseBuilder {
18 id: String,
19 object: String,
20 created: u64,
21 model: String,
22 choices: Vec<ChatStreamChoice>,
23 usage: Option<Usage>,
24 system_fingerprint: Option<String>,
25}
26
27impl ChatCompletionStreamResponseBuilder {
28 pub fn new(id: impl Into<String>, model: impl Into<String>) -> Self {
34 Self {
35 id: id.into(),
36 object: "chat.completion.chunk".to_string(),
37 created: chrono::Utc::now().timestamp() as u64,
38 model: model.into(),
39 choices: Vec::new(),
40 usage: None,
41 system_fingerprint: None,
42 }
43 }
44
45 pub fn copy_from_request(mut self, request: &ChatCompletionRequest) -> Self {
49 self.model = request.model.clone();
50 self
51 }
52
53 pub fn object(mut self, object: impl Into<String>) -> Self {
55 self.object = object.into();
56 self
57 }
58
59 pub fn created(mut self, timestamp: u64) -> Self {
61 self.created = timestamp;
62 self
63 }
64
65 pub fn choices(mut self, choices: Vec<ChatStreamChoice>) -> Self {
67 self.choices = choices;
68 self
69 }
70
71 pub fn add_choice(mut self, choice: ChatStreamChoice) -> Self {
73 self.choices.push(choice);
74 self
75 }
76
77 pub fn usage(mut self, usage: Usage) -> Self {
79 self.usage = Some(usage);
80 self
81 }
82
83 pub fn maybe_system_fingerprint(mut self, fingerprint: Option<impl Into<String>>) -> Self {
85 if let Some(fp) = fingerprint {
86 self.system_fingerprint = Some(fp.into());
87 }
88 self
89 }
90
91 pub fn maybe_usage(mut self, usage: Option<Usage>) -> Self {
93 if let Some(u) = usage {
94 self.usage = Some(u);
95 }
96 self
97 }
98
99 pub fn add_choice_content(
101 mut self,
102 index: u32,
103 role: impl Into<String>,
104 content: impl Into<String>,
105 ) -> Self {
106 self.choices.push(ChatStreamChoice {
107 index,
108 delta: ChatMessageDelta {
109 role: Some(role.into()),
110 content: Some(content.into()),
111 tool_calls: None,
112 reasoning_content: None,
113 },
114 logprobs: None,
115 finish_reason: None,
116 matched_stop: None,
117 });
118 self
119 }
120
121 pub fn add_choice_content_with_logprobs(
123 mut self,
124 index: u32,
125 role: impl Into<String>,
126 content: impl Into<String>,
127 logprobs: Option<crate::common::ChatLogProbs>,
128 ) -> Self {
129 self.choices.push(ChatStreamChoice {
130 index,
131 delta: ChatMessageDelta {
132 role: Some(role.into()),
133 content: Some(content.into()),
134 tool_calls: None,
135 reasoning_content: None,
136 },
137 logprobs,
138 finish_reason: None,
139 matched_stop: None,
140 });
141 self
142 }
143
144 pub fn add_choice_role(mut self, index: u32, role: impl Into<String>) -> Self {
146 self.choices.push(ChatStreamChoice {
147 index,
148 delta: ChatMessageDelta {
149 role: Some(role.into()),
150 content: None,
151 tool_calls: None,
152 reasoning_content: None,
153 },
154 logprobs: None,
155 finish_reason: None,
156 matched_stop: None,
157 });
158 self
159 }
160
161 pub fn add_choice_tool_args(
164 mut self,
165 index: u32,
166 args_delta: impl Into<Cow<'static, str>>,
167 ) -> Self {
168 self.choices.push(ChatStreamChoice {
169 index,
170 delta: ChatMessageDelta {
171 role: Some("assistant".to_string()),
172 content: None,
173 tool_calls: Some(vec![ToolCallDelta {
174 index: 0,
175 id: None,
176 tool_type: None,
177 function: Some(FunctionCallDelta {
178 name: None,
179 arguments: Some(args_delta.into().into_owned()),
180 }),
181 }]),
182 reasoning_content: None,
183 },
184 logprobs: None,
185 finish_reason: None,
186 matched_stop: None,
187 });
188 self
189 }
190
191 pub fn add_choice_reasoning(mut self, index: u32, reasoning: impl Into<String>) -> Self {
193 self.choices.push(ChatStreamChoice {
194 index,
195 delta: ChatMessageDelta {
196 role: Some("assistant".to_string()),
197 content: None,
198 tool_calls: None,
199 reasoning_content: Some(reasoning.into()),
200 },
201 logprobs: None,
202 finish_reason: None,
203 matched_stop: None,
204 });
205 self
206 }
207
208 pub fn add_choice_tool_name(
210 mut self,
211 index: u32,
212 tool_call_id: impl Into<String>,
213 function_name: impl Into<String>,
214 ) -> Self {
215 self.choices.push(ChatStreamChoice {
216 index,
217 delta: ChatMessageDelta {
218 role: Some("assistant".to_string()),
219 content: None,
220 tool_calls: Some(vec![ToolCallDelta {
221 index: 0,
222 id: Some(tool_call_id.into()),
223 tool_type: Some("function".to_string()),
224 function: Some(FunctionCallDelta {
225 name: Some(function_name.into()),
226 arguments: None,
227 }),
228 }]),
229 reasoning_content: None,
230 },
231 logprobs: None,
232 finish_reason: None,
233 matched_stop: None,
234 });
235 self
236 }
237
238 pub fn add_choice_tool_call_delta(
241 mut self,
242 index: u32,
243 tool_call_delta: ToolCallDelta,
244 ) -> Self {
245 self.choices.push(ChatStreamChoice {
246 index,
247 delta: ChatMessageDelta {
248 role: Some("assistant".to_string()),
249 content: None,
250 tool_calls: Some(vec![tool_call_delta]),
251 reasoning_content: None,
252 },
253 logprobs: None,
254 finish_reason: None,
255 matched_stop: None,
256 });
257 self
258 }
259
260 pub fn add_choice_finish_reason(
263 mut self,
264 index: u32,
265 finish_reason: impl Into<String>,
266 matched_stop: Option<serde_json::Value>,
267 ) -> Self {
268 self.choices.push(ChatStreamChoice {
269 index,
270 delta: ChatMessageDelta {
271 role: None,
272 content: None,
273 tool_calls: None,
274 reasoning_content: None,
275 },
276 logprobs: None,
277 finish_reason: Some(finish_reason.into()),
278 matched_stop,
279 });
280 self
281 }
282
283 pub fn build(self) -> ChatCompletionStreamResponse {
285 ChatCompletionStreamResponse {
286 id: self.id,
287 object: self.object,
288 created: self.created,
289 model: self.model,
290 system_fingerprint: self.system_fingerprint,
291 choices: self.choices,
292 usage: self.usage,
293 }
294 }
295}
296
297#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_build_minimal() {
307 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_123", "gpt-4").build();
308
309 assert_eq!(chunk.id, "chatcmpl_123");
310 assert_eq!(chunk.model, "gpt-4");
311 assert_eq!(chunk.object, "chat.completion.chunk");
312 assert!(chunk.choices.is_empty());
313 assert!(chunk.usage.is_none());
314 }
315
316 #[test]
317 fn test_with_content_delta() {
318 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_456", "gpt-4")
319 .add_choice_content(0, "assistant", "Hello")
320 .build();
321
322 assert_eq!(chunk.choices.len(), 1);
323 assert_eq!(chunk.choices[0].index, 0);
324 assert_eq!(chunk.choices[0].delta.content.as_ref().unwrap(), "Hello");
325 assert_eq!(chunk.choices[0].delta.role.as_ref().unwrap(), "assistant");
326 assert!(chunk.choices[0].finish_reason.is_none());
327 }
328
329 #[test]
330 fn test_with_role_delta() {
331 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_789", "gpt-4")
332 .add_choice_role(0, "assistant")
333 .build();
334
335 assert_eq!(chunk.choices.len(), 1);
336 assert_eq!(chunk.choices[0].delta.role.as_ref().unwrap(), "assistant");
337 assert!(chunk.choices[0].delta.content.is_none());
338 }
339
340 #[test]
341 fn test_with_finish_reason() {
342 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_101", "gpt-4")
343 .add_choice_finish_reason(0, "stop", None)
344 .build();
345
346 assert_eq!(chunk.choices.len(), 1);
347 assert_eq!(chunk.choices[0].finish_reason.as_ref().unwrap(), "stop");
348 assert!(chunk.choices[0].delta.content.is_none());
349 assert!(chunk.choices[0].delta.role.is_none());
350 }
351
352 #[test]
353 fn test_multiple_deltas() {
354 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_202", "gpt-4")
355 .add_choice_role(0, "assistant")
356 .add_choice_content(0, "assistant", "Hello")
357 .add_choice_content(0, "assistant", " world")
358 .add_choice_finish_reason(0, "stop", None)
359 .build();
360
361 assert_eq!(chunk.choices.len(), 4); }
363
364 #[test]
365 fn test_with_usage() {
366 let usage = Usage {
367 prompt_tokens: 10,
368 completion_tokens: 20,
369 total_tokens: 30,
370 completion_tokens_details: None,
371 };
372
373 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_303", "gpt-4")
374 .add_choice_finish_reason(0, "stop", None)
375 .usage(usage)
376 .build();
377
378 assert!(chunk.usage.is_some());
379 assert_eq!(chunk.usage.as_ref().unwrap().total_tokens, 30);
380 }
381
382 #[test]
383 fn test_copy_from_request() {
384 let request = ChatCompletionRequest {
385 messages: vec![],
386 model: "gpt-3.5-turbo".to_string(),
387 ..Default::default()
388 };
389
390 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_404", "gpt-4")
391 .copy_from_request(&request)
392 .add_choice_content(0, "assistant", "test")
393 .build();
394
395 assert_eq!(chunk.model, "gpt-3.5-turbo"); }
397
398 #[test]
399 fn test_add_choice_explicit() {
400 let choice = ChatStreamChoice {
401 index: 0,
402 delta: ChatMessageDelta {
403 role: Some("assistant".to_string()),
404 content: Some("Hello".to_string()),
405 tool_calls: None,
406 reasoning_content: None,
407 },
408 logprobs: None,
409 finish_reason: None,
410 matched_stop: None,
411 };
412
413 let chunk = ChatCompletionStreamResponseBuilder::new("chatcmpl_505", "gpt-4")
414 .add_choice(choice)
415 .build();
416
417 assert_eq!(chunk.choices.len(), 1);
418 assert_eq!(chunk.choices[0].delta.role.as_ref().unwrap(), "assistant");
419 assert_eq!(chunk.choices[0].delta.content.as_ref().unwrap(), "Hello");
420 }
421}