Skip to main content

agent_sdk/llm/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// Configuration for extended thinking.
4///
5/// When enabled, the model will show its reasoning process before
6/// generating the final response.
7#[derive(Debug, Clone)]
8pub struct ThinkingConfig {
9    /// Maximum tokens the model can use for thinking.
10    /// Default is 10,000 tokens.
11    pub budget_tokens: u32,
12}
13
14impl ThinkingConfig {
15    /// Default budget: 10,000 tokens.
16    ///
17    /// This provides enough capacity for meaningful reasoning on most tasks
18    /// while keeping costs reasonable. Increase for complex multi-step problems.
19    pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
20
21    /// Minimum budget required by the Anthropic API.
22    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    /// Optional extended thinking configuration.
45    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        /// Gemini thought signature for preserving reasoning context.
153        /// Required for Gemini 3 models when sending function calls back.
154        #[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}