Accept write alias during forced edit streams

ober

16ac3b58ce6d034cc4df0bc432f3ed767f9cde42

diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index a37fc03..2a9983d 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -1038,6 +1038,13 @@
     expected
     "."))
 
+(def (forced-tool-name-accepted? expected actual)
+  (or (string=? actual expected)
+      (and (string=? expected "edit")
+           (string=? actual "write"))
+      (and (string=? expected "write")
+           (string=? actual "edit"))))
+
 (def (openai-max-tokens provider)
   (let* ((configured
            (or (positive-int-value (getenv "JCODE_MAX_TOKENS"))
@@ -2537,7 +2544,8 @@
                                      (when name
                                        (when (and forced-tool-name
                                                   (string? name)
-                                                  (not (string=? name forced-tool-name)))
+                                                  (not (forced-tool-name-accepted?
+                                                         forced-tool-name name)))
                                          (set-box! forced-tool-mismatch-box
                                                    (cons forced-tool-name
                                                          (string-append
diff --git a/test/run.ss b/test/run.ss
index 7b7aa6a..e8c8f55 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -13146,6 +13146,48 @@
 
 (let* ([sse-body
          (string-append
+           "data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_write_alias\",\"type\":\"function\",\"function\":{\"name\":\"write\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\n"
+           "data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"path\\\":\\\"tetris.ss\\\",\\\"content\\\":\\\"alias write ok\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n\n"
+           "data: [DONE]\n\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)]
+       [edit-tool (uuid-recovery-tool "edit" (args "path" (args "type" "string")) '("path"))]
+       [write-tool (uuid-recovery-tool "write" (args "path" (args "type" "string")) '("path"))]
+       [old-max-tokens (getenv "JCODE_MAX_TOKENS")])
+  (dynamic-wind
+    (lambda () (putenv "JCODE_MAX_TOKENS" ""))
+    (lambda ()
+      (serve-one-captured-sse! srv captured sse-body)
+      (let* ([p (make-provider "mlx" "" "deepseek-v4-pro" base-url)]
+             [result (call-with-values
+                       (lambda ()
+                         (parameterize ([current-tool-choice-override
+                                         (openai-function-tool-choice "edit")])
+                           (provider-stream-chat
+                             p
+                             (list (make-user-message "hi"))
+                             (list edit-tool write-tool)
+                             (lambda (_token) #f))))
+                       (lambda (content tcs usage) (list content tcs usage)))]
+             [req (vector-ref captured 0)]
+             [tcs (list-ref result 1)])
+        (check! "mlx forced streaming edit accepts write alias content"
+                (car result) "")
+        (check! "mlx forced streaming edit accepts write alias call count"
+                (length tcs) 1)
+        (check! "mlx forced streaming edit accepts write alias tool"
+                (tool-call-name (car tcs)) "write")
+        (check! "mlx forced streaming edit alias request required edit"
+                (and req
+                     (str-contains? req "\"tool_choice\"")
+                     (str-contains? req "\"name\":\"edit\"")) #t)))
+    (lambda ()
+      (putenv "JCODE_MAX_TOKENS" (or old-max-tokens ""))
+      (tcp-close srv))))
+
+(let* ([sse-body
+         (string-append
            "data: {\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"I should check balance, but writing instead.\"},\"finish_reason\":null}]}\n\n"
            "data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_bad\",\"type\":\"function\",\"function\":{\"name\":\"write\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\n"
            "data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"path\\\":\\\"tetris.ss\\\",\\\"content\\\":\\\"large ignored write\\\"}\"}}]},\"finish_reason\":null}]}\n\n")]