va_ai_api_bridge/providers/
dashscope.rs1use serde_json::{json, Map, Value};
2
3mod content;
4
5#[derive(Debug, Clone)]
6pub struct DashScopeBridgeAdapter {
7 thinking_enabled: bool,
8}
9
10impl DashScopeBridgeAdapter {
11 pub fn new(thinking_enabled: Option<bool>) -> Self {
12 let thinking_enabled = thinking_enabled.unwrap_or(true);
13 Self { thinking_enabled }
14 }
15
16 pub fn prepare_chat_request(&mut self, original_request: &Value, chat_request: &mut Value) {
17 content::convert_text_file_parts_to_text(chat_request);
18
19 let Some(object) = chat_request.as_object_mut() else {
20 return;
21 };
22
23 object.remove("reasoning");
24 object.remove("reasoning_effort");
25 object.remove("reasoningEffort");
26 let forced_tool_choice = normalize_tool_choice_for_dashscope(object);
27
28 let Some(model) = object.get("model").and_then(Value::as_str) else {
29 return;
30 };
31 if !model_uses_dashscope_enable_thinking(model) {
32 return;
33 }
34
35 let enabled = if forced_tool_choice {
36 false
37 } else {
38 thinking_from_original_request(original_request).unwrap_or(self.thinking_enabled)
39 };
40 object.insert("enable_thinking".to_string(), Value::Bool(enabled));
41 }
42}
43
44fn normalize_tool_choice_for_dashscope(object: &mut Map<String, Value>) -> bool {
45 let Some(tool_choice) = object.get("tool_choice") else {
46 return false;
47 };
48
49 if is_specific_function_tool_choice(tool_choice) {
50 return true;
51 }
52
53 if tool_choice.as_str() != Some("required") {
54 return false;
55 }
56
57 if let Some(name) = single_function_tool_name(object.get("tools")) {
58 object.insert(
59 "tool_choice".to_string(),
60 json!({
61 "type": "function",
62 "function": { "name": name }
63 }),
64 );
65 return true;
66 }
67
68 object.insert("tool_choice".to_string(), Value::String("auto".to_string()));
69 false
70}
71
72fn is_specific_function_tool_choice(value: &Value) -> bool {
73 let Some(object) = value.as_object() else {
74 return false;
75 };
76 object.get("type").and_then(Value::as_str) == Some("function")
77 && object
78 .get("function")
79 .and_then(Value::as_object)
80 .and_then(|function| function.get("name"))
81 .or_else(|| object.get("name"))
82 .and_then(Value::as_str)
83 .is_some_and(|name| !name.trim().is_empty())
84}
85
86fn single_function_tool_name(tools: Option<&Value>) -> Option<String> {
87 let tools = tools?.as_array()?;
88 let mut names = tools.iter().filter_map(function_tool_name);
89 let name = names.next()?;
90 names.next().is_none().then_some(name)
91}
92
93fn function_tool_name(tool: &Value) -> Option<String> {
94 let object = tool.as_object()?;
95 if object.get("type").and_then(Value::as_str) != Some("function") {
96 return None;
97 }
98 object
99 .get("function")
100 .and_then(Value::as_object)
101 .and_then(|function| function.get("name"))
102 .and_then(Value::as_str)
103 .map(str::trim)
104 .filter(|name| !name.is_empty())
105 .map(ToOwned::to_owned)
106}
107
108fn model_uses_dashscope_enable_thinking(model: &str) -> bool {
109 let model = model.trim().to_ascii_lowercase();
110 model.starts_with("qwen3.5")
111 || model.starts_with("qwen3.6")
112 || model.starts_with("qwen3-max")
113 || model.starts_with("deepseek-v4")
114 || matches!(
115 model.as_str(),
116 "glm-5.1"
117 | "glm-5"
118 | "glm-4.7"
119 | "kimi-k2.6"
120 | "kimi-k2.5"
121 | "minimax-m2.5"
122 | "minimax-m2.5-highspeed"
123 )
124}
125
126fn thinking_from_original_request(request: &Value) -> Option<bool> {
127 request
128 .get("reasoning")
129 .and_then(|reasoning| {
130 reasoning
131 .get("effort")
132 .or_else(|| reasoning.get("reasoning_effort"))
133 .or_else(|| reasoning.get("reasoningEffort"))
134 .and_then(Value::as_str)
135 })
136 .or_else(|| request.get("reasoning_effort").and_then(Value::as_str))
137 .or_else(|| request.get("reasoningEffort").and_then(Value::as_str))
138 .map(reasoning_effort_enabled)
139}
140
141fn reasoning_effort_enabled(value: &str) -> bool {
142 !matches!(
143 value.trim().to_ascii_lowercase().as_str(),
144 "off" | "none" | "disabled" | "disable" | "false"
145 )
146}
147
148#[cfg(test)]
149mod tests {
150 use serde_json::json;
151
152 use super::*;
153
154 #[test]
155 fn maps_reasoning_effort_to_dashscope_enable_thinking_for_reasoning_models() {
156 let mut adapter = DashScopeBridgeAdapter::new(None);
157 let mut chat_request = json!({ "model": "qwen3.5-plus", "messages": [] });
158
159 adapter.prepare_chat_request(
160 &json!({ "reasoning": { "effort": "none" } }),
161 &mut chat_request,
162 );
163
164 assert_eq!(chat_request["enable_thinking"], false);
165 }
166
167 #[test]
168 fn leaves_non_reasoning_qwen_models_without_enable_thinking() {
169 let mut adapter = DashScopeBridgeAdapter::new(None);
170 let mut chat_request = json!({ "model": "qwen3-coder-plus", "messages": [] });
171
172 adapter.prepare_chat_request(
173 &json!({ "reasoning": { "effort": "high" } }),
174 &mut chat_request,
175 );
176
177 assert!(chat_request.get("enable_thinking").is_none());
178 }
179
180 #[test]
181 fn maps_reasoning_effort_to_dashscope_partner_reasoning_models() {
182 let mut adapter = DashScopeBridgeAdapter::new(None);
183 let mut chat_request = json!({ "model": "glm-5", "messages": [] });
184
185 adapter.prepare_chat_request(
186 &json!({ "reasoning": { "effort": "high" } }),
187 &mut chat_request,
188 );
189
190 assert_eq!(chat_request["enable_thinking"], true);
191 }
192
193 #[test]
194 fn disables_thinking_for_specific_tool_choice() {
195 let mut adapter = DashScopeBridgeAdapter::new(None);
196 let mut chat_request = json!({
197 "model": "qwen3.6-plus",
198 "messages": [],
199 "tools": [{
200 "type": "function",
201 "function": { "name": "lookup" }
202 }],
203 "tool_choice": {
204 "type": "function",
205 "function": { "name": "lookup" }
206 }
207 });
208
209 adapter.prepare_chat_request(&json!({}), &mut chat_request);
210
211 assert_eq!(chat_request["enable_thinking"], false);
212 assert_eq!(chat_request["tool_choice"]["function"]["name"], "lookup");
213 }
214
215 #[test]
216 fn rewrites_required_tool_choice_to_single_function() {
217 let mut adapter = DashScopeBridgeAdapter::new(None);
218 let mut chat_request = json!({
219 "model": "qwen3.6-plus",
220 "messages": [],
221 "tools": [{
222 "type": "function",
223 "function": { "name": "lookup" }
224 }],
225 "tool_choice": "required"
226 });
227
228 adapter.prepare_chat_request(&json!({}), &mut chat_request);
229
230 assert_eq!(chat_request["enable_thinking"], false);
231 assert_eq!(
232 chat_request["tool_choice"],
233 json!({
234 "type": "function",
235 "function": { "name": "lookup" }
236 })
237 );
238 }
239
240 #[test]
241 fn rewrites_ambiguous_required_tool_choice_to_auto() {
242 let mut adapter = DashScopeBridgeAdapter::new(None);
243 let mut chat_request = json!({
244 "model": "qwen3.6-plus",
245 "messages": [],
246 "tools": [
247 { "type": "function", "function": { "name": "lookup" } },
248 { "type": "function", "function": { "name": "search" } }
249 ],
250 "tool_choice": "required"
251 });
252
253 adapter.prepare_chat_request(&json!({}), &mut chat_request);
254
255 assert_eq!(chat_request["enable_thinking"], true);
256 assert_eq!(chat_request["tool_choice"], "auto");
257 }
258}