1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct ChatMessage {
5 pub role: String,
6 #[serde(default, skip_serializing_if = "Option::is_none")]
10 pub content: Option<serde_json::Value>,
11 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pub name: Option<String>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub tool_calls: Option<serde_json::Value>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub tool_call_id: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub thinking_blocks: Option<Vec<serde_json::Value>>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub reasoning_content: Option<String>,
27}
28
29impl ChatMessage {
30 pub fn text(&self) -> String {
31 match &self.content {
32 Some(serde_json::Value::String(s)) => s.clone(),
33 Some(serde_json::Value::Array(parts)) => join_text_parts(parts),
34 _ => String::new(),
35 }
36 }
37}
38
39pub fn join_text_parts(parts: &[serde_json::Value]) -> String {
41 parts
42 .iter()
43 .filter_map(|p| p.get("text").and_then(|t| t.as_str()))
44 .collect::<Vec<_>>()
45 .join("")
46}
47
48pub(crate) fn json_str(s: &str) -> String {
53 serde_json::to_string(s).expect("string JSON encoding is infallible")
54}
55
56pub(crate) fn write_json_str(buf: &mut String, s: &str) {
65 fn needs_escape(c: char) -> bool {
67 matches!(c, '"' | '\\' | '\u{0000}'..='\u{001f}')
68 }
69 if !s.contains(needs_escape) {
70 buf.push('"');
71 buf.push_str(s);
72 buf.push('"');
73 return;
74 }
75 serde_json::to_writer(StrWrite(buf), s).expect("string JSON encoding is infallible");
76}
77
78struct StrWrite<'a>(&'a mut String);
83
84impl std::io::Write for StrWrite<'_> {
85 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
86 match std::str::from_utf8(buf) {
87 Ok(s) => {
88 self.0.push_str(s);
89 Ok(buf.len())
90 }
91 Err(_) => Err(std::io::Error::new(
92 std::io::ErrorKind::InvalidData,
93 "non-UTF-8 byte in JSON writer output",
94 )),
95 }
96 }
97 fn flush(&mut self) -> std::io::Result<()> {
98 Ok(())
99 }
100}
101
102pub const PREFILL_MARKER: &str = "_prefill";
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ChatRequest {
110 pub model: String,
111 pub messages: Vec<ChatMessage>,
112 #[serde(default)]
113 pub stream: bool,
114 #[serde(default)]
115 pub temperature: Option<f64>,
116 #[serde(default)]
117 pub top_p: Option<f64>,
118 #[serde(default)]
119 pub max_tokens: Option<u32>,
120 #[serde(default)]
121 pub stop: Option<serde_json::Value>,
122 #[serde(default)]
123 pub tools: Option<serde_json::Value>,
124 #[serde(default)]
125 pub stream_options: Option<serde_json::Value>,
126 #[serde(flatten)]
127 pub extra: serde_json::Map<String, serde_json::Value>,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131pub struct Usage {
132 pub prompt_tokens: u64,
139 pub completion_tokens: u64,
140 #[serde(default)]
142 pub cached_read_tokens: u64,
143 #[serde(default)]
146 pub cache_write_tokens: u64,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub reasoning_tokens: Option<u64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Choice {
156 pub index: u32,
157 pub message: ChatMessage,
158 pub finish_reason: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ChatResponse {
163 pub id: String,
164 pub object: String,
165 pub created: u64,
166 pub model: String,
167 pub choices: Vec<Choice>,
168 pub usage: UsageJson,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UsageJson {
173 pub prompt_tokens: u64,
174 pub completion_tokens: u64,
175 pub total_tokens: u64,
176 #[serde(default, skip_serializing_if = "is_zero")]
177 pub cached_read_tokens: u64,
178 #[serde(default, skip_serializing_if = "is_zero")]
179 pub cache_write_tokens: u64,
180 #[serde(skip)]
183 pub reasoning_tokens: Option<u64>,
184}
185
186fn is_zero(v: &u64) -> bool {
187 *v == 0
188}
189
190impl ChatResponse {
191 pub fn new(model: &str, content: String, finish_reason: Option<String>, usage: Usage) -> Self {
192 Self::full(model, content, None, finish_reason, usage)
193 }
194
195 pub fn full(
196 model: &str,
197 content: String,
198 tool_calls: Option<serde_json::Value>,
199 finish_reason: Option<String>,
200 usage: Usage,
201 ) -> Self {
202 let now = std::time::SystemTime::now()
203 .duration_since(std::time::UNIX_EPOCH)
204 .unwrap_or_default()
205 .as_secs();
206 ChatResponse {
207 id: format!("chatcmpl-{}", uuid::Uuid::new_v4().simple()),
208 object: "chat.completion".into(),
209 created: now,
210 model: model.to_string(),
211 choices: vec![Choice {
212 index: 0,
213 message: ChatMessage {
214 role: "assistant".into(),
215 content: Some(serde_json::Value::String(content)),
216 name: None,
217 tool_calls,
218 tool_call_id: None,
219 thinking_blocks: None,
220 reasoning_content: None,
221 },
222 finish_reason,
223 }],
224 usage: UsageJson {
225 prompt_tokens: usage.prompt_tokens,
226 completion_tokens: usage.completion_tokens,
227 total_tokens: usage.prompt_tokens + usage.completion_tokens,
228 cached_read_tokens: usage.cached_read_tokens,
229 cache_write_tokens: usage.cache_write_tokens,
230 reasoning_tokens: usage.reasoning_tokens,
231 },
232 }
233 }
234}
235
236#[derive(Debug, Clone, Default)]
238pub struct CanonChunk {
239 pub delta_text: String,
240 pub tool_calls: Option<serde_json::Value>,
242 pub finish_reason: Option<String>,
243 pub usage: Option<Usage>,
244 pub thinking: Option<ThinkingDelta>,
249 pub input_tokens: Option<u64>,
253}
254
255#[derive(Debug, Clone)]
256pub struct ThinkingDelta {
257 pub block_index: u64,
258 pub kind: &'static str,
259 pub text: String,
260}
261
262impl CanonChunk {
263 pub fn to_sse_json(
264 &self,
265 id: &str,
266 model: &str,
267 created: u64,
268 include_usage: bool,
269 ) -> Option<String> {
270 if include_usage {
276 return self
277 .usage
278 .as_ref()
279 .map(|u| self.usage_frame(id, model, created, u));
280 }
281 if self.delta_text.is_empty()
282 && self.tool_calls.is_none()
283 && self.finish_reason.is_none()
284 && self.thinking.is_none()
285 {
286 return None;
287 }
288 Some(self.delta_frame(id, model, created))
289 }
290
291 fn usage_frame(&self, id: &str, model: &str, created: u64, u: &Usage) -> String {
294 let mut out = String::with_capacity(160 + id.len() + model.len());
295 out.push_str("{\"choices\":[],\"created\":");
296 out.push_str(&created.to_string());
297 out.push_str(",\"id\":");
298 write_json_str(&mut out, id);
299 out.push_str(",\"model\":");
300 write_json_str(&mut out, model);
301 out.push_str(",\"object\":\"chat.completion.chunk\",\"usage\":{\"cache_write_tokens\":");
302 out.push_str(&u.cache_write_tokens.to_string());
303 out.push_str(",\"cached_read_tokens\":");
304 out.push_str(&u.cached_read_tokens.to_string());
305 out.push_str(",\"completion_tokens\":");
306 out.push_str(&u.completion_tokens.to_string());
307 out.push_str(",\"prompt_tokens\":");
308 out.push_str(&u.prompt_tokens.to_string());
309 out.push_str(",\"total_tokens\":");
310 out.push_str(&(u.prompt_tokens + u.completion_tokens).to_string());
311 out.push_str("}}");
312 out
313 }
314
315 fn delta_frame(&self, id: &str, model: &str, created: u64) -> String {
323 let mut out = String::with_capacity(112 + id.len() + model.len() + self.delta_text.len());
324 out.push_str("{\"choices\":[{\"delta\":{");
325 let mut wrote_key = false;
326 if !self.delta_text.is_empty() {
327 out.push_str("\"content\":");
328 write_json_str(&mut out, &self.delta_text);
329 wrote_key = true;
330 }
331 if let Some(th) = &self.thinking {
332 if wrote_key {
333 out.push(',');
334 }
335 wrote_key = true;
336 out.push_str("\"thinking\":{\"block_index\":");
337 out.push_str(&th.block_index.to_string());
338 out.push_str(",\"kind\":");
339 write_json_str(&mut out, th.kind);
340 out.push_str(",\"text\":");
341 write_json_str(&mut out, &th.text);
342 out.push('}');
343 }
344 if let Some(tcs) = &self.tool_calls {
345 if wrote_key {
346 out.push(',');
347 }
348 out.push_str("\"tool_calls\":");
349 serde_json::to_writer(StrWrite(&mut out), tcs)
352 .expect("Value serialization is infallible");
353 }
354 out.push_str("},\"finish_reason\":");
355 match &self.finish_reason {
356 Some(fr) => write_json_str(&mut out, fr),
357 None => out.push_str("null"),
358 }
359 out.push_str(",\"index\":0}],\"created\":");
360 out.push_str(&created.to_string());
361 out.push_str(",\"id\":");
362 write_json_str(&mut out, id);
363 out.push_str(",\"model\":");
364 write_json_str(&mut out, model);
365 out.push_str(",\"object\":\"chat.completion.chunk\"}");
366 out
367 }
368}