use super::request::{Role, ToolCall};
use super::response::Usage;
use serde::{Deserialize, Serialize};
#[cfg(feature = "streaming")]
use futures_util::Stream;
#[cfg(feature = "streaming")]
use std::pin::Pin;
#[cfg(feature = "streaming")]
pub type ChatStream =
Pin<Box<dyn Stream<Item = Result<StreamingResponse, crate::error::LlmConnectorError>> + Send>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingResponse {
pub id: String,
#[serde(default)]
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<StreamingChoice>,
#[serde(default)]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingChoice {
pub index: u32,
pub delta: Delta,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Delta {
#[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<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<String>,
}
impl Delta {
pub fn reasoning_any(&self) -> Option<&str> {
self.reasoning_content
.as_deref()
.or(self.reasoning.as_deref())
.or(self.thought.as_deref())
.or(self.thinking.as_deref())
}
pub fn populate_reasoning_from_json(&mut self, raw: &serde_json::Value) {
fn collect_synonyms(val: &serde_json::Value, acc: &mut std::collections::HashMap<String, String>) {
match val {
serde_json::Value::Array(arr) => {
for v in arr { collect_synonyms(v, acc); }
}
serde_json::Value::Object(map) => {
for (k, v) in map {
let key = k.to_ascii_lowercase();
if let serde_json::Value::String(s) = v {
match key.as_str() {
"reasoning_content" | "reasoning" | "thought" | "thinking" => {
acc.entry(key).or_insert_with(|| s.clone());
}
_ => {}
}
}
collect_synonyms(v, acc);
}
}
_ => {}
}
}
let mut found = std::collections::HashMap::<String, String>::new();
collect_synonyms(raw, &mut found);
if self.reasoning_content.is_none() {
if let Some(v) = found.get("reasoning_content") { self.reasoning_content = Some(v.clone()); }
}
if self.reasoning.is_none() {
if let Some(v) = found.get("reasoning") { self.reasoning = Some(v.clone()); }
}
if self.thought.is_none() {
if let Some(v) = found.get("thought") { self.thought = Some(v.clone()); }
}
if self.thinking.is_none() {
if let Some(v) = found.get("thinking") { self.thinking = Some(v.clone()); }
}
}
}
impl StreamingResponse {
pub fn populate_reasoning_synonyms(&mut self, raw: &serde_json::Value) {
for choice in &mut self.choices {
choice.delta.populate_reasoning_from_json(raw);
}
if self.reasoning_content.is_none() {
if let Some(reason) = self
.choices
.iter()
.find_map(|c| c.delta.reasoning_any().map(|s| s.to_string()))
{
self.reasoning_content = Some(reason);
}
}
}
pub fn get_content(&self) -> Option<&str> {
if self.content.is_empty() { None } else { Some(&self.content) }
}
}