@@ -13,31 +13,50 @@ use crate::dto::openai::{Request, ThinkingConfig, ThinkingType};
1313///
1414/// - If `reasoning.enabled == Some(true)` → `thinking = {"type": "enabled"}`
1515/// - If `reasoning.enabled == Some(false)` → `thinking = {"type": "disabled"}`
16- /// - If `reasoning` is None or `enabled` is None → no `thinking` field added
16+ /// - If `reasoning.enabled == None` and `reasoning.effort ==
17+ /// Some(Effort::None)` → `thinking = {"type": "disabled"}`. The orchestrator
18+ /// preserves this opt-out for z.ai providers so it can be mapped here.
19+ /// - If `reasoning.enabled == None` and `reasoning.effort` is any other value →
20+ /// `thinking = {"type": "enabled"}`
21+ /// - If `reasoning` is None or both `enabled` and `effort` are None → no
22+ /// `thinking` field added
1723/// - Original `reasoning` field is removed after transformation
1824///
1925/// # Note
2026///
21- /// Z.ai only supports enabled/disabled state. Other ReasoningConfig fields
22- /// (`max_tokens`, `effort`, `exclude`) are ignored as they are not supported by
23- /// z.ai's API.
27+ /// Z.ai only supports enabled/disabled state. `effort` is mapped to that
28+ /// on/off state when `enabled` is unset. Other ReasoningConfig fields
29+ /// (`max_tokens`, `exclude`) are ignored as they are not supported by z.ai's
30+ /// API.
2431pub struct SetZaiThinking ;
2532
2633impl Transformer for SetZaiThinking {
2734 type Value = Request ;
2835
2936 fn transform ( & mut self , mut request : Self :: Value ) -> Self :: Value {
30- // Check if reasoning config exists and has enabled field set
31- if let Some ( reasoning) = request. reasoning . take ( )
32- && let Some ( enabled) = reasoning. enabled
33- {
34- request. thinking = Some ( ThinkingConfig {
35- r#type : if enabled {
36- ThinkingType :: Enabled
37- } else {
38- ThinkingType :: Disabled
39- } ,
40- } ) ;
37+ if let Some ( reasoning) = request. reasoning . take ( ) {
38+ // Effort::None is a strong opt-out and wins over `enabled`, matching
39+ // Context::is_reasoning_supported's resolution order in
40+ // forge_domain. Without this, an explicit `:reasoning-effort none`
41+ // followed later by `enabled: true` (e.g. from defaults) would
42+ // re-enable thinking, contradicting the user's most recent intent.
43+ let enabled = if matches ! ( reasoning. effort, Some ( forge_domain:: Effort :: None ) ) {
44+ Some ( false )
45+ } else if reasoning. enabled . is_some ( ) {
46+ reasoning. enabled
47+ } else {
48+ reasoning. effort . map ( |_| true )
49+ } ;
50+
51+ if let Some ( enabled) = enabled {
52+ request. thinking = Some ( ThinkingConfig {
53+ r#type : if enabled {
54+ ThinkingType :: Enabled
55+ } else {
56+ ThinkingType :: Disabled
57+ } ,
58+ } ) ;
59+ }
4160 }
4261
4362 request
@@ -111,6 +130,79 @@ mod tests {
111130 assert_eq ! ( actual. reasoning, None ) ;
112131 }
113132
133+ #[ test]
134+ fn test_effort_none_overrides_enabled_true ( ) {
135+ // Matches Context::is_reasoning_supported precedence: Effort::None wins
136+ // over enabled: true. Otherwise an opt-out at the effort level would
137+ // be silently re-enabled by a stale `enabled: true` from defaults.
138+ let fixture = Request :: default ( ) . reasoning ( forge_domain:: ReasoningConfig {
139+ enabled : Some ( true ) ,
140+ effort : Some ( forge_domain:: Effort :: None ) ,
141+ max_tokens : None ,
142+ exclude : None ,
143+ } ) ;
144+
145+ let mut transformer = SetZaiThinking ;
146+ let actual = transformer. transform ( fixture) ;
147+
148+ let expected_thinking = Some ( ThinkingConfig { r#type : ThinkingType :: Disabled } ) ;
149+ assert_eq ! ( actual. thinking, expected_thinking) ;
150+ assert_eq ! ( actual. reasoning, None ) ;
151+ }
152+
153+ #[ test]
154+ fn test_reasoning_effort_none_converts_to_thinking_disabled ( ) {
155+ let fixture = Request :: default ( ) . reasoning ( forge_domain:: ReasoningConfig {
156+ enabled : None ,
157+ effort : Some ( forge_domain:: Effort :: None ) ,
158+ max_tokens : None ,
159+ exclude : None ,
160+ } ) ;
161+
162+ let mut transformer = SetZaiThinking ;
163+ let actual = transformer. transform ( fixture) ;
164+
165+ let expected_thinking = Some ( ThinkingConfig { r#type : ThinkingType :: Disabled } ) ;
166+ assert_eq ! ( actual. thinking, expected_thinking) ;
167+ assert_eq ! ( actual. reasoning, None ) ;
168+ }
169+
170+ #[ test]
171+ fn test_glm5_effort_none_passthrough_converts_to_thinking_disabled ( ) {
172+ let fixture = Request :: default ( )
173+ . model ( forge_domain:: ModelId :: new ( "glm-5" ) )
174+ . reasoning ( forge_domain:: ReasoningConfig {
175+ enabled : None ,
176+ effort : Some ( forge_domain:: Effort :: None ) ,
177+ max_tokens : None ,
178+ exclude : None ,
179+ } ) ;
180+
181+ let mut transformer = SetZaiThinking ;
182+ let actual = transformer. transform ( fixture) ;
183+
184+ let expected_thinking = Some ( ThinkingConfig { r#type : ThinkingType :: Disabled } ) ;
185+ assert_eq ! ( actual. thinking, expected_thinking) ;
186+ assert_eq ! ( actual. reasoning, None ) ;
187+ }
188+
189+ #[ test]
190+ fn test_reasoning_effort_high_converts_to_thinking_enabled ( ) {
191+ let fixture = Request :: default ( ) . reasoning ( forge_domain:: ReasoningConfig {
192+ enabled : None ,
193+ effort : Some ( forge_domain:: Effort :: High ) ,
194+ max_tokens : None ,
195+ exclude : None ,
196+ } ) ;
197+
198+ let mut transformer = SetZaiThinking ;
199+ let actual = transformer. transform ( fixture) ;
200+
201+ let expected_thinking = Some ( ThinkingConfig { r#type : ThinkingType :: Enabled } ) ;
202+ assert_eq ! ( actual. thinking, expected_thinking) ;
203+ assert_eq ! ( actual. reasoning, None ) ;
204+ }
205+
114206 #[ test]
115207 fn test_reasoning_with_max_tokens_ignores_max_tokens ( ) {
116208 let fixture = Request :: default ( ) . reasoning ( forge_domain:: ReasoningConfig {
0 commit comments