claude_agent/client/messages/
config.rs1use serde::{Deserialize, Serialize};
4
5pub const MIN_THINKING_BUDGET: u32 = 1024;
6pub const DEFAULT_MAX_TOKENS: u32 = 8192;
7pub const MAX_TOKENS_128K: u32 = 128_000;
8pub const MIN_MAX_TOKENS: u32 = 1;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum TokenValidationError {
12 ThinkingBudgetExceedsMaxTokens { budget: u32, max_tokens: u32 },
13 MaxTokensTooLow { min: u32, actual: u32 },
14 MaxTokensTooHigh { max: u32, actual: u32 },
15}
16
17impl std::fmt::Display for TokenValidationError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::ThinkingBudgetExceedsMaxTokens { budget, max_tokens } => {
21 write!(
22 f,
23 "thinking budget_tokens ({budget}) must be < max_tokens ({max_tokens})"
24 )
25 }
26 Self::MaxTokensTooLow { min, actual } => {
27 write!(f, "max_tokens ({actual}) must be >= {min}")
28 }
29 Self::MaxTokensTooHigh { max, actual } => {
30 write!(f, "max_tokens ({actual}) exceeds maximum allowed ({max})")
31 }
32 }
33 }
34}
35
36impl std::error::Error for TokenValidationError {}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum EffortLevel {
41 Low,
42 Medium,
43 High,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(tag = "type", rename_all = "snake_case")]
48pub enum ToolChoice {
49 Auto,
50 Any,
51 Tool { name: String },
52 None,
53}
54
55impl ToolChoice {
56 pub fn tool(name: impl Into<String>) -> Self {
57 Self::Tool { name: name.into() }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct OutputConfig {
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub effort: Option<EffortLevel>,
65}
66
67impl OutputConfig {
68 pub fn with_effort(level: EffortLevel) -> Self {
69 Self {
70 effort: Some(level),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ThinkingConfig {
77 #[serde(rename = "type")]
78 pub thinking_type: ThinkingType,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub budget_tokens: Option<u32>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum ThinkingType {
86 Enabled,
87 Disabled,
88}
89
90impl ThinkingConfig {
91 pub fn enabled(budget_tokens: u32) -> Self {
92 Self {
93 thinking_type: ThinkingType::Enabled,
94 budget_tokens: Some(budget_tokens.max(MIN_THINKING_BUDGET)),
95 }
96 }
97
98 pub fn disabled() -> Self {
99 Self {
100 thinking_type: ThinkingType::Disabled,
101 budget_tokens: None,
102 }
103 }
104
105 pub fn is_enabled(&self) -> bool {
106 self.thinking_type == ThinkingType::Enabled
107 }
108
109 pub fn budget(&self) -> Option<u32> {
110 self.budget_tokens
111 }
112
113 pub fn validate_against_max_tokens(&self, max_tokens: u32) -> Result<(), TokenValidationError> {
114 if let Some(budget) = self.budget_tokens
115 && budget >= max_tokens
116 {
117 return Err(TokenValidationError::ThinkingBudgetExceedsMaxTokens {
118 budget,
119 max_tokens,
120 });
121 }
122 Ok(())
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(tag = "type", rename_all = "snake_case")]
128pub enum OutputFormat {
129 JsonSchema {
130 #[serde(skip_serializing_if = "Option::is_none")]
131 name: Option<String>,
132 schema: serde_json::Value,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 description: Option<String>,
135 },
136}
137
138impl OutputFormat {
139 pub fn json_schema(schema: serde_json::Value) -> Self {
140 Self::JsonSchema {
141 name: None,
142 schema,
143 description: None,
144 }
145 }
146
147 pub fn json_schema_named(name: impl Into<String>, schema: serde_json::Value) -> Self {
148 Self::JsonSchema {
149 name: Some(name.into()),
150 schema,
151 description: None,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_thinking_config_enabled() {
162 let config = ThinkingConfig::enabled(10000);
163 assert_eq!(config.thinking_type, ThinkingType::Enabled);
164 assert_eq!(config.budget(), Some(10000));
165 assert!(config.is_enabled());
166 }
167
168 #[test]
169 fn test_thinking_config_enabled_auto_clamp() {
170 let config = ThinkingConfig::enabled(500);
171 assert!(config.is_enabled());
172 assert_eq!(config.budget(), Some(MIN_THINKING_BUDGET));
173
174 let config = ThinkingConfig::enabled(MIN_THINKING_BUDGET);
175 assert_eq!(config.budget(), Some(MIN_THINKING_BUDGET));
176 }
177
178 #[test]
179 fn test_thinking_config_disabled() {
180 let config = ThinkingConfig::disabled();
181 assert_eq!(config.thinking_type, ThinkingType::Disabled);
182 assert_eq!(config.budget(), None);
183 assert!(!config.is_enabled());
184 }
185
186 #[test]
187 fn test_thinking_config_validate_against_max_tokens() {
188 let config = ThinkingConfig::enabled(2000);
189 assert!(config.validate_against_max_tokens(4000).is_ok());
190 assert!(config.validate_against_max_tokens(2000).is_err());
191 assert!(config.validate_against_max_tokens(1000).is_err());
192 }
193
194 #[test]
195 fn test_thinking_config_serialization() {
196 let config = ThinkingConfig::enabled(5000);
197 let json = serde_json::to_string(&config).unwrap();
198 assert!(json.contains("\"type\":\"enabled\""));
199 assert!(json.contains("\"budget_tokens\":5000"));
200 }
201
202 #[test]
203 fn test_token_validation_error_display() {
204 let err = TokenValidationError::ThinkingBudgetExceedsMaxTokens {
205 budget: 5000,
206 max_tokens: 4000,
207 };
208 assert_eq!(
209 err.to_string(),
210 "thinking budget_tokens (5000) must be < max_tokens (4000)"
211 );
212
213 let err = TokenValidationError::MaxTokensTooLow { min: 1, actual: 0 };
214 assert_eq!(err.to_string(), "max_tokens (0) must be >= 1");
215
216 let err = TokenValidationError::MaxTokensTooHigh {
217 max: 128_000,
218 actual: 200_000,
219 };
220 assert_eq!(
221 err.to_string(),
222 "max_tokens (200000) exceeds maximum allowed (128000)"
223 );
224 }
225
226 #[test]
227 fn test_output_format_json_schema() {
228 let schema = serde_json::json!({
229 "type": "object",
230 "properties": {
231 "name": {"type": "string"}
232 }
233 });
234 let format = OutputFormat::json_schema(schema);
235 let json = serde_json::to_string(&format).unwrap();
236 assert!(json.contains("\"type\":\"json_schema\""));
237 assert!(json.contains("\"schema\""));
238 }
239
240 #[test]
241 fn test_output_format_named_schema() {
242 let schema = serde_json::json!({"type": "string"});
243 let format = OutputFormat::json_schema_named("PersonName", schema);
244 let json = serde_json::to_string(&format).unwrap();
245 assert!(json.contains("\"name\":\"PersonName\""));
246 }
247
248 #[test]
249 fn test_effort_level_serialization() {
250 assert_eq!(serde_json::to_string(&EffortLevel::Low).unwrap(), "\"low\"");
251 assert_eq!(
252 serde_json::to_string(&EffortLevel::Medium).unwrap(),
253 "\"medium\""
254 );
255 assert_eq!(
256 serde_json::to_string(&EffortLevel::High).unwrap(),
257 "\"high\""
258 );
259 }
260
261 #[test]
262 fn test_output_config_serialization() {
263 let config = OutputConfig::with_effort(EffortLevel::High);
264 let json = serde_json::to_string(&config).unwrap();
265 assert!(json.contains("\"effort\":\"high\""));
266 }
267
268 #[test]
269 fn test_tool_choice_serialization() {
270 assert_eq!(
271 serde_json::to_string(&ToolChoice::Auto).unwrap(),
272 r#"{"type":"auto"}"#
273 );
274 assert_eq!(
275 serde_json::to_string(&ToolChoice::Any).unwrap(),
276 r#"{"type":"any"}"#
277 );
278 assert_eq!(
279 serde_json::to_string(&ToolChoice::None).unwrap(),
280 r#"{"type":"none"}"#
281 );
282 assert_eq!(
283 serde_json::to_string(&ToolChoice::tool("Bash")).unwrap(),
284 r#"{"type":"tool","name":"Bash"}"#
285 );
286 }
287
288 #[test]
289 fn test_token_constants() {
290 assert_eq!(MIN_THINKING_BUDGET, 1024);
291 assert_eq!(DEFAULT_MAX_TOKENS, 8192);
292 assert_eq!(MAX_TOKENS_128K, 128_000);
293 assert_eq!(MIN_MAX_TOKENS, 1);
294 }
295}