返回 CodeWhale
reasoning_preference.rs
根目录 / crates / tui / src / reasoning_preference.rs
1 //! Reasoning preferences and effective tiers (#5316).
2 //!
3 //! Separated from TUI app state so engine, client routing, settings, and
4 //! tools can resolve and normalize reasoning effort without depending on
5 //! presentation types.
6
7 use crate::config::ProviderKind;
8 use crate::work_graph::ReasoningEffortTier;
9
10 /// Reasoning-effort tier, mirrored across DeepSeek and Codex effort pickers.
11 ///
12 /// The config file accepts every supported string value for forward-compat with
13 /// providers that expose the full spectrum; first-party DeepSeek routes send
14 /// `low`/`high`/`max`, so `Minimal` → `low`, `Medium`/`XHigh` → `high`, and
15 /// `Ultra` → `max`. OpenAI Codex normalizes inherited DeepSeek-only
16 /// `Off` to `Low` and keeps `XHigh`, `Max`, and `Ultra` distinct at the
17 /// provider boundary. The default keyboard cycler walks the three DeepSeek-distinct
18 /// tiers: `Off` → `High` → `Max` → `Off`; provider-aware callers should use
19 /// [`ReasoningEffort::cycle_next_in`] with the route's picker ladder
20 /// (`picker_efforts_for_route`), which also covers Auto routing.
21 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
22 pub enum ReasoningEffort {
23 Off,
24 Minimal,
25 Low,
26 Medium,
27 High,
28 XHigh,
29 Ultra,
30 Auto,
31 #[default]
32 Max,
33 }
34
35 /// Provider-effective reasoning state used by durable receipts and visible
36 /// requested-to-effective labels.
37 ///
38 /// Some routes, notably first-party GLM-5-Turbo, support a thinking toggle but
39 /// publish no effort tiers. Keeping that state distinct prevents a requested
40 /// `max` from being displayed or persisted as an effective `max` claim.
41 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42 pub enum EffectiveReasoningEffort {
43 Tier(ReasoningEffort),
44 ThinkingEnabledGranularityUnavailable,
45 Unavailable,
46 }
47
48 impl EffectiveReasoningEffort {
49 /// Reconstruct a safe request tier for cache replay and inspection.
50 ///
51 /// Routes with an enabled-but-untiered receipt collapse every non-Off
52 /// request to the same wire toggle, so High is the canonical value that
53 /// keeps reasoning enabled without claiming a granular effective tier.
54 #[must_use]
55 pub const fn request_tier_for_replay(self) -> Option<ReasoningEffort> {
56 match self {
57 Self::Tier(tier) => Some(tier),
58 Self::ThinkingEnabledGranularityUnavailable => Some(ReasoningEffort::High),
59 Self::Unavailable => None,
60 }
61 }
62 }
63
64 impl From<EffectiveReasoningEffort> for ReasoningEffortTier {
65 fn from(value: EffectiveReasoningEffort) -> Self {
66 match value {
67 EffectiveReasoningEffort::Tier(tier) => tier.into(),
68 EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable => {
69 Self::ThinkingEnabledGranularityUnavailable
70 }
71 EffectiveReasoningEffort::Unavailable => Self::Unavailable,
72 }
73 }
74 }
75
76 impl From<ReasoningEffortTier> for EffectiveReasoningEffort {
77 fn from(value: ReasoningEffortTier) -> Self {
78 match value {
79 ReasoningEffortTier::Off => Self::Tier(ReasoningEffort::Off),
80 ReasoningEffortTier::Minimal => Self::Tier(ReasoningEffort::Minimal),
81 ReasoningEffortTier::Low => Self::Tier(ReasoningEffort::Low),
82 ReasoningEffortTier::Medium => Self::Tier(ReasoningEffort::Medium),
83 ReasoningEffortTier::High => Self::Tier(ReasoningEffort::High),
84 ReasoningEffortTier::XHigh => Self::Tier(ReasoningEffort::XHigh),
85 ReasoningEffortTier::Ultra => Self::Tier(ReasoningEffort::Ultra),
86 ReasoningEffortTier::Auto => Self::Tier(ReasoningEffort::Auto),
87 ReasoningEffortTier::Max => Self::Tier(ReasoningEffort::Max),
88 ReasoningEffortTier::ThinkingEnabledGranularityUnavailable => {
89 Self::ThinkingEnabledGranularityUnavailable
90 }
91 ReasoningEffortTier::Unavailable => Self::Unavailable,
92 }
93 }
94 }
95
96 impl From<ReasoningEffort> for ReasoningEffortTier {
97 fn from(value: ReasoningEffort) -> Self {
98 match value {
99 ReasoningEffort::Off => Self::Off,
100 ReasoningEffort::Minimal => Self::Minimal,
101 ReasoningEffort::Low => Self::Low,
102 ReasoningEffort::Medium => Self::Medium,
103 ReasoningEffort::High => Self::High,
104 ReasoningEffort::XHigh => Self::XHigh,
105 ReasoningEffort::Ultra => Self::Ultra,
106 ReasoningEffort::Auto => Self::Auto,
107 ReasoningEffort::Max => Self::Max,
108 }
109 }
110 }
111
112 impl ReasoningEffort {
113 /// Parse an operator-supplied effort value.
114 ///
115 /// This is deliberately the one canonical spelling table for every
116 /// human-facing route. Every canonical setting spelling round-trips:
117 /// `parse_strict(as_setting(effort)) == effort`, including `minimal`.
118 /// Callers that read an old persisted config may use [`Self::from_setting`]
119 /// for its compatibility fallback, but a new CLI, settings, or tool input
120 /// must reject an unknown value instead of quietly turning it into `max`.
121 pub fn parse_strict(value: &str) -> Result<Self, String> {
122 let trimmed = value.trim();
123 match trimmed.to_ascii_lowercase().as_str() {
124 "off" | "disabled" | "none" | "false" => Ok(Self::Off),
125 "minimal" => Ok(Self::Minimal),
126 "low" | "minimum" | "light" => Ok(Self::Low),
127 "medium" | "mid" => Ok(Self::Medium),
128 "high" => Ok(Self::High),
129 "xhigh" => Ok(Self::XHigh),
130 "auto" | "automatic" => Ok(Self::Auto),
131 "ultra" | "ultracode" => Ok(Self::Ultra),
132 "max" | "maximum" => Ok(Self::Max),
133 _ => Err(format!(
134 "Unrecognized reasoning effort {trimmed:?}. Expected one of: auto, off, minimal, low, medium, high, xhigh, ultra, or max."
135 )),
136 }
137 }
138
139 /// Parse a persisted config-file string into an effort tier. Unknown
140 /// legacy values fall back to the default (`Max`) so an old malformed
141 /// settings file never prevents startup. New user input should use
142 /// [`Self::parse_strict`] instead.
143 #[must_use]
144 pub fn from_setting(value: &str) -> Self {
145 Self::parse_strict(value).unwrap_or_default()
146 }
147
148 #[must_use]
149 pub fn from_setting_for_provider(value: &str, provider: ProviderKind) -> Self {
150 Self::from_setting(value).normalize_for_provider(provider)
151 }
152
153 /// Canonical lowercase label used for config storage and UI hints.
154 #[must_use]
155 pub fn as_setting(self) -> &'static str {
156 match self {
157 Self::Off => "off",
158 Self::Minimal => "minimal",
159 Self::Low => "low",
160 Self::Medium => "medium",
161 Self::High => "high",
162 Self::XHigh => "xhigh",
163 Self::Ultra => "ultra",
164 Self::Auto => "auto",
165 Self::Max => "max",
166 }
167 }
168
169 /// Short label for the header chip.
170 #[must_use]
171 pub fn short_label(self) -> &'static str {
172 match self {
173 Self::Off => "off",
174 Self::Minimal => "minimal",
175 Self::Low => "low",
176 Self::Medium => "med",
177 Self::High => "high",
178 Self::XHigh => "xhigh",
179 Self::Ultra => "ultra",
180 Self::Auto => "auto",
181 Self::Max => "max",
182 }
183 }
184
185 /// Provider-facing label for user-visible surfaces.
186 #[must_use]
187 pub fn display_label_for_provider(self, provider: ProviderKind) -> &'static str {
188 match (provider, self.normalize_for_provider(provider)) {
189 (ProviderKind::OpenaiCodex, Self::Minimal) => "low",
190 (ProviderKind::OpenaiCodex, Self::Low) => "low",
191 (ProviderKind::OpenaiCodex, Self::Medium) => "medium",
192 (ProviderKind::OpenaiCodex, Self::High) => "high",
193 // `xhigh`, `max` and `ultra` are three distinct rungs the Codex
194 // roster publishes per model; collapsing them onto "xhigh" dates
195 // from when xhigh was the ceiling and made the top tiers
196 // unreachable and indistinguishable.
197 (ProviderKind::OpenaiCodex, Self::XHigh) => "xhigh",
198 (ProviderKind::OpenaiCodex, Self::Ultra) => "ultra",
199 (ProviderKind::OpenaiCodex, Self::Max) => "max",
200 (ProviderKind::Xai, Self::XHigh) => "xhigh",
201 (_, effort) => effort.short_label(),
202 }
203 }
204
205 /// Value forwarded to the engine/client. `None` means "provider default"
206 /// (for `Off` we still emit `"off"` so the client can inject
207 /// `thinking = {"type": "disabled"}`).
208 #[must_use]
209 pub fn api_value(self) -> Option<&'static str> {
210 Some(self.as_setting())
211 }
212
213 #[must_use]
214 pub fn normalize_for_provider(self, provider: ProviderKind) -> Self {
215 if provider != ProviderKind::OpenaiCodex {
216 return self;
217 }
218 match self {
219 Self::Off => Self::Low,
220 Self::Auto => Self::Medium,
221 other => other,
222 }
223 }
224
225 /// Resolve an effort against the exact provider route that will receive
226 /// the request. Both K3 routes are always-thinking, so `off` becomes the
227 /// lowest supported tier. The Kimi Code membership route otherwise keeps
228 /// its low/high/max mapping; direct Moonshot K3 additionally maps `medium`
229 /// to `high`. First-party DeepSeek routes keep `low` (the wire documents
230 /// low/high/max) while rounding `medium` up to `high`. Models that publish
231 /// a Models.dev `reasoning_options` effort list keep that vocabulary
232 /// instead of the historic Low/Medium collapse. Generic Moonshot and
233 /// every other non-Codex route retain the historic high coercion.
234 /// This intentionally does not change [`Self::normalize_for_provider`],
235 /// whose generic wire semantics are used by older callers that do not yet
236 /// have a route receipt.
237 #[must_use]
238 pub fn normalize_for_route(
239 self,
240 provider: ProviderKind,
241 base_url: &str,
242 wire_model: &str,
243 ) -> Self {
244 let normalized = self.normalize_for_provider(provider);
245 if crate::config::is_exact_kimi_code_k3_route(provider, base_url, wire_model) {
246 return match normalized {
247 Self::Off => Self::Low,
248 other => other,
249 };
250 }
251 if crate::config::is_exact_direct_moonshot_k3_route(provider, base_url, wire_model) {
252 return match normalized {
253 Self::Off => Self::Low,
254 Self::Medium => Self::High,
255 other => other,
256 };
257 }
258 if provider == ProviderKind::OpenaiCodex {
259 return normalized;
260 }
261 // First-party DeepSeek routes document `reasoning_effort` low/high/max
262 // on the wire (no medium), so `low` is a real, cheaper tier there and
263 // must reach the wire as low; `medium` rounds up to high because the
264 // dialect has no such value (#52). `minimal`, `xhigh`, and `ultra`
265 // collapse exactly as `client::deepseek_effort` sends them, so the
266 // effective tier names what the wire receives (#6650).
267 if matches!(provider, ProviderKind::Deepseek) {
268 return match normalized {
269 Self::Minimal => Self::Low,
270 Self::Medium | Self::XHigh => Self::High,
271 Self::Ultra => Self::Max,
272 other => other,
273 };
274 }
275 // Ollama's current OpenAI-compatible Chat Completions contract
276 // documents the complete none/low/medium/high/max ladder. Keep every
277 // real tier distinct for normal turns; only Codewhale-only synonyms
278 // are folded onto the nearest documented spelling.
279 if provider == ProviderKind::OllamaCloud {
280 return match normalized {
281 Self::Minimal => Self::Low,
282 Self::XHigh | Self::Ultra => Self::Max,
283 other => other,
284 };
285 }
286 if let Some(values) = Self::catalog_effort_values(provider, wire_model) {
287 return Self::clamp_to_catalog_efforts(normalized, provider, wire_model, &values);
288 }
289 match normalized {
290 Self::Low | Self::Medium => Self::High,
291 other => other,
292 }
293 }
294
295 pub fn catalog_default(provider: ProviderKind, wire_model: &str) -> Option<Self> {
296 let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
297 offering.reasoning_options.iter().find_map(|option| {
298 option
299 .get("type")
300 .and_then(|value| value.as_str())
301 .filter(|kind| kind.eq_ignore_ascii_case("effort"))?;
302 option
303 .get("default")
304 .and_then(|value| value.as_str())
305 .and_then(Self::from_catalog_token)
306 })
307 }
308
309 pub fn catalog_effort_values(provider: ProviderKind, wire_model: &str) -> Option<Vec<Self>> {
310 let offering = crate::provider_lake::catalog_offering_for_model(provider, wire_model)?;
311 let mut efforts = Vec::new();
312 let mut has_toggle = false;
313 for option in &offering.reasoning_options {
314 let kind = option.get("type").and_then(|value| value.as_str());
315 if kind.is_some_and(|kind| kind.eq_ignore_ascii_case("toggle")) {
316 has_toggle = true;
317 continue;
318 }
319 if !kind.is_some_and(|kind| kind.eq_ignore_ascii_case("effort")) {
320 continue;
321 }
322 let Some(values) = option.get("values").and_then(|value| value.as_array()) else {
323 continue;
324 };
325 for value in values {
326 if let Some(effort) = value.as_str().and_then(Self::from_catalog_token)
327 && !efforts.contains(&effort)
328 {
329 efforts.push(effort);
330 }
331 }
332 }
333 // A Models.dev `toggle` beside an effort ladder means reasoning can also
334 // be switched off (#6396); an explicit off must not round up.
335 if has_toggle && !efforts.is_empty() && !efforts.contains(&Self::Off) {
336 efforts.insert(0, Self::Off);
337 }
338 (!efforts.is_empty()).then_some(efforts)
339 }
340
341 pub fn from_catalog_token(raw: &str) -> Option<Self> {
342 match raw.trim().to_ascii_lowercase().as_str() {
343 "off" | "disabled" | "none" | "false" => Some(Self::Off),
344 "minimal" | "minimum" => Some(Self::Minimal),
345 "low" | "light" => Some(Self::Low),
346 "medium" | "mid" => Some(Self::Medium),
347 "high" => Some(Self::High),
348 "xhigh" => Some(Self::XHigh),
349 "ultra" | "ultracode" => Some(Self::Ultra),
350 "max" | "maximum" => Some(Self::Max),
351 "auto" | "automatic" | "adaptive" => Some(Self::Auto),
352 _ => None,
353 }
354 }
355
356 fn clamp_to_catalog_efforts(
357 normalized: Self,
358 provider: ProviderKind,
359 wire_model: &str,
360 values: &[Self],
361 ) -> Self {
362 if matches!(normalized, Self::Auto) || values.contains(&normalized) {
363 return normalized;
364 }
365 let aliased = match normalized {
366 Self::Minimal if values.contains(&Self::Low) => Self::Low,
367 Self::Max | Self::Ultra if values.contains(&Self::XHigh) => Self::XHigh,
368 Self::Off => Self::catalog_default(provider, wire_model).unwrap_or(Self::High),
369 other => other,
370 };
371 if values.contains(&aliased) {
372 aliased
373 } else {
374 Self::catalog_default(provider, wire_model).unwrap_or(Self::High)
375 }
376 }
377
378 #[must_use]
379 pub fn api_value_for_provider(self, provider: ProviderKind) -> Option<&'static str> {
380 if provider != ProviderKind::OpenaiCodex {
381 return self.api_value();
382 }
383 Some(match self.normalize_for_provider(provider) {
384 Self::Minimal => "low",
385 Self::Low => "low",
386 Self::Medium => "medium",
387 Self::High => "high",
388 Self::XHigh => "xhigh",
389 Self::Ultra => "ultra",
390 Self::Max => "max",
391 Self::Off => "low",
392 Self::Auto => "medium",
393 })
394 }
395
396 /// Provider-facing value after exact-route normalization.
397 #[must_use]
398 pub fn api_value_for_route(
399 self,
400 provider: ProviderKind,
401 base_url: &str,
402 wire_model: &str,
403 ) -> Option<&'static str> {
404 self.normalize_for_route(provider, base_url, wire_model)
405 .api_value_for_provider(provider)
406 }
407
408 #[must_use]
409 pub fn as_setting_for_provider(self, provider: ProviderKind) -> &'static str {
410 self.api_value_for_provider(provider)
411 .unwrap_or_else(|| self.as_setting())
412 }
413
414 /// Persist the canonical setting after exact-route normalization.
415 #[must_use]
416 pub fn as_setting_for_route(
417 self,
418 provider: ProviderKind,
419 base_url: &str,
420 wire_model: &str,
421 ) -> &'static str {
422 self.normalize_for_route(provider, base_url, wire_model)
423 .as_setting_for_provider(provider)
424 }
425
426 /// Cycle through the three behaviorally distinct tiers.
427 #[must_use]
428 pub fn cycle_next(self) -> Self {
429 match self {
430 Self::Off => Self::High,
431 Self::Auto => Self::Off,
432 Self::Minimal | Self::Low | Self::Medium | Self::High | Self::XHigh | Self::Ultra => {
433 Self::Max
434 }
435 Self::Max => Self::Off,
436 }
437 }
438
439 /// Advance through an exact-route effort list.
440 ///
441 /// A value that is literally on the ladder advances one rung and wraps.
442 /// An off-ladder value enters at [`Self::nearest_in`]'s rung and advances
443 /// from there, so the cycler only ever moves forward: a persisted `max` on
444 /// an `xhigh` ladder, `ultra` on a `max` ladder, or `minimal` on a ladder
445 /// starting at `off` still walks upward instead of reversing.
446 #[must_use]
447 pub fn cycle_next_in(self, efforts: &[Self]) -> Self {
448 if efforts.is_empty() {
449 return self.cycle_next();
450 }
451 if let Some(index) = self.index_in(efforts) {
452 return efforts[(index + 1) % efforts.len()];
453 }
454 let Some(entry) = self.nearest_in(efforts) else {
455 return self.cycle_next();
456 };
457 let index = entry.index_in(efforts).unwrap_or(0);
458 efforts[(index + 1) % efforts.len()]
459 }
460
461 /// Ladder rank of a concrete tier.
462 ///
463 /// `Auto` is the unresolved sentinel: it names no rung, so it has no rank
464 /// and is never a projection target.
465 const fn rung_rank(self) -> Option<u8> {
466 Some(match self {
467 Self::Off => 0,
468 Self::Minimal => 1,
469 Self::Low => 2,
470 Self::Medium => 3,
471 Self::High => 4,
472 Self::XHigh => 5,
473 Self::Ultra => 6,
474 Self::Max => 7,
475 Self::Auto => return None,
476 })
477 }
478
479 /// Project an effort onto the rungs a route actually offers.
480 ///
481 /// One deterministic rule replaces the hand-written alias buckets: the
482 /// highest allowed rung at or below the current one, or the lowest allowed
483 /// rung when the current value sits below every rung or names no rung at
484 /// all (`Auto`). `None` only when the ladder publishes no rung.
485 #[must_use]
486 pub fn nearest_in(self, efforts: &[Self]) -> Option<Self> {
487 let own_rank = self.rung_rank();
488 let mut nearest_below: Option<Self> = None;
489 let mut lowest: Option<Self> = None;
490 for effort in efforts.iter().copied() {
491 let Some(rank) = effort.rung_rank() else {
492 continue;
493 };
494 if lowest.is_none_or(|current: Self| rank < current.rung_rank().unwrap_or(u8::MAX)) {
495 lowest = Some(effort);
496 }
497 if own_rank.is_some_and(|own| rank <= own)
498 && nearest_below.is_none_or(|current: Self| rank > current.rung_rank().unwrap_or(0))
499 {
500 nearest_below = Some(effort);
501 }
502 }
503 nearest_below.or(lowest)
504 }
505
506 /// Position of a literally listed value on the ladder. Values that are not
507 /// listed are handled by [`Self::nearest_in`], never by an alias table.
508 fn index_in(self, efforts: &[Self]) -> Option<usize> {
509 efforts.iter().position(|&effort| effort == self)
510 }
511 }
512
513 #[cfg(test)]
514 mod tests {
515 use super::ReasoningEffort;
516
517 const CANONICAL: [ReasoningEffort; 9] = [
518 ReasoningEffort::Auto,
519 ReasoningEffort::Off,
520 ReasoningEffort::Minimal,
521 ReasoningEffort::Low,
522 ReasoningEffort::Medium,
523 ReasoningEffort::High,
524 ReasoningEffort::XHigh,
525 ReasoningEffort::Ultra,
526 ReasoningEffort::Max,
527 ];
528
529 #[test]
530 fn reasoning_effort_parse_strict_round_trips_every_canonical_spelling() {
531 for effort in CANONICAL {
532 assert_eq!(
533 ReasoningEffort::parse_strict(effort.as_setting()),
534 Ok(effort),
535 "{} must parse back to itself",
536 effort.as_setting()
537 );
538 assert_eq!(
539 ReasoningEffort::from_setting(effort.as_setting()),
540 effort,
541 "a persisted {} must load as itself",
542 effort.as_setting()
543 );
544 }
545 assert_eq!(
546 ReasoningEffort::parse_strict("minimal"),
547 Ok(ReasoningEffort::Minimal)
548 );
549 assert_eq!(
550 ReasoningEffort::parse_strict("minimum"),
551 Ok(ReasoningEffort::Low)
552 );
553 assert_eq!(
554 ReasoningEffort::parse_strict("light"),
555 Ok(ReasoningEffort::Low)
556 );
557 }
558
559 #[test]
560 fn reasoning_effort_parse_strict_error_lists_the_complete_vocabulary() {
561 let error = ReasoningEffort::parse_strict("xhihg").expect_err("unknown value must fail");
562 for spelling in [
563 "auto", "off", "minimal", "low", "medium", "high", "xhigh", "ultra", "max",
564 ] {
565 assert!(
566 error.contains(spelling),
567 "the error must name {spelling}, got {error:?}"
568 );
569 }
570 }
571
572 #[test]
573 fn reasoning_effort_nearest_in_projects_one_value_per_direction() {
574 use ReasoningEffort::*;
575 let full = [Off, Minimal, Low, Medium, High, XHigh, Ultra, Max];
576 // Literal members project onto themselves.
577 for effort in full {
578 assert_eq!(effort.nearest_in(&full), Some(effort));
579 }
580 // A capped ladder: the highest rung at or below wins, never a rung above.
581 let capped = [Off, Low, Medium, High, XHigh];
582 assert_eq!(Max.nearest_in(&capped), Some(XHigh));
583 assert_eq!(Ultra.nearest_in(&capped), Some(XHigh));
584 assert_eq!(Minimal.nearest_in(&capped), Some(Off));
585 // Below every rung: the lowest allowed rung is the entry point.
586 assert_eq!(Off.nearest_in(&[Low, Medium, High, Max]), Some(Low));
587 // `Auto` names no rung, so it enters at the lowest allowed rung.
588 assert_eq!(Auto.nearest_in(&[Low, Medium, High, Max]), Some(Low));
589 assert_eq!(Auto.nearest_in(&[Off, Low, High, Max]), Some(Off));
590 // A ladder that publishes no rung has nothing to project onto.
591 assert_eq!(Medium.nearest_in(&[Auto]), None);
592 assert_eq!(Medium.nearest_in(&[]), None);
593 }
594
595 #[test]
596 fn reasoning_effort_cycle_next_in_only_moves_forward_on_any_ladder() {
597 use ReasoningEffort::*;
598 // Off-ladder values enter at their nearest rung and advance one rung.
599 let capped = [Off, Low, Medium, High, XHigh];
600 assert_eq!(
601 Max.cycle_next_in(&capped),
602 Off,
603 "xhigh-capped ladder wraps forward"
604 );
605 assert_eq!(Ultra.cycle_next_in(&capped), Off);
606 assert_eq!(Minimal.cycle_next_in(&capped), Low);
607 // A value below every rung enters at the lowest rung.
608 assert_eq!(Off.cycle_next_in(&[Low, Medium, High, Max]), Medium);
609 // `Auto` enters at the ladder's lowest rung.
610 assert_eq!(Auto.cycle_next_in(&[Off, Low, High, Max]), Low);
611 // Literal members advance and wrap without an alias table.
612 assert_eq!(High.cycle_next_in(&capped), XHigh);
613 assert_eq!(XHigh.cycle_next_in(&capped), Off);
614 // An empty ladder falls back to the provider-neutral cycle.
615 assert_eq!(Off.cycle_next_in(&[]), Off.cycle_next());
616 }
617 }
618
618 lines RUST