1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone)]
5pub enum ThinkingMode {
6 Enabled { budget_tokens: u32 },
8 Adaptive,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum Effort {
16 Low,
17 Medium,
18 High,
19 Max,
20}
21
22#[derive(Debug, Clone)]
27pub struct ThinkingConfig {
28 pub mode: ThinkingMode,
30 pub effort: Option<Effort>,
32}
33
34impl ThinkingConfig {
35 pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
40
41 pub const MIN_BUDGET_TOKENS: u32 = 1_024;
43
44 #[must_use]
46 pub const fn new(budget_tokens: u32) -> Self {
47 Self {
48 mode: ThinkingMode::Enabled { budget_tokens },
49 effort: None,
50 }
51 }
52
53 #[must_use]
55 pub const fn adaptive() -> Self {
56 Self {
57 mode: ThinkingMode::Adaptive,
58 effort: None,
59 }
60 }
61
62 #[must_use]
64 pub const fn adaptive_with_effort(effort: Effort) -> Self {
65 Self {
66 mode: ThinkingMode::Adaptive,
67 effort: Some(effort),
68 }
69 }
70
71 #[must_use]
73 pub const fn with_effort(mut self, effort: Effort) -> Self {
74 self.effort = Some(effort);
75 self
76 }
77}
78
79impl Default for ThinkingConfig {
80 fn default() -> Self {
81 Self::new(Self::DEFAULT_BUDGET_TOKENS)
82 }
83}
84
85#[derive(Debug, Clone)]
86pub struct ChatRequest {
87 pub system: String,
88 pub messages: Vec<Message>,
89 pub tools: Option<Vec<Tool>>,
90 pub max_tokens: u32,
91 pub thinking: Option<ThinkingConfig>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Message {
97 pub role: Role,
98 pub content: Content,
99}
100
101impl Message {
102 #[must_use]
103 pub fn user(text: impl Into<String>) -> Self {
104 Self {
105 role: Role::User,
106 content: Content::Text(text.into()),
107 }
108 }
109
110 #[must_use]
111 pub const fn user_with_content(blocks: Vec<ContentBlock>) -> Self {
112 Self {
113 role: Role::User,
114 content: Content::Blocks(blocks),
115 }
116 }
117
118 #[must_use]
119 pub fn assistant(text: impl Into<String>) -> Self {
120 Self {
121 role: Role::Assistant,
122 content: Content::Text(text.into()),
123 }
124 }
125
126 #[must_use]
127 pub fn assistant_with_tool_use(
128 text: Option<String>,
129 id: impl Into<String>,
130 name: impl Into<String>,
131 input: serde_json::Value,
132 ) -> Self {
133 let mut blocks = Vec::new();
134 if let Some(t) = text {
135 blocks.push(ContentBlock::Text { text: t });
136 }
137 blocks.push(ContentBlock::ToolUse {
138 id: id.into(),
139 name: name.into(),
140 input,
141 thought_signature: None,
142 });
143 Self {
144 role: Role::Assistant,
145 content: Content::Blocks(blocks),
146 }
147 }
148
149 #[must_use]
150 pub fn tool_result(
151 tool_use_id: impl Into<String>,
152 content: impl Into<String>,
153 is_error: bool,
154 ) -> Self {
155 Self {
156 role: Role::User,
157 content: Content::Blocks(vec![ContentBlock::ToolResult {
158 tool_use_id: tool_use_id.into(),
159 content: content.into(),
160 is_error: if is_error { Some(true) } else { None },
161 }]),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
167#[serde(rename_all = "lowercase")]
168pub enum Role {
169 User,
170 Assistant,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(untagged)]
175pub enum Content {
176 Text(String),
177 Blocks(Vec<ContentBlock>),
178}
179
180impl Content {
181 #[must_use]
182 pub fn first_text(&self) -> Option<&str> {
183 match self {
184 Self::Text(s) => Some(s),
185 Self::Blocks(blocks) => blocks.iter().find_map(|b| match b {
186 ContentBlock::Text { text } => Some(text.as_str()),
187 _ => None,
188 }),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ContentSource {
196 pub media_type: String,
197 pub data: String,
198}
199
200impl ContentSource {
201 #[must_use]
202 pub fn new(media_type: impl Into<String>, data: impl Into<String>) -> Self {
203 Self {
204 media_type: media_type.into(),
205 data: data.into(),
206 }
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(tag = "type")]
212pub enum ContentBlock {
213 #[serde(rename = "text")]
214 Text { text: String },
215
216 #[serde(rename = "thinking")]
217 Thinking {
218 thinking: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 signature: Option<String>,
222 },
223
224 #[serde(rename = "redacted_thinking")]
225 RedactedThinking { data: String },
226
227 #[serde(rename = "tool_use")]
228 ToolUse {
229 id: String,
230 name: String,
231 input: serde_json::Value,
232 #[serde(skip_serializing_if = "Option::is_none")]
235 thought_signature: Option<String>,
236 },
237
238 #[serde(rename = "tool_result")]
239 ToolResult {
240 tool_use_id: String,
241 content: String,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 is_error: Option<bool>,
244 },
245
246 #[serde(rename = "image")]
247 Image { source: ContentSource },
248
249 #[serde(rename = "document")]
250 Document { source: ContentSource },
251}
252
253#[derive(Debug, Clone, Serialize)]
254pub struct Tool {
255 pub name: String,
256 pub description: String,
257 pub input_schema: serde_json::Value,
258}
259
260#[derive(Debug, Clone)]
261pub struct ChatResponse {
262 pub id: String,
263 pub content: Vec<ContentBlock>,
264 pub model: String,
265 pub stop_reason: Option<StopReason>,
266 pub usage: Usage,
267}
268
269impl ChatResponse {
270 #[must_use]
271 pub fn first_text(&self) -> Option<&str> {
272 self.content.iter().find_map(|b| match b {
273 ContentBlock::Text { text } => Some(text.as_str()),
274 _ => None,
275 })
276 }
277
278 #[must_use]
279 pub fn first_thinking(&self) -> Option<&str> {
280 self.content.iter().find_map(|b| match b {
281 ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
282 _ => None,
283 })
284 }
285
286 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &serde_json::Value)> {
287 self.content.iter().filter_map(|b| match b {
288 ContentBlock::ToolUse {
289 id, name, input, ..
290 } => Some((id.as_str(), name.as_str(), input)),
291 _ => None,
292 })
293 }
294
295 #[must_use]
296 pub fn has_tool_use(&self) -> bool {
297 self.content
298 .iter()
299 .any(|b| matches!(b, ContentBlock::ToolUse { .. }))
300 }
301}
302
303#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "snake_case")]
305pub enum StopReason {
306 EndTurn,
307 ToolUse,
308 MaxTokens,
309 StopSequence,
310 Refusal,
311 ModelContextWindowExceeded,
312}
313
314#[derive(Debug, Clone, Deserialize)]
315pub struct Usage {
316 pub input_tokens: u32,
317 pub output_tokens: u32,
318}
319
320#[derive(Debug)]
321pub enum ChatOutcome {
322 Success(ChatResponse),
323 RateLimited,
324 InvalidRequest(String),
325 ServerError(String),
326}