Downgrade forced tool choice for OpenRouter Qwen

ober

c9c43f1f297082e1b09ae1f6fb5606c48e138fab

diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index 2325737..83b1f37 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -1566,6 +1566,20 @@
                (openai-extended-cache-model? (provider-model provider)))
       (hash-put! body "prompt_cache_retention" "24h"))))
 
+(def (openrouter-qwen-model? provider)
+  (and (equal? (provider-name provider) "openrouter")
+       (string? (provider-model provider))
+       (or (string-prefix? "qwen/" (provider-model provider))
+           (string-contains (provider-model provider) "/qwen"))))
+
+(def (openai-tool-choice provider)
+  ;; Alibaba-backed Qwen on OpenRouter rejects object/required tool_choice in
+  ;; thinking mode. Keep tools advertised, but let the model choose tools.
+  (let ((override (current-tool-choice-override)))
+    (if (and override (openrouter-qwen-model? provider))
+      "auto"
+      (or override "auto"))))
+
 (def (openai-cached-tokens usage)
   ;; OpenAI-compat servers (openai, openrouter, vLLM, llama-server) report
   ;; prefix-cache hits in prompt_tokens_details.cached_tokens. Absent on
@@ -1588,8 +1602,7 @@
     (when (and tools (not (null? tools))
                (not (model-rejects-tools? (provider-model provider))))
       (hash-put! body "tools" tools)
-      (hash-put! body "tool_choice"
-                 (or (current-tool-choice-override) "auto")))
+      (hash-put! body "tool_choice" (openai-tool-choice provider)))
     body))
 
 (def (moonshot-kimi-k3? provider)
@@ -2331,8 +2344,7 @@
     (when (and tools (not (null? tools))
                (not (model-rejects-tools? (provider-model provider))))
       (hash-put! body "tools" tools)
-      (hash-put! body "tool_choice"
-                 (or (current-tool-choice-override) "auto")))
+      (hash-put! body "tool_choice" (openai-tool-choice provider)))
     body))
 
 (def (openai-stream-chat provider messages tools token-cb)
diff --git a/test/run.ss b/test/run.ss
index 5fdf781..9db5e31 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -12294,6 +12294,38 @@
        [captured (vector #f)]
        [tool (make-hashtable equal-hash equal?)]
        [fn (make-hashtable equal-hash equal?)]
+       [params (make-hashtable equal-hash equal?)])
+  (hashtable-set! params "type" "object")
+  (hashtable-set! fn "name" "edit")
+  (hashtable-set! fn "description" "edit")
+  (hashtable-set! fn "parameters" params)
+  (hashtable-set! tool "type" "function")
+  (hashtable-set! tool "function" fn)
+  (dynamic-wind
+    (lambda () (void))
+    (lambda ()
+      (serve-one-captured-json! srv captured 200 chat-body)
+      (let* ([p (make-provider "openrouter" "unit-key"
+                               "qwen/qwen3.7-plus" base-url)]
+             [_ (parameterize ([current-tool-choice-override
+                                (openai-function-tool-choice "edit")])
+                  (provider-chat p (list (make-user-message "hi")) (list tool)))]
+             [req (vector-ref captured 0)]
+             [req-json (and req (string->json-object req))]
+             [tool-choice (and req-json
+                               (hashtable-ref req-json "tool_choice" #f))])
+        (check! "openrouter qwen forced tool choice downgrades to auto"
+                tool-choice "auto")
+        (check! "openrouter qwen forced tool choice still advertises tools"
+                (and req (str-contains? req "\"tools\"")) #t)))
+    (lambda () (tcp-close srv))))
+
+(let* ([chat-body "{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}\n"]
+       [srv (tcp-listen "127.0.0.1" 0)]
+       [base-url (format "http://127.0.0.1:~a/v1" (tcp-server-port srv))]
+       [captured (vector #f)]
+       [tool (make-hashtable equal-hash equal?)]
+       [fn (make-hashtable equal-hash equal?)]
        [params (make-hashtable equal-hash equal?)]
        [old-max-tokens (getenv "JCODE_MAX_TOKENS")])
   (hashtable-set! params "type" "object")