1use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
22#[serde(rename_all = "lowercase")]
23pub enum ThinkingLevel {
24 Off,
26 Minimal,
28 Low,
30 #[default]
32 Medium,
33 High,
35 Max,
37}
38
39impl ThinkingLevel {
40 pub fn from_str_insensitive(s: &str) -> Option<Self> {
42 match s.to_lowercase().as_str() {
43 "off" | "none" => Some(Self::Off),
44 "minimal" | "min" => Some(Self::Minimal),
45 "low" => Some(Self::Low),
46 "medium" | "med" | "default" => Some(Self::Medium),
47 "high" => Some(Self::High),
48 "max" | "maximum" => Some(Self::Max),
49 _ => None,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
56pub struct ThinkingConfig {
57 #[serde(default)]
59 pub default_level: ThinkingLevel,
60}
61
62impl Default for ThinkingConfig {
63 fn default() -> Self {
64 Self {
65 default_level: ThinkingLevel::Medium,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub struct ThinkingParams {
73 pub temperature_adjustment: f64,
75 pub max_tokens_adjustment: i64,
77 pub system_prompt_prefix: Option<String>,
79}
80
81pub fn parse_thinking_directive(message: &str) -> Option<(ThinkingLevel, String)> {
87 let trimmed = message.trim_start();
88 if !trimmed.starts_with("/think:") {
89 return None;
90 }
91
92 let after_prefix = &trimmed["/think:".len()..];
94 let level_end = after_prefix
95 .find(|c: char| c.is_whitespace())
96 .unwrap_or(after_prefix.len());
97 let level_str = &after_prefix[..level_end];
98
99 let level = ThinkingLevel::from_str_insensitive(level_str)?;
100
101 let remaining = after_prefix[level_end..].trim_start().to_string();
102 Some((level, remaining))
103}
104
105pub fn apply_thinking_level(level: ThinkingLevel) -> ThinkingParams {
107 match level {
108 ThinkingLevel::Off => ThinkingParams {
109 temperature_adjustment: -0.2,
110 max_tokens_adjustment: -1000,
111 system_prompt_prefix: Some(
112 "Be extremely concise. Give direct answers without explanation \
113 unless explicitly asked. No preamble."
114 .into(),
115 ),
116 },
117 ThinkingLevel::Minimal => ThinkingParams {
118 temperature_adjustment: -0.1,
119 max_tokens_adjustment: -500,
120 system_prompt_prefix: Some(
121 "Be concise and fast. Keep explanations brief. \
122 Prioritize speed over thoroughness."
123 .into(),
124 ),
125 },
126 ThinkingLevel::Low => ThinkingParams {
127 temperature_adjustment: -0.05,
128 max_tokens_adjustment: 0,
129 system_prompt_prefix: Some("Keep reasoning light. Explain only when helpful.".into()),
130 },
131 ThinkingLevel::Medium => ThinkingParams {
132 temperature_adjustment: 0.0,
133 max_tokens_adjustment: 0,
134 system_prompt_prefix: None,
135 },
136 ThinkingLevel::High => ThinkingParams {
137 temperature_adjustment: 0.05,
138 max_tokens_adjustment: 1000,
139 system_prompt_prefix: Some(
140 "Think step by step. Provide thorough analysis and \
141 consider edge cases before answering."
142 .into(),
143 ),
144 },
145 ThinkingLevel::Max => ThinkingParams {
146 temperature_adjustment: 0.1,
147 max_tokens_adjustment: 2000,
148 system_prompt_prefix: Some(
149 "Think very carefully and exhaustively. Break down the problem \
150 into sub-problems, consider all angles, verify your reasoning, \
151 and provide the most thorough analysis possible."
152 .into(),
153 ),
154 },
155 }
156}
157
158pub fn resolve_thinking_level(
164 inline_directive: Option<ThinkingLevel>,
165 session_override: Option<ThinkingLevel>,
166 config: &ThinkingConfig,
167) -> ThinkingLevel {
168 inline_directive
169 .or(session_override)
170 .unwrap_or(config.default_level)
171}
172
173pub fn clamp_temperature(temp: f64) -> f64 {
175 temp.clamp(0.0, 2.0)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
185 fn thinking_level_from_str_canonical_names() {
186 assert_eq!(
187 ThinkingLevel::from_str_insensitive("off"),
188 Some(ThinkingLevel::Off)
189 );
190 assert_eq!(
191 ThinkingLevel::from_str_insensitive("minimal"),
192 Some(ThinkingLevel::Minimal)
193 );
194 assert_eq!(
195 ThinkingLevel::from_str_insensitive("low"),
196 Some(ThinkingLevel::Low)
197 );
198 assert_eq!(
199 ThinkingLevel::from_str_insensitive("medium"),
200 Some(ThinkingLevel::Medium)
201 );
202 assert_eq!(
203 ThinkingLevel::from_str_insensitive("high"),
204 Some(ThinkingLevel::High)
205 );
206 assert_eq!(
207 ThinkingLevel::from_str_insensitive("max"),
208 Some(ThinkingLevel::Max)
209 );
210 }
211
212 #[test]
213 fn thinking_level_from_str_aliases() {
214 assert_eq!(
215 ThinkingLevel::from_str_insensitive("none"),
216 Some(ThinkingLevel::Off)
217 );
218 assert_eq!(
219 ThinkingLevel::from_str_insensitive("min"),
220 Some(ThinkingLevel::Minimal)
221 );
222 assert_eq!(
223 ThinkingLevel::from_str_insensitive("med"),
224 Some(ThinkingLevel::Medium)
225 );
226 assert_eq!(
227 ThinkingLevel::from_str_insensitive("default"),
228 Some(ThinkingLevel::Medium)
229 );
230 assert_eq!(
231 ThinkingLevel::from_str_insensitive("maximum"),
232 Some(ThinkingLevel::Max)
233 );
234 }
235
236 #[test]
237 fn thinking_level_from_str_case_insensitive() {
238 assert_eq!(
239 ThinkingLevel::from_str_insensitive("HIGH"),
240 Some(ThinkingLevel::High)
241 );
242 assert_eq!(
243 ThinkingLevel::from_str_insensitive("Max"),
244 Some(ThinkingLevel::Max)
245 );
246 assert_eq!(
247 ThinkingLevel::from_str_insensitive("OFF"),
248 Some(ThinkingLevel::Off)
249 );
250 }
251
252 #[test]
253 fn thinking_level_from_str_invalid_returns_none() {
254 assert_eq!(ThinkingLevel::from_str_insensitive("turbo"), None);
255 assert_eq!(ThinkingLevel::from_str_insensitive(""), None);
256 assert_eq!(ThinkingLevel::from_str_insensitive("super-high"), None);
257 }
258
259 #[test]
262 fn parse_directive_extracts_level_and_remaining_message() {
263 let result = parse_thinking_directive("/think:high What is Rust?");
264 assert!(result.is_some());
265 let (level, remaining) = result.unwrap();
266 assert_eq!(level, ThinkingLevel::High);
267 assert_eq!(remaining, "What is Rust?");
268 }
269
270 #[test]
271 fn parse_directive_handles_directive_only() {
272 let result = parse_thinking_directive("/think:off");
273 assert!(result.is_some());
274 let (level, remaining) = result.unwrap();
275 assert_eq!(level, ThinkingLevel::Off);
276 assert_eq!(remaining, "");
277 }
278
279 #[test]
280 fn parse_directive_strips_leading_whitespace() {
281 let result = parse_thinking_directive(" /think:low Tell me about Rust");
282 assert!(result.is_some());
283 let (level, remaining) = result.unwrap();
284 assert_eq!(level, ThinkingLevel::Low);
285 assert_eq!(remaining, "Tell me about Rust");
286 }
287
288 #[test]
289 fn parse_directive_returns_none_for_no_directive() {
290 assert!(parse_thinking_directive("Hello world").is_none());
291 assert!(parse_thinking_directive("").is_none());
292 assert!(parse_thinking_directive("/think").is_none());
293 }
294
295 #[test]
296 fn parse_directive_returns_none_for_invalid_level() {
297 assert!(parse_thinking_directive("/think:turbo What?").is_none());
298 }
299
300 #[test]
301 fn parse_directive_not_triggered_mid_message() {
302 assert!(parse_thinking_directive("Hello /think:high world").is_none());
303 }
304
305 #[test]
308 fn apply_thinking_level_off_is_concise() {
309 let params = apply_thinking_level(ThinkingLevel::Off);
310 assert!(params.temperature_adjustment < 0.0);
311 assert!(params.max_tokens_adjustment < 0);
312 assert!(params.system_prompt_prefix.is_some());
313 assert!(
314 params
315 .system_prompt_prefix
316 .unwrap()
317 .to_lowercase()
318 .contains("concise")
319 );
320 }
321
322 #[test]
323 fn apply_thinking_level_medium_is_neutral() {
324 let params = apply_thinking_level(ThinkingLevel::Medium);
325 assert!((params.temperature_adjustment - 0.0).abs() < f64::EPSILON);
326 assert_eq!(params.max_tokens_adjustment, 0);
327 assert!(params.system_prompt_prefix.is_none());
328 }
329
330 #[test]
331 fn apply_thinking_level_high_adds_step_by_step() {
332 let params = apply_thinking_level(ThinkingLevel::High);
333 assert!(params.temperature_adjustment > 0.0);
334 assert!(params.max_tokens_adjustment > 0);
335 let prefix = params.system_prompt_prefix.unwrap();
336 assert!(prefix.to_lowercase().contains("step by step"));
337 }
338
339 #[test]
340 fn apply_thinking_level_max_is_most_thorough() {
341 let params = apply_thinking_level(ThinkingLevel::Max);
342 assert!(params.temperature_adjustment > 0.0);
343 assert!(params.max_tokens_adjustment > 0);
344 let prefix = params.system_prompt_prefix.unwrap();
345 assert!(prefix.to_lowercase().contains("exhaustively"));
346 }
347
348 #[test]
351 fn resolve_inline_directive_takes_priority() {
352 let config = ThinkingConfig {
353 default_level: ThinkingLevel::Low,
354 };
355 let result =
356 resolve_thinking_level(Some(ThinkingLevel::Max), Some(ThinkingLevel::High), &config);
357 assert_eq!(result, ThinkingLevel::Max);
358 }
359
360 #[test]
361 fn resolve_session_override_takes_priority_over_config() {
362 let config = ThinkingConfig {
363 default_level: ThinkingLevel::Low,
364 };
365 let result = resolve_thinking_level(None, Some(ThinkingLevel::High), &config);
366 assert_eq!(result, ThinkingLevel::High);
367 }
368
369 #[test]
370 fn resolve_falls_back_to_config_default() {
371 let config = ThinkingConfig {
372 default_level: ThinkingLevel::Minimal,
373 };
374 let result = resolve_thinking_level(None, None, &config);
375 assert_eq!(result, ThinkingLevel::Minimal);
376 }
377
378 #[test]
379 fn resolve_default_config_uses_medium() {
380 let config = ThinkingConfig::default();
381 let result = resolve_thinking_level(None, None, &config);
382 assert_eq!(result, ThinkingLevel::Medium);
383 }
384
385 #[test]
388 fn clamp_temperature_within_range() {
389 assert!((clamp_temperature(0.7) - 0.7).abs() < f64::EPSILON);
390 assert!((clamp_temperature(0.0) - 0.0).abs() < f64::EPSILON);
391 assert!((clamp_temperature(2.0) - 2.0).abs() < f64::EPSILON);
392 }
393
394 #[test]
395 fn clamp_temperature_below_minimum() {
396 assert!((clamp_temperature(-0.5) - 0.0).abs() < f64::EPSILON);
397 }
398
399 #[test]
400 fn clamp_temperature_above_maximum() {
401 assert!((clamp_temperature(3.0) - 2.0).abs() < f64::EPSILON);
402 }
403
404 #[test]
407 fn thinking_config_deserializes_from_toml() {
408 let toml_str = r#"default_level = "high""#;
409 let config: ThinkingConfig = toml::from_str(toml_str).unwrap();
410 assert_eq!(config.default_level, ThinkingLevel::High);
411 }
412
413 #[test]
414 fn thinking_config_default_level_deserializes() {
415 let toml_str = "";
416 let config: ThinkingConfig = toml::from_str(toml_str).unwrap();
417 assert_eq!(config.default_level, ThinkingLevel::Medium);
418 }
419
420 #[test]
421 fn thinking_level_serializes_lowercase() {
422 let level = ThinkingLevel::High;
423 let json = serde_json::to_string(&level).unwrap();
424 assert_eq!(json, "\"high\"");
425 }
426}