1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
6#[serde(rename_all = "lowercase")]
7pub enum ReasoningEffort {
8 Minimal,
9 Low,
10 Medium,
11 High,
12 Xhigh,
13 Max,
14}
15
16impl ReasoningEffort {
17 pub fn as_str(self) -> &'static str {
18 match self {
19 Self::Minimal => "minimal",
20 Self::Low => "low",
21 Self::Medium => "medium",
22 Self::High => "high",
23 Self::Xhigh => "xhigh",
24 Self::Max => "max",
25 }
26 }
27
28 pub fn all() -> &'static [ReasoningEffort] {
29 &[Self::Minimal, Self::Low, Self::Medium, Self::High, Self::Xhigh, Self::Max]
30 }
31
32 pub fn cycle_within(current: Option<Self>, levels: &[Self]) -> Option<Self> {
35 if levels.is_empty() {
36 return None;
37 }
38 match current {
39 None => Some(levels[0]),
40 Some(effort) => levels.iter().position(|&l| l == effort).and_then(|i| levels.get(i + 1)).copied(),
41 }
42 }
43
44 pub fn clamp_to(self, levels: &[Self]) -> Self {
47 levels.iter().rev().find(|&&l| l <= self).copied().unwrap_or(*levels.first().expect("levels must not be empty"))
48 }
49
50 pub fn config_str(effort: Option<Self>) -> &'static str {
52 effort.map_or("none", Self::as_str)
53 }
54
55 pub fn parse(s: &str) -> Result<Option<Self>, String> {
58 match s {
59 "none" | "" => Ok(None),
60 other => other.parse().map(Some),
61 }
62 }
63}
64
65impl fmt::Display for ReasoningEffort {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(self.as_str())
68 }
69}
70
71impl FromStr for ReasoningEffort {
72 type Err = String;
73
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
75 match s {
76 "minimal" => Ok(Self::Minimal),
77 "low" => Ok(Self::Low),
78 "medium" => Ok(Self::Medium),
79 "high" => Ok(Self::High),
80 "xhigh" => Ok(Self::Xhigh),
81 "max" => Ok(Self::Max),
82 _ => Err(format!("Unknown reasoning effort: '{s}'")),
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn display_roundtrip() {
93 for effort in ReasoningEffort::all() {
94 let s = effort.to_string();
95 let parsed: ReasoningEffort = s.parse().unwrap();
96 assert_eq!(*effort, parsed);
97 }
98 }
99
100 #[test]
101 fn as_str_matches_display() {
102 for effort in ReasoningEffort::all() {
103 assert_eq!(effort.as_str(), effort.to_string());
104 }
105 }
106
107 #[test]
108 fn from_str_rejects_unknown() {
109 assert!("extreme".parse::<ReasoningEffort>().is_err());
110 }
111
112 #[test]
113 fn all_returns_six_variants() {
114 assert_eq!(ReasoningEffort::all().len(), 6);
115 }
116
117 #[test]
118 fn parse_none_and_empty() {
119 assert_eq!(ReasoningEffort::parse("none").unwrap(), None);
120 assert_eq!(ReasoningEffort::parse("").unwrap(), None);
121 }
122
123 #[test]
124 fn parse_valid_levels() {
125 assert_eq!(ReasoningEffort::parse("high").unwrap(), Some(ReasoningEffort::High));
126 assert_eq!(ReasoningEffort::parse("low").unwrap(), Some(ReasoningEffort::Low));
127 }
128
129 #[test]
130 fn parse_rejects_unknown() {
131 assert!(ReasoningEffort::parse("extreme").is_err());
132 }
133
134 #[test]
135 fn config_str_values() {
136 assert_eq!(ReasoningEffort::config_str(None), "none");
137 assert_eq!(ReasoningEffort::config_str(Some(ReasoningEffort::Low)), "low");
138 assert_eq!(ReasoningEffort::config_str(Some(ReasoningEffort::High)), "high");
139 }
140
141 #[test]
142 fn serialize_produces_lowercase() {
143 for effort in ReasoningEffort::all() {
144 let json = serde_json::to_value(effort).unwrap();
145 assert_eq!(json.as_str().unwrap(), effort.as_str());
146 }
147 }
148
149 #[test]
150 fn variants_are_ordered_by_effort() {
151 let mut sorted = ReasoningEffort::all().to_vec();
152 sorted.sort();
153 assert_eq!(sorted, ReasoningEffort::all());
154 assert!(ReasoningEffort::Minimal < ReasoningEffort::Low);
155 assert!(ReasoningEffort::Xhigh < ReasoningEffort::Max);
156 }
157
158 #[test]
159 fn cycle_within_three_levels() {
160 use ReasoningEffort::*;
161 let levels = &[Low, Medium, High];
162 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
163 assert_eq!(ReasoningEffort::cycle_within(Some(Low), levels), Some(Medium));
164 assert_eq!(ReasoningEffort::cycle_within(Some(Medium), levels), Some(High));
165 assert_eq!(ReasoningEffort::cycle_within(Some(High), levels), None);
166 }
167
168 #[test]
169 fn cycle_within_five_levels() {
170 use ReasoningEffort::*;
171 let levels = &[Low, Medium, High, Xhigh, Max];
172 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
173 assert_eq!(ReasoningEffort::cycle_within(Some(High), levels), Some(Xhigh));
174 assert_eq!(ReasoningEffort::cycle_within(Some(Xhigh), levels), Some(Max));
175 assert_eq!(ReasoningEffort::cycle_within(Some(Max), levels), None);
176 }
177
178 #[test]
179 fn cycle_within_empty_returns_none() {
180 assert_eq!(ReasoningEffort::cycle_within(None, &[]), None);
181 assert_eq!(ReasoningEffort::cycle_within(Some(ReasoningEffort::Low), &[]), None);
182 }
183
184 #[test]
185 fn cycle_within_unknown_current_wraps_to_none() {
186 use ReasoningEffort::*;
187 assert_eq!(ReasoningEffort::cycle_within(Some(Xhigh), &[Low, Medium, High]), None);
189 }
190
191 #[test]
192 fn clamp_to_self_in_levels() {
193 use ReasoningEffort::*;
194 assert_eq!(High.clamp_to(&[Low, Medium, High]), High);
195 assert_eq!(Xhigh.clamp_to(&[Low, Medium, High, Xhigh]), Xhigh);
196 assert_eq!(Max.clamp_to(&[Low, Medium, High, Xhigh, Max]), Max);
197 }
198
199 #[test]
200 fn clamp_to_highest_le() {
201 use ReasoningEffort::*;
202 assert_eq!(Max.clamp_to(&[Low, Medium, High, Xhigh]), Xhigh);
204 }
205
206 #[test]
207 fn clamp_to_fallback_first() {
208 use ReasoningEffort::*;
209 assert_eq!(Low.clamp_to(&[Medium, High]), Medium);
211 }
212
213 #[test]
214 fn parse_extended_levels() {
215 assert_eq!(ReasoningEffort::parse("minimal").unwrap(), Some(ReasoningEffort::Minimal));
216 assert_eq!(ReasoningEffort::parse("xhigh").unwrap(), Some(ReasoningEffort::Xhigh));
217 assert_eq!(ReasoningEffort::parse("max").unwrap(), Some(ReasoningEffort::Max));
218 assert!(ReasoningEffort::parse("ultra").is_err());
219 }
220}