use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatCompletionChunk {
pub id: String,
pub choices: Vec<super::Choice>,
pub created: u64,
pub model: String,
pub object: super::Object,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<super::Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}
impl ChatCompletionChunk {
pub fn into_downstream(
self,
id: String,
created: u64,
agent: String,
index: u64,
is_byok: bool,
cost_multiplier: rust_decimal::Decimal,
) -> objectiveai_sdk::agent::completions::response::streaming::AgentCompletionChunk {
let choice = self.choices.into_iter().next().unwrap_or_default();
let content = match (choice.delta.content, choice.delta.images) {
(Some(text), Some(images)) => {
let mut parts = vec![
objectiveai_sdk::agent::completions::message::RichContentPart::Text {
text,
},
];
parts.extend(images.into_iter().map(
objectiveai_sdk::agent::completions::message::RichContentPart::from,
));
Some(objectiveai_sdk::agent::completions::message::RichContent::Parts(parts))
}
(Some(text), None) => {
Some(objectiveai_sdk::agent::completions::message::RichContent::Text(text))
}
(None, Some(images)) => {
Some(objectiveai_sdk::agent::completions::message::RichContent::Parts(
images
.into_iter()
.map(objectiveai_sdk::agent::completions::message::RichContentPart::from)
.collect(),
))
}
(None, None) => None,
};
let message = objectiveai_sdk::agent::completions::response::streaming::MessageChunk::Assistant(
objectiveai_sdk::agent::completions::response::streaming::AssistantResponseChunk {
role: Default::default(),
index,
created: self.created,
agent,
model: self.model,
upstream_id: self.id,
reasoning: choice.delta.reasoning,
tool_calls: choice.delta.tool_calls,
content,
refusal: choice.delta.refusal,
finish_reason: choice.finish_reason,
logprobs: choice.logprobs,
service_tier: self.service_tier,
system_fingerprint: self.system_fingerprint,
provider: self.provider,
usage: self
.usage
.map(|usage| usage.into_downstream(is_byok, cost_multiplier)),
},
);
objectiveai_sdk::agent::completions::response::streaming::AgentCompletionChunk {
id,
created,
messages: vec![message],
object: Default::default(),
usage: None,
upstream: objectiveai_sdk::agent::Upstream::Openrouter,
error: None,
continuation: None,
}
}
pub fn push(
&mut self,
ChatCompletionChunk {
choices,
service_tier,
system_fingerprint,
usage,
provider,
..
}: &ChatCompletionChunk,
) {
self.push_choices(choices);
if self.service_tier.is_none() {
self.service_tier = service_tier.clone();
}
if self.system_fingerprint.is_none() {
self.system_fingerprint = system_fingerprint.clone();
}
match (&mut self.usage, usage) {
(Some(self_usage), Some(other_usage)) => {
self_usage.push(other_usage);
}
(None, Some(other_usage)) => {
self.usage = Some(other_usage.clone());
}
_ => {}
}
if self.provider.is_none() {
self.provider = provider.clone();
}
}
fn push_choices(&mut self, other_choices: &[super::Choice]) {
fn push_choice(
choices: &mut Vec<super::Choice>,
other: &super::Choice,
) {
fn find_choice(
choices: &mut Vec<super::Choice>,
index: u64,
) -> Option<&mut super::Choice> {
for choice in choices {
if choice.index == index {
return Some(choice);
}
}
None
}
if let Some(choice) = find_choice(choices, other.index) {
choice.push(other);
} else {
choices.push(other.clone());
}
}
for other_choice in other_choices {
push_choice(&mut self.choices, other_choice);
}
}
}