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 max_tokens_explicit: bool,
93 pub session_id: Option<String>,
95 pub cached_content: Option<String>,
99 pub thinking: Option<ThinkingConfig>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Message {
105 pub role: Role,
106 pub content: Content,
107}
108
109impl Message {
110 #[must_use]
111 pub fn user(text: impl Into<String>) -> Self {
112 Self {
113 role: Role::User,
114 content: Content::Text(text.into()),
115 }
116 }
117
118 #[must_use]
119 pub const fn user_with_content(blocks: Vec<ContentBlock>) -> Self {
120 Self {
121 role: Role::User,
122 content: Content::Blocks(blocks),
123 }
124 }
125
126 #[must_use]
127 pub fn assistant(text: impl Into<String>) -> Self {
128 Self {
129 role: Role::Assistant,
130 content: Content::Text(text.into()),
131 }
132 }
133
134 #[must_use]
135 pub fn assistant_with_tool_use(
136 text: Option<String>,
137 id: impl Into<String>,
138 name: impl Into<String>,
139 input: serde_json::Value,
140 ) -> Self {
141 let mut blocks = Vec::new();
142 if let Some(t) = text {
143 blocks.push(ContentBlock::Text { text: t });
144 }
145 blocks.push(ContentBlock::ToolUse {
146 id: id.into(),
147 name: name.into(),
148 input,
149 thought_signature: None,
150 });
151 Self {
152 role: Role::Assistant,
153 content: Content::Blocks(blocks),
154 }
155 }
156
157 #[must_use]
158 pub fn tool_result(
159 tool_use_id: impl Into<String>,
160 content: impl Into<String>,
161 is_error: bool,
162 ) -> Self {
163 Self {
164 role: Role::User,
165 content: Content::Blocks(vec![ContentBlock::ToolResult {
166 tool_use_id: tool_use_id.into(),
167 content: content.into(),
168 is_error: if is_error { Some(true) } else { None },
169 }]),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
175#[serde(rename_all = "lowercase")]
176pub enum Role {
177 User,
178 Assistant,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(untagged)]
183pub enum Content {
184 Text(String),
185 Blocks(Vec<ContentBlock>),
186}
187
188impl Content {
189 #[must_use]
190 pub fn first_text(&self) -> Option<&str> {
191 match self {
192 Self::Text(s) => Some(s),
193 Self::Blocks(blocks) => blocks.iter().find_map(|b| match b {
194 ContentBlock::Text { text } => Some(text.as_str()),
195 _ => None,
196 }),
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ContentSource {
204 pub media_type: String,
205 pub data: String,
206}
207
208impl ContentSource {
209 #[must_use]
210 pub fn new(media_type: impl Into<String>, data: impl Into<String>) -> Self {
211 Self {
212 media_type: media_type.into(),
213 data: data.into(),
214 }
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(tag = "type")]
220pub enum ContentBlock {
221 #[serde(rename = "text")]
222 Text { text: String },
223
224 #[serde(rename = "thinking")]
225 Thinking {
226 thinking: String,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 signature: Option<String>,
230 },
231
232 #[serde(rename = "redacted_thinking")]
233 RedactedThinking { data: String },
234
235 #[serde(rename = "tool_use")]
236 ToolUse {
237 id: String,
238 name: String,
239 input: serde_json::Value,
240 #[serde(skip_serializing_if = "Option::is_none")]
243 thought_signature: Option<String>,
244 },
245
246 #[serde(rename = "tool_result")]
247 ToolResult {
248 tool_use_id: String,
249 content: String,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 is_error: Option<bool>,
252 },
253
254 #[serde(rename = "image")]
255 Image { source: ContentSource },
256
257 #[serde(rename = "document")]
258 Document { source: ContentSource },
259}
260
261#[derive(Debug, Clone, Serialize)]
262pub struct Tool {
263 pub name: String,
264 pub description: String,
265 pub input_schema: serde_json::Value,
266}
267
268#[derive(Debug, Clone)]
269pub struct ChatResponse {
270 pub id: String,
271 pub content: Vec<ContentBlock>,
272 pub model: String,
273 pub stop_reason: Option<StopReason>,
274 pub usage: Usage,
275}
276
277impl ChatResponse {
278 #[must_use]
279 pub fn first_text(&self) -> Option<&str> {
280 self.content.iter().find_map(|b| match b {
281 ContentBlock::Text { text } => Some(text.as_str()),
282 _ => None,
283 })
284 }
285
286 #[must_use]
287 pub fn first_thinking(&self) -> Option<&str> {
288 self.content.iter().find_map(|b| match b {
289 ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
290 _ => None,
291 })
292 }
293
294 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &serde_json::Value)> {
295 self.content.iter().filter_map(|b| match b {
296 ContentBlock::ToolUse {
297 id, name, input, ..
298 } => Some((id.as_str(), name.as_str(), input)),
299 _ => None,
300 })
301 }
302
303 #[must_use]
304 pub fn has_tool_use(&self) -> bool {
305 self.content
306 .iter()
307 .any(|b| matches!(b, ContentBlock::ToolUse { .. }))
308 }
309}
310
311#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "snake_case")]
313pub enum StopReason {
314 EndTurn,
315 ToolUse,
316 MaxTokens,
317 StopSequence,
318 Refusal,
319 ModelContextWindowExceeded,
320}
321
322#[derive(Debug, Clone, Deserialize)]
323pub struct Usage {
324 pub input_tokens: u32,
326 pub output_tokens: u32,
327 #[serde(default)]
329 pub cached_input_tokens: u32,
330}
331
332#[derive(Debug)]
333pub enum ChatOutcome {
334 Success(ChatResponse),
335 RateLimited,
336 InvalidRequest(String),
337 ServerError(String),
338}