use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_blocks: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
impl ChatMessage {
pub fn text(&self) -> String {
match &self.content {
Some(serde_json::Value::String(s)) => s.clone(),
Some(serde_json::Value::Array(parts)) => join_text_parts(parts),
_ => String::new(),
}
}
}
pub fn join_text_parts(parts: &[serde_json::Value]) -> String {
parts
.iter()
.filter_map(|p| p.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("")
}
pub(crate) fn json_str(s: &str) -> String {
serde_json::to_string(s).expect("string JSON encoding is infallible")
}
pub(crate) fn write_json_str(buf: &mut String, s: &str) {
fn needs_escape(c: char) -> bool {
matches!(c, '"' | '\\' | '\u{0000}'..='\u{001f}')
}
if !s.contains(needs_escape) {
buf.push('"');
buf.push_str(s);
buf.push('"');
return;
}
serde_json::to_writer(StrWrite(buf), s).expect("string JSON encoding is infallible");
}
struct StrWrite<'a>(&'a mut String);
impl std::io::Write for StrWrite<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match std::str::from_utf8(buf) {
Ok(s) => {
self.0.push_str(s);
Ok(buf.len())
}
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"non-UTF-8 byte in JSON writer output",
)),
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub const PREFILL_MARKER: &str = "_prefill";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(default)]
pub stream: bool,
#[serde(default)]
pub temperature: Option<f64>,
#[serde(default)]
pub top_p: Option<f64>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub stop: Option<serde_json::Value>,
#[serde(default)]
pub tools: Option<serde_json::Value>,
#[serde(default)]
pub stream_options: Option<serde_json::Value>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u64,
pub completion_tokens: u64,
#[serde(default)]
pub cached_read_tokens: u64,
#[serde(default)]
pub cache_write_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub index: u32,
pub message: ChatMessage,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: UsageJson,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageJson {
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub cached_read_tokens: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub cache_write_tokens: u64,
#[serde(skip)]
pub reasoning_tokens: Option<u64>,
}
fn is_zero(v: &u64) -> bool {
*v == 0
}
impl ChatResponse {
pub fn new(model: &str, content: String, finish_reason: Option<String>, usage: Usage) -> Self {
Self::full(model, content, None, finish_reason, usage)
}
pub fn full(
model: &str,
content: String,
tool_calls: Option<serde_json::Value>,
finish_reason: Option<String>,
usage: Usage,
) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
ChatResponse {
id: format!("chatcmpl-{}", uuid::Uuid::new_v4().simple()),
object: "chat.completion".into(),
created: now,
model: model.to_string(),
choices: vec![Choice {
index: 0,
message: ChatMessage {
role: "assistant".into(),
content: Some(serde_json::Value::String(content)),
name: None,
tool_calls,
tool_call_id: None,
thinking_blocks: None,
reasoning_content: None,
},
finish_reason,
}],
usage: UsageJson {
prompt_tokens: usage.prompt_tokens,
completion_tokens: usage.completion_tokens,
total_tokens: usage.prompt_tokens + usage.completion_tokens,
cached_read_tokens: usage.cached_read_tokens,
cache_write_tokens: usage.cache_write_tokens,
reasoning_tokens: usage.reasoning_tokens,
},
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CanonChunk {
pub delta_text: String,
pub tool_calls: Option<serde_json::Value>,
pub finish_reason: Option<String>,
pub usage: Option<Usage>,
pub thinking: Option<ThinkingDelta>,
pub input_tokens: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ThinkingDelta {
pub block_index: u64,
pub kind: &'static str,
pub text: String,
}
impl CanonChunk {
pub fn to_sse_json(
&self,
id: &str,
model: &str,
created: u64,
include_usage: bool,
) -> Option<String> {
if include_usage {
return self
.usage
.as_ref()
.map(|u| self.usage_frame(id, model, created, u));
}
if self.delta_text.is_empty()
&& self.tool_calls.is_none()
&& self.finish_reason.is_none()
&& self.thinking.is_none()
{
return None;
}
Some(self.delta_frame(id, model, created))
}
fn usage_frame(&self, id: &str, model: &str, created: u64, u: &Usage) -> String {
let mut out = String::with_capacity(160 + id.len() + model.len());
out.push_str("{\"choices\":[],\"created\":");
out.push_str(&created.to_string());
out.push_str(",\"id\":");
write_json_str(&mut out, id);
out.push_str(",\"model\":");
write_json_str(&mut out, model);
out.push_str(",\"object\":\"chat.completion.chunk\",\"usage\":{\"cache_write_tokens\":");
out.push_str(&u.cache_write_tokens.to_string());
out.push_str(",\"cached_read_tokens\":");
out.push_str(&u.cached_read_tokens.to_string());
out.push_str(",\"completion_tokens\":");
out.push_str(&u.completion_tokens.to_string());
out.push_str(",\"prompt_tokens\":");
out.push_str(&u.prompt_tokens.to_string());
out.push_str(",\"total_tokens\":");
out.push_str(&(u.prompt_tokens + u.completion_tokens).to_string());
out.push_str("}}");
out
}
fn delta_frame(&self, id: &str, model: &str, created: u64) -> String {
let mut out = String::with_capacity(112 + id.len() + model.len() + self.delta_text.len());
out.push_str("{\"choices\":[{\"delta\":{");
let mut wrote_key = false;
if !self.delta_text.is_empty() {
out.push_str("\"content\":");
write_json_str(&mut out, &self.delta_text);
wrote_key = true;
}
if let Some(th) = &self.thinking {
if wrote_key {
out.push(',');
}
wrote_key = true;
out.push_str("\"thinking\":{\"block_index\":");
out.push_str(&th.block_index.to_string());
out.push_str(",\"kind\":");
write_json_str(&mut out, th.kind);
out.push_str(",\"text\":");
write_json_str(&mut out, &th.text);
out.push('}');
}
if let Some(tcs) = &self.tool_calls {
if wrote_key {
out.push(',');
}
out.push_str("\"tool_calls\":");
serde_json::to_writer(StrWrite(&mut out), tcs)
.expect("Value serialization is infallible");
}
out.push_str("},\"finish_reason\":");
match &self.finish_reason {
Some(fr) => write_json_str(&mut out, fr),
None => out.push_str("null"),
}
out.push_str(",\"index\":0}],\"created\":");
out.push_str(&created.to_string());
out.push_str(",\"id\":");
write_json_str(&mut out, id);
out.push_str(",\"model\":");
write_json_str(&mut out, model);
out.push_str(",\"object\":\"chat.completion.chunk\"}");
out
}
}