No user query found in messages is raised by a 14-line block in the Qwen 3.5, 3.6 and 3.8 chat templates that walks the message list backwards and fails when no role:user message renders to text outside <tool_response> tags. Tool messages never count, and array content is flattened before the test, so it is not a cause. The older Qwen3-8B template runs the same loop without the raise, and the Qwen3-235B-A22B-Instruct-2507 and Qwen3-Coder-Next templates have no such check at all, which is why the error arrived with Qwen 3.5. On Ollama v0.34.3 the check lives in the Go renderer and runs only for qwen3.8, so the same body returns HTTP 500 on qwen3.8:27b and 200 on qwen3.6:27b. vLLM returns 400 BadRequestError, and llama-server returns 500 server_error with a Jinja Exception pointing at line 100, column 24 of the 3.8 template. On vLLM and llama.cpp the client sent that shape, because neither prunes messages; on Ollama, a tool loop that outgrows num_ctx drops the only user turn during server-side pruning. Start by counting plain user turns in the failing body with jq: zero means fix the client, one or more means fix the Ollama window.
No user query found in messages is a template check, not a model failure. The Qwen 3.5, 3.6 and 3.8 chat templates on Hugging Face walk your message list from the end, looking for one message with role user whose text is not a wrapped tool result. If it finds none, it raises. Nothing is generated, nothing is retried by the engine, and depending on which server you run, you get a 400 or a 500 and a different spelling of the same sentence on each engine.
The interesting part is how a transcript loses its user turn. On vLLM and llama.cpp the answer is always the client, because neither server drops messages. On Ollama there is a second route: a tool loop that outgrows num_ctx gets its oldest messages pruned from the front, the guard protects only the last message, and in a tool loop the last message is a tool result. The user turn that started the loop is the one that goes. Hundreds of GitHub issues across agent clients and serving engines carry this error string, and Ollama issue 17778 alone had 44 comments as of September 2026.
Everything below is read from source: the Qwen3.8-27B template on Hugging Face, Ollama at tag v0.34.3, vLLM at v0.30.0 and llama.cpp at build b11125.
What the template actually checks
This is lines 88 to 101 of the Qwen3.8-27B chat_template.jinja, verbatim. The same block sits at lines 67 to 80 of the Qwen3.5-27B and Qwen3.6-27B templates, and the same raise appears in the Qwen3.5-35B-A3B and Qwen3.6-35B-A3B templates.
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
{%- for message in messages[::-1] %}
{%- set index = (messages|length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == "user" %}
{%- set content = render_content(message.content, false)|trim %}
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if ns.multi_step_tool %}
{{- raise_exception('No user query found in messages.') }}
{%- endif %}Three consequences follow from the text, and each one rules a suspect in or out.
Tool messages never count. The template later renders role: tool messages inside a user turn as <tool_response>...</tool_response>, but the check only inspects message.role == "user". A conversation of one system message and fifty tool results has zero queries.
Tool output sent as a user message does not count either. Some clients send tool results as role: user with the text already wrapped in <tool_response> tags. The startswith and endswith test exists to reject exactly that.
Array content is not the cause. The render_content macro, lines 3 to 40 of the same file, iterates a list and emits each text item as plain text before the test runs. An OpenAI-style [{"type": "text", "text": "..."}] user message counts as a query. A hypothesis in OpenCode issue 25168 blamed array content after compaction; the 3.5+ template text contradicts it.
The error is new with Qwen 3.5. The Qwen3-8B template runs the same backwards loop but has no raise_exception after it, and the Qwen3-235B-A22B-Instruct-2507 and Qwen3-Coder-Next templates have no query check at all, so a query-less transcript rendered silently. A harness that worked against Qwen3 can start failing on the upgrade to 3.5 without changing a line.
Which engine you are on, from the wording alone
The same check produces different strings and status codes, because each engine either runs the Jinja template or reimplements it. Paste your error, match the row.
Ollama's version is lowercase with no trailing period, because it comes from validateMessages in model/renderers/qwen35.go, a Go port of the check. That function opens with if r.variant != qwen35Renderer38 { return nil }: it runs only for the qwen3.8 renderer. The registry config for qwen3.8:27b selects renderer qwen3.8, while qwen3.6:27b selects renderer qwen3.5. That is why a community reproduction on issue 17778 got a 500 on qwen3.8:27b and a 200 on qwen3.6:27b with an identical body, even though the Hugging Face template for Qwen3.6 does raise.
The llama.cpp column number is exact. Line 100 of the 3.8 template is four spaces, then {{- , then raise_exception, which puts the opening parenthesis at column 24.
One symptom does not look like an error at all. OpenCode issue 47273, open since September 4, describes an OpenCode agent on Ollama qwen3.8:27b that stalls after several successful tool calls: the client retries silently with exponential backoff and the GPU idles. Sending another user message wakes it temporarily, which fits this check, since that message gives the template a query again.
| Engine and route | Status | What you see |
|---|---|---|
Ollama /api/chat, qwen3.8 | 500 | {"error":"no user query found in messages"} |
Ollama /v1/chat/completions, qwen3.8 | 500 | {"error":{"message":"no user query found in messages","type":"api_error","param":null,"code":null}} |
| llama-server (b11125) | 500 | type: server_error, message ends Jinja Exception: No user query found in messages., preceded by line 100, column 24 for the 3.8 template or line 79 for 3.5 and 3.6 |
| vLLM v0.30.0 | 400 | BadRequestError, message No user query found in messages. |
| vLLM before v0.30.0 | 400 | Logged as a ValueError from the transformers chat template path, mapped to BadRequestError |
| LM Studio | In-app | Error rendering prompt with jinja template: "No user query found in messages." |
The four transcript shapes that produce it
1. Ollama prunes the user turn out of a tool loop
When the rendered conversation exceeds the window, chatPrompt in server/prompt.go drops whole messages from the front, re-collects system messages from the dropped span, and always keeps the last message. The mechanics, including why this is logged only at DEBUG, are in how Ollama drops messages from the front when num_ctx overflows. What that post does not reach is the tool loop. The loop calls renderPrompt on each shorter suffix and returns immediately on error, before tokenizing. On qwen3.8, the first suffix that no longer contains the user message fails validation, and the request returns 500 without ever trying a later suffix that might have fit. The guard if i == lastMsgIdx protects only the last message, and nothing in either pruning loop looks at roles other than system. A community bisect on issue 17778 pins it down. An OpenCode body shaped [system, user, assistant, tool, tool, assistant, tool, tool, assistant, tool, assistant, tool] with roughly 95,000 characters of tool results and the only user turn at index 1 failed at num_ctx 32768. Trimming each tool result to 2,000 characters made it pass. A four-message body with one tool result of about 132,000 characters failed. Every suffix small enough to fit passed. The source and the bisect agree: the 500 happens when every suffix that still holds the user turn overflows the window.
2. Client compaction that summarises the task away
Agent clients that compact history rewrite the message list themselves. If the rewrite folds the original task into a summary placed in a system or assistant message, or drops it, the next request has zero user turns and every engine rejects it. OpenCode users on LM Studio report the error after auto-compaction; the root cause in that thread was not established, and it was closed by a stale bot. Claude Code pointed at a local model adds a timing problem: its compaction window cannot be set below 100,000 tokens, so against a 32768-token Ollama window the server prunes long before the client compacts. Why Claude Code cannot be told a window under 100K covers that clamp.
3. Tool-first continuations and tool output as user
A client that sends [system, assistant(tool_calls), tool] as a continuation, without resending the user turn, fails the check on the first request. So does a client that sends tool results as role: user wrapped in <tool_response> tags. Both are bugs in the client's history management, and both reproduce on every engine running the 3.5+ template.
4. System-only requests
A workflow node that puts its instructions in a single system message and sends nothing else has no user turn by construction. That is the case reported on vLLM issue 36432, filed against Qwen3.5 27B from a parameter-extraction node in a workflow builder. Only shape 1 is Ollama-specific. vLLM never drops messages to fit: in v0.30.0 an over-long prompt gets its own 400 naming the model's maximum context length, and template rendering runs before that check. llama-server behaves the same way, returning a 400 exceed_context_size_error for oversize prompts. On those two engines, this error always describes the transcript the client sent.
Find your shape in five minutes
Capture the failing request body first. On a bench box running Ollama, the server can write it out for you:
OLLAMA_DEBUG_LOG_REQUESTS=1 ollama serve # the failure itself is logged at ERROR on a stock server: # level=ERROR msg="chat prompt error" error="no user query found in messages"
OLLAMA_DEBUG_LOG_REQUESTS writes request bodies and replay curl commands to a temporary directory. Do not turn it on for hosts that hold regulated data; capture from a bench replay instead.
Then count what the body actually contains. The second query mirrors the template's test: user role, content flattened to text, trimmed, not wrapped in <tool_response> tags.
# Roles in the body you actually sent
jq -c '[.messages[].role] | group_by(.) | map({(.[0]): length}) | add' body.json
# User turns the Qwen template will accept as a query (0 = this request will fail on any engine)
jq '[.messages[]
| select(.role == "user")
| (if (.content | type) == "string" then .content
else ([.content[]? | .text? // ""] | join("")) end)
| gsub("^\\s+|\\s+$"; "")
| select((startswith("<tool_response>") and endswith("</tool_response>")) | not)
] | length' body.jsonThe reading is binary. A count of 0 means the client sent shape 2, 3 or 4, and no server setting will fix it. A count of 1 or more, with Ollama still returning the error, means shape 1: server-side pruning removed a user turn that was in the body. The jq approximation reads only text parts, so an image-only user turn becomes an empty string and still counts, just as it does in the template, which renders it as a vision placeholder.
To reproduce on Ollama without generating a token, set _debug_render_only. The debug branch in server/routes.go runs after chatPrompt, so a failing body returns the same 500 at zero cost. This minimal body mirrors the community reproduction:
curl -s -w '\nHTTP %{http_code}\n' http://localhost:11434/api/chat -d '{
"model": "qwen3.8:27b",
"stream": false,
"_debug_render_only": true,
"messages": [
{"role": "system", "content": "You are a coding agent."},
{"role": "assistant", "content": "", "tool_calls": [{"function": {"name": "read_file", "arguments": {"path": "README.md"}}}]},
{"role": "tool", "tool_name": "read_file", "content": "# project"}
]
}'
# expected per the v0.34.3 source:
# {"error":"no user query found in messages"}
# HTTP 500
# Add {"role": "user", "content": "Summarise the README"} after the system message and it renders.Swap the model to qwen3.6:27b and the same body renders, because that model uses the qwen3.5 renderer. For shape 1, replay your captured body the same way and compare its rendered size against the CONTEXT column of ollama ps, which the num_ctx post explains.
Fix it per engine
cat > Modelfile <<'EOF' FROM qwen3.8:27b PARAMETER num_ctx 65536 EOF ollama create qwen3.8-64k -f Modelfile # point the agent at model "qwen3.8-64k"; confirm with: ollama ps (CONTEXT column)
curl -s http://localhost:11434/api/chat -d '{
"model": "qwen3.8:27b",
"messages": [ ... ],
"tools": [ ... ],
"truncate": false,
"shift": false,
"stream": false
}'
# overflow now returns HTTP 400 carrying the runner's context error:
# ... request (N tokens) exceeds the available context size (N tokens), try increasing it ...FROM qwen3.8:27b RENDERER qwen3.5
curl -sL https://huggingface.co/Qwen/Qwen3.5-27B/raw/main/chat_template.jinja -o qwen35.jinja # edit qwen35.jinja, then: vllm serve Qwen/Qwen3.5-27B --chat-template ./qwen35.jinja
{%- if ns.multi_step_tool %}
{%- if messages|length == 1 and messages[0].role == "system" %}
{# do nothing #}
{%- else %}
{{- raise_exception('No user query found in messages.') }}
{%- endif %}
{%- endif %}llama-server -m qwen3.5-27b-q4_k_m.gguf --jinja --chat-template-file ./qwen35.jinja
Ollama
Give the loop a window it fits in. If the count was 1 or more, the window is too small for the loop. From an OpenAI-compatible client the reachable surfaces are a Modelfile or OLLAMA_CONTEXT_LENGTH on the server; the precedence rules and VRAM tiers are in the num_ctx post. The 65536 is an example: size it to your VRAM. A bigger window moves the cliff; a long enough tool loop still reaches it. Fail loud instead of pruning. On the native /api/chat route, truncate and shift are request fields. With truncate: false the message-level pruning never runs, so the user turn is never dropped and the qwen3.8 check passes. With shift: false the overflow then comes back as a 400 your client can react to by compacting and retrying. Neither field exists on /v1/chat/completions, so this path needs a client that speaks the native API. The RENDERER stopgap, with its costs. Community commenters on issue 17778 report that rebuilding qwen3.8 with the qwen3.5 renderer makes the error go away: RENDERER is accepted by parser/parser.go at v0.34.3 but does not appear in the instruction table of Ollama's Modelfile documentation, so treat it as undocumented. It removes the check, and it also removes two other things the qwen3.8 renderer does: injecting the reasoning-effort instructions into the system turn, and folding system and developer messages into one leading system turn. When pruning was the cause, the request now succeeds with no user task in the prompt at all. The model keeps calling tools without knowing what it was asked. Use this on a bench box to unblock a demo, not in anything you ship. What the open pull request would change. Ollama PR 17894, "chat: always preserve the most recent user message during truncation", clamps the pruning start so the last user message and everything after it survive, and hands any remaining overflow to token-level truncation. It is open and unmerged as of September 2026. One commenter cherry-picked it onto v0.32.15 and the failing session completed. An Ollama collaborator on the issue objects that it lets the prompt through "at an unknown cost to token generation", and argues that "the best solution is for the client to manage the message list, not have ollama discard messages." We agree with the collaborator: a merged PR would trade a loud 500 for a quieter middle cut.
vLLM
Fix the client. vLLM does not prune, so a 400 carrying this message means the body had no plain user turn. The only shape where a template override is defensible is shape 4, the system-only request. --chat-template takes a file path: A community patch on vLLM issue 36432 replaces the raise block (lines 78 to 80 in the 3.5 and 3.6 templates, 99 to 101 in 3.8) with this: Read the condition: it admits a request that is exactly one system message and nothing else. A [system, assistant, tool] tool loop still raises. It is a community patch, not from Qwen or vLLM, and it does nothing for agents. If your Qwen tool calls on vLLM come back empty rather than rejected, that is the other Qwen tool-calling failure on vLLM, in the parser rather than the template.
llama.cpp
Same answer as vLLM: llama-server does not prune, so fix the transcript. --jinja is on by default in current builds, and --chat-template-file loads an edited template for the system-only case: The GGUF filename is a placeholder, and --jinja is redundant on b11125 but states intent.
LM Studio
LM Studio's own error text points to the override surface: My Models, then the model's settings, then Prompt Template. LM Studio bug tracker issue 1586, filed against 0.4.6 with qwen3.5-35b-a3b in MCP agent workflows, is open with no staff reply as of September 2026. Some "working templates" posted there drop tool definitions and tool messages entirely; do not use them. Count user turns in the body and fix the client.
A sibling error that is fixed in one place only
The related Ollama 500, "system message must be at the beginning" on qwen3.8:27b, was fixed by Ollama PR 17757, merged on August 14, 2026, which stopped the Go renderer rejecting non-leading system messages; at v0.34.3 the qwen3.8 renderer folds system and developer messages into one leading system turn. Upgrade if you still see it on Ollama. The Hugging Face Jinja template still raises System message must be at the beginning. for a non-leading system message, so vLLM, llama.cpp and LM Studio users can hit it.
The client-side fix that holds
Every engine-level fix above moves the cliff, hides the symptom, or turns it into a different error. The fix that works on all four engines is two rules in the harness: the original user task never enters the compactable region, and no tool result enters history uncapped.
MAX_TOOL_CHARS = 8_000 # size to your window; the 17778 bisect passed at 2K per result
def add_tool_result(history, call_id, text):
if len(text) > MAX_TOOL_CHARS:
text = text[:MAX_TOOL_CHARS] + f"\n[truncated {len(text) - MAX_TOOL_CHARS} chars; ask for a range]"
history.append({"role": "tool", "tool_call_id": call_id, "content": text})
def is_query(m):
if m["role"] != "user":
return False
c = m.get("content") or ""
if isinstance(c, list): # the template flattens text parts before testing
c = "".join(p.get("text", "") for p in c)
c = c.strip()
return not (c.startswith("<tool_response>") and c.endswith("</tool_response>"))
def build_request(system, task, history):
# The original user task is never inside the compactable region.
msgs = [{"role": "system", "content": system},
{"role": "user", "content": task}] + history
assert any(is_query(m) for m in msgs), "no plain user turn"
return msgshistory holds the assistant tool calls and tool results; whatever compaction does to it, task is rebuilt into position 1 on every request. The assertion is the template's own test, run before the bytes leave your process, so the failure is a local exception with a stack trace instead of a remote 500 hidden behind a retry loop.
Capping tool results at write time is what keeps shape 1 from appearing in the first place: the bisect's body failed on 95,000 characters of tool output, not on the number of turns. Offloading large results to a file and passing a reference is the stronger version, covered with compaction triggers in when to compact and how to offload large tool results. When you drop old history, drop assistant tool calls and their tool results as pairs; the sibling transcript-shape error on the Anthropic API is what happens when you split them. And a summary that loses the task can restart work the agent already did, which is how summary loss restarts tool loops.
This is also where the on-prem answer differs. Ollama's pruning keeps the request alive by deciding for you which turns the model loses, and the only record of that decision is a DEBUG log line that a stock server does not emit. System messages survive by design; operator instructions and policy text sent as user or assistant turns do not. In a regulated deployment, where you may need to show which instructions governed an action, a silent server-side cut is worse than a crash. Use truncate: false on the native API or an engine that rejects oversize prompts, and let the client own the message list. On this point the pruning trade-off is a real difference in Ollama versus vLLM, not a detail.
Replay the failing body as a CI check
Keep the captured body as a fixture and run it against the model name and window you actually deploy. The case-insensitive grep catches both the lowercase Ollama spelling and the capitalized Jinja one.
code=$(curl -s -o out.json -w '%{http_code}' http://localhost:11434/v1/chat/completions \
-H 'Content-Type: application/json' -d @fixtures/failing-body.json)
if grep -qi 'no user query found in messages' out.json; then
echo "regression: transcript lost its user turn (HTTP $code)"; exit 1
fiPair it with the jq count as a pre-send gate in the harness's own tests: every request the agent builds in a recorded long session must have a plain user count of at least 1. The replay catches the server side, the count catches the client side, and between them a Qwen upgrade or a context change cannot reintroduce the error unnoticed.
This week, take the last failing request from your agent logs, run the two jq commands on it, and write the plain user count next to the ticket. If it is 0, the fix is in your harness; if it is 1 or more, it is in your Ollama window. The rest of the local-serving decisions sit in our LLMs and models pillar.
FAQ
Quick answers to the questions this post tends to raise.



