1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum ThinkingLevel {
7 #[default]
8 Off,
9 Minimal,
10 Low,
11 Medium,
12 High,
13 Xhigh,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "lowercase")]
18pub enum Content {
19 #[serde(rename = "text")]
20 Text { text: String },
21 #[serde(rename = "thinking")]
22 Thinking {
23 thinking: String,
24 #[serde(skip_serializing_if = "Option::is_none", default)]
25 thinking_signature: Option<String>,
26 },
27 #[serde(rename = "image")]
28 Image { data: String, mime_type: String },
29 #[serde(rename = "toolCall")]
30 ToolCall {
31 id: String,
32 name: String,
33 #[serde(default)]
34 arguments: Value,
35 },
36}
37
38impl Content {
39 pub fn text(s: impl Into<String>) -> Self {
40 Content::Text { text: s.into() }
41 }
42 pub fn as_text(&self) -> Option<&str> {
43 if let Content::Text { text } = self {
44 Some(text)
45 } else {
46 None
47 }
48 }
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct Usage {
53 #[serde(default)]
54 pub input: u64,
55 #[serde(default)]
56 pub output: u64,
57 #[serde(default)]
58 pub cache_read: u64,
59 #[serde(default)]
60 pub cache_write: u64,
61 #[serde(default)]
62 pub total_tokens: u64,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub enum StopReason {
68 Stop,
69 Length,
70 ToolUse,
71 Error,
72 Aborted,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(tag = "role", rename_all = "camelCase")]
77pub enum Message {
78 #[serde(rename = "user")]
79 User {
80 content: Vec<Content>,
81 #[serde(default = "now_ms")]
82 timestamp: i64,
83 },
84 #[serde(rename = "assistant")]
85 Assistant(AssistantMessage),
86 #[serde(rename = "toolResult")]
87 ToolResult(ToolResultMessage),
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct AssistantMessage {
92 pub content: Vec<Content>,
93 pub api: String,
94 pub provider: String,
95 pub model: String,
96 #[serde(default)]
97 pub usage: Usage,
98 pub stop_reason: StopReason,
99 #[serde(skip_serializing_if = "Option::is_none", default)]
100 pub error_message: Option<String>,
101 #[serde(default = "now_ms")]
102 pub timestamp: i64,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ToolResultMessage {
107 pub tool_call_id: String,
108 pub tool_name: String,
109 pub content: Vec<Content>,
110 pub is_error: bool,
111 #[serde(default = "now_ms")]
112 pub timestamp: i64,
113}
114
115impl Message {
116 pub fn user_text(s: impl Into<String>) -> Self {
117 Message::User {
118 content: vec![Content::text(s)],
119 timestamp: now_ms(),
120 }
121 }
122}
123
124pub fn now_ms() -> i64 {
125 chrono::Utc::now().timestamp_millis()
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Tool {
131 pub name: String,
132 pub description: String,
133 pub parameters: Value,
135}
136
137#[derive(Debug, Clone, Default)]
138pub struct Context {
139 pub system_prompt: Option<String>,
140 pub messages: Vec<Message>,
141 pub tools: Vec<Tool>,
142}
143
144#[derive(Debug, Clone, Default)]
145pub struct StreamOptions {
146 pub temperature: Option<f32>,
147 pub max_tokens: Option<u32>,
148 pub api_key: Option<String>,
149 pub reasoning: Option<ThinkingLevel>,
150 pub cancel: Option<tokio_util::sync::CancellationToken>,
152 pub base_url: Option<String>,
154 pub headers: std::collections::BTreeMap<String, String>,
156}
157
158#[derive(Debug, Clone)]
160pub struct Model {
161 pub id: String,
162 pub name: String,
163 pub api: String,
164 pub provider: String,
165 pub base_url: String,
166 pub reasoning: bool,
167 pub context_window: u32,
168 pub max_tokens: u32,
169}
170
171impl Model {
172 pub fn anthropic_claude_sonnet_4_6() -> Self {
174 Self {
175 id: "claude-sonnet-4-6".into(),
176 name: "Claude Sonnet 4.6".into(),
177 api: "anthropic-messages".into(),
178 provider: "anthropic".into(),
179 base_url: "https://api.anthropic.com".into(),
180 reasoning: true,
181 context_window: 200_000,
182 max_tokens: 8_192,
183 }
184 }
185
186 pub fn anthropic_claude_opus_4_7() -> Self {
187 Self {
188 id: "claude-opus-4-7".into(),
189 name: "Claude Opus 4.7".into(),
190 api: "anthropic-messages".into(),
191 provider: "anthropic".into(),
192 base_url: "https://api.anthropic.com".into(),
193 reasoning: true,
194 context_window: 200_000,
195 max_tokens: 8_192,
196 }
197 }
198
199 pub fn openai_gpt_4o_mini() -> Self {
200 Self {
201 id: "gpt-4o-mini".into(),
202 name: "GPT-4o mini".into(),
203 api: "openai-completions".into(),
204 provider: "openai".into(),
205 base_url: "https://api.openai.com/v1".into(),
206 reasoning: false,
207 context_window: 128_000,
208 max_tokens: 16_384,
209 }
210 }
211
212 pub fn openai_gpt_4o() -> Self {
213 Self {
214 id: "gpt-4o".into(),
215 name: "GPT-4o".into(),
216 api: "openai-completions".into(),
217 provider: "openai".into(),
218 base_url: "https://api.openai.com/v1".into(),
219 reasoning: false,
220 context_window: 128_000,
221 max_tokens: 16_384,
222 }
223 }
224
225 pub fn gemini_2_0_flash() -> Self {
226 Self {
227 id: "gemini-2.0-flash".into(),
228 name: "Gemini 2.0 Flash".into(),
229 api: "google-generative-ai".into(),
230 provider: "google".into(),
231 base_url: "https://generativelanguage.googleapis.com".into(),
232 reasoning: false,
233 context_window: 1_000_000,
234 max_tokens: 8_192,
235 }
236 }
237
238 pub fn openai_compat(
243 provider: impl Into<String>,
244 id: impl Into<String>,
245 base_url: impl Into<String>,
246 context_window: u32,
247 max_tokens: u32,
248 ) -> Self {
249 let id = id.into();
250 Self {
251 name: id.clone(),
252 id,
253 api: "openai-completions".into(),
254 provider: provider.into(),
255 base_url: base_url.into(),
256 reasoning: false,
257 context_window,
258 max_tokens,
259 }
260 }
261}
262
263#[derive(Debug, Clone)]
265pub enum AssistantMessageEvent {
266 Start,
267 TextStart {
268 content_index: usize,
269 },
270 TextDelta {
271 content_index: usize,
272 delta: String,
273 },
274 TextEnd {
275 content_index: usize,
276 content: String,
277 },
278 ThinkingStart {
279 content_index: usize,
280 },
281 ThinkingDelta {
282 content_index: usize,
283 delta: String,
284 },
285 ThinkingEnd {
286 content_index: usize,
287 content: String,
288 },
289 ToolCallStart {
290 content_index: usize,
291 id: String,
292 name: String,
293 },
294 ToolCallDelta {
295 content_index: usize,
296 delta: String,
297 },
298 ToolCallEnd {
299 content_index: usize,
300 id: String,
301 name: String,
302 arguments: Value,
303 },
304 Done {
305 reason: StopReason,
306 message: AssistantMessage,
307 },
308 Error {
309 reason: StopReason,
310 error: AssistantMessage,
311 },
312}