You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gemma-4-26b and gemma-4-31b opencode interaction issues leading to tool loops/failures #21034
Just following up on the possible duplicates flagged by github-actions.
This issue is not a duplicate of #20995. In that issue, the (smaller) model is unable to make a tool call (model responds with: "I do not have the capability to execute system commands like listing directory contents.") In this issue, the tool calls are attempted.
This issue is not a duplicate of #20669. In that issue, the tools are called with invalid arguments ("The bash tool was called with invalid arguments ... expected string, received undefined"). In this issue, tool calls succeed and are repeated numerous times.
Updated to metal llama v2.12.0 and tested both unsloth/gemma-4-26b-a4b-it@q4_k_xl and unsloth/gemma-4-31b-it@q4_k_xl.
OpenCode seems to be working now. Model appears to be parsing tool responses properly. Tool use is working without error, and redundant/repeated tool call loops are no longer occurring.
Updated to metal llama v2.12.0 and tested both unsloth/gemma-4-26b-a4b-it@q4_k_xl and unsloth/gemma-4-31b-it@q4_k_xl.
OpenCode seems to be working now. Model appears to be parsing tool responses properly. Tool use is working without error, and redundant/repeated tool call loops are no longer occurring.
I can confirm this same behavior on PopOS/RTX 5090 setup. Also any request that requires a tool call ends up in an infinite loop with Gemma4 26b and 31b models.
I see this and then the agent just stops <edit_file> <file_path>/Users/bni/Documents/freesynd/engine/src/system/sdl/system_sdl.cpp</file_path> <old_string> #include <SDL_image.h> </old_string> <new_string> #include <SDL3/SDL_image.h> </new_string> </edit_file> <edit_file> <file_path>/Users/bni/Documents/freesynd/engine/src/system/sdl/fstexture_sdl.h</file_path> <old_string> #include <SDL.h> </old_string> <new_string> #include <SDL3/SDL.h> </new_string> </edit_file>
I have ecountered the same issue with 26B and 32B, works great in chat mode via unsloth or even straight llama-server frontend. But tool usage in opencode is really inconsistent. It will run tools for a bit and then require constant "continue" prompts from the user. Seems like a failed tool call chaining or something?
Have tried it with multiple different quants, built llama.cpp with lastet gemma patches and toknenizers. Definitely seems to be something off with opencode tool parsing?
Also having issues on gemma 4 31b-it - just gets stuck thinking in opencode. Still trialing other things. I dropped the reasoning parser and same issue.
There's an issue in the Gemma 4 chat templates in handling the system message sent by OpenCode. The chat template expects the system message to be a string, not a list of text content parts. That's at least part of the confusion these models are running into with OpenCode. Here's a local diff that gets the system prompt rendering properly instead of as an inline JSON array:
--- chat_template.jinja 2026-04-16 18:46:34.506453697 -0400+++ gemma_4_e4b_chat_template.jinja 2026-04-16 18:36:00.864299411 -0400@@ -186,7 +186,15 @@
{%- endif -%}
{%- if messages[0]['role'] in ['system', 'developer'] -%}
- {{- messages[0]['content'] | trim -}}+ {%- if messages[0]['content'] is string -%}+ {{- messages[0]['content'] | trim -}}+ {%- elif messages[0]['content'] is sequence -%}+ {%- for item in messages[0]['content'] -%}+ {%- if item['type'] == 'text' -%}+ {{- item['text'] | trim -}}+ {%- endif -%}+ {%- endfor -%}+ {%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- endif -%}
This may not fix all issues, but it's definitely part of the problem.
Reacted by Ewout van Mansom, Jack Kelly, 林博仁 Buo-ren Lin and Christophe Gueret
Here's a full chat template with the diff above plus an additional fix that properly handles the empty context blocks OpenCode sends in multi-turn scenarios that was tripping up the Gemma 4 chat template's insertion of the end turn (<turn|>) special token - https://gist.github.com/bbrowning/c584eb2dbd79e4cc9ecedf92eee2d135
I'm getting decent results from OpenCode even with the smaller Gemma 4 models with the template change.
Here's a full chat template with the diff above plus an additional fix that properly handles the empty context blocks OpenCode sends in multi-turn scenarios that was tripping up the Gemma 4 chat template's insertion of the end turn (<turn|>) special token - https://gist.github.com/bbrowning/c584eb2dbd79e4cc9ecedf92eee2d135
I'm getting decent results from OpenCode even with the smaller Gemma 4 models with the template change.
Good progress! Also working on some fixes, to eventually be upstreamed (if still relevant, and actually beneficial, and upstream agrees etc. etc.) in this branch here: https://github.com/emansom/llama.cpp/tree/gemma4-fixes
Give it a try and tell me if it reduces tool call failures! Feel free to send PRs to my fork to improve it further! I'll debug your template improvements mentioned above this weekend and merge those in if need be, my fork also contains a few alterations there.
Give it a try and tell me if it reduces tool call failures! Feel free to send PRs to my fork to improve it further! I'll debug your template improvements mentioned above this weekend and merge those in if need be, my fork also contains a few alterations there.
Here's the full diff of the chat template change I made, shown here as a diff against the chat template shipped with the larger Gemma 4 (26B and 31B) models (can be adapted to the smaller ones as well, but the base templates are slightly different):
--- chat_template_gemma_large_orig.jinja 2026-04-16 19:13:08.903438297 -0400+++ chat_template_gemma_large_fixed.jinja 2026-04-16 20:21:41.983833992 -0400@@ -186,7 +186,15 @@
{%- endif -%}
{%- if messages[0]['role'] in ['system', 'developer'] -%}
- {{- messages[0]['content'] | trim -}}+ {%- if messages[0]['content'] is string -%}+ {{- messages[0]['content'] | trim -}}+ {%- elif messages[0]['content'] is sequence -%}+ {%- for item in messages[0]['content'] -%}+ {%- if item['type'] == 'text' -%}+ {{- item['text'] | trim -}}+ {%- endif -%}+ {%- endfor -%}+ {%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- endif -%}
@@ -302,19 +310,34 @@
{%- endfor -%}
{%- endif -%}
+ {%- set ns_text = namespace(ok=false) -%}
{%- if message['content'] is string -%}
{%- if role == 'model' -%}
- {{- strip_thinking(message['content']) -}}+ {%- set _s = strip_thinking(message['content']) -%}+ {{- _s -}}+ {%- if (_s | trim | length) > 0 -%}+ {%- set ns_text.ok = true -%}+ {%- endif -%}
{%- else -%}
{{- message['content'] | trim -}}
+ {%- if (message['content'] | trim | length) > 0 -%}+ {%- set ns_text.ok = true -%}+ {%- endif -%}
{%- endif -%}
{%- elif message['content'] is sequence -%}
{%- for item in message['content'] -%}
{%- if item['type'] == 'text' -%}
{%- if role == 'model' -%}
- {{- strip_thinking(item['text']) -}}+ {%- set _s = strip_thinking(item['text']) -%}+ {{- _s -}}+ {%- if (_s | trim | length) > 0 -%}+ {%- set ns_text.ok = true -%}+ {%- endif -%}
{%- else -%}
{{- item['text'] | trim -}}
+ {%- if (item['text'] | trim | length) > 0 -%}+ {%- set ns_text.ok = true -%}+ {%- endif -%}
{%- endif -%}
{%- elif item['type'] == 'image' -%}
{{- '<|image|>' -}}
@@ -331,7 +354,7 @@
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
{{- '<|tool_response>' -}}
- {%- elif not (ns_tr_out.flag and not message.get('content')) -%}+ {%- elif not (ns_tr_out.flag and not ns_text.ok) -%}
{{- '<turn|>\n' -}}
{%- endif -%}
{%- endif -%}
The first change in that diff is to handle how OpenCode sends in the system role not as a string (which the template expected) but as a text content part. This was resulting in the entire system prompt from OpenCode getting rendered to the model as a JSON array instead instead of as a simple text prompt.
The second change in that diff is because OpenCode is not sending in the exact same messages it received, and instead including an empty text content part for the content of every tool call message instead of setting that content field to None or an empty string. So, we have to detect if a message has only empty content and not render the closing turn tag in that case. This fixes an issue where the model was seeing closing turn tags prematurely, while the tool call -> tool response -> tool call -> tool response loop was still ongoing between OpenCode and the model.
The larger unquantized models actually handled these failure cases reasonably well early in a conversation, but in more complex multi-turn conversations these errors compounded and led to substantial failures. I found these by dumping the model outputs directly from vLLM and dumping the model input inside vLLM to get the full prompt exactly as seen by the model and the full output exactly as generated by the model.
The official Gemma 4 chat templates have been updated within the last 24 hours with some of the fixes we identified here for OpenCode. I've tested the chat template updates with vLLM and gemma-4-31B-it and things are working very well for me. If you're using OpenCode with vLLM (or any other inference server that uses the model's chat templates), it's worth updating your local copy of the model from HuggingFace again to get the newer changes to chat_template.jinja.
That may help determine whether Gemma is emitting malformed tool calls, whether OpenCode parses them incorrectly, or whether repeated tool
results are being fed back into later context.
Activity
github-actions commentedon Apr 4, 2026
This issue might be a duplicate of existing issues. Please check:
pchuck commentedon Apr 4, 2026
Just following up on the possible duplicates flagged by github-actions.
This issue is not a duplicate of #20995. In that issue, the (smaller) model is unable to make a tool call (model responds with: "I do not have the capability to execute system commands like listing directory contents.") In this issue, the tool calls are attempted.
This issue is not a duplicate of #20669. In that issue, the tools are called with invalid arguments ("The bash tool was called with invalid arguments ... expected string, received undefined"). In this issue, tool calls succeed and are repeated numerous times.
pchuck commentedon Apr 6, 2026
Updated to metal llama v2.12.0 and tested both unsloth/gemma-4-26b-a4b-it@q4_k_xl and unsloth/gemma-4-31b-it@q4_k_xl.
OpenCode seems to be working now. Model appears to be parsing tool responses properly. Tool use is working without error, and redundant/repeated tool call loops are no longer occurring.
NikolaKrstic94 commentedon Apr 7, 2026
Same here ,can confirm it works much much better.
eikonomega commentedon Apr 7, 2026
I can confirm this same behavior on PopOS/RTX 5090 setup. Also any request that requires a tool call ends up in an infinite loop with Gemma4 26b and 31b models.
bni commentedon Apr 7, 2026
I see this and then the agent just stops
<edit_file> <file_path>/Users/bni/Documents/freesynd/engine/src/system/sdl/system_sdl.cpp</file_path> <old_string> #include <SDL_image.h> </old_string> <new_string> #include <SDL3/SDL_image.h> </new_string> </edit_file> <edit_file> <file_path>/Users/bni/Documents/freesynd/engine/src/system/sdl/fstexture_sdl.h</file_path> <old_string> #include <SDL.h> </old_string> <new_string> #include <SDL3/SDL.h> </new_string> </edit_file>Running latest llama.cpp like this:
.\llama-server.exe--hf-repo unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL
--temp 1.0--top-p 0.95
--top-k 64--alias "unsloth/gemma-4-26B-A4B-it-GGUF"
--host 0.0.0.0--port 8080
--reasoning onFricoRico commentedon Apr 7, 2026
VLLM v0.19.0 still showed issues and so did the nightly from yesterday. I'm unsure if this is an OpenCode problem or an inference side problem.
JackKelly commentedon Apr 8, 2026
I can confirm that I'm seeing similar behaviour with
opencode,vLLM v0.19.0, andGemma 4 26B.BrutchsamaJeanLouis commentedon Apr 8, 2026
Same with gemma4-31b tokenizer tool parsing is broken in opencode but works in openwebui. latest llama.cpp b8705
there is also issue 16531 that blocks gemma4 support
Racemase commentedon Apr 9, 2026
I have ecountered the same issue with 26B and 32B, works great in chat mode via unsloth or even straight llama-server frontend. But tool usage in opencode is really inconsistent. It will run tools for a bit and then require constant "continue" prompts from the user. Seems like a failed tool call chaining or something?
Have tried it with multiple different quants, built llama.cpp with lastet gemma patches and toknenizers. Definitely seems to be something off with opencode tool parsing?
i-am-rad commentedon Apr 9, 2026
Also having issues on gemma 4 31b-it - just gets stuck thinking in opencode. Still trialing other things. I dropped the reasoning parser and same issue.
bbrowning commentedon Apr 16, 2026
There's an issue in the Gemma 4 chat templates in handling the system message sent by OpenCode. The chat template expects the system message to be a string, not a list of text content parts. That's at least part of the confusion these models are running into with OpenCode. Here's a local diff that gets the system prompt rendering properly instead of as an inline JSON array:
This may not fix all issues, but it's definitely part of the problem.
bbrowning commentedon Apr 17, 2026
Here's a full chat template with the diff above plus an additional fix that properly handles the empty context blocks OpenCode sends in multi-turn scenarios that was tripping up the Gemma 4 chat template's insertion of the end turn (
<turn|>) special token - https://gist.github.com/bbrowning/c584eb2dbd79e4cc9ecedf92eee2d135I'm getting decent results from OpenCode even with the smaller Gemma 4 models with the template change.
emansom commentedon Apr 17, 2026
Good progress! Also working on some fixes, to eventually be upstreamed (if still relevant, and actually beneficial, and upstream agrees etc. etc.) in this branch here: https://github.com/emansom/llama.cpp/tree/gemma4-fixes
https://github.com/emansom/llama.cpp/commits/gemma4-fixes/
Give it a try and tell me if it reduces tool call failures! Feel free to send PRs to my fork to improve it further! I'll debug your template improvements mentioned above this weekend and merge those in if need be, my fork also contains a few alterations there.
bbrowning commentedon Apr 17, 2026
Here's the full diff of the chat template change I made, shown here as a diff against the chat template shipped with the larger Gemma 4 (26B and 31B) models (can be adapted to the smaller ones as well, but the base templates are slightly different):
The first change in that diff is to handle how OpenCode sends in the system role not as a string (which the template expected) but as a text content part. This was resulting in the entire system prompt from OpenCode getting rendered to the model as a JSON array instead instead of as a simple text prompt.
The second change in that diff is because OpenCode is not sending in the exact same messages it received, and instead including an empty text content part for the
contentof every tool call message instead of setting thatcontentfield to None or an empty string. So, we have to detect if a message has only empty content and not render the closing turn tag in that case. This fixes an issue where the model was seeing closing turn tags prematurely, while the tool call -> tool response -> tool call -> tool response loop was still ongoing between OpenCode and the model.The larger unquantized models actually handled these failure cases reasonably well early in a conversation, but in more complex multi-turn conversations these errors compounded and led to substantial failures. I found these by dumping the model outputs directly from vLLM and dumping the model input inside vLLM to get the full prompt exactly as seen by the model and the full output exactly as generated by the model.
bbrowning commentedon Apr 29, 2026
The official Gemma 4 chat templates have been updated within the last 24 hours with some of the fixes we identified here for OpenCode. I've tested the chat template updates with vLLM and gemma-4-31B-it and things are working very well for me. If you're using OpenCode with vLLM (or any other inference server that uses the model's chat templates), it's worth updating your local copy of the model from HuggingFace again to get the newer changes to chat_template.jinja.
rkuo2000 commentedon May 31, 2026
modifying opencode.json for increasing context window size did not increase ollama model's num_ctx
journalctl -u ollama -f to check if prompt > num_ctx
if yes, then
ollama run gemma4-31b
/set parameter num_ctx 65536
/save gemma4-31-ctx64k
edit ~/.config/opencode/opencode.json to use the newly saved model
rerun opencode
Hope this works for you !
Bazmundi commentedon Jun 8, 2026
opencode 1.16.2 with ollama 0.30.6 and gemma4:latest exhibits the same problem
bartlettroscoe commentedon Jun 28, 2026
So this does need to be fixed on the llama.cpp side in the same way that it was addressed in vLLM?
houleixx commentedon Jun 29, 2026
For tool loops, I would try to capture several failed turns and compare them.
ccglass can show the request body, tool schemas, tool calls/results, response bodies, token/cache/cost, latency, and diffs between turns.
Project: https://github.com/jianshuo/ccglass
That may help determine whether Gemma is emitting malformed tool calls, whether OpenCode parses them incorrectly, or whether repeated tool
results are being fed back into later context.
github-actions commentedon Aug 28, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.