Skip to main content

agent_sdk/llm/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// The mode of extended thinking.
4#[derive(Debug, Clone)]
5pub enum ThinkingMode {
6    /// Explicitly enabled with a token budget.
7    Enabled { budget_tokens: u32 },
8    /// Adaptive thinking — the model decides how much to think.
9    Adaptive,
10}
11
12/// Effort level for adaptive thinking via `output_config`.
13#[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/// Configuration for extended thinking.
23///
24/// When enabled, the model will show its reasoning process before
25/// generating the final response.
26#[derive(Debug, Clone)]
27pub struct ThinkingConfig {
28    /// Which thinking mode to use.
29    pub mode: ThinkingMode,
30    /// Optional effort level (sent via `output_config`).
31    pub effort: Option<Effort>,
32}
33
34impl ThinkingConfig {
35    /// Default budget: 10,000 tokens.
36    ///
37    /// This provides enough capacity for meaningful reasoning on most tasks
38    /// while keeping costs reasonable. Increase for complex multi-step problems.
39    pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
40
41    /// Minimum budget required by the Anthropic API.
42    pub const MIN_BUDGET_TOKENS: u32 = 1_024;
43
44    /// Create a config with an explicit token budget (Enabled mode).
45    #[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    /// Create an adaptive thinking config.
54    #[must_use]
55    pub const fn adaptive() -> Self {
56        Self {
57            mode: ThinkingMode::Adaptive,
58            effort: None,
59        }
60    }
61
62    /// Create an adaptive thinking config with an effort level.
63    #[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    /// Set the effort level on an existing config.
72    #[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    /// Optional extended thinking configuration.
92    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/// Source data for image and document content blocks.
194#[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        /// Opaque signature for round-tripping thinking blocks back to the API.
220        #[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        /// Gemini thought signature for preserving reasoning context.
233        /// Required for Gemini 3 models when sending function calls back.
234        #[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}