Ollama tool-template 500 fallback; empty content for tool-call messages

ober

55322125aa740be76b27d4d62ff02adf78cfc078

diff --git a/src/jcode/core/message.ss b/src/jcode/core/message.ss
index 7ae86aa..2a84cf3 100644
--- a/src/jcode/core/message.ss
+++ b/src/jcode/core/message.ss
@@ -79,10 +79,13 @@
 (def (message->json msg)
   (let ((ht (make-hash-table)))
     (hash-put! ht "role" (message-role msg))
-    (when (message-content msg)
-      ;; Strip <think>...</think> blocks before sending back to the model —
-      ;; reasoning is for display only, the LLM doesn't need its own thoughts echoed.
-      (hash-put! ht "content" (cdr (extract-thinking (message-content msg)))))
+    ;; Strip <think>...</think> blocks before sending back to the model —
+    ;; reasoning is for display only, the LLM doesn't need its own thoughts echoed.
+    (if (message-content msg)
+      (hash-put! ht "content" (cdr (extract-thinking (message-content msg))))
+      (when (or (string=? (message-role msg) "assistant")
+                (string=? (message-role msg) "tool"))
+        (hash-put! ht "content" "")))
     (when (message-tool-calls msg)
       (hash-put! ht "tool_calls"
         (map tool-call->json (message-tool-calls msg))))
diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index a164a58..5f4dfe3 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -1231,9 +1231,52 @@
 
 ;;; Ollama API (OpenAI-compatible) ;;;
 
+(def (ollama-tools-present? tools)
+  (and tools (not (null? tools))))
+
+(def (ollama-tool-template-error? e)
+  ;; Ollama's OpenAI-compatible endpoint parses tool calls through the
+  ;; model template. Some local Qwen/Hermes models occasionally emit malformed
+  ;; XML such as <function>...</parameter>, and Ollama returns HTTP 500 before
+  ;; jcode can recover the textual call. Retry those specific failures without
+  ;; the structured tools field; the system prompt still lists tools and
+  ;; jcode's text-tool-call recovery can lift text calls back into tool calls.
+  (let ((msg (err->string e)))
+    (and (string-contains msg "API error 500")
+         (or (string-contains msg "unexpected EOF")
+             (string-contains msg "tool_call")
+             (and (string-contains msg "closed by")
+                  (or (string-contains msg "function")
+                      (string-contains msg "parameter")))))))
+
+(def (short-error-string e)
+  (let ((msg (err->string e)))
+    (if (> (string-length msg) 240)
+      (substring msg 0 240)
+      msg)))
+
 (def (ollama-chat provider messages tools)
   ;; Ollama exposes an OpenAI-compatible /v1/chat/completions endpoint
-  (openai-chat provider messages tools))
+  (guard (e [#t
+             (if (and (ollama-tools-present? tools)
+                      (ollama-tool-template-error? e))
+               (begin
+                 (log-warn logger "ollama-tool-template-fallback"
+                   `((mode . "chat") (err . ,(short-error-string e))))
+                 (openai-chat provider messages '()))
+               (raise e))])
+    (openai-chat provider messages tools)))
+
+(def (ollama-stream-chat provider messages tools token-cb)
+  (guard (e [#t
+             (if (and (ollama-tools-present? tools)
+                      (ollama-tool-template-error? e))
+               (begin
+                 (log-warn logger "ollama-tool-template-fallback"
+                   `((mode . "stream") (err . ,(short-error-string e))))
+                 (openai-stream-chat provider messages '() token-cb))
+               (raise e))])
+    (openai-stream-chat provider messages tools token-cb)))
 
 ;;; OpenAI Streaming ;;;
 
@@ -1571,8 +1614,9 @@
       (model    . ,(provider-model provider))
       (messages . ,(length messages))))
   (case (string->symbol (provider-name provider))
-    ((openai openrouter deepseek ollama mlx xai groq mistral together cerebras perplexity)
+    ((openai openrouter deepseek mlx xai groq mistral together cerebras perplexity)
      (openai-stream-chat  provider messages tools token-cb))
+    ((ollama) (ollama-stream-chat provider messages tools token-cb))
     ((grok)
      (case (string->symbol (grok-backend provider))
        ((chat_completions) (openai-stream-chat provider messages tools token-cb))
diff --git a/test/run.ss b/test/run.ss
index 54e0561..cf7424e 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -168,6 +168,12 @@
   (check! "assistant role"    (message-role m)    "assistant")
   (check! "assistant content" (message-content m) "hi"))
 
+(let* ([tc (make-tool-call "read" "{\"path\":\"foo\"}")]
+       [m (make-assistant-message #f (list tc))]
+       [j (message->json m)])
+  (check! "assistant tool-call json content" (hashtable-ref j "content" #f) "")
+  (check-pred! "assistant tool-call json tcs" (hashtable-ref j "tool_calls" #f) pair?))
+
 (let ([tc (make-tool-call "read" "{\"path\":\"foo\"}")])
   (check! "tool-call name" (tool-call-name tc)      "read")
   (check! "tool-call args" (tool-call-arguments tc) "{\"path\":\"foo\"}")