1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use super::{ReasoningItem, ToolCallBuilder, tools::ToolCall};
8
9#[derive(Clone, Builder, Debug, Serialize, Deserialize, PartialEq)]
14#[builder(setter(strip_option, into), build_fn(error = anyhow::Error))]
15pub struct ChatCompletionResponse {
16 #[builder(private, default = Uuid::new_v4())]
20 pub id: Uuid,
21
22 #[builder(default)]
23 pub message: Option<String>,
24
25 #[builder(default)]
26 pub tool_calls: Option<Vec<ToolCall>>,
27
28 #[builder(default)]
29 pub usage: Option<Usage>,
30
31 #[builder(default)]
32 pub reasoning: Option<Vec<ReasoningItem>>,
33
34 #[builder(default)]
36 pub delta: Option<ChatCompletionResponseDelta>,
37}
38
39impl Default for ChatCompletionResponse {
40 fn default() -> Self {
41 Self {
42 id: Uuid::new_v4(),
43 message: None,
44 tool_calls: None,
45 delta: None,
46 usage: None,
47 reasoning: None,
48 }
49 }
50}
51
52#[derive(Clone, Default, Builder, Debug, Serialize, Deserialize, PartialEq)]
54#[allow(clippy::struct_field_names)]
55pub struct Usage {
56 pub prompt_tokens: u32,
58 pub completion_tokens: u32,
60 pub total_tokens: u32,
62 #[builder(default)]
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub details: Option<UsageDetails>,
66}
67
68impl Usage {
69 pub fn builder() -> UsageBuilder {
70 UsageBuilder::default()
71 }
72
73 pub fn normalized(&self) -> NormalizedUsage {
78 let details = self.details.as_ref().map(|details| {
79 let input = NormalizedInputUsageDetails {
80 cached_tokens: details
81 .input_tokens_details
82 .as_ref()
83 .and_then(|input| input.cached_tokens)
84 .or_else(|| {
85 details
86 .prompt_tokens_details
87 .as_ref()
88 .and_then(|prompt| prompt.cached_tokens)
89 }),
90 audio_tokens: details
91 .prompt_tokens_details
92 .as_ref()
93 .and_then(|prompt| prompt.audio_tokens),
94 };
95 let output = NormalizedOutputUsageDetails {
96 reasoning_tokens: details
97 .output_tokens_details
98 .as_ref()
99 .and_then(|output| output.reasoning_tokens)
100 .or_else(|| {
101 details
102 .completion_tokens_details
103 .as_ref()
104 .and_then(|completion| completion.reasoning_tokens)
105 }),
106 audio_tokens: details
107 .completion_tokens_details
108 .as_ref()
109 .and_then(|completion| completion.audio_tokens),
110 accepted_prediction_tokens: details
111 .completion_tokens_details
112 .as_ref()
113 .and_then(|completion| completion.accepted_prediction_tokens),
114 rejected_prediction_tokens: details
115 .completion_tokens_details
116 .as_ref()
117 .and_then(|completion| completion.rejected_prediction_tokens),
118 };
119
120 if input.is_empty() && output.is_empty() {
121 None
122 } else {
123 Some(NormalizedUsageDetails { input, output })
124 }
125 });
126
127 NormalizedUsage {
128 prompt_tokens: self.prompt_tokens,
129 completion_tokens: self.completion_tokens,
130 total_tokens: self.total_tokens,
131 details: details.flatten(),
132 }
133 }
134}
135
136#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
138pub struct UsageDetails {
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub prompt_tokens_details: Option<PromptTokensDetails>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub completion_tokens_details: Option<CompletionTokensDetails>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub input_tokens_details: Option<InputTokenDetails>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub output_tokens_details: Option<OutputTokenDetails>,
151}
152
153#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
155pub struct NormalizedUsage {
156 pub prompt_tokens: u32,
158 pub completion_tokens: u32,
160 pub total_tokens: u32,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub details: Option<NormalizedUsageDetails>,
165}
166
167#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
169pub struct NormalizedUsageDetails {
170 pub input: NormalizedInputUsageDetails,
172 pub output: NormalizedOutputUsageDetails,
174}
175
176#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
178pub struct NormalizedInputUsageDetails {
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub cached_tokens: Option<u32>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub audio_tokens: Option<u32>,
185}
186
187impl NormalizedInputUsageDetails {
188 fn is_empty(&self) -> bool {
189 self.cached_tokens.is_none() && self.audio_tokens.is_none()
190 }
191}
192
193#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
195pub struct NormalizedOutputUsageDetails {
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub reasoning_tokens: Option<u32>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub audio_tokens: Option<u32>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub accepted_prediction_tokens: Option<u32>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub rejected_prediction_tokens: Option<u32>,
208}
209
210impl NormalizedOutputUsageDetails {
211 fn is_empty(&self) -> bool {
212 self.reasoning_tokens.is_none()
213 && self.audio_tokens.is_none()
214 && self.accepted_prediction_tokens.is_none()
215 && self.rejected_prediction_tokens.is_none()
216 }
217}
218
219#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
221pub struct PromptTokensDetails {
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub audio_tokens: Option<u32>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub cached_tokens: Option<u32>,
228}
229
230impl PromptTokensDetails {
231 pub fn is_empty(&self) -> bool {
233 self.audio_tokens.is_none() && self.cached_tokens.is_none()
234 }
235}
236
237#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
239pub struct CompletionTokensDetails {
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub accepted_prediction_tokens: Option<u32>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub audio_tokens: Option<u32>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub reasoning_tokens: Option<u32>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub rejected_prediction_tokens: Option<u32>,
252}
253
254impl CompletionTokensDetails {
255 pub fn is_empty(&self) -> bool {
257 self.accepted_prediction_tokens.is_none()
258 && self.audio_tokens.is_none()
259 && self.reasoning_tokens.is_none()
260 && self.rejected_prediction_tokens.is_none()
261 }
262}
263
264#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
266pub struct InputTokenDetails {
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub cached_tokens: Option<u32>,
270}
271
272#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
274pub struct OutputTokenDetails {
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub reasoning_tokens: Option<u32>,
278}
279
280#[cfg(feature = "openai")]
281mod openai_usage {
282 use super::{
283 CompletionTokensDetails, InputTokenDetails, OutputTokenDetails, PromptTokensDetails, Usage,
284 UsageDetails,
285 };
286 use async_openai::types::{
287 chat::CompletionUsage, embeddings::EmbeddingUsage, responses::ResponseUsage,
288 };
289
290 impl From<&CompletionUsage> for Usage {
291 fn from(usage: &CompletionUsage) -> Self {
292 let prompt_details = usage.prompt_tokens_details.as_ref().and_then(|details| {
293 let details = PromptTokensDetails {
294 audio_tokens: details.audio_tokens,
295 cached_tokens: details.cached_tokens,
296 };
297 if details.is_empty() {
298 None
299 } else {
300 Some(details)
301 }
302 });
303 let completion_details = usage
304 .completion_tokens_details
305 .as_ref()
306 .and_then(|details| {
307 let details = CompletionTokensDetails {
308 accepted_prediction_tokens: details.accepted_prediction_tokens,
309 audio_tokens: details.audio_tokens,
310 reasoning_tokens: details.reasoning_tokens,
311 rejected_prediction_tokens: details.rejected_prediction_tokens,
312 };
313 if details.is_empty() {
314 None
315 } else {
316 Some(details)
317 }
318 });
319 let details = if prompt_details.is_some() || completion_details.is_some() {
320 Some(UsageDetails {
321 prompt_tokens_details: prompt_details,
322 completion_tokens_details: completion_details,
323 input_tokens_details: None,
324 output_tokens_details: None,
325 })
326 } else {
327 None
328 };
329
330 Usage {
331 prompt_tokens: usage.prompt_tokens,
332 completion_tokens: usage.completion_tokens,
333 total_tokens: usage.total_tokens,
334 details,
335 }
336 }
337 }
338
339 impl From<&ResponseUsage> for Usage {
340 fn from(usage: &ResponseUsage) -> Self {
341 Usage {
342 prompt_tokens: usage.input_tokens,
343 completion_tokens: usage.output_tokens,
344 total_tokens: usage.total_tokens,
345 details: Some(UsageDetails {
346 prompt_tokens_details: None,
347 completion_tokens_details: None,
348 input_tokens_details: Some(InputTokenDetails {
349 cached_tokens: Some(usage.input_tokens_details.cached_tokens),
350 }),
351 output_tokens_details: Some(OutputTokenDetails {
352 reasoning_tokens: Some(usage.output_tokens_details.reasoning_tokens),
353 }),
354 }),
355 }
356 }
357 }
358
359 impl From<&EmbeddingUsage> for Usage {
360 fn from(usage: &EmbeddingUsage) -> Self {
361 Usage {
362 prompt_tokens: usage.prompt_tokens,
363 completion_tokens: 0,
364 total_tokens: usage.total_tokens,
365 details: None,
366 }
367 }
368 }
369}
370
371#[derive(Clone, Builder, Debug, Serialize, Deserialize, PartialEq)]
372pub struct ChatCompletionResponseDelta {
373 #[builder(default)]
374 pub message_chunk: Option<String>,
375
376 #[builder(default)]
377 pub tool_calls_chunk: Option<HashMap<usize, ToolCallAccum>>,
378}
379
380#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
382pub struct ToolCallAccum {
383 pub id: Option<String>,
384 pub name: Option<String>,
385 pub arguments: Option<String>,
386}
387
388impl ChatCompletionResponse {
389 pub fn builder() -> ChatCompletionResponseBuilder {
390 ChatCompletionResponseBuilder::default()
391 }
392
393 pub fn message(&self) -> Option<&str> {
394 self.message.as_deref()
395 }
396
397 pub fn tool_calls(&self) -> Option<&[ToolCall]> {
398 self.tool_calls.as_deref()
399 }
400
401 pub fn append_message_delta(&mut self, message_delta: Option<&str>) -> &mut Self {
403 let Some(message_delta) = message_delta else {
405 return self;
406 };
407
408 if let Some(delta) = &mut self.delta {
409 delta.message_chunk = Some(message_delta.to_string());
410 } else {
411 self.delta = Some(ChatCompletionResponseDelta {
412 message_chunk: Some(message_delta.to_string()),
413 tool_calls_chunk: None,
414 });
415 }
416
417 self.message
418 .as_mut()
419 .map(|m| {
420 m.push_str(message_delta);
421 })
422 .unwrap_or_else(|| {
423 self.message = Some(message_delta.to_string());
424 });
425 self
426 }
427
428 pub fn append_tool_call_delta(
431 &mut self,
432 index: usize,
433 id: Option<&str>,
434 name: Option<&str>,
435 arguments: Option<&str>,
436 ) -> &mut Self {
437 if let Some(delta) = &mut self.delta {
438 let map = delta.tool_calls_chunk.get_or_insert_with(HashMap::new);
439 map.entry(index)
440 .and_modify(|v| {
441 if v.id.is_none() {
442 v.id = id.map(Into::into);
443 }
444 if v.name.is_none() {
445 v.name = name.map(Into::into);
446 }
447 if let Some(v) = v.arguments.as_mut() {
448 if let Some(arguments) = arguments {
449 v.push_str(arguments);
450 }
451 } else {
452 v.arguments = arguments.map(Into::into);
453 }
454 })
455 .or_insert(ToolCallAccum {
456 id: id.map(Into::into),
457 name: name.map(Into::into),
458 arguments: arguments.map(Into::into),
459 });
460 } else {
461 self.delta = Some(ChatCompletionResponseDelta {
462 message_chunk: None,
463 tool_calls_chunk: Some(HashMap::from([(
464 index,
465 ToolCallAccum {
466 id: id.map(Into::into),
467 name: name.map(Into::into),
468 arguments: arguments.map(Into::into),
469 },
470 )])),
471 });
472 }
473
474 self.finalize_tools_from_stream();
477
478 self
479 }
480
481 pub fn append_usage_delta(
482 &mut self,
483 prompt_tokens: u32,
484 completion_tokens: u32,
485 total_tokens: u32,
486 ) -> &mut Self {
487 debug_assert_eq!(prompt_tokens + completion_tokens, total_tokens);
488
489 if let Some(usage) = &mut self.usage {
490 usage.prompt_tokens += prompt_tokens;
491 usage.completion_tokens += completion_tokens;
492 usage.total_tokens += total_tokens;
493 } else {
494 self.usage = Some(Usage {
495 prompt_tokens,
496 completion_tokens,
497 total_tokens,
498 details: None,
499 });
500 }
501 self
502 }
503
504 fn finalize_tools_from_stream(&mut self) {
505 if let Some(values) = self
506 .delta
507 .as_ref()
508 .and_then(|d| d.tool_calls_chunk.as_ref().map(|t| t.values()))
509 {
510 let maybe_tool_calls = values
511 .filter_map(|maybe_tool_call| {
512 ToolCallBuilder::default()
513 .maybe_id(maybe_tool_call.id.clone())
514 .maybe_name(maybe_tool_call.name.clone())
515 .maybe_args(maybe_tool_call.arguments.clone())
516 .build()
517 .ok()
518 })
519 .collect::<Vec<_>>();
520
521 if !maybe_tool_calls.is_empty() {
522 self.tool_calls = Some(maybe_tool_calls);
523 }
524 }
525 }
526}
527
528impl ChatCompletionResponseBuilder {
529 pub fn maybe_message<T: Into<Option<String>>>(&mut self, message: T) -> &mut Self {
530 self.message = Some(message.into());
531 self
532 }
533
534 pub fn maybe_tool_calls<T: Into<Option<Vec<ToolCall>>>>(&mut self, tool_calls: T) -> &mut Self {
535 self.tool_calls = Some(tool_calls.into());
536 self
537 }
538}