Skip to main content

locode_provider/
effort.rs

1//! The user-facing effort ladder: **our** five tiers, and how they reach a wire.
2//!
3//! Effort naming is not portable. Anthropic exposes `low`/`medium`/`high`/
4//! `xhigh`/`max`; other vendors ship fewer tiers, different names, or none — and
5//! a tier that exists on one model can be rejected by the next. Exposing the
6//! provider's vocabulary directly would make `/effort` mean different things on
7//! different days and break the moment a model is switched.
8//!
9//! So the ladder below is **ours**. It is what the CLI flag, the settings key,
10//! and `/effort` speak; each wire maps it onto whatever that API accepts. The
11//! five rungs deliberately mirror Anthropic's, because Anthropic is what we run
12//! today and a 1:1 mapping is the honest starting point — but the indirection is
13//! the point: a wire with three tiers collapses rungs in *its* mapping rather
14//! than forcing a different menu on the user.
15//!
16//! The rungs were confirmed against the live API rather than read off the
17//! vendored Claude Code snapshot, which predates Fable 5 and lists only
18//! `low|medium|high|max`. Probing `claude-fable-5`, `claude-opus-5` and
19//! `claude-sonnet-5`: `low`, `medium`, `high`, `xhigh`, `max` are all accepted;
20//! `ultra`, `ultrathink` and `extreme` are 400s. **`max` is the top rung.**
21//!
22//! "ultrathink" is not a rung and is deliberately absent: in Claude Code it is a
23//! keyword matched in the *prompt* (`utils/thinking.ts:45`) that bumps effort for
24//! that turn (`utils/effort.ts:321`) — a per-message nudge layered over the
25//! setting, not a level of it.
26
27use crate::request::ReasoningEffort;
28
29/// One rung of the locode effort ladder.
30///
31/// Ordered shallow → deep. This is a closed set on purpose: an arbitrary
32/// vendor string still reaches a wire through
33/// [`ReasoningEffort::Other`](crate::ReasoningEffort::Other), but the *menu* is
34/// a fixed ladder so the same word means the same intent everywhere.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36pub enum Effort {
37    /// Short, scoped work; latency over depth.
38    Low,
39    /// The balanced middle.
40    Medium,
41    /// The default for intelligence-sensitive work.
42    High,
43    /// Best for coding and agentic runs.
44    XHigh,
45    /// Correctness over cost; can overthink simple tasks.
46    Max,
47}
48
49impl Effort {
50    /// Every rung, shallow → deep (the menu order).
51    pub const ALL: [Effort; 5] = [
52        Effort::Low,
53        Effort::Medium,
54        Effort::High,
55        Effort::XHigh,
56        Effort::Max,
57    ];
58
59    /// The canonical locode name (what the flag, settings, and `/effort` take).
60    #[must_use]
61    pub fn as_str(self) -> &'static str {
62        match self {
63            Effort::Low => "low",
64            Effort::Medium => "medium",
65            Effort::High => "high",
66            Effort::XHigh => "xhigh",
67            Effort::Max => "max",
68        }
69    }
70
71    /// Parse a locode name, case-insensitively. `None` for anything else.
72    #[must_use]
73    pub fn parse(name: &str) -> Option<Effort> {
74        let name = name.trim().to_ascii_lowercase();
75        Effort::ALL.into_iter().find(|e| e.as_str() == name)
76    }
77
78    /// What this rung means, for the menu's second column.
79    #[must_use]
80    pub fn hint(self) -> &'static str {
81        match self {
82            Effort::Low => "fastest; short, scoped tasks",
83            Effort::Medium => "balanced",
84            Effort::High => "the API default",
85            Effort::XHigh => "best for coding and agentic work",
86            Effort::Max => "deepest; correctness over cost",
87        }
88    }
89
90    /// The tier this rung becomes on `api_schema`, for the menu's mapping
91    /// column.
92    ///
93    /// Anthropic is 1:1 today, which is why the rungs are named as they are.
94    /// The function exists so that stops being an assumption the moment a wire
95    /// with a different ladder is added — the menu will show the collapse
96    /// instead of hiding it.
97    #[must_use]
98    pub fn maps_to(self, api_schema: &str) -> &'static str {
99        match api_schema {
100            "anthropic" | "openai-responses" => self.as_str(),
101            _ => "—",
102        }
103    }
104}
105
106impl From<Effort> for ReasoningEffort {
107    fn from(effort: Effort) -> Self {
108        match effort {
109            Effort::Low => ReasoningEffort::Low,
110            Effort::Medium => ReasoningEffort::Medium,
111            Effort::High => ReasoningEffort::High,
112            Effort::XHigh => ReasoningEffort::XHigh,
113            Effort::Max => ReasoningEffort::Max,
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn names_round_trip_case_insensitively() {
124        for effort in Effort::ALL {
125            assert_eq!(Effort::parse(effort.as_str()), Some(effort));
126            assert_eq!(Effort::parse(&effort.as_str().to_uppercase()), Some(effort));
127            assert_eq!(
128                Effort::parse(&format!("  {}  ", effort.as_str())),
129                Some(effort)
130            );
131        }
132        assert_eq!(Effort::parse("ultra"), None, "the ladder is closed");
133        assert_eq!(Effort::parse(""), None);
134    }
135
136    #[test]
137    fn the_ladder_is_ordered_shallow_to_deep() {
138        assert!(Effort::Low < Effort::Medium);
139        assert!(Effort::High < Effort::XHigh);
140        assert!(Effort::XHigh < Effort::Max);
141    }
142
143    /// Anthropic is 1:1 today — the indirection exists for the wire that is not.
144    #[test]
145    fn every_rung_has_a_distinct_neutral_tier() {
146        let tiers: Vec<ReasoningEffort> = Effort::ALL.into_iter().map(Into::into).collect();
147        for (i, a) in tiers.iter().enumerate() {
148            for b in &tiers[i + 1..] {
149                assert_ne!(a, b, "two rungs collapsed onto the same neutral tier");
150            }
151        }
152    }
153
154    #[test]
155    fn anthropic_mapping_is_identity_and_unknown_wires_say_so() {
156        for effort in Effort::ALL {
157            assert_eq!(effort.maps_to("anthropic"), effort.as_str());
158        }
159        assert_eq!(Effort::High.maps_to("mock"), "—");
160    }
161}