[GH-ISSUE #14181] /v1/chat/completions: content: "" on assistant messages with tool_calls causes model template rendering issues #35003

Open
opened 2026-04-22 19:06:58 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @lbijeau on GitHub (Feb 10, 2026).
Original GitHub issue: https://github.com/ollama/ollama/issues/14181

Description

When the /v1/chat/completions endpoint receives assistant messages in the conversation history with content: "" (empty string) alongside tool_calls, models like qwen3-coder switch from structured tool calling to text-based markup mode on subsequent turns.

The /v1 OpenAI-compatible layer should normalize content: "" to content: null (or treat them equivalently) before feeding messages to the model template, since the OpenAI API spec treats both as "no text content."

Reproduction

# Turn 3 with content: "" in prior assistant messages → BROKEN
# Model switches to <function=write_file> text markup instead of structured tool_calls
curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" -d '{
  "model": "qwen3-coder:latest",
  "stream": false,
  "messages": [
    {"role": "system", "content": "You are a helpful coding assistant. Use the provided tools to complete tasks."},
    {"role": "user", "content": "Can you help me build a todo list app?"},
    {"role": "assistant", "content": "", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "list_files", "arguments": "{\"directory\":\".\"}"}  }]},
    {"role": "tool", "tool_call_id": "call_1", "content": "src/\npackage.json\nREADME.md"},
    {"role": "assistant", "content": "", "tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\":\"package.json\"}"}}]},
    {"role": "tool", "tool_call_id": "call_2", "content": "{\"name\": \"my-app\", \"dependencies\": {\"react\": \"^18\"}}"},
    {"role": "user", "content": "Great, now create the App.js with a basic todo list component"}
  ],
  "tools": [
    {"type": "function", "function": {"name": "list_files", "description": "List files in a directory", "parameters": {"type": "object", "properties": {"directory": {"type": "string"}}, "required": ["directory"]}}},
    {"type": "function", "function": {"name": "read_file", "description": "Read contents of a file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
    {"type": "function", "function": {"name": "write_file", "description": "Write content to a file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}}
  ]
}'
# Result: finish_reason: "stop", content contains <function=write_file>{...}</function> text markup
# Expected: finish_reason: "tool_calls", structured tool_calls array
# Same conversation with non-empty content in history → WORKS
curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" -d '{
  "model": "qwen3-coder:latest",
  "stream": false,
  "messages": [
    {"role": "system", "content": "You are a helpful coding assistant. Use the provided tools to complete tasks."},
    {"role": "user", "content": "Can you help me build a todo list app?"},
    {"role": "assistant", "content": "Sure! Let me check what files exist first.", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "list_files", "arguments": "{\"directory\":\".\"}"}  }]},
    {"role": "tool", "tool_call_id": "call_1", "content": "src/\npackage.json\nREADME.md"},
    {"role": "assistant", "content": "I see you have a React project. Let me read the package.json.", "tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\":\"package.json\"}"}}]},
    {"role": "tool", "tool_call_id": "call_2", "content": "{\"name\": \"my-app\", \"dependencies\": {\"react\": \"^18\"}}"},
    {"role": "user", "content": "Great, now create the App.js with a basic todo list component"}
  ],
  "tools": [...]
}'
# Result: finish_reason: "tool_calls", clean structured tool_calls array ✓

The only difference is content: "" vs content: "Sure! Let me check..." on the assistant messages.

Root cause

The /v1 compatibility layer passes content: "" through to the model template renderer. For qwen3-coder (which uses RENDERER qwen3-coder / PARSER qwen3-coder), an empty string is rendered differently from null/absent content, causing the model to lose its tool-calling mode on subsequent turns.

Expected behavior

The /v1 endpoint should normalize content: "" to null on assistant messages that have tool_calls, since per the OpenAI spec both represent "no text content." This would make template rendering consistent regardless of whether the client sends "" or null.

This is particularly impactful because the Vercel AI SDK (@ai-sdk/openai) sends content: "" for tool-call-only assistant messages (filed as https://github.com/vercel/ai/issues/12389), making this a common real-world pattern.

Environment

  • Ollama 0.9.x
  • Model: qwen3-coder:latest
  • Also likely affects other models with complex tool-call templates
Originally created by @lbijeau on GitHub (Feb 10, 2026). Original GitHub issue: https://github.com/ollama/ollama/issues/14181 ## Description When the `/v1/chat/completions` endpoint receives assistant messages in the conversation history with `content: ""` (empty string) alongside `tool_calls`, models like `qwen3-coder` switch from structured tool calling to text-based markup mode on subsequent turns. The `/v1` OpenAI-compatible layer should normalize `content: ""` to `content: null` (or treat them equivalently) before feeding messages to the model template, since the OpenAI API spec treats both as "no text content." ## Reproduction ```bash # Turn 3 with content: "" in prior assistant messages → BROKEN # Model switches to <function=write_file> text markup instead of structured tool_calls curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "qwen3-coder:latest", "stream": false, "messages": [ {"role": "system", "content": "You are a helpful coding assistant. Use the provided tools to complete tasks."}, {"role": "user", "content": "Can you help me build a todo list app?"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "list_files", "arguments": "{\"directory\":\".\"}"} }]}, {"role": "tool", "tool_call_id": "call_1", "content": "src/\npackage.json\nREADME.md"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\":\"package.json\"}"}}]}, {"role": "tool", "tool_call_id": "call_2", "content": "{\"name\": \"my-app\", \"dependencies\": {\"react\": \"^18\"}}"}, {"role": "user", "content": "Great, now create the App.js with a basic todo list component"} ], "tools": [ {"type": "function", "function": {"name": "list_files", "description": "List files in a directory", "parameters": {"type": "object", "properties": {"directory": {"type": "string"}}, "required": ["directory"]}}}, {"type": "function", "function": {"name": "read_file", "description": "Read contents of a file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}}, {"type": "function", "function": {"name": "write_file", "description": "Write content to a file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}}} ] }' # Result: finish_reason: "stop", content contains <function=write_file>{...}</function> text markup # Expected: finish_reason: "tool_calls", structured tool_calls array ``` ```bash # Same conversation with non-empty content in history → WORKS curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "qwen3-coder:latest", "stream": false, "messages": [ {"role": "system", "content": "You are a helpful coding assistant. Use the provided tools to complete tasks."}, {"role": "user", "content": "Can you help me build a todo list app?"}, {"role": "assistant", "content": "Sure! Let me check what files exist first.", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "list_files", "arguments": "{\"directory\":\".\"}"} }]}, {"role": "tool", "tool_call_id": "call_1", "content": "src/\npackage.json\nREADME.md"}, {"role": "assistant", "content": "I see you have a React project. Let me read the package.json.", "tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "read_file", "arguments": "{\"path\":\"package.json\"}"}}]}, {"role": "tool", "tool_call_id": "call_2", "content": "{\"name\": \"my-app\", \"dependencies\": {\"react\": \"^18\"}}"}, {"role": "user", "content": "Great, now create the App.js with a basic todo list component"} ], "tools": [...] }' # Result: finish_reason: "tool_calls", clean structured tool_calls array ✓ ``` The only difference is `content: ""` vs `content: "Sure! Let me check..."` on the assistant messages. ## Root cause The `/v1` compatibility layer passes `content: ""` through to the model template renderer. For qwen3-coder (which uses `RENDERER qwen3-coder` / `PARSER qwen3-coder`), an empty string is rendered differently from null/absent content, causing the model to lose its tool-calling mode on subsequent turns. ## Expected behavior The `/v1` endpoint should normalize `content: ""` to `null` on assistant messages that have `tool_calls`, since per the OpenAI spec both represent "no text content." This would make template rendering consistent regardless of whether the client sends `""` or `null`. This is particularly impactful because the Vercel AI SDK (`@ai-sdk/openai`) sends `content: ""` for tool-call-only assistant messages (filed as https://github.com/vercel/ai/issues/12389), making this a common real-world pattern. ## Environment - Ollama 0.9.x - Model: qwen3-coder:latest - Also likely affects other models with complex tool-call templates
Author
Owner

@drifkin commented on GitHub (Feb 12, 2026):

hi @lbijeau, thanks for tracking this down and the PRs!

re:

The /v1 endpoint should normalize content: "" to null on assistant messages that have tool_calls, since per the OpenAI spec both represent "no text content."

I'm not seeing the part of the spec that says that empty string is equivalent to null

I'm looking at the OpenAI OpenAPI spec that's found at the first link here: https://github.com/openai/openai-openapi, but maybe you found that information elsewhere? Or maybe my interpretation is off

But regardless I get that even if the spec didn't say that, this might be the pragmatic thing to do since empty messages like that are rarely intended, and the heuristic of also requiring tool_calls limits the scope a lot.

<!-- gh-comment-id:3893820509 --> @drifkin commented on GitHub (Feb 12, 2026): hi @lbijeau, thanks for tracking this down and the PRs! re: > The /v1 endpoint should normalize content: "" to null on assistant messages that have tool_calls, since per the OpenAI spec both represent "no text content." I'm not seeing the part of the spec that says that empty string is equivalent to null I'm looking at the OpenAI OpenAPI spec that's found at the first link here: <https://github.com/openai/openai-openapi>, but maybe you found that information elsewhere? Or maybe my interpretation is off But regardless I get that even if the spec didn't say that, this might be the pragmatic thing to do since empty messages like that are rarely intended, and the heuristic of also requiring `tool_calls` limits the scope a lot.
Author
Owner

@lbijeau commented on GitHub (Feb 12, 2026):

The /v1 endpoint should normalize content: "" to null on assistant messages that have tool_calls, since per the OpenAI spec both represent "no text content."

@drifkin This was generated by my code assistant mid troubleshooting session. I should have found the reference before putting this up.
I did find the following, which makes the case for a normalization of the content value:

https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create

content: optional string or array of ChatCompletionContentPartText or ChatCompletionContentPartRefusal
The contents of the assistant message. Required unless tool_calls or function_call is specified.

In any case, I've worked around this for now, by forcing the client to send null when tools are involved. Cheers!

<!-- gh-comment-id:3893890153 --> @lbijeau commented on GitHub (Feb 12, 2026): > The /v1 endpoint should normalize content: "" to null on assistant messages that have tool_calls, since per the OpenAI spec both represent "no text content." @drifkin This was generated by my code assistant mid troubleshooting session. I should have found the reference before putting this up. I did find the following, which makes the case for a normalization of the content value: https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create content: optional string or array of ChatCompletionContentPartText or ChatCompletionContentPartRefusal The contents of the assistant message. Required unless tool_calls or function_call is specified. In any case, I've worked around this for now, by forcing the client to send null when tools are involved. Cheers!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/ollama#35003