1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Qwen (DashScope / Model Studio) thinking-knob resolution, per family.
//!
//! DashScope qwen models expose two different thinking controls and each
//! family reads only one of them:
//!
//! - **qwen3.8-max** — the tiered `reasoning_effort` ladder. Thinking is
//! always on; `enable_thinking` is inert here and shipping it alongside a
//! tier is a second competing knob.
//! - **older qwen hybrids** (qwen3.6-*, qwen3.7-*, qwen3-*, qwen-*) — the
//! on/off `enable_thinking` switch only. They treat `reasoning_effort` as
//! an opaque sampling override, so a configured value still passes through
//! (that pass-through is #691 and must not regress).
//!
//! Sending the wrong knob is not a no-op: the inert field is ignored while the
//! knob the model actually reads goes unset, so thinking silently runs at the
//! server default instead of what the user configured.
//!
//! Separately, every remote qwen request carries `preserve_thinking: true` so
//! reasoning carries across turns instead of being re-derived from scratch
//! each time. See [`preserve_thinking_for`].
//!
//! None of this is Alibaba-specific. The same models are served by ModelScope,
//! NVIDIA NIM, OpenRouter and other OpenAI-compatible gateways, often under a
//! namespaced id like `Vendor/Qwen3.8-Max`, so both the host check and the
//! model check are written to reach those too (#1040).
//!
//! Mirrors `DashScopeOpenAICompatibleProvider.buildQwenEffortConfig` /
//! `dropConflictingThinkingKnobs` in qwen-code. Same shape as
//! [`super::kimi_reasoning`], which solves this per-family problem for the
//! Kimi models.
/// A qwen family, distinguished by which thinking knob it reads.
pub
/// Model-id prefix for the tiered-effort family. Matches qwen-code's
/// `isTieredEffortWireModel`, which is likewise a plain prefix test so
/// dated and `-preview` suffixes are covered without enumerating them.
const TIERED_EFFORT_PREFIX: &str = "qwen3.8-max";
/// The canonical disable for the tiered family. `enable_thinking = false` is
/// the documented escape hatch, and on a model that reads only
/// `reasoning_effort` it has to be translated rather than dropped, or the
/// user's explicit "off" would silently become "server default".
const EFFORT_DISABLED: &str = "none";
/// Effort tier applied to the tiered family when nothing is configured. The
/// top of the ladder is the vendor's recommended setting for this family, so
/// an unconfigured install gets it rather than whatever the endpoint happens
/// to fall back to. An explicit `reasoning_effort` in config still wins.
const DEFAULT_TIERED_EFFORT: &str = "xhigh";
/// Classify a model id, or `None` when it is not a qwen model at all.
///
/// Classified on the vendor-stripped, lowercased id (see
/// [`super::qwen::bare_model_id`]). Matching the raw id meant a namespaced
/// tiered-effort model failed the tiered prefix, fell through to the `qwen`
/// arm, and was silently classified as a hybrid: it received the switch it
/// does not read while the effort tier went unset. Silent misclassification
/// rather than a clean miss, since the vendor segment often begins with
/// `qwen` too (#1040).
pub
/// The thinking fields to put on an outgoing request body. Each is `None`
/// when that field must not be sent at all.
pub
/// Resolve the configured thinking settings into the exact fields the active
/// model reads, dropping the one it does not.
///
/// `configured_effort` is the provider's `reasoning_effort` setting and
/// `configured_enable_thinking` its `enable_thinking` setting; either may be
/// unset.
///
/// - **Tiered family** — the effort tier ships alone. A co-present
/// `enable_thinking` is dropped, except that an explicit `false` becomes
/// `reasoning_effort = "none"`. With nothing configured the tier defaults
/// to [`DEFAULT_TIERED_EFFORT`] rather than being omitted, so an
/// unconfigured install still gets the vendor's recommended setting.
/// - **Hybrid family** — `enable_thinking` ships, defaulting to on (every
/// Model Studio catalogue entry for these declares thinking enabled). A
/// configured effort still passes through untouched: the model does not
/// read it, so it is an opaque override rather than a competing knob.
/// - **Not a qwen model** — nothing; the caller's existing fields stand.
pub
/// Whether this request should carry `preserve_thinking: true`.
///
/// True for a qwen model on any remote host, not just Alibaba's. Reasoning
/// models need the flag for multi-turn reasoning continuity: without it the
/// model re-derives its reasoning every turn instead of carrying it forward,
/// which shows up as drifting and invented intermediate steps on long
/// sessions.
///
/// A locally served qwen GGUF (llama.cpp, LM Studio, Ollama) is excluded:
/// those take thinking through `chat_template_kwargs`, so this would be a
/// second, inert mechanism there.
pub