Skip to main content

swiftide_core/chat_completion/
chat_completion_response.rs

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/// A generic response from chat completions
10///
11/// When streaming, the delta is available. Every response will have the accumulated message if
12/// present. The final message will also have the final tool calls.
13#[derive(Clone, Builder, Debug, Serialize, Deserialize, PartialEq)]
14#[builder(setter(strip_option, into), build_fn(error = anyhow::Error))]
15pub struct ChatCompletionResponse {
16    /// An identifier for the response
17    ///
18    /// Useful when streaming to make sure chunks can be mapped to the right response
19    #[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    /// Streaming response
35    #[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/// Usage statistics for a language model response.
53#[derive(Clone, Default, Builder, Debug, Serialize, Deserialize, PartialEq)]
54#[allow(clippy::struct_field_names)]
55pub struct Usage {
56    /// Tokens used in the prompt or input.
57    pub prompt_tokens: u32,
58    /// Tokens generated in the completion or output.
59    pub completion_tokens: u32,
60    /// Total tokens used for the request.
61    pub total_tokens: u32,
62    /// Provider-specific usage breakdowns, when available.
63    #[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    /// Returns a normalized view of usage details when available.
74    ///
75    /// This keeps the public `Usage` fields intact and derives a consistent input/output breakdown
76    /// across providers (e.g. `OpenAI` chat vs. responses). Missing data is left as `None`.
77    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/// Provider-specific usage breakdowns for a response.
137#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
138pub struct UsageDetails {
139    /// Chat-completions style prompt token details.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub prompt_tokens_details: Option<PromptTokensDetails>,
142    /// Chat-completions style completion token details.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub completion_tokens_details: Option<CompletionTokensDetails>,
145    /// Responses-style input token details.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub input_tokens_details: Option<InputTokenDetails>,
148    /// Responses-style output token details.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub output_tokens_details: Option<OutputTokenDetails>,
151}
152
153/// Normalized usage totals with optional normalized details.
154#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
155pub struct NormalizedUsage {
156    /// Tokens used in the prompt or input.
157    pub prompt_tokens: u32,
158    /// Tokens generated in the completion or output.
159    pub completion_tokens: u32,
160    /// Total tokens used for the request.
161    pub total_tokens: u32,
162    /// Normalized input/output breakdown, when available.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub details: Option<NormalizedUsageDetails>,
165}
166
167/// Normalized input/output usage breakdown.
168#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
169pub struct NormalizedUsageDetails {
170    /// Normalized input usage details.
171    pub input: NormalizedInputUsageDetails,
172    /// Normalized output usage details.
173    pub output: NormalizedOutputUsageDetails,
174}
175
176/// Normalized input usage details.
177#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
178pub struct NormalizedInputUsageDetails {
179    /// Tokens retrieved from cache, when provided.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub cached_tokens: Option<u32>,
182    /// Audio tokens in the input, when provided.
183    #[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/// Normalized output usage details.
194#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
195pub struct NormalizedOutputUsageDetails {
196    /// Tokens used for reasoning, when provided.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub reasoning_tokens: Option<u32>,
199    /// Audio tokens in the output, when provided.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub audio_tokens: Option<u32>,
202    /// Accepted prediction tokens, when provided.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub accepted_prediction_tokens: Option<u32>,
205    /// Rejected prediction tokens, when provided.
206    #[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/// OpenAI-style prompt token details (chat completions).
220#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
221pub struct PromptTokensDetails {
222    /// Audio input tokens present in the prompt.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub audio_tokens: Option<u32>,
225    /// Cached tokens present in the prompt.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub cached_tokens: Option<u32>,
228}
229
230impl PromptTokensDetails {
231    /// Returns true when no prompt token detail values are set.
232    pub fn is_empty(&self) -> bool {
233        self.audio_tokens.is_none() && self.cached_tokens.is_none()
234    }
235}
236
237/// OpenAI-style completion token details (chat completions).
238#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
239pub struct CompletionTokensDetails {
240    /// Tokens accepted from predicted output, when provided.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub accepted_prediction_tokens: Option<u32>,
243    /// Audio tokens generated by the model, when provided.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub audio_tokens: Option<u32>,
246    /// Tokens generated by the model for reasoning, when provided.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub reasoning_tokens: Option<u32>,
249    /// Tokens rejected from predicted output, when provided.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub rejected_prediction_tokens: Option<u32>,
252}
253
254impl CompletionTokensDetails {
255    /// Returns true when no completion token detail values are set.
256    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/// OpenAI-style input token details (Responses API).
265#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
266pub struct InputTokenDetails {
267    /// Tokens retrieved from cache, when provided.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub cached_tokens: Option<u32>,
270}
271
272/// OpenAI-style output token details (Responses API).
273#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
274pub struct OutputTokenDetails {
275    /// Tokens used for reasoning, when provided.
276    #[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// Accumulator for streamed tool calls
381#[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    /// Adds a streaming chunk to the message and also the delta
402    pub fn append_message_delta(&mut self, message_delta: Option<&str>) -> &mut Self {
403        // let message: Option<String> = message;
404        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    /// Adds a streaming chunk to the tool calls, if it can be build, the tool call will be build,
429    /// otherwise it will remain in the delta and retried on the next call
430    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        // Now let's try to rebuild _every_ tool call and overwrite
475        // Performance wise very meh but it works, in reality it's only a couple of tool calls most
476        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}