返回 CodeWhale
deepseek_effort.rs
根目录 / crates / tui / src / client / deepseek_effort.rs
1 //! The single source of truth for DeepSeek's reasoning-effort mapping.
2 //!
3 //! DeepSeek exposes the same four thinking tiers on both wires it speaks to
4 //! us, but spells them differently, and until #5055 each spelling lived inline
5 //! at its call site — so a documented mapping change meant code archaeology
6 //! across two files instead of one edit here.
7 //!
8 //! Source: <https://api-docs.deepseek.com/api/create-chat-completion> for the
9 //! Chat Completions `reasoning_effort` values, and DeepSeek's Thinking Mode
10 //! guide <https://api-docs.deepseek.com/guides/reasoning_model> for the
11 //! requested-to-actual mapping (verified 2026-09-17).
12 //!
13 //! What the wires document, and what this table encodes:
14 //!
15 //! | Tier | Chat Completions | Responses API |
16 //! |------|-----------------------------|--------------------|
17 //! | Off | `thinking: {type: disabled}` | `effort: "none"` |
18 //! | Low | `reasoning_effort: "low"` | `effort: "low"` |
19 //! | High | `reasoning_effort: "high"` | `effort: "high"` |
20 //! | Max | `reasoning_effort: "max"` | `effort: "max"` |
21 //!
22 //! DeepSeek's Thinking Mode guide documents the requested-to-actual effort
23 //! mapping; thinking is enabled by default with an actual effort of `high`:
24 //!
25 //! | Requested | Actual |
26 //! |-----------|--------|
27 //! | minimal | low |
28 //! | low | low |
29 //! | medium | high |
30 //! | high | high |
31 //! | xhigh | high |
32 //! | max | max |
33 //! | ultra | max |
34 //!
35 //! `medium`, `xhigh`, and `ultra` are client-side requests that collapse onto
36 //! the documented wire tiers — they are not new wire values. When DeepSeek
37 //! changes the mapping, edit [`DeepseekEffortTier`]'s two wire accessors and
38 //! [`DEEPSEEK_EFFORT_ALIASES`] below, bump the verified date in this comment,
39 //! and both call sites move together — that is the whole point of this module.
40
41 /// A documented DeepSeek thinking tier, independent of which wire carries it.
42 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43 pub(super) enum DeepseekEffortTier {
44 /// Thinking disabled.
45 Off,
46 /// The cheaper real reasoning tier.
47 Low,
48 /// The default reasoning tier; also where an undocumented `medium` lands.
49 High,
50 /// The deepest documented tier.
51 Max,
52 }
53
54 impl DeepseekEffortTier {
55 /// Chat Completions `reasoning_effort` value, or `None` for the off tier —
56 /// which that wire expresses with `thinking: {"type": "disabled"}` rather
57 /// than an effort string.
58 pub(super) fn chat_reasoning_effort(self) -> Option<&'static str> {
59 match self {
60 Self::Off => None,
61 Self::Low => Some("low"),
62 Self::High => Some("high"),
63 Self::Max => Some("max"),
64 }
65 }
66
67 /// Whether this tier enables thinking on the Chat Completions wire.
68 pub(super) fn chat_thinking_enabled(self) -> bool {
69 !matches!(self, Self::Off)
70 }
71
72 /// Responses API `reasoning.effort` value. Every tier has a documented
73 /// label here, including off (`"none"`), so the picker's Off entry stays
74 /// off instead of collapsing into a still-thinking low.
75 pub(super) fn responses_effort(self) -> &'static str {
76 match self {
77 Self::Off => "none",
78 Self::Low => "low",
79 Self::High => "high",
80 Self::Max => "max",
81 }
82 }
83 }
84
85 /// Every CodeWhale-side effort spelling that maps onto a documented DeepSeek
86 /// tier. The empty string is CodeWhale's "unset" effort, which means the
87 /// default tier.
88 ///
89 /// Anything absent from this table is not a DeepSeek tier request: the
90 /// Responses wire normalizes it to the default tier (it must send *some*
91 /// documented label), while the Chat wire writes nothing at all rather than
92 /// guessing a field the user did not ask for.
93 pub(super) const DEEPSEEK_EFFORT_ALIASES: &[(&str, DeepseekEffortTier)] = &[
94 ("off", DeepseekEffortTier::Off),
95 ("disabled", DeepseekEffortTier::Off),
96 ("none", DeepseekEffortTier::Off),
97 ("false", DeepseekEffortTier::Off),
98 ("minimal", DeepseekEffortTier::Low),
99 ("low", DeepseekEffortTier::Low),
100 ("medium", DeepseekEffortTier::High),
101 ("mid", DeepseekEffortTier::High),
102 ("high", DeepseekEffortTier::High),
103 ("", DeepseekEffortTier::High),
104 ("xhigh", DeepseekEffortTier::High),
105 ("max", DeepseekEffortTier::Max),
106 ("maximum", DeepseekEffortTier::Max),
107 ("highest", DeepseekEffortTier::Max),
108 ("ultra", DeepseekEffortTier::Max),
109 ("ultracode", DeepseekEffortTier::Max),
110 ];
111
112 /// The tier CodeWhale falls back to when a caller asks for something this
113 /// table does not name (`auto`, a future provider label, a typo).
114 pub(super) const DEEPSEEK_DEFAULT_EFFORT_TIER: DeepseekEffortTier = DeepseekEffortTier::High;
115
116 /// Look up a raw effort string. Case- and whitespace-insensitive.
117 pub(super) fn deepseek_effort_tier(raw: &str) -> Option<DeepseekEffortTier> {
118 let normalized = raw.trim().to_ascii_lowercase();
119 DEEPSEEK_EFFORT_ALIASES
120 .iter()
121 .find(|(alias, _)| *alias == normalized)
122 .map(|(_, tier)| *tier)
123 }
124
125 /// Look up a raw effort string, falling back to
126 /// [`DEEPSEEK_DEFAULT_EFFORT_TIER`] for unknown spellings.
127 pub(super) fn deepseek_effort_tier_or_default(raw: &str) -> DeepseekEffortTier {
128 deepseek_effort_tier(raw).unwrap_or(DEEPSEEK_DEFAULT_EFFORT_TIER)
129 }
130
131 #[cfg(test)]
132 mod tests {
133 use super::*;
134
135 /// DeepSeek's documented requested-to-actual mapping (Thinking Mode
136 /// guide, verified 2026-09-17). Pin every row so a future edit to the
137 /// table cannot silently disagree with the vendor.
138 #[test]
139 fn vendor_requested_to_actual_mapping_is_pinned() {
140 for (requested, tier) in [
141 ("minimal", DeepseekEffortTier::Low),
142 ("low", DeepseekEffortTier::Low),
143 ("medium", DeepseekEffortTier::High),
144 ("high", DeepseekEffortTier::High),
145 ("xhigh", DeepseekEffortTier::High),
146 ("max", DeepseekEffortTier::Max),
147 ("ultra", DeepseekEffortTier::Max),
148 ] {
149 assert_eq!(deepseek_effort_tier(requested), Some(tier), "{requested}");
150 }
151 }
152
153 #[test]
154 fn aliases_are_unique_and_normalized() {
155 for (index, (alias, _)) in DEEPSEEK_EFFORT_ALIASES.iter().enumerate() {
156 assert_eq!(
157 *alias,
158 alias.trim().to_ascii_lowercase(),
159 "alias {alias:?} must be stored pre-normalized"
160 );
161 assert!(
162 !DEEPSEEK_EFFORT_ALIASES[..index]
163 .iter()
164 .any(|(seen, _)| seen == alias),
165 "duplicate alias {alias:?}"
166 );
167 }
168 }
169
170 #[test]
171 fn lookup_is_case_and_whitespace_insensitive() {
172 assert_eq!(
173 deepseek_effort_tier(" MAX "),
174 Some(DeepseekEffortTier::Max)
175 );
176 assert_eq!(deepseek_effort_tier("auto"), None);
177 assert_eq!(
178 deepseek_effort_tier_or_default("auto"),
179 DeepseekEffortTier::High
180 );
181 }
182
183 #[test]
184 fn ultra_and_legacy_ultracode_alias_resolve_to_max() {
185 assert_eq!(deepseek_effort_tier("ultra"), Some(DeepseekEffortTier::Max));
186 assert_eq!(
187 deepseek_effort_tier("ultracode"),
188 Some(DeepseekEffortTier::Max)
189 );
190 assert_eq!(
191 deepseek_effort_tier_or_default("ultra").responses_effort(),
192 "max"
193 );
194 }
195
196 #[test]
197 fn every_tier_has_both_wire_spellings() {
198 for tier in [
199 DeepseekEffortTier::Off,
200 DeepseekEffortTier::Low,
201 DeepseekEffortTier::High,
202 DeepseekEffortTier::Max,
203 ] {
204 let responses = tier.responses_effort();
205 assert!(!responses.is_empty(), "{tier:?} needs a Responses label");
206 match tier {
207 DeepseekEffortTier::Off => {
208 assert_eq!(tier.chat_reasoning_effort(), None);
209 assert!(!tier.chat_thinking_enabled());
210 }
211 _ => {
212 assert_eq!(tier.chat_reasoning_effort(), Some(responses));
213 assert!(tier.chat_thinking_enabled());
214 }
215 }
216 }
217 }
218 }
219
219 lines RUST