1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use itertools::Itertools as _;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use super::{ReasoningItem, ToolCallBuilder, tools::ToolCall};
9
10#[derive(Clone, Builder, Debug, Serialize, Deserialize, PartialEq)]
15#[builder(setter(strip_option, into), build_fn(error = anyhow::Error))]
16pub struct ChatCompletionResponse {
17 #[builder(private, default = Uuid::new_v4())]
21 pub id: Uuid,
22
23 #[builder(default)]
24 pub message: Option<String>,
25
26 #[builder(default)]
27 pub tool_calls: Option<Vec<ToolCall>>,
28
29 #[builder(default)]
30 pub usage: Option<Usage>,
31
32 #[builder(default)]
33 pub reasoning: Option<Vec<ReasoningItem>>,
34
35 #[builder(default)]
37 pub delta: Option<ChatCompletionResponseDelta>,
38}
39
40impl Default for ChatCompletionResponse {
41 fn default() -> Self {
42 Self {
43 id: Uuid::new_v4(),
44 message: None,
45 tool_calls: None,
46 delta: None,
47 usage: None,
48 reasoning: None,
49 }
50 }
51}
52
53#[derive(Clone, Default, Builder, Debug, Serialize, Deserialize, PartialEq)]
55#[allow(clippy::struct_field_names)]
56pub struct Usage {
57 pub prompt_tokens: u32,
59 pub completion_tokens: u32,
61 pub total_tokens: u32,
63 #[builder(default)]
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub details: Option<UsageDetails>,
67}
68
69impl Usage {
70 pub fn builder() -> UsageBuilder {
71 UsageBuilder::default()
72 }
73
74 pub fn normalized(&self) -> NormalizedUsage {
79 let details = self.details.as_ref().map(|details| {
80 let input = NormalizedInputUsageDetails {
81 cached_tokens: details
82 .input_tokens_details
83 .as_ref()
84 .and_then(|input| input.cached_tokens)
85 .or_else(|| {
86 details
87 .prompt_tokens_details
88 .as_ref()
89 .and_then(|prompt| prompt.cached_tokens)
90 }),
91 audio_tokens: details
92 .prompt_tokens_details
93 .as_ref()
94 .and_then(|prompt| prompt.audio_tokens),
95 };
96 let output = NormalizedOutputUsageDetails {
97 reasoning_tokens: details
98 .output_tokens_details
99 .as_ref()
100 .and_then(|output| output.reasoning_tokens)
101 .or_else(|| {
102 details
103 .completion_tokens_details
104 .as_ref()
105 .and_then(|completion| completion.reasoning_tokens)
106 }),
107 audio_tokens: details
108 .completion_tokens_details
109 .as_ref()
110 .and_then(|completion| completion.audio_tokens),
111 accepted_prediction_tokens: details
112 .completion_tokens_details
113 .as_ref()
114 .and_then(|completion| completion.accepted_prediction_tokens),
115 rejected_prediction_tokens: details
116 .completion_tokens_details
117 .as_ref()
118 .and_then(|completion| completion.rejected_prediction_tokens),
119 };
120
121 if input.is_empty() && output.is_empty() {
122 None
123 } else {
124 Some(NormalizedUsageDetails { input, output })
125 }
126 });
127
128 NormalizedUsage {
129 prompt_tokens: self.prompt_tokens,
130 completion_tokens: self.completion_tokens,
131 total_tokens: self.total_tokens,
132 details: details.flatten(),
133 }
134 }
135}
136
137#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
139pub struct UsageDetails {
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub prompt_tokens_details: Option<PromptTokensDetails>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub completion_tokens_details: Option<CompletionTokensDetails>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub input_tokens_details: Option<InputTokenDetails>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub output_tokens_details: Option<OutputTokenDetails>,
152}
153
154#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
156pub struct NormalizedUsage {
157 pub prompt_tokens: u32,
159 pub completion_tokens: u32,
161 pub total_tokens: u32,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub details: Option<NormalizedUsageDetails>,
166}
167
168#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
170pub struct NormalizedUsageDetails {
171 pub input: NormalizedInputUsageDetails,
173 pub output: NormalizedOutputUsageDetails,
175}
176
177#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
179pub struct NormalizedInputUsageDetails {
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub cached_tokens: Option<u32>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub audio_tokens: Option<u32>,
186}
187
188impl NormalizedInputUsageDetails {
189 fn is_empty(&self) -> bool {
190 self.cached_tokens.is_none() && self.audio_tokens.is_none()
191 }
192}
193
194#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
196pub struct NormalizedOutputUsageDetails {
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub reasoning_tokens: Option<u32>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub audio_tokens: Option<u32>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub accepted_prediction_tokens: Option<u32>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub rejected_prediction_tokens: Option<u32>,
209}
210
211impl NormalizedOutputUsageDetails {
212 fn is_empty(&self) -> bool {
213 self.reasoning_tokens.is_none()
214 && self.audio_tokens.is_none()
215 && self.accepted_prediction_tokens.is_none()
216 && self.rejected_prediction_tokens.is_none()
217 }
218}
219
220#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
222pub struct PromptTokensDetails {
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub audio_tokens: Option<u32>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub cached_tokens: Option<u32>,
229}
230
231impl PromptTokensDetails {
232 pub fn is_empty(&self) -> bool {
234 self.audio_tokens.is_none() && self.cached_tokens.is_none()
235 }
236}
237
238#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
240pub struct CompletionTokensDetails {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub accepted_prediction_tokens: Option<u32>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub audio_tokens: Option<u32>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub reasoning_tokens: Option<u32>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub rejected_prediction_tokens: Option<u32>,
253}
254
255impl CompletionTokensDetails {
256 pub fn is_empty(&self) -> bool {
258 self.accepted_prediction_tokens.is_none()
259 && self.audio_tokens.is_none()
260 && self.reasoning_tokens.is_none()
261 && self.rejected_prediction_tokens.is_none()
262 }
263}
264
265#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
267pub struct InputTokenDetails {
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub cached_tokens: Option<u32>,
271}
272
273#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
275pub struct OutputTokenDetails {
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub reasoning_tokens: Option<u32>,
279}
280
281#[cfg(feature = "openai")]
282mod openai_usage {
283 use super::{
284 CompletionTokensDetails, InputTokenDetails, OutputTokenDetails, PromptTokensDetails, Usage,
285 UsageDetails,
286 };
287 use async_openai::types::{
288 chat::CompletionUsage, embeddings::EmbeddingUsage, responses::ResponseUsage,
289 };
290
291 impl From<&CompletionUsage> for Usage {
292 fn from(usage: &CompletionUsage) -> Self {
293 let prompt_details = usage.prompt_tokens_details.as_ref().and_then(|details| {
294 let details = PromptTokensDetails {
295 audio_tokens: details.audio_tokens,
296 cached_tokens: details.cached_tokens,
297 };
298 if details.is_empty() {
299 None
300 } else {
301 Some(details)
302 }
303 });
304 let completion_details = usage
305 .completion_tokens_details
306 .as_ref()
307 .and_then(|details| {
308 let details = CompletionTokensDetails {
309 accepted_prediction_tokens: details.accepted_prediction_tokens,
310 audio_tokens: details.audio_tokens,
311 reasoning_tokens: details.reasoning_tokens,
312 rejected_prediction_tokens: details.rejected_prediction_tokens,
313 };
314 if details.is_empty() {
315 None
316 } else {
317 Some(details)
318 }
319 });
320 let details = if prompt_details.is_some() || completion_details.is_some() {
321 Some(UsageDetails {
322 prompt_tokens_details: prompt_details,
323 completion_tokens_details: completion_details,
324 input_tokens_details: None,
325 output_tokens_details: None,
326 })
327 } else {
328 None
329 };
330
331 Usage {
332 prompt_tokens: usage.prompt_tokens,
333 completion_tokens: usage.completion_tokens,
334 total_tokens: usage.total_tokens,
335 details,
336 }
337 }
338 }
339
340 impl From<&ResponseUsage> for Usage {
341 fn from(usage: &ResponseUsage) -> Self {
342 Usage {
343 prompt_tokens: usage.input_tokens,
344 completion_tokens: usage.output_tokens,
345 total_tokens: usage.total_tokens,
346 details: Some(UsageDetails {
347 prompt_tokens_details: None,
348 completion_tokens_details: None,
349 input_tokens_details: Some(InputTokenDetails {
350 cached_tokens: Some(usage.input_tokens_details.cached_tokens),
351 }),
352 output_tokens_details: Some(OutputTokenDetails {
353 reasoning_tokens: Some(usage.output_tokens_details.reasoning_tokens),
354 }),
355 }),
356 }
357 }
358 }
359
360 impl From<&EmbeddingUsage> for Usage {
361 fn from(usage: &EmbeddingUsage) -> Self {
362 Usage {
363 prompt_tokens: usage.prompt_tokens,
364 completion_tokens: 0,
365 total_tokens: usage.total_tokens,
366 details: None,
367 }
368 }
369 }
370}
371
372#[derive(Clone, Builder, Debug, Serialize, Deserialize, PartialEq)]
373pub struct ChatCompletionResponseDelta {
374 #[builder(default)]
375 pub message_chunk: Option<String>,
376
377 #[builder(default)]
378 pub tool_calls_chunk: Option<HashMap<usize, ToolCallAccum>>,
379}
380
381#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
383pub struct ToolCallAccum {
384 pub id: Option<String>,
385 pub name: Option<String>,
386 pub arguments: Option<String>,
387}
388
389impl ChatCompletionResponse {
390 pub fn builder() -> ChatCompletionResponseBuilder {
391 ChatCompletionResponseBuilder::default()
392 }
393
394 pub fn message(&self) -> Option<&str> {
395 self.message.as_deref()
396 }
397
398 pub fn tool_calls(&self) -> Option<&[ToolCall]> {
399 self.tool_calls.as_deref()
400 }
401
402 pub fn append_message_delta(&mut self, message_delta: Option<&str>) -> &mut Self {
407 let Some(message_delta) = message_delta else {
408 if let Some(delta) = &mut self.delta {
409 delta.message_chunk = None;
410 }
411 return self;
412 };
413
414 if let Some(delta) = &mut self.delta {
415 delta.message_chunk = Some(message_delta.to_string());
416 } else {
417 self.delta = Some(ChatCompletionResponseDelta {
418 message_chunk: Some(message_delta.to_string()),
419 tool_calls_chunk: None,
420 });
421 }
422
423 self.message
424 .as_mut()
425 .map(|m| {
426 m.push_str(message_delta);
427 })
428 .unwrap_or_else(|| {
429 self.message = Some(message_delta.to_string());
430 });
431 self
432 }
433
434 pub fn append_tool_call_delta(
439 &mut self,
440 index: usize,
441 id: Option<&str>,
442 name: Option<&str>,
443 arguments: Option<&str>,
444 ) -> &mut Self {
445 if let Some(delta) = &mut self.delta {
446 let map = delta.tool_calls_chunk.get_or_insert_with(HashMap::new);
447 map.entry(index)
448 .and_modify(|v| {
449 if v.id.is_none() {
450 v.id = id.map(Into::into);
451 }
452 if v.name.is_none() {
453 v.name = name.map(Into::into);
454 }
455 if let Some(v) = v.arguments.as_mut() {
456 if let Some(arguments) = arguments {
457 v.push_str(arguments);
458 }
459 } else {
460 v.arguments = arguments.map(Into::into);
461 }
462 })
463 .or_insert(ToolCallAccum {
464 id: id.map(Into::into),
465 name: name.map(Into::into),
466 arguments: arguments.map(Into::into),
467 });
468 } else {
469 self.delta = Some(ChatCompletionResponseDelta {
470 message_chunk: None,
471 tool_calls_chunk: Some(HashMap::from([(
472 index,
473 ToolCallAccum {
474 id: id.map(Into::into),
475 name: name.map(Into::into),
476 arguments: arguments.map(Into::into),
477 },
478 )])),
479 });
480 }
481
482 self.finalize_tools_from_stream();
485
486 self
487 }
488
489 pub fn append_usage_delta(
490 &mut self,
491 prompt_tokens: u32,
492 completion_tokens: u32,
493 total_tokens: u32,
494 ) -> &mut Self {
495 debug_assert_eq!(prompt_tokens + completion_tokens, total_tokens);
496
497 if let Some(usage) = &mut self.usage {
498 usage.prompt_tokens += prompt_tokens;
499 usage.completion_tokens += completion_tokens;
500 usage.total_tokens += total_tokens;
501 } else {
502 self.usage = Some(Usage {
503 prompt_tokens,
504 completion_tokens,
505 total_tokens,
506 details: None,
507 });
508 }
509 self
510 }
511
512 fn finalize_tools_from_stream(&mut self) {
513 if let Some(values) = self
514 .delta
515 .as_ref()
516 .and_then(|d| d.tool_calls_chunk.as_ref())
517 {
518 let maybe_tool_calls = values
519 .iter()
520 .sorted_by_key(|(index, _)| **index)
521 .filter_map(|(_, maybe_tool_call)| {
522 ToolCallBuilder::default()
523 .maybe_id(maybe_tool_call.id.clone())
524 .maybe_name(maybe_tool_call.name.clone())
525 .maybe_args(maybe_tool_call.arguments.clone())
526 .build()
527 .ok()
528 })
529 .collect::<Vec<_>>();
530
531 if !maybe_tool_calls.is_empty() {
532 self.tool_calls = Some(maybe_tool_calls);
533 }
534 }
535 }
536}
537
538impl ChatCompletionResponseBuilder {
539 pub fn maybe_message<T: Into<Option<String>>>(&mut self, message: T) -> &mut Self {
540 self.message = Some(message.into());
541 self
542 }
543
544 pub fn maybe_tool_calls<T: Into<Option<Vec<ToolCall>>>>(&mut self, tool_calls: T) -> &mut Self {
545 self.tool_calls = Some(tool_calls.into());
546 self
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn test_streamed_tool_calls_follow_their_indices() {
556 let mut response = ChatCompletionResponse::default();
557 for index in (0..16).rev() {
558 response.append_tool_call_delta(
559 index,
560 Some(&format!("call_{index}")),
561 Some("test_tool"),
562 Some("{}"),
563 );
564 }
565
566 let ids = response
567 .tool_calls()
568 .unwrap()
569 .iter()
570 .map(ToolCall::id)
571 .collect::<Vec<_>>();
572 let expected = (0..16)
573 .map(|index| format!("call_{index}"))
574 .collect::<Vec<_>>();
575 assert_eq!(ids, expected);
576 }
577
578 #[test]
579 fn test_append_message_delta_none_on_empty_response() {
580 let mut response = ChatCompletionResponse::default();
581 let original = response.clone();
582
583 response.append_message_delta(None);
584
585 assert_eq!(response, original);
586 }
587
588 #[test]
589 fn test_append_message_delta_clears_text_and_preserves_tool_fragments() {
590 let mut response = ChatCompletionResponse::default();
591 response.append_message_delta(Some("Hello"));
592 response.append_tool_call_delta(0, Some("call-1"), Some("weather"), Some("{\"city\":"));
593 let tool_fragments = response.delta.as_ref().unwrap().tool_calls_chunk.clone();
594
595 response.append_message_delta(None);
596
597 let delta = response.delta.as_ref().unwrap();
598 assert_eq!(delta.message_chunk, None);
599 assert_eq!(delta.tool_calls_chunk, tool_fragments);
600 assert_eq!(response.message(), Some("Hello"));
601
602 response.append_tool_call_delta(0, None, None, Some("\"Amsterdam\"}"));
603 let tool = &response.tool_calls().unwrap()[0];
604 assert_eq!(tool.id(), "call-1");
605 assert_eq!(tool.name(), "weather");
606 assert_eq!(tool.args(), Some("{\"city\":\"Amsterdam\"}"));
607
608 response.append_message_delta(Some(" again"));
609 assert_eq!(response.message(), Some("Hello again"));
610 assert_eq!(
611 response.delta.as_ref().unwrap().message_chunk.as_deref(),
612 Some(" again")
613 );
614 }
615}