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    /// Whether `max_tokens` was explicitly configured by the caller.
92    pub max_tokens_explicit: bool,
93    /// Optional session identifier for provider-side prompt caching or routing.
94    pub session_id: Option<String>,
95    /// Optional provider-managed cached content reference.
96    ///
97    /// This currently maps to Gemini / Vertex AI `cachedContent` handles.
98    pub cached_content: Option<String>,
99    /// Optional extended thinking configuration.
100    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/// Source data for image and document content blocks.
202#[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        /// Opaque signature for round-tripping thinking blocks back to the API.
228        #[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        /// Gemini thought signature for preserving reasoning context.
241        /// Required for Gemini 3 models when sending function calls back.
242        #[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    /// Total input tokens reported by the provider.
325    pub input_tokens: u32,
326    pub output_tokens: u32,
327    /// Portion of `input_tokens` billed at a cached-input rate, when reported.
328    #[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}