1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone)]
8pub struct ThinkingConfig {
9 pub budget_tokens: u32,
12}
13
14impl ThinkingConfig {
15 pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
20
21 pub const MIN_BUDGET_TOKENS: u32 = 1_024;
23
24 #[must_use]
25 pub const fn new(budget_tokens: u32) -> Self {
26 Self { budget_tokens }
27 }
28}
29
30impl Default for ThinkingConfig {
31 fn default() -> Self {
32 Self {
33 budget_tokens: Self::DEFAULT_BUDGET_TOKENS,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct ChatRequest {
40 pub system: String,
41 pub messages: Vec<Message>,
42 pub tools: Option<Vec<Tool>>,
43 pub max_tokens: u32,
44 pub thinking: Option<ThinkingConfig>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Message {
50 pub role: Role,
51 pub content: Content,
52}
53
54impl Message {
55 #[must_use]
56 pub fn user(text: impl Into<String>) -> Self {
57 Self {
58 role: Role::User,
59 content: Content::Text(text.into()),
60 }
61 }
62
63 #[must_use]
64 pub fn assistant(text: impl Into<String>) -> Self {
65 Self {
66 role: Role::Assistant,
67 content: Content::Text(text.into()),
68 }
69 }
70
71 #[must_use]
72 pub fn assistant_with_tool_use(
73 text: Option<String>,
74 id: impl Into<String>,
75 name: impl Into<String>,
76 input: serde_json::Value,
77 ) -> Self {
78 let mut blocks = Vec::new();
79 if let Some(t) = text {
80 blocks.push(ContentBlock::Text { text: t });
81 }
82 blocks.push(ContentBlock::ToolUse {
83 id: id.into(),
84 name: name.into(),
85 input,
86 thought_signature: None,
87 });
88 Self {
89 role: Role::Assistant,
90 content: Content::Blocks(blocks),
91 }
92 }
93
94 #[must_use]
95 pub fn tool_result(
96 tool_use_id: impl Into<String>,
97 content: impl Into<String>,
98 is_error: bool,
99 ) -> Self {
100 Self {
101 role: Role::User,
102 content: Content::Blocks(vec![ContentBlock::ToolResult {
103 tool_use_id: tool_use_id.into(),
104 content: content.into(),
105 is_error: if is_error { Some(true) } else { None },
106 }]),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "lowercase")]
113pub enum Role {
114 User,
115 Assistant,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(untagged)]
120pub enum Content {
121 Text(String),
122 Blocks(Vec<ContentBlock>),
123}
124
125impl Content {
126 #[must_use]
127 pub fn first_text(&self) -> Option<&str> {
128 match self {
129 Self::Text(s) => Some(s),
130 Self::Blocks(blocks) => blocks.iter().find_map(|b| match b {
131 ContentBlock::Text { text } => Some(text.as_str()),
132 _ => None,
133 }),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(tag = "type")]
140pub enum ContentBlock {
141 #[serde(rename = "text")]
142 Text { text: String },
143
144 #[serde(rename = "thinking")]
145 Thinking { thinking: String },
146
147 #[serde(rename = "tool_use")]
148 ToolUse {
149 id: String,
150 name: String,
151 input: serde_json::Value,
152 #[serde(skip_serializing_if = "Option::is_none")]
155 thought_signature: Option<String>,
156 },
157
158 #[serde(rename = "tool_result")]
159 ToolResult {
160 tool_use_id: String,
161 content: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 is_error: Option<bool>,
164 },
165}
166
167#[derive(Debug, Clone, Serialize)]
168pub struct Tool {
169 pub name: String,
170 pub description: String,
171 pub input_schema: serde_json::Value,
172}
173
174#[derive(Debug, Clone)]
175pub struct ChatResponse {
176 pub id: String,
177 pub content: Vec<ContentBlock>,
178 pub model: String,
179 pub stop_reason: Option<StopReason>,
180 pub usage: Usage,
181}
182
183impl ChatResponse {
184 #[must_use]
185 pub fn first_text(&self) -> Option<&str> {
186 self.content.iter().find_map(|b| match b {
187 ContentBlock::Text { text } => Some(text.as_str()),
188 _ => None,
189 })
190 }
191
192 #[must_use]
193 pub fn first_thinking(&self) -> Option<&str> {
194 self.content.iter().find_map(|b| match b {
195 ContentBlock::Thinking { thinking } => Some(thinking.as_str()),
196 _ => None,
197 })
198 }
199
200 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &serde_json::Value)> {
201 self.content.iter().filter_map(|b| match b {
202 ContentBlock::ToolUse {
203 id, name, input, ..
204 } => Some((id.as_str(), name.as_str(), input)),
205 _ => None,
206 })
207 }
208
209 #[must_use]
210 pub fn has_tool_use(&self) -> bool {
211 self.content
212 .iter()
213 .any(|b| matches!(b, ContentBlock::ToolUse { .. }))
214 }
215}
216
217#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "snake_case")]
219pub enum StopReason {
220 EndTurn,
221 ToolUse,
222 MaxTokens,
223 StopSequence,
224}
225
226#[derive(Debug, Clone, Deserialize)]
227pub struct Usage {
228 pub input_tokens: u32,
229 pub output_tokens: u32,
230}
231
232#[derive(Debug)]
233pub enum ChatOutcome {
234 Success(ChatResponse),
235 RateLimited,
236 InvalidRequest(String),
237 ServerError(String),
238}