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 const PREFILL_MARKER: &str = "_prefill";
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ChatRequest {
56 pub model: String,
57 pub messages: Vec<ChatMessage>,
58 #[serde(default)]
59 pub stream: bool,
60 #[serde(default)]
61 pub temperature: Option<f64>,
62 #[serde(default)]
63 pub top_p: Option<f64>,
64 #[serde(default)]
65 pub max_tokens: Option<u32>,
66 #[serde(default)]
67 pub stop: Option<serde_json::Value>,
68 #[serde(default)]
69 pub tools: Option<serde_json::Value>,
70 #[serde(default)]
71 pub stream_options: Option<serde_json::Value>,
72 #[serde(flatten)]
73 pub extra: serde_json::Map<String, serde_json::Value>,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct Usage {
78 pub prompt_tokens: u64,
85 pub completion_tokens: u64,
86 #[serde(default)]
88 pub cached_read_tokens: u64,
89 #[serde(default)]
92 pub cache_write_tokens: u64,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub reasoning_tokens: Option<u64>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Choice {
102 pub index: u32,
103 pub message: ChatMessage,
104 pub finish_reason: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ChatResponse {
109 pub id: String,
110 pub object: String,
111 pub created: u64,
112 pub model: String,
113 pub choices: Vec<Choice>,
114 pub usage: UsageJson,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct UsageJson {
119 pub prompt_tokens: u64,
120 pub completion_tokens: u64,
121 pub total_tokens: u64,
122 #[serde(default, skip_serializing_if = "is_zero")]
123 pub cached_read_tokens: u64,
124 #[serde(default, skip_serializing_if = "is_zero")]
125 pub cache_write_tokens: u64,
126 #[serde(skip)]
129 pub reasoning_tokens: Option<u64>,
130}
131
132fn is_zero(v: &u64) -> bool {
133 *v == 0
134}
135
136impl ChatResponse {
137 pub fn new(model: &str, content: String, finish_reason: Option<String>, usage: Usage) -> Self {
138 Self::full(model, content, None, finish_reason, usage)
139 }
140
141 pub fn full(
142 model: &str,
143 content: String,
144 tool_calls: Option<serde_json::Value>,
145 finish_reason: Option<String>,
146 usage: Usage,
147 ) -> Self {
148 let now = std::time::SystemTime::now()
149 .duration_since(std::time::UNIX_EPOCH)
150 .unwrap_or_default()
151 .as_secs();
152 ChatResponse {
153 id: format!("chatcmpl-{}", uuid::Uuid::new_v4().simple()),
154 object: "chat.completion".into(),
155 created: now,
156 model: model.to_string(),
157 choices: vec![Choice {
158 index: 0,
159 message: ChatMessage {
160 role: "assistant".into(),
161 content: Some(serde_json::Value::String(content)),
162 name: None,
163 tool_calls,
164 tool_call_id: None,
165 thinking_blocks: None,
166 reasoning_content: None,
167 },
168 finish_reason,
169 }],
170 usage: UsageJson {
171 prompt_tokens: usage.prompt_tokens,
172 completion_tokens: usage.completion_tokens,
173 total_tokens: usage.prompt_tokens + usage.completion_tokens,
174 cached_read_tokens: usage.cached_read_tokens,
175 cache_write_tokens: usage.cache_write_tokens,
176 reasoning_tokens: usage.reasoning_tokens,
177 },
178 }
179 }
180}
181
182#[derive(Debug, Clone, Default)]
184pub struct CanonChunk {
185 pub delta_text: String,
186 pub tool_calls: Option<serde_json::Value>,
188 pub finish_reason: Option<String>,
189 pub usage: Option<Usage>,
190 pub thinking: Option<ThinkingDelta>,
195 pub input_tokens: Option<u64>,
199}
200
201#[derive(Debug, Clone)]
202pub struct ThinkingDelta {
203 pub block_index: u64,
204 pub kind: &'static str,
205 pub text: String,
206}
207
208impl CanonChunk {
209 pub fn to_sse_json(
210 &self,
211 id: &str,
212 model: &str,
213 created: u64,
214 include_usage: bool,
215 ) -> Option<String> {
216 if include_usage {
217 let u = self.usage.as_ref()?;
218 return Some(
219 serde_json::json!({
220 "id": id, "object": "chat.completion.chunk", "created": created,
221 "model": model, "choices": [],
222 "usage": {"prompt_tokens": u.prompt_tokens, "completion_tokens": u.completion_tokens,
223 "total_tokens": u.prompt_tokens + u.completion_tokens,
224 "cached_read_tokens": u.cached_read_tokens,
225 "cache_write_tokens": u.cache_write_tokens}
226 })
227 .to_string(),
228 );
229 }
230 if self.delta_text.is_empty()
231 && self.tool_calls.is_none()
232 && self.finish_reason.is_none()
233 && self.thinking.is_none()
234 {
235 return None;
236 }
237 let mut delta = serde_json::json!({});
240 if !self.delta_text.is_empty() {
241 delta["content"] = serde_json::json!(self.delta_text);
242 }
243 if let Some(tcs) = &self.tool_calls {
244 delta["tool_calls"] = tcs.clone();
245 }
246 if let Some(th) = &self.thinking {
247 delta["thinking"] = serde_json::json!({
248 "block_index": th.block_index,
249 "kind": th.kind,
250 "text": th.text,
251 });
252 }
253 Some(
254 serde_json::json!({
255 "id": id, "object": "chat.completion.chunk", "created": created,
256 "model": model,
257 "choices": [{"index": 0, "delta": delta, "finish_reason": self.finish_reason}]
258 })
259 .to_string(),
260 )
261 }
262}