use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::completion::Reasoning;
use super::message::Role;
use super::tool::ToolCallDelta;
use super::usage::CompletionUsage;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ChoiceDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChunkChoice {
pub index: u32,
pub delta: ChoiceDelta,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: String,
pub created: i64,
pub model: String,
pub choices: Vec<ChunkChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<CompletionUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
}
impl ChatCompletionChunk {
pub(crate) fn empty(model: &str) -> Self {
Self {
id: String::new(),
object: "chat.completion.chunk".to_string(),
created: 0,
model: model.to_string(),
choices: vec![],
usage: None,
system_fingerprint: None,
}
}
pub fn content(&self) -> Option<&str> {
self.choices
.first()
.and_then(|c| c.delta.content.as_deref())
}
pub fn reasoning(&self) -> Option<&str> {
self.choices
.first()
.and_then(|c| c.delta.reasoning.as_ref())
.map(|r| r.content.as_str())
}
pub fn tool_calls(&self) -> Option<&[ToolCallDelta]> {
self.choices
.first()
.and_then(|c| c.delta.tool_calls.as_deref())
}
pub fn is_final(&self) -> bool {
self.choices
.first()
.map(|c| c.finish_reason.is_some())
.unwrap_or(false)
}
pub fn finish_reason(&self) -> Option<&str> {
self.choices
.first()
.and_then(|c| c.finish_reason.as_deref())
}
}
#[derive(Debug, Default)]
pub struct ChunkAccumulator {
pub content: String,
pub reasoning: String,
pub tool_calls: Vec<AccumulatedToolCall>,
pub finish_reason: Option<String>,
pub model: Option<String>,
pub id: Option<String>,
pub usage: Option<CompletionUsage>,
}
#[derive(Debug, Clone, Default)]
pub struct AccumulatedToolCall {
pub id: String,
pub tool_type: String,
pub name: String,
pub arguments: String,
}
impl ChunkAccumulator {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, chunk: &ChatCompletionChunk) {
if self.id.is_none() {
self.id = Some(chunk.id.clone());
self.model = Some(chunk.model.clone());
}
for choice in &chunk.choices {
if let Some(content) = &choice.delta.content {
self.content.push_str(content);
}
if let Some(reasoning) = &choice.delta.reasoning {
self.reasoning.push_str(&reasoning.content);
}
if let Some(tool_calls) = &choice.delta.tool_calls {
for tc in tool_calls {
let index = tc.index.unwrap_or(0) as usize;
while self.tool_calls.len() <= index {
self.tool_calls.push(AccumulatedToolCall::default());
}
let accumulated = &mut self.tool_calls[index];
if let Some(id) = &tc.id {
accumulated.id = id.clone();
}
if let Some(tool_type) = &tc.tool_type {
accumulated.tool_type = tool_type.clone();
}
if let Some(function) = &tc.function {
if let Some(name) = &function.name {
accumulated.name = name.clone();
}
if let Some(args) = &function.arguments {
accumulated.arguments.push_str(args);
}
}
}
}
if let Some(reason) = &choice.finish_reason {
self.finish_reason = Some(reason.clone());
}
}
if let Some(usage) = &chunk.usage {
self.usage = Some(usage.clone());
}
}
pub fn has_content(&self) -> bool {
!self.content.is_empty()
}
pub fn has_reasoning(&self) -> bool {
!self.reasoning.is_empty()
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
}