Skip to main content

va_ai_api_bridge/providers/
dashscope.rs

1use 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}