claude_agent/client/messages/
config.rs

1//! Configuration types for message requests.
2
3use 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}