1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ReasoningMode {
9 None,
11 Effort,
13 Thinking,
15 Both,
17}
18
19#[derive(Debug, Clone)]
23pub enum ReasoningSpec {
24 None,
26 Effort(ReasoningEffort),
28 Thinking { budget_tokens: u64 },
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct ReasoningConfig {
35 pub enabled: Option<bool>,
37 pub budget_tokens: Option<u64>,
39 pub effort: Option<ReasoningEffort>,
41}
42
43#[derive(Debug, Clone, Default)]
45pub enum ReasoningEffort {
46 #[default]
47 None,
48 Low,
49 Medium,
50 High,
51 XHigh,
52}
53
54impl ReasoningConfig {
55 pub fn to_spec(&self, mode: ReasoningMode) -> ReasoningSpec {
58 if self.enabled == Some(false) {
60 return ReasoningSpec::None;
61 }
62
63 match mode {
64 ReasoningMode::None => ReasoningSpec::None,
65 ReasoningMode::Effort => {
66 self.effort
68 .as_ref()
69 .filter(|e| !matches!(e, ReasoningEffort::None))
70 .map(|e| ReasoningSpec::Effort(e.clone()))
71 .unwrap_or(ReasoningSpec::None)
72 }
73 ReasoningMode::Thinking => {
74 ReasoningSpec::Thinking {
76 budget_tokens: self.budget_tokens.unwrap_or(2048),
77 }
78 }
79 ReasoningMode::Both => {
80 if let Some(budget_tokens) = self.budget_tokens {
84 ReasoningSpec::Thinking { budget_tokens }
85 } else if let Some(effort) = &self.effort {
86 if !matches!(effort, ReasoningEffort::None) {
87 return ReasoningSpec::Effort(effort.clone());
88 }
89 ReasoningSpec::Thinking {
90 budget_tokens: 2048,
91 }
92 } else {
93 ReasoningSpec::Thinking {
94 budget_tokens: 2048,
95 }
96 }
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn to_spec_none_mode() {
108 let config = ReasoningConfig {
109 effort: Some(ReasoningEffort::High),
110 budget_tokens: Some(4096),
111 ..Default::default()
112 };
113 assert!(matches!(
114 config.to_spec(ReasoningMode::None),
115 ReasoningSpec::None
116 ));
117 }
118
119 #[test]
120 fn to_spec_effort_mode_with_effort() {
121 let config = ReasoningConfig {
122 effort: Some(ReasoningEffort::Medium),
123 ..Default::default()
124 };
125 assert!(matches!(
126 config.to_spec(ReasoningMode::Effort),
127 ReasoningSpec::Effort(ReasoningEffort::Medium)
128 ));
129 }
130
131 #[test]
132 fn to_spec_effort_mode_without_effort() {
133 let config = ReasoningConfig {
134 effort: None,
135 budget_tokens: Some(4096),
136 ..Default::default()
137 };
138 assert!(matches!(
139 config.to_spec(ReasoningMode::Effort),
140 ReasoningSpec::None
141 ));
142 }
143
144 #[test]
145 fn to_spec_thinking_mode() {
146 let config = ReasoningConfig {
147 budget_tokens: Some(4096),
148 ..Default::default()
149 };
150 assert!(matches!(
151 config.to_spec(ReasoningMode::Thinking),
152 ReasoningSpec::Thinking {
153 budget_tokens: 4096
154 }
155 ));
156 }
157
158 #[test]
159 fn to_spec_thinking_mode_default_budget() {
160 let config = ReasoningConfig::default();
161 assert!(matches!(
162 config.to_spec(ReasoningMode::Thinking),
163 ReasoningSpec::Thinking {
164 budget_tokens: 2048
165 }
166 ));
167 }
168
169 #[test]
170 fn to_spec_both_effort_priority() {
171 let config = ReasoningConfig {
172 effort: Some(ReasoningEffort::High),
173 budget_tokens: Some(4096),
174 ..Default::default()
175 };
176 assert!(matches!(
180 config.to_spec(ReasoningMode::Both),
181 ReasoningSpec::Thinking {
182 budget_tokens: 4096
183 }
184 ));
185 }
186
187 #[test]
188 fn to_spec_both_fallback_to_thinking() {
189 let config = ReasoningConfig {
190 effort: None,
191 budget_tokens: Some(4096),
192 ..Default::default()
193 };
194 assert!(matches!(
195 config.to_spec(ReasoningMode::Both),
196 ReasoningSpec::Thinking {
197 budget_tokens: 4096
198 }
199 ));
200 }
201
202 #[test]
203 fn to_spec_both_effort_none_fallback() {
204 let config = ReasoningConfig {
205 effort: Some(ReasoningEffort::None),
206 budget_tokens: Some(4096),
207 ..Default::default()
208 };
209 assert!(matches!(
211 config.to_spec(ReasoningMode::Both),
212 ReasoningSpec::Thinking {
213 budget_tokens: 4096
214 }
215 ));
216 }
217
218 #[test]
219 fn to_spec_enabled_false_disables_reasoning() {
220 let config = ReasoningConfig {
222 enabled: Some(false),
223 effort: Some(ReasoningEffort::High),
224 ..Default::default()
225 };
226 let spec = config.to_spec(ReasoningMode::Effort);
227 assert!(
228 matches!(spec, ReasoningSpec::None),
229 "enabled=false should disable reasoning"
230 );
231
232 let config2 = ReasoningConfig {
233 enabled: Some(false),
234 budget_tokens: Some(4096),
235 ..Default::default()
236 };
237 let spec2 = config2.to_spec(ReasoningMode::Thinking);
238 assert!(
239 matches!(spec2, ReasoningSpec::None),
240 "enabled=false should disable thinking"
241 );
242 }
243
244 mod proptest_tests {
247 use super::*;
248 use proptest::prelude::*;
249
250 proptest! {
251 #[test]
252 fn to_spec_never_panics(
253 enabled in proptest::option::of(proptest::bool::ANY),
254 budget_tokens in proptest::option::of(0u64..100_000),
255 effort_idx in 0usize..5,
256 mode_idx in 0usize..4,
257 ) {
258 let effort = match effort_idx {
259 0 => None,
260 1 => Some(ReasoningEffort::None),
261 2 => Some(ReasoningEffort::Low),
262 3 => Some(ReasoningEffort::Medium),
263 4 => Some(ReasoningEffort::High),
264 _ => unreachable!(),
265 };
266 let mode = match mode_idx {
267 0 => ReasoningMode::None,
268 1 => ReasoningMode::Effort,
269 2 => ReasoningMode::Thinking,
270 3 => ReasoningMode::Both,
271 _ => unreachable!(),
272 };
273 let config = ReasoningConfig { enabled, budget_tokens, effort };
274 let _spec = config.to_spec(mode);
275 }
276
277 #[test]
278 fn none_mode_always_returns_none(
279 enabled in proptest::option::of(proptest::bool::ANY),
280 budget_tokens in proptest::option::of(0u64..100_000),
281 effort_idx in 0usize..5,
282 ) {
283 let effort = match effort_idx {
284 0 => None,
285 1 => Some(ReasoningEffort::None),
286 2 => Some(ReasoningEffort::Low),
287 3 => Some(ReasoningEffort::Medium),
288 4 => Some(ReasoningEffort::High),
289 _ => unreachable!(),
290 };
291 let config = ReasoningConfig { enabled, budget_tokens, effort };
292 let spec = config.to_spec(ReasoningMode::None);
293 assert!(matches!(spec, ReasoningSpec::None),
294 "Mode::None should always produce Spec::None, got {:?}", spec);
295 }
296
297 #[test]
298 fn enabled_false_always_returns_none(
299 budget_tokens in proptest::option::of(0u64..100_000),
300 effort_idx in 0usize..5,
301 mode_idx in 0usize..4,
302 ) {
303 let effort = match effort_idx {
304 0 => None,
305 1 => Some(ReasoningEffort::None),
306 2 => Some(ReasoningEffort::Low),
307 3 => Some(ReasoningEffort::Medium),
308 4 => Some(ReasoningEffort::High),
309 _ => unreachable!(),
310 };
311 let mode = match mode_idx {
312 0 => ReasoningMode::None,
313 1 => ReasoningMode::Effort,
314 2 => ReasoningMode::Thinking,
315 3 => ReasoningMode::Both,
316 _ => unreachable!(),
317 };
318 let config = ReasoningConfig { enabled: Some(false), budget_tokens, effort };
319 let spec = config.to_spec(mode);
320 assert!(matches!(spec, ReasoningSpec::None),
321 "enabled=false should disable reasoning for any mode, got {:?}", spec);
322 }
323 }
324 }
325}