Ollama tool-template 500 fallback; empty content for tool-call messages
ober
55322125aa740be76b27d4d62ff02adf78cfc078
--- 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)))) --- 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)) --- 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\"}")