[GH-ISSUE #15315] gemma4:e4b with ollama 0.20.1 still has tool parsing errors #35555

Open
opened 2026-04-22 20:07:36 -05:00 by GiteaMirror · 45 comments
Owner

Originally created by @KeuntekLee on GitHub (Apr 4, 2026).
Original GitHub issue: https://github.com/ollama/ollama/issues/15315

Originally assigned to: @drifkin on GitHub.

What is the issue?

After updating with ollama version 0.20.1 for fixing gemma4 tool parsing error(#15254), ollama still show errors on tool calling.
Using gemma4:e4b with opencode, oh-my-opencode, ollama 0.20.1

Relevant log output

time=2026-04-04T13:58:18.757+09:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '`' looking for beginning of value" content="call:write{content:\n\n# Project Style Guide for Autonomous Agents' Code Generation (AGENTS.md)\n\nThis document captures the *de facto* coding standards observed across the `src/` and `components/` source code, designed to ensure consistency for all generated code and modules consumed by the agent system.

time=2026-04-04T13:57:33.487+09:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '\\'' looking for beginning of value" content="call:grep{include:<|\"|>*.py<|\"|>,output_mode:<|\"|>content<|\"|>,path:<|\"|>/data/robotics/experiment1<|\"|>,pattern:':\\s*\\w+'<|\"|>}"

OS

Linux

GPU

Nvidia

CPU

AMD

Ollama version

0.20.1

Originally created by @KeuntekLee on GitHub (Apr 4, 2026). Original GitHub issue: https://github.com/ollama/ollama/issues/15315 Originally assigned to: @drifkin on GitHub. ### What is the issue? After updating with ollama version 0.20.1 for fixing gemma4 tool parsing error(#15254), ollama still show errors on tool calling. Using gemma4:e4b with opencode, oh-my-opencode, ollama 0.20.1 ### Relevant log output ```shell time=2026-04-04T13:58:18.757+09:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '`' looking for beginning of value" content="call:write{content:\n\n# Project Style Guide for Autonomous Agents' Code Generation (AGENTS.md)\n\nThis document captures the *de facto* coding standards observed across the `src/` and `components/` source code, designed to ensure consistency for all generated code and modules consumed by the agent system. time=2026-04-04T13:57:33.487+09:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '\\'' looking for beginning of value" content="call:grep{include:<|\"|>*.py<|\"|>,output_mode:<|\"|>content<|\"|>,path:<|\"|>/data/robotics/experiment1<|\"|>,pattern:':\\s*\\w+'<|\"|>}" ``` ### OS Linux ### GPU Nvidia ### CPU AMD ### Ollama version 0.20.1
GiteaMirror added the bug label 2026-04-22 20:07:36 -05:00
Author
Owner

@Habitante commented on GitHub (Apr 4, 2026):

Root cause analysis

The underlying issue is that Gemma 4's tool call format is not JSON — it's a custom serialization:

<|tool_call>call:func_name{key:<|"|>value<|"|>,num:42}<tool_call|>

Rules:

  • String values are delimited by <|"|> (special token 52), not regular " quotes
  • Keys are unquoted (bare identifiers)
  • Non-string values (numbers, booleans) are bare
  • Content between <|"|> delimiters can contain any characters: double quotes, single quotes, backticks, backslashes, newlines, etc.

The current parser appears to do a naive <|"|>" replacement and then call json.Unmarshal. This breaks whenever the string content contains characters that aren't valid unescaped JSON — which is exactly what both error messages in this issue show:

  • invalid character ''` — backtick inside a markdown code block argument
  • invalid character '\'' — single quote inside a regex pattern argument like ':\s*\w+'

PR #15255 fixed one case (internal double quotes), but the problem is more fundamental: you can't convert Gemma 4's format to JSON with string replacement alone, because the content between <|"|> delimiters is arbitrary text that needs proper JSON string escaping (backslashes, control chars, etc.) before it can be placed between " quotes.

What a correct parser looks like

vLLM's gemma4_tool_parser.py has a working reference implementation. Their approach:

  1. Extract function name and raw argument string from call:name{...}
  2. Parse the key:value pairs with a custom tokenizer that understands <|"|> as string delimiters
  3. For string values (between <|"|> delimiters): capture the raw content, then properly escape it for JSON
  4. For bare values: parse as number/boolean/null directly
  5. Build a native dict/map, then serialize to JSON via json.dumps() / json.Marshal

The critical step is #3 — the raw content between <|"|> delimiters must be JSON-escaped (\n\\n, "\", \\\, etc.) before being placed into a JSON string value.

Reproduction (minimal, no app needed)

This reliably triggers the bug because print("hello world") contains double quotes that generate <|"|> escaping:

curl http://localhost:11434/api/chat -d '{
  "model": "gemma4:26b",
  "messages": [{"role": "user", "content": "use run_script to print hello world"}],
  "stream": false,
  "tools": [{
    "type": "function",
    "function": {
      "name": "run_script",
      "description": "Execute Python code",
      "parameters": {
        "type": "object",
        "properties": {
          "code": {"type": "string", "description": "Python code"}
        },
        "required": ["code"]
      }
    }
  }]
}'

Expected: tool_calls with print("hello world")
Actual: Empty content, done_reason: "stop", no tool_calls field. The WARN log shows parsing failed.

Contrast with a quote-free argument that works fine:

# Same setup, but "compute 2+2" generates print(2 + 2) — no quotes, no <|"|> escaping
curl ... "content": "use run_script to compute 2+2" ...

This returns a valid tool_calls response because print(2 + 2) has no internal quotes.

Additional failure modes from systematic testing

User prompt Generated code Quotes in args? Result
"compute 2+2" print(2 + 2) No Works
"fibonacci up to 20" def fibonacci(n):... Yes (in strings) Works sometimes
"print hello world" print("hello world") Yes Fails silently
"build a calculator app" Full tkinter program Many Fails (1618 eval tokens, all thinking)
"generate 10 random passwords" Full script Yes Falls back to markdown text

The pattern: any tool argument containing characters that need JSON escaping will fail. Simple numeric expressions work; anything with string literals, regex patterns, shell commands, or multi-line code fails.

References

<!-- gh-comment-id:4186695864 --> @Habitante commented on GitHub (Apr 4, 2026): ## Root cause analysis The underlying issue is that Gemma 4's tool call format is **not JSON** — it's a custom serialization: ``` <|tool_call>call:func_name{key:<|"|>value<|"|>,num:42}<tool_call|> ``` Rules: - String values are delimited by `<|"|>` (special token 52), **not** regular `"` quotes - Keys are **unquoted** (bare identifiers) - Non-string values (numbers, booleans) are bare - Content between `<|"|>` delimiters can contain **any characters**: double quotes, single quotes, backticks, backslashes, newlines, etc. The current parser appears to do a naive `<|"|>` → `"` replacement and then call `json.Unmarshal`. This breaks whenever the string content contains characters that aren't valid unescaped JSON — which is exactly what both error messages in this issue show: - `invalid character '`'` — backtick inside a markdown code block argument - `invalid character '\''` — single quote inside a regex pattern argument like `':\s*\w+'` PR #15255 fixed one case (internal double quotes), but the problem is more fundamental: **you can't convert Gemma 4's format to JSON with string replacement alone**, because the content between `<|"|>` delimiters is arbitrary text that needs proper JSON string escaping (backslashes, control chars, etc.) before it can be placed between `"` quotes. ## What a correct parser looks like vLLM's `gemma4_tool_parser.py` has a working reference implementation. Their approach: 1. Extract function name and raw argument string from `call:name{...}` 2. Parse the `key:value` pairs with a **custom tokenizer** that understands `<|"|>` as string delimiters 3. For string values (between `<|"|>` delimiters): capture the raw content, then properly escape it for JSON 4. For bare values: parse as number/boolean/null directly 5. Build a native dict/map, then serialize to JSON via `json.dumps()` / `json.Marshal` The critical step is #3 — the raw content between `<|"|>` delimiters must be JSON-escaped (`\n` → `\\n`, `"` → `\"`, `\` → `\\`, etc.) before being placed into a JSON string value. ## Reproduction (minimal, no app needed) This reliably triggers the bug because `print("hello world")` contains double quotes that generate `<|"|>` escaping: ```bash curl http://localhost:11434/api/chat -d '{ "model": "gemma4:26b", "messages": [{"role": "user", "content": "use run_script to print hello world"}], "stream": false, "tools": [{ "type": "function", "function": { "name": "run_script", "description": "Execute Python code", "parameters": { "type": "object", "properties": { "code": {"type": "string", "description": "Python code"} }, "required": ["code"] } } }] }' ``` **Expected:** `tool_calls` with `print("hello world")` **Actual:** Empty content, `done_reason: "stop"`, no `tool_calls` field. The WARN log shows parsing failed. Contrast with a quote-free argument that works fine: ```bash # Same setup, but "compute 2+2" generates print(2 + 2) — no quotes, no <|"|> escaping curl ... "content": "use run_script to compute 2+2" ... ``` This returns a valid `tool_calls` response because `print(2 + 2)` has no internal quotes. ## Additional failure modes from systematic testing | User prompt | Generated code | Quotes in args? | Result | |---|---|---|---| | "compute 2+2" | `print(2 + 2)` | No | ✅ Works | | "fibonacci up to 20" | `def fibonacci(n):...` | Yes (in strings) | ✅ Works sometimes | | "print hello world" | `print("hello world")` | Yes | ❌ Fails silently | | "build a calculator app" | Full tkinter program | Many | ❌ Fails (1618 eval tokens, all thinking) | | "generate 10 random passwords" | Full script | Yes | ❌ Falls back to markdown text | The pattern: **any tool argument containing characters that need JSON escaping will fail**. Simple numeric expressions work; anything with string literals, regex patterns, shell commands, or multi-line code fails. ## References - vLLM's working parser: [`vllm/tool_parsers/gemma4_tool_parser.py`](https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/gemma4_tool_parser.py) and `gemma4_utils.py` - llama.cpp has the same issue: [ggml-org/llama.cpp#21316](https://github.com/ggml-org/llama.cpp/issues/21316) - mlx-lm has no parser at all yet: [ml-explore/mlx-lm#1096](https://github.com/ml-explore/mlx-lm/issues/1096)
Author
Owner

@drifkin commented on GitHub (Apr 6, 2026):

Thanks for reporting. These look like the model emitted invalid tool calls:

  • The first log shows is missing the gemma4-style <|"|> quotes around the value
  • The second log is missing the opening gemma4-style quote, but does have the closing one

Sometimes very small models will make mistakes in tool calling, but perhaps there are some other issues at play, we'll investigate. Depending on what we find, we may make the tool parser try to repair these bad calls. Thanks again for reporting, the logs were extremely helpful.

<!-- gh-comment-id:4189993741 --> @drifkin commented on GitHub (Apr 6, 2026): Thanks for reporting. These look like the model emitted invalid tool calls: - The first log shows is missing the gemma4-style `<|"|>` quotes around the value - The second log is missing the opening gemma4-style quote, but _does_ have the closing one Sometimes very small models will make mistakes in tool calling, but perhaps there are some other issues at play, we'll investigate. Depending on what we find, we may make the tool parser try to repair these bad calls. Thanks again for reporting, the logs were extremely helpful.
Author
Owner

@AbeEstrada commented on GitHub (Apr 6, 2026):

What is the issue?

Error while calling a tool, I'm using gemma4:26b-a4b-it-q4_K_M

I hope this is relevant and can help fix this issue

Relevant log output

time=2026-04-05T22:48:05.951-06:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '<' looking for beginning of value" content="call:bash{command:<|\"|>find . -maxdepth 3 -not -path '*/.*'</div>"

OS

macOS 15.7.4

GPU

M2 Max

CPU

M2 Max

Ollama version

0.20.2

<!-- gh-comment-id:4190415945 --> @AbeEstrada commented on GitHub (Apr 6, 2026): ### What is the issue? Error while calling a tool, I'm using `gemma4:26b-a4b-it-q4_K_M` I hope this is relevant and can help fix this issue ### Relevant log output ```shell time=2026-04-05T22:48:05.951-06:00 level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '<' looking for beginning of value" content="call:bash{command:<|\"|>find . -maxdepth 3 -not -path '*/.*'</div>" ``` ### OS macOS 15.7.4 ### GPU M2 Max ### CPU M2 Max ### Ollama version 0.20.2
Author
Owner

@AdriRayns commented on GitHub (Apr 6, 2026):

I've also been dealing with that error all weekend while running the 31b model. I ran a lot of tests to see if I could adjust the behavior using the modelfile, stops, templates, temperature, and a prompt to “force” it to make the tool calls correctly... but nothing worked. I saw that there were open issues over the weekend, and I figured it was something “common” to other users.

<!-- gh-comment-id:4192459917 --> @AdriRayns commented on GitHub (Apr 6, 2026): I've also been dealing with that error all weekend while running the 31b model. I ran a lot of tests to see if I could adjust the behavior using the modelfile, stops, templates, temperature, and a prompt to “force” it to make the tool calls correctly... but nothing worked. I saw that there were open issues over the weekend, and I figured it was something “common” to other users.
Author
Owner

@BaneusCatrix commented on GitHub (Apr 6, 2026):

Same:

time=2026-04-06T19:16:04.667Z level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '<' looking for beginning of value" content="call:bash{command:<|\"|>ls}"

ollama: 0.20.2
model: gemma4:26b
os: cachyos
gpu: Nvidia RTX 4080

<!-- gh-comment-id:4194547092 --> @BaneusCatrix commented on GitHub (Apr 6, 2026): Same: `time=2026-04-06T19:16:04.667Z level=WARN source=gemma4.go:293 msg="gemma4 tool call parsing failed" error="invalid character '<' looking for beginning of value" content="call:bash{command:<|\"|>ls}"` ollama: 0.20.2 model: gemma4:26b os: cachyos gpu: Nvidia RTX 4080
Author
Owner

@aalzehla commented on GitHub (Apr 6, 2026):

same issue here
MacBook Pro M1
LM Studio 0.4.9+1
Model: gemma4:26b

<!-- gh-comment-id:4194582977 --> @aalzehla commented on GitHub (Apr 6, 2026): same issue here MacBook Pro M1 LM Studio 0.4.9+1 Model: gemma4:26b
Author
Owner

@AdriRayns commented on GitHub (Apr 7, 2026):

I can confirm that the new version 0.20.3 resolves this issue (which was discussed at https://github.com/ollama/ollama/issues/15241)

I think this issue should be closed.

<!-- gh-comment-id:4200380353 --> @AdriRayns commented on GitHub (Apr 7, 2026): I can confirm that the new version 0.20.3 resolves this issue (which was discussed at https://github.com/ollama/ollama/issues/15241) I think this issue should be closed.
Author
Owner

@kevincox commented on GitHub (Apr 7, 2026):

Forgive me if I am missing something, but isn't this being handled at the wrong later. IIUC Gemma 4 has a specific token for tool call arguments. But this token is currently being mapped to the text <|"|> then this text is being parsed for cases of that replacement string.

But this means that the argument parsing will also interpret other tokens that "evaluate" to <|"|> as tool call argument delimiters. I think this will cause issues if the delimiter appears inside of the tool call.

It seems that Google did something really clever here to make the delimiters clear and unambiguous but the way Ollama is handling this defeats that by doing this logic after converting from tokens to strings rather than before.

So the new patch probably "mostly works" but isn't fully correct.

<!-- gh-comment-id:4200424999 --> @kevincox commented on GitHub (Apr 7, 2026): Forgive me if I am missing something, but isn't this being handled at the wrong later. IIUC Gemma 4 has a specific token for tool call arguments. But this token is currently being mapped to the text `<|"|>` then this text is being parsed for cases of that replacement string. But this means that the argument parsing will also interpret other tokens that "evaluate" to `<|"|>` as tool call argument delimiters. I think this will cause issues if the delimiter appears inside of the tool call. It seems that Google did something really clever here to make the delimiters clear and unambiguous but the way Ollama is handling this defeats that by doing this logic after converting from tokens to strings rather than before. So the new patch probably "mostly works" but isn't fully correct.
Author
Owner

@kevincox commented on GitHub (Apr 7, 2026):

Also I'm still getting issues with tool calls on 0.20.3 It may be a touch better, but still fails very often on gemma4:e4b.

Apr 07 13:09:59 kevinryzen ollama[402661]: time=2026-04-07T13:09:59.783-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="expected '{' in tool call" content="call:ls -F src/<|\"|>,description:<|\"|>List files and directories inside the src directory<|\"|>}"
Apr 07 13:10:00 kevinryzen ollama[402661]: time=2026-04-07T13:10:00.818-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="expected '{' in tool call" content="call:ls -F web/<|\"|>,description:<|\"|>List files and directories inside the web directory<|\"|>}"
<!-- gh-comment-id:4200887809 --> @kevincox commented on GitHub (Apr 7, 2026): Also I'm still getting issues with tool calls on 0.20.3 It may be a touch better, but still fails very often on `gemma4:e4b`. ``` Apr 07 13:09:59 kevinryzen ollama[402661]: time=2026-04-07T13:09:59.783-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="expected '{' in tool call" content="call:ls -F src/<|\"|>,description:<|\"|>List files and directories inside the src directory<|\"|>}" Apr 07 13:10:00 kevinryzen ollama[402661]: time=2026-04-07T13:10:00.818-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="expected '{' in tool call" content="call:ls -F web/<|\"|>,description:<|\"|>List files and directories inside the web directory<|\"|>}" ```
Author
Owner

@drifkin commented on GitHub (Apr 7, 2026):

Hi @kevincox you're right that operating at the token-level rather than string-level is more correct, and I'm similarly very happy about this new special token approach, escaping is one of the trickiest issues for models and I think this will ultimately help that issue a lot. Switching everything to token-based will be fairly invasive, but it is on my list of things to investigate. However, I don't think that this is the root cause of any of the issues in this thread, or any failed tool calls I've looked at so far. They've all been from actually malformed tool calls. (Though I'm sure I could easily repro the token v. string problem by trying to use gemma4 to work on this gemma4 parser, for example!)

Thanks for the additional two logs, I'll work on a repair for those as well. Notice that they're not ambiguous, but rather malformed:

call:ls -F web/<|\"|>,

is missing the opening <|"|>, but has a closing one.

<!-- gh-comment-id:4201114803 --> @drifkin commented on GitHub (Apr 7, 2026): Hi @kevincox you're right that operating at the token-level rather than string-level is more correct, and I'm similarly very happy about this new special token approach, escaping is one of the trickiest issues for models and I think this will ultimately help that issue a lot. Switching everything to token-based will be fairly invasive, but it is on my list of things to investigate. However, I don't think that this is the root cause of any of the issues in this thread, or any failed tool calls I've looked at so far. They've all been from actually malformed tool calls. (Though I'm sure I could easily repro the token v. string problem by trying to use gemma4 to work on this gemma4 parser, for example!) Thanks for the additional two logs, I'll work on a repair for those as well. Notice that they're not ambiguous, but rather malformed: ``` call:ls -F web/<|\"|>, ``` is missing the opening `<|"|>`, but has a closing one.
Author
Owner

@drifkin commented on GitHub (Apr 7, 2026):

@kevincox: I took a closer look and those examples are more malformed than I thought. They're actually missing the tool name itself (e.g., if the intended tool name wasbash, then call:bash{command:<|"|>ls -F web/<|"|>,description:<|"|>...<|"|>})

I wonder if you can provide more example logs to see if there are any other repairable cases or if they're all in this very malformed state? Or if you have a straightforward repro, that would be helpful too.

<!-- gh-comment-id:4202751868 --> @drifkin commented on GitHub (Apr 7, 2026): @kevincox: I took a closer look and those examples are more malformed than I thought. They're actually missing the tool name itself (e.g., if the intended tool name was`bash`, then `call:bash{command:<|"|>ls -F web/<|"|>,description:<|"|>...<|"|>}`) I wonder if you can provide more example logs to see if there are any other repairable cases or if they're all in this very malformed state? Or if you have a straightforward repro, that would be helpful too.
Author
Owner

@kevincox commented on GitHub (Apr 7, 2026):

I can't seem to find any super simple cases. I'm mostly using opencode, I'm trying to figure out what logging is available there. I found a different case here:

Apr 07 19:33:02 kevinryzen ollama[402661]: time=2026-04-07T19:33:02.146-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'r' looking for beginning of value\nrepair failed to produce valid JSON arguments" content="call:edit{filePath:<|\"|>src/robot_test.rs<|\"|>,newString:r#\"#[cfg(test)]\nmod tests {\n    use super::Robot;\n\n    #[test]\n    fn test_from_str_all_colors() {\n        // Test known string representations\n        assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n\n        assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n\n        assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n\n        assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n\n        assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n    }\n\n    #[test]\n    fn test_from_str_unknown() {\n        // Test unknown or malformed strings\n        assert_eq!(Robot::from_str(\"5\").unwrap_err(), \"Unknown robot\");\n        assert_eq!(Robot::from_str(\"unknown_color\").unwrap_err(), \"Unknown robot\");\n    }\n\n     #[test]\n    fn test_from_u8_conversion() {\n        assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n    }\n}\n\"#,oldString:r#\"#[cfg(test)]\nmod robot_test {\n    use super::Robot;\n\n    // Helper function to test FromStr implementation coverage\n    #[test]\n    fn test_from_str_black() {\n        assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n    }\n\n    #[test]\n    fn test_from_str_red() {\n        assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n    }\n\n    #[test]\n    fn test_from_str_green() {\n        assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n    }\n\n    #[test]\n    fn test_from_str_yellow() {\n        assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n    }\n\n    #[test]\n    fn test_from_str_blue() {\n        assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n    }\n\n    #[test]\n    fn test_from_str_unknown() {\n        // Test unknown or malformed strings\n        assert!(Robot::from_str(\"5\").is_err());\n        assert!(Robot::from_str(\"unknown_color\").is_err());\n    }\n\n    // Test the u8 conversion\n    #[test]\n    fn test_from_u8() {\n        assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n        // Test an invalid u8 value which should fail (assuming the TryFrom implementation correctly handles this, \n        // current implementation relies on ROBOTS array size, testing failure mode directly is hard without access to internal logic)\n    }\n}\n\"#,oldString:r#\"#[cfg(test)]\nmod robot_test {\n    use super::Robot;\n\n    // Helper function to test FromStr implementation coverage\n    #[test]\n    fn test_from_str_black() {\n        assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n    }\n\n    #[test]\n    fn test_from_str_red() {\n        assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n        assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n    }\n\n    #[test]\n    fn test_from_str_green() {\n        assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n        assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n    }\n\n    #[test]\n    fn test_from_str_yellow() {\n        assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n        assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n    }\n\n    #[test]\n    fn test_from_str_blue() {\n        assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n        assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n    }\n\n    #[test]\n    fn test_from_str_unknown() {\n        // Test unknown or malformed strings\n        assert!(Robot::from_str(\"5\").is_err());\n        assert!(Robot::from_str(\"unknown_color\").is_err());\n    }\n\n    // Test the u8 conversion\n    #[test]\n    fn test_from_u8() {\n        assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n        assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n        // Test an invalid u8 value which should fail (assuming the TryFrom implementation correctly handles this, \n        // current implementation relies on ROBOTS array size, testing failure mode directly is hard without access to internal logic)\n    }\n}\n\"#,replaceAll:true}"

It does seem unexpected that tool calling is so inconsistent. I know it is a fairly small model but it seems that this behaviour should be well baked in. I'll try to gather more evidence and post soon.

<!-- gh-comment-id:4202829551 --> @kevincox commented on GitHub (Apr 7, 2026): I can't seem to find any super simple cases. I'm mostly using opencode, I'm trying to figure out what logging is available there. I found a different case here: ``` Apr 07 19:33:02 kevinryzen ollama[402661]: time=2026-04-07T19:33:02.146-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'r' looking for beginning of value\nrepair failed to produce valid JSON arguments" content="call:edit{filePath:<|\"|>src/robot_test.rs<|\"|>,newString:r#\"#[cfg(test)]\nmod tests {\n use super::Robot;\n\n #[test]\n fn test_from_str_all_colors() {\n // Test known string representations\n assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n\n assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n\n assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n\n assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n\n assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n }\n\n #[test]\n fn test_from_str_unknown() {\n // Test unknown or malformed strings\n assert_eq!(Robot::from_str(\"5\").unwrap_err(), \"Unknown robot\");\n assert_eq!(Robot::from_str(\"unknown_color\").unwrap_err(), \"Unknown robot\");\n }\n\n #[test]\n fn test_from_u8_conversion() {\n assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n }\n}\n\"#,oldString:r#\"#[cfg(test)]\nmod robot_test {\n use super::Robot;\n\n // Helper function to test FromStr implementation coverage\n #[test]\n fn test_from_str_black() {\n assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n }\n\n #[test]\n fn test_from_str_red() {\n assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n }\n\n #[test]\n fn test_from_str_green() {\n assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n }\n\n #[test]\n fn test_from_str_yellow() {\n assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n }\n\n #[test]\n fn test_from_str_blue() {\n assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n }\n\n #[test]\n fn test_from_str_unknown() {\n // Test unknown or malformed strings\n assert!(Robot::from_str(\"5\").is_err());\n assert!(Robot::from_str(\"unknown_color\").is_err());\n }\n\n // Test the u8 conversion\n #[test]\n fn test_from_u8() {\n assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n // Test an invalid u8 value which should fail (assuming the TryFrom implementation correctly handles this, \n // current implementation relies on ROBOTS array size, testing failure mode directly is hard without access to internal logic)\n }\n}\n\"#,oldString:r#\"#[cfg(test)]\nmod robot_test {\n use super::Robot;\n\n // Helper function to test FromStr implementation coverage\n #[test]\n fn test_from_str_black() {\n assert_eq!(Robot::from_str(\"0\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"k\").unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_str(\"black\").unwrap(), Robot::BLACK);\n }\n\n #[test]\n fn test_from_str_red() {\n assert_eq!(Robot::from_str(\"1\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"r\").unwrap(), Robot::RED);\n assert_eq!(Robot::from_str(\"red\").unwrap(), Robot::RED);\n }\n\n #[test]\n fn test_from_str_green() {\n assert_eq!(Robot::from_str(\"2\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"g\").unwrap(), Robot::GREEN);\n assert_eq!(Robot::from_str(\"green\").unwrap(), Robot::GREEN);\n }\n\n #[test]\n fn test_from_str_yellow() {\n assert_eq!(Robot::from_str(\"3\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"y\").unwrap(), Robot::YELLOW);\n assert_eq!(Robot::from_str(\"yellow\").unwrap(), Robot::YELLOW);\n }\n\n #[test]\n fn test_from_str_blue() {\n assert_eq!(Robot::from_str(\"4\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"u\").unwrap(), Robot::BLUE);\n assert_eq!(Robot::from_str(\"blue\").unwrap(), Robot::BLUE);\n }\n\n #[test]\n fn test_from_str_unknown() {\n // Test unknown or malformed strings\n assert!(Robot::from_str(\"5\").is_err());\n assert!(Robot::from_str(\"unknown_color\").is_err());\n }\n\n // Test the u8 conversion\n #[test]\n fn test_from_u8() {\n assert_eq!(Robot::from_u8(0).unwrap(), Robot::BLACK);\n assert_eq!(Robot::from_u8(4).unwrap(), Robot::BLUE);\n // Test an invalid u8 value which should fail (assuming the TryFrom implementation correctly handles this, \n // current implementation relies on ROBOTS array size, testing failure mode directly is hard without access to internal logic)\n }\n}\n\"#,replaceAll:true}" ``` It does seem unexpected that tool calling is so inconsistent. I know it is a fairly small model but it seems that this behaviour should be well baked in. I'll try to gather more evidence and post soon.
Author
Owner

@drifkin commented on GitHub (Apr 7, 2026):

thanks so much! super helpful. I'll see if I can repro in opencode as well.

<!-- gh-comment-id:4202861056 --> @drifkin commented on GitHub (Apr 7, 2026): thanks so much! super helpful. I'll see if I can repro in opencode as well.
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

Opencode's instructions to enable debug logging don't seem to work, so I don't now how to get a reproducable example. But I'm trying to get a working example with just the Ollama API. For now I'll log failed tool calls in this message.

time=2026-04-07T20:05:17.165-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx,vue}\", path: \".\"}"
<!-- gh-comment-id:4202943931 --> @kevincox commented on GitHub (Apr 8, 2026): Opencode's instructions to enable debug logging don't seem to work, so I don't now how to get a reproducable example. But I'm trying to get a working example with just the Ollama API. For now I'll log failed tool calls in this message. ``` time=2026-04-07T20:05:17.165-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx,vue}\", path: \".\"}" ```
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

Ok, found a relatively small example:

curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{
	"model": "gemma4:e4b",
	"messages": [
		{"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
		{"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun."},
			{"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."}
	],
	"stream": false,
	"tools": [{
		"type": "function",
		"function": {
			"name": "write_file",
			"description": "Write the data to a file.",
			"parameters": {
				"type": "object",
				"required": ["file", "contents"],
				"properties": {
					"file": {"type": "string", "description": "The path of the file"},
					"contents": {"type": "string", "description": "The new contents of the file"}
				}
			}
		}
	}, {
		"name": "glob",
		"description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
		"parameters": {
			"type": "object",
			"properties": {
				"path": {
					"description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
					"type": "string"
				},
				"pattern": {
					"description": "The glob pattern to match files against",
					"type": "string",
					"required": "true"
				}
			},
			"required": [
				"pattern"
			]
		}
	}],
	"options": {
		"seed": 0
	}
}'
<!-- gh-comment-id:4203014394 --> @kevincox commented on GitHub (Apr 8, 2026): Ok, found a relatively small example: ``` curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun."}, {"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."} ], "stream": false, "tools": [{ "type": "function", "function": { "name": "write_file", "description": "Write the data to a file.", "parameters": { "type": "object", "required": ["file", "contents"], "properties": { "file": {"type": "string", "description": "The path of the file"}, "contents": {"type": "string", "description": "The new contents of the file"} } } } }, { "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "properties": { "path": { "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.", "type": "string" }, "pattern": { "description": "The glob pattern to match files against", "type": "string", "required": "true" } }, "required": [ "pattern" ] } }], "options": { "seed": 0 } }' ```
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

This has a different failure mode:

curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{
        "model": "gemma4:e4b",
        "messages": [
                {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
                {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun."}
        ],
        "stream": false,
        "tools": [{
                "name": "glob",
                "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
                "parameters": {
                        "type": "object",
                        "properties": {
                                "path": {
                                        "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
                                        "type": "string"
                                },
                                "pattern": {
                                        "description": "The glob pattern to match files against",
                                        "type": "string",
                                        "required": "true"
                                }
                        },
                        "required": [
                                "pattern"
                        ]
                }
        }],
        "options": {
                "seed": 4
        }
}'
time=2026-04-07T20:35:49.967-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '<' after object key:value pair\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*web-ui*js\"<|\"|>}"
time=2026-04-07T20:35:50.449-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '<' after object key:value pair\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*solver*js\"<|\"|>}"
<!-- gh-comment-id:4203042892 --> @kevincox commented on GitHub (Apr 8, 2026): This has a different failure mode: ``` curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun."} ], "stream": false, "tools": [{ "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "properties": { "path": { "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.", "type": "string" }, "pattern": { "description": "The glob pattern to match files against", "type": "string", "required": "true" } }, "required": [ "pattern" ] } }], "options": { "seed": 4 } }' ``` ``` time=2026-04-07T20:35:49.967-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '<' after object key:value pair\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*web-ui*js\"<|\"|>}" time=2026-04-07T20:35:50.449-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '<' after object key:value pair\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*solver*js\"<|\"|>}" ```
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{
        "model": "gemma4:e4b",
        "messages": [
                {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
                {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."}
        ],
        "stream": false,
        "options": {
                "seed": 15
        }
}'
time=2026-04-07T20:41:49.652-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="unexpected end of JSON input\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx}''}"
<!-- gh-comment-id:4203067925 --> @kevincox commented on GitHub (Apr 8, 2026): ``` curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."} ], "stream": false, "options": { "seed": 15 } }' ``` ``` time=2026-04-07T20:41:49.652-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="unexpected end of JSON input\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx}''}" ```
Author
Owner

@drifkin commented on GitHub (Apr 8, 2026):

@kevincox thanks so much! I was able to repro (though for me I had to use a different seed)

these look much more repairable, so I'll take care of these at least

<!-- gh-comment-id:4203068346 --> @drifkin commented on GitHub (Apr 8, 2026): @kevincox thanks so much! I was able to repro (though for me I had to use a different seed) these look much more repairable, so I'll take care of these at least
Author
Owner

@drifkin commented on GitHub (Apr 8, 2026):

One thing that's interesting about all these examples is that "tools" is either not provided, or it contains invalid entries (e.g., the first one has a valid tool definition for write_file, but then glob is missing the { "type": "function", "function}: { ... } } "wrapper".

Beyond us needing better warnings instead of silently failing to parse unexpected tool shapes, I wonder if this could be the main source of these remaining malformed tools: if the model is told to make a tool call, but doesn't "see" the definition of such a tool, it starts hallucinating and is worse at following its trained format. Your final example is particularly helpful since it only defines the tool in the system message, rather than in the format it expects.

If this is the case, and you're still seeing issues with opencode, I wonder if there's perhaps a bug somewhere that's dropping some of their tool definitions (or they're incorrect?). Or it could just be that this is an easier form to try to get a repro for

<!-- gh-comment-id:4203193293 --> @drifkin commented on GitHub (Apr 8, 2026): One thing that's interesting about all these examples is that `"tools"` is either not provided, or it contains invalid entries (e.g., the first one has a valid tool definition for `write_file`, but then `glob` is missing the `{ "type": "function", "function}: { ... } }` "wrapper". Beyond us needing better warnings instead of silently failing to parse unexpected tool shapes, I wonder if this could be the main source of these remaining malformed tools: if the model is told to make a tool call, but doesn't "see" the definition of such a tool, it starts hallucinating and is worse at following its trained format. Your final example is particularly helpful since it only defines the tool in the system message, rather than in the format it expects. If this is the case, and you're still seeing issues with opencode, I wonder if there's perhaps a bug somewhere that's dropping some of their tool definitions (or they're incorrect?). Or it could just be that this is an easier form to try to get a repro for
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

Those are surely not the case with opencode. That was just me trying to make an API call too quickly. It seems that it doesn't really matter if the definition is there or not. I noticed the error when I switched to trying llama.cpp (which doesn't seem to have tool call issues at all, but I need more testing to say for sure). So I would assume it is a red herring.

<!-- gh-comment-id:4203215422 --> @kevincox commented on GitHub (Apr 8, 2026): Those are surely not the case with opencode. That was just me trying to make an API call too quickly. It seems that it doesn't really matter if the definition is there or not. I noticed the error when I switched to trying llama.cpp (which doesn't seem to have tool call issues at all, but I need more testing to say for sure). So I would assume it is a red herring.
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

Yeah, found a case with a valid tool call definition (I think)

curl -if "http://localhost:11434/api/chat?seed=$seed" -H "Content-Type: application/json" -d '{
  "model": "gemma4:e4b",
  "messages": [
        {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
        {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."},
        {"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."}
        ],
  "stream": false,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "write_file",
        "description": "Write the data to a file.",
        "parameters": {
          "type": "object",
          "required": ["file", "contents"],
          "properties": {
            "file": {"type": "string", "description": "The path of the file"},
            "contents": {"type": "string", "description": "The new contents of the file"}
          }
        }
      }
    },
{
      "type": "function",
      "function": {
  "name": "glob",
  "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
  "parameters": {
    "type": "object",
    "parameters": {
      "path": {
        "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
        "type": "string"
      },
      "pattern": {
        "description": "The glob pattern to match files against",
        "type": "string",
        "required": true
      }
    },
    "required": [
      "pattern"
    ]
  }}
}
  ],
  "options": {
    "seed": 19
  }
}'
time=2026-04-07T21:35:01.840-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*/*.js\", path: \"src\"}"

Might have been coincidence but it did take a lot longer to find a seed. So maybe this makes the model less likely to fail the call.

<!-- gh-comment-id:4203232956 --> @kevincox commented on GitHub (Apr 8, 2026): Yeah, found a case with a valid tool call definition (I think) ``` curl -if "http://localhost:11434/api/chat?seed=$seed" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."}, {"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."} ], "stream": false, "tools": [ { "type": "function", "function": { "name": "write_file", "description": "Write the data to a file.", "parameters": { "type": "object", "required": ["file", "contents"], "properties": { "file": {"type": "string", "description": "The path of the file"}, "contents": {"type": "string", "description": "The new contents of the file"} } } } }, { "type": "function", "function": { "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "parameters": { "path": { "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.", "type": "string" }, "pattern": { "description": "The glob pattern to match files against", "type": "string", "required": true } }, "required": [ "pattern" ] }} } ], "options": { "seed": 19 } }' ``` ``` time=2026-04-07T21:35:01.840-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*/*.js\", path: \"src\"}" ``` Might have been coincidence but it did take a lot longer to find a seed. So maybe this makes the model less likely to fail the call.
Author
Owner

@drifkin commented on GitHub (Apr 8, 2026):

The arguments belong under function.parameters.properties instead of function.parameters.parameters, so this example still only has the tool partially defined. Here's a corrected one:

curl -if "http://localhost:11434/api/chat" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemma4:e4b",
    "messages": [
      {
        "role": "system",
        "content": "You have a `glob` tool available."
      },
      {
        "role": "user",
        "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."
      },
      {
        "role": "assistant",
        "content": "The files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that are not directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add flashy animations to the solver part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that is where core application logic often resides, or check for a `components/` directory if one exists. Since there is a `src/` directory, I will explore that for components related to the game or solving."
      }
    ],
    "stream": false,
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "write_file",
          "description": "Write the data to a file.",
          "parameters": {
            "type": "object",
            "properties": {
              "file": {
                "type": "string",
                "description": "The path of the file"
              },
              "contents": {
                "type": "string",
                "description": "The new contents of the file"
              }
            },
            "required": ["file", "contents"]
          }
        }
      },
      {
        "type": "function",
        "function": {
          "name": "glob",
          "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
          "parameters": {
            "type": "object",
            "properties": {
              "path": {
                "type": "string",
                "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided."
              },
              "pattern": {
                "type": "string",
                "description": "The glob pattern to match files against"
              }
            },
            "required": ["pattern"]
          }
        }
      }
    ],
    "options": {
      "seed": 19
    }
  }'
<!-- gh-comment-id:4203426162 --> @drifkin commented on GitHub (Apr 8, 2026): The arguments belong under `function.parameters.properties` instead of `function.parameters.parameters`, so this example still only has the tool partially defined. Here's a corrected one: <details> ``` curl -if "http://localhost:11434/api/chat" \ -H "Content-Type: application/json" \ -d '{ "model": "gemma4:e4b", "messages": [ { "role": "system", "content": "You have a `glob` tool available." }, { "role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project." }, { "role": "assistant", "content": "The files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that are not directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add flashy animations to the solver part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that is where core application logic often resides, or check for a `components/` directory if one exists. Since there is a `src/` directory, I will explore that for components related to the game or solving." } ], "stream": false, "tools": [ { "type": "function", "function": { "name": "write_file", "description": "Write the data to a file.", "parameters": { "type": "object", "properties": { "file": { "type": "string", "description": "The path of the file" }, "contents": { "type": "string", "description": "The new contents of the file" } }, "required": ["file", "contents"] } } }, { "type": "function", "function": { "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided." }, "pattern": { "type": "string", "description": "The glob pattern to match files against" } }, "required": ["pattern"] } } } ], "options": { "seed": 19 } }' ``` </details>
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

Thanks for the fixing. Some better validation here would definitely be a UX improvement :)

But after a bit of seed-searching I quickly found an example:

curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{
  "model": "gemma4:e4b",
  "messages": [
        {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
        {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."},
        {"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."}
        ],
  "stream": false,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "write_file",
        "description": "Write the data to a file.",
        "parameters": {
          "type": "object",
          "required": ["file", "contents"],
          "properties": {
            "file": {"type": "string", "description": "The path of the file"},
            "contents": {"type": "string", "description": "The new contents of the file"}
          }
        }
      }
    },
{
      "type": "function",
      "function": {
  "name": "glob",
  "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
  "parameters": {
    "type": "object",
    "parameters": {
      "path": {
        "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
        "type": "string"
      },
      "pattern": {
        "description": "The glob pattern to match files against",
        "type": "string",
        "required": true
      }
    },
    "required": [
      "pattern"
    ]
  }}
}
  ],
  "options": {
    "seed": 25
  }
}'
time=2026-04-08T08:05:01.998-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '\\'' after top-level value\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.js\"}'}"
<!-- gh-comment-id:4206098119 --> @kevincox commented on GitHub (Apr 8, 2026): Thanks for the fixing. Some better validation here would definitely be a UX improvement :) But after a bit of seed-searching I quickly found an example: ``` curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."}, {"role": "assistant", "content": "he files suggest `web/index.html` is the entry point, and `web/service-worker.js` handles caching. The actual UI and game logic components are likely managed by JavaScript/JSX/TSX files that aren''t directly in the root `web/` folder, but perhaps loaded dynamically or in a different directory relative to service-worker.js.\n\nTo add *flashy animations* to the *solver* part of the web UI, I need to find the main component that renders the game board/solver interface.\n\nI will check the `src/` directory now, as that''s where core application logic often resides, or check for a `components/` directory if one exists. Since there''s a `src/` directory, I''ll explore that for components related to the game or solving."} ], "stream": false, "tools": [ { "type": "function", "function": { "name": "write_file", "description": "Write the data to a file.", "parameters": { "type": "object", "required": ["file", "contents"], "properties": { "file": {"type": "string", "description": "The path of the file"}, "contents": {"type": "string", "description": "The new contents of the file"} } } } }, { "type": "function", "function": { "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "parameters": { "path": { "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.", "type": "string" }, "pattern": { "description": "The glob pattern to match files against", "type": "string", "required": true } }, "required": [ "pattern" ] }} } ], "options": { "seed": 25 } }' ``` ``` time=2026-04-08T08:05:01.998-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '\\'' after top-level value\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.js\"}'}" ```
Author
Owner

@kevincox commented on GitHub (Apr 8, 2026):

And a slightly cut-down case:

curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{
  "model": "gemma4:e4b",
  "messages": [
    {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"},
    {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."}
  ],
  "stream": false,
  "tools": [{
    "type": "function",
    "function": {
      "name": "glob",
      "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead",
      "parameters": {
        "type": "object",
        "parameters": {
          "path": {
            "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
            "type": "string"
          },
          "pattern": {
            "description": "The glob pattern to match files against",
            "type": "string",
            "required": true
          }
        },
        "required": [
          "pattern"
        ]
      }
    }
  }],
  "options": {
    "seed": 68
  }
}'
time=2026-04-08T08:21:34.880-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx}\", path: \".\"}"

But I really don't think the specific examples are too important. It just shows that cases like this are easy to hit. I've also seen examples with other tool calls.

<!-- gh-comment-id:4206187707 --> @kevincox commented on GitHub (Apr 8, 2026): And a slightly cut-down case: ``` curl -if "http://localhost:11434/api/chat" -H "Content-Type: application/json" -d '{ "model": "gemma4:e4b", "messages": [ {"role": "system", "content": "You have a `glob` tool available: { \"name\": \"glob\", \"description\": \"Fast file pattern matching tool that works with any codebase size\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\"\\n- Returns matching file paths sorted by modification time\\n- Use this tool when you need to find files by name patterns\\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\", \"parameters\": { \"type\": \"object\", \"properties\": { \"path\": { \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\\"undefined\\\" or \\\"null\\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\", \"type\": \"string\" }, \"pattern\": { \"description\": \"The glob pattern to match files against\", \"type\": \"string\", \"required\": \"true\" } }, \"required\": [ \"pattern\" ] } }"}, {"role": "user", "content": "Look at the web-ui and add a few flashy animations to make using the solver to play games more fun. Start by using your `glob` tool to scope out the files in the project."} ], "stream": false, "tools": [{ "type": "function", "function": { "name": "glob", "description": "Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead", "parameters": { "type": "object", "parameters": { "path": { "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.", "type": "string" }, "pattern": { "description": "The glob pattern to match files against", "type": "string", "required": true } }, "required": [ "pattern" ] } } }], "options": { "seed": 68 } }' ``` ``` time=2026-04-08T08:21:34.880-04:00 level=WARN source=gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character 'p' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:glob{pattern: \"**/*.{js,jsx,ts,tsx}\", path: \".\"}" ``` But I really don't think the specific examples are too important. It just shows that cases like this are easy to hit. I've also seen examples with other tool calls.
Author
Owner

@warabe1122 commented on GitHub (Apr 9, 2026):

Environment

  • macOS Tahoe 26.3.1 (a)
  • Mac Studio M2 Max 64GB
  • Ollama 0.20.4
  • Model: gemma4:26b (Q4_K_M)
  • Client: OpenClaw (openclaw 2026.4.1) using /api/chat

Reproduction

Using OpenClaw's writer agent with gemma4:26b to edit a ~15K-character Markdown file. The model generates correct edit content but the tool call JSON parsing fails every time.

Log output (Ollama 0.20.4)

Attempt 1

gemma4.go:299 msg="gemma4 tool call parsing failed"
error="invalid character '<' after object key:value pair"

Attempt 2

gemma4.go:299 msg="gemma4 tool call parsing failed"
error="invalid character ']' after object key:value pair\nrepair failed to produce valid JSON arguments"

In both cases the raw content field shows Gemma4 using its native <|"|> string delimiters inside call:edit{edits:[{newText:<|"|>...

Key observation

The model IS generating semantically correct edits. The content inside newText / oldText is valid and relevant. The failure is in Ollama's gemma4.go parser not handling the <|"|> string delimiters that Gemma4 uses in its native tool call format.

This is blocking all file-editing tool calls with gemma4:26b, making it unusable as an agentic writer despite the model's actual capability being fine.

Updated from 0.20.0 to 0.20.4, same issue persists.

<!-- gh-comment-id:4212573357 --> @warabe1122 commented on GitHub (Apr 9, 2026): ## Environment - macOS Tahoe 26.3.1 (a) - Mac Studio M2 Max 64GB - Ollama 0.20.4 - Model: gemma4:26b (Q4_K_M) - Client: OpenClaw (openclaw 2026.4.1) using `/api/chat` ## Reproduction Using OpenClaw's writer agent with gemma4:26b to edit a ~15K-character Markdown file. The model generates correct edit content but the tool call JSON parsing fails every time. ## Log output (Ollama 0.20.4) ### Attempt 1 ``` gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character '<' after object key:value pair" ``` ### Attempt 2 ``` gemma4.go:299 msg="gemma4 tool call parsing failed" error="invalid character ']' after object key:value pair\nrepair failed to produce valid JSON arguments" ``` In both cases the raw `content` field shows Gemma4 using its native `<|"|>` string delimiters inside `call:edit{edits:[{newText:<|"|>...` ## Key observation The model IS generating semantically correct edits. The content inside `newText` / `oldText` is valid and relevant. The failure is in Ollama's `gemma4.go` parser not handling the `<|"|>` string delimiters that Gemma4 uses in its native tool call format. This is blocking all file-editing tool calls with gemma4:26b, making it unusable as an agentic writer despite the model's actual capability being fine. Updated from 0.20.0 to 0.20.4, same issue persists.
Author
Owner

@drifkin commented on GitHub (Apr 9, 2026):

Key observation

The model IS generating semantically correct edits. The content inside newText / oldText is valid and relevant. The failure is in Ollama's gemma4.go parser not handling the <|"|> string delimiters that Gemma4 uses in its native tool call format.

This is blocking all file-editing tool calls with gemma4:26b, making it unusable as an agentic writer despite the model's actual capability being fine.

Updated from 0.20.0 to 0.20.4, same issue persists.

Do you have an example raw call so I can check whether it is indeed well-formed? And if it is, then I can fix whatever gap the parser has (this would be the first well-formed example provided in this thread that misparses).

<!-- gh-comment-id:4216027875 --> @drifkin commented on GitHub (Apr 9, 2026): > ## Key observation > The model IS generating semantically correct edits. The content inside `newText` / `oldText` is valid and relevant. The failure is in Ollama's `gemma4.go` parser not handling the `<|"|>` string delimiters that Gemma4 uses in its native tool call format. > > This is blocking all file-editing tool calls with gemma4:26b, making it unusable as an agentic writer despite the model's actual capability being fine. > > Updated from 0.20.0 to 0.20.4, same issue persists. Do you have an example raw call so I can check whether it is indeed well-formed? And if it is, then I can fix whatever gap the parser has (this would be the first well-formed example provided in this thread that misparses).
Author
Owner

@warabe1122 commented on GitHub (Apr 10, 2026):

Thanks for jumping on this! Here are captured raw parser failures from my logs. Environment:

  • Ollama 0.20.4 (same issue on 0.20.0)
  • gemma4:26b Q4_K_M
  • macOS / M2 Max
  • Agent harness: openclaude / OpenClaw writer agent editing markdown files

Note: I've since uninstalled gemma4:26b, so I can't capture fresh traffic right now, but the content= field in the warnings below is the exact text the parser was given. Happy to reinstall and capture a full /api/chat request/response pair on whichever pattern is most useful.

Pattern A — simple exec, embedded quotes

[gemma4.go:287] invalid character 'v' after object key:value pair
content: call:exec{command:<|"|>ls -F "vault/"<|"|>}
[gemma4.go:287] invalid character '"' after object key:value pair
content: call:exec{command:<|"|>summarize "https://www.youtube.com/watch?v=Rl9CF8mslaQ" --youtube auto --extract<|"|>}

Semantically both are correct — ls -F "vault/" and the summarize invocation are exactly what I asked for. The gap looks specific to <|"|>-delimited string values that themselves contain ".

Pattern C — file edit, edits:[{newText, oldText}, ...]

This is the one that made gemma4:26b unusable as a writer agent. Note it's hitting gemma4.go:299 and the error mentions the repair path, so it's going through repair and still failing:

[gemma4.go:299] invalid character ']' after object key:value pair
                repair failed to produce valid JSON arguments
timestamp: 2026-04-09T16:49:47+09:00
content: call:edit{edits:[
  {newText:<|"|># Gemma4 + openclaude speed optimization guide...<|"|>,
   oldText:<|"|># Gemma4 + openclaude speed optimization guide...<|"|>},
  {newText:<|"|>## 14. Methods tried but not adopted\n\n### 1. Model quantization...<|"|>,
   oldText:<|"|>## 14. Methods tried but not adopted\n<|"|>}
]}

Semantically this is a perfectly valid {newText, oldText} edit pair list targeting a markdown file. The gap looks like (a) <|"|>-delimited strings containing newlines and ", combined with (b) the array-of-objects shape around edits:[...]. I have multiple captures of this exact pattern — happy to drop the raw log lines in a gist if that helps.


Let me know what's most useful — raw log gist, or a fresh /api/chat capture on Pattern A or C once I reinstall. Thanks for looking at this.

<!-- gh-comment-id:4221913705 --> @warabe1122 commented on GitHub (Apr 10, 2026): Thanks for jumping on this! Here are captured raw parser failures from my logs. Environment: - Ollama 0.20.4 (same issue on 0.20.0) - `gemma4:26b` Q4_K_M - macOS / M2 Max - Agent harness: openclaude / OpenClaw writer agent editing markdown files Note: I've since uninstalled gemma4:26b, so I can't capture fresh traffic right now, but the `content=` field in the warnings below is the exact text the parser was given. Happy to reinstall and capture a full `/api/chat` request/response pair on whichever pattern is most useful. ## Pattern A — simple exec, embedded quotes ``` [gemma4.go:287] invalid character 'v' after object key:value pair content: call:exec{command:<|"|>ls -F "vault/"<|"|>} ``` ``` [gemma4.go:287] invalid character '"' after object key:value pair content: call:exec{command:<|"|>summarize "https://www.youtube.com/watch?v=Rl9CF8mslaQ" --youtube auto --extract<|"|>} ``` Semantically both are correct — `ls -F "vault/"` and the `summarize` invocation are exactly what I asked for. The gap looks specific to `<|"|>`-delimited string values that themselves contain `"`. ## Pattern C — file edit, `edits:[{newText, oldText}, ...]` This is the one that made gemma4:26b unusable as a writer agent. Note it's hitting `gemma4.go:299` and the error mentions the repair path, so it's going through repair and still failing: ``` [gemma4.go:299] invalid character ']' after object key:value pair repair failed to produce valid JSON arguments timestamp: 2026-04-09T16:49:47+09:00 content: call:edit{edits:[ {newText:<|"|># Gemma4 + openclaude speed optimization guide...<|"|>, oldText:<|"|># Gemma4 + openclaude speed optimization guide...<|"|>}, {newText:<|"|>## 14. Methods tried but not adopted\n\n### 1. Model quantization...<|"|>, oldText:<|"|>## 14. Methods tried but not adopted\n<|"|>} ]} ``` Semantically this is a perfectly valid `{newText, oldText}` edit pair list targeting a markdown file. The gap looks like (a) `<|"|>`-delimited strings containing newlines and `"`, combined with (b) the array-of-objects shape around `edits:[...]`. I have multiple captures of this exact pattern — happy to drop the raw log lines in a gist if that helps. --- Let me know what's most useful — raw log gist, or a fresh `/api/chat` capture on Pattern A or C once I reinstall. Thanks for looking at this.
Author
Owner

@drifkin commented on GitHub (Apr 11, 2026):

thanks @warabe1122 for the logs, I'll take a look at them soon. Earlier today Google released a set of changes to their Gemma 4 format, and we have a new prerelease up that incorporates those same changes: https://github.com/ollama/ollama/releases/tag/v0.20.6-rc0

I'd be curious if using this pre-release fixes some of the tool calling issues you've been running into.

<!-- gh-comment-id:4227607849 --> @drifkin commented on GitHub (Apr 11, 2026): thanks @warabe1122 for the logs, I'll take a look at them soon. Earlier today Google released a set of changes to their Gemma 4 format, and we have a new prerelease up that incorporates those same changes: https://github.com/ollama/ollama/releases/tag/v0.20.6-rc0 I'd be curious if using this pre-release fixes some of the tool calling issues you've been running into.
Author
Owner

@drifkin commented on GitHub (Apr 11, 2026):

@warabe1122: looking more closely at your logs: Pattern A is almost certainly fixed by Ollama 0.20.4, so I wonder if maybe you're still running a stale (i.e., 0.20.0) server? Running ollama --version can help check this.

Pattern C is still a problem (assuming v0.20.6-rc0 and later will still generate such a call), appears to be from those newlines. I don't think the upstream parser would handle that either, but I'll either modify ours to accept it or make it part of the repair process, I don't see any reason not to. If you could post a full trace in a gist it would be useful to make sure that the newline is the only issue. Thank you!

<!-- gh-comment-id:4227711607 --> @drifkin commented on GitHub (Apr 11, 2026): @warabe1122: looking more closely at your logs: Pattern A is almost certainly fixed by Ollama 0.20.4, so I wonder if maybe you're still running a stale (i.e., 0.20.0) server? Running `ollama --version` can help check this. Pattern C is still a problem (assuming v0.20.6-rc0 and later will still generate such a call), appears to be from those newlines. I don't think the upstream parser would handle that either, but I'll either modify ours to accept it or make it part of the repair process, I don't see any reason not to. If you could post a full trace in a gist it would be useful to make sure that the newline is the only issue. Thank you!
Author
Owner

@warabe1122 commented on GitHub (Apr 11, 2026):

You were right — I double-checked my log headers:

  • server-4.log: Listening on 127.0.0.1:11434 (version 0.20.0) — this is where Pattern A came from (2026-04-03)
  • server-1.log: Listening on 127.0.0.1:11434 (version 0.20.4) — this is where Pattern C came from (2026-04-09)

So Pattern A was on stale 0.20.0 and is consistent with being fixed by 0.20.4. Sorry for the noise there.

Pattern C is on 0.20.4 — full trace in a gist here:

https://gist.github.com/warabe1122/d2835d3c2b2adabcdc72c816b933b26e

It includes the raw log line, a decoded/pretty-printed version of the content= field so the structure is easy to read, and notes on what I think is going on (newlines inside <|"|>, plus the edits:[ ... ] array-of-objects shape). The edited file was a Japanese markdown doc, so the strings contain CJK + **bold** + embedded ", in case any of those matter.

I'll reinstall gemma4:26b on v0.20.6-rc0 and try to reproduce Pattern C with a fresh /api/chat capture — will post back if I can get a clean trace on the prerelease. Thanks for looking into this!

<!-- gh-comment-id:4229311644 --> @warabe1122 commented on GitHub (Apr 11, 2026): You were right — I double-checked my log headers: - `server-4.log`: `Listening on 127.0.0.1:11434 (version 0.20.0)` — this is where **Pattern A** came from (2026-04-03) - `server-1.log`: `Listening on 127.0.0.1:11434 (version 0.20.4)` — this is where **Pattern C** came from (2026-04-09) So Pattern A was on stale 0.20.0 and is consistent with being fixed by 0.20.4. Sorry for the noise there. Pattern C is on 0.20.4 — full trace in a gist here: https://gist.github.com/warabe1122/d2835d3c2b2adabcdc72c816b933b26e It includes the raw log line, a decoded/pretty-printed version of the `content=` field so the structure is easy to read, and notes on what I think is going on (newlines inside `<|"|>`, plus the `edits:[ ... ]` array-of-objects shape). The edited file was a Japanese markdown doc, so the strings contain CJK + `**bold**` + embedded `"`, in case any of those matter. I'll reinstall `gemma4:26b` on `v0.20.6-rc0` and try to reproduce Pattern C with a fresh `/api/chat` capture — will post back if I can get a clean trace on the prerelease. Thanks for looking into this!
Author
Owner

@drifkin commented on GitHub (Apr 11, 2026):

Great, thanks so much! It wouldn't surprise me if it still repros, it might just be the model seeing a lot of newlines and then getting confused about the tool calling format itself not usually having newlines. I have a fix up at https://github.com/ollama/ollama/pull/15494 that relaxes the parser into allowing whitespace before tool call keys, which I expect to fix this issue. You could run that branch yourself via go run . serve if you're curious, but should have a second release candidate up soon-ish that has that patch in it, will post here when it's ready.

<!-- gh-comment-id:4230243585 --> @drifkin commented on GitHub (Apr 11, 2026): Great, thanks so much! It wouldn't surprise me if it still repros, it might just be the model seeing a lot of newlines and then getting confused about the tool calling format itself not usually having newlines. I have a fix up at https://github.com/ollama/ollama/pull/15494 that relaxes the parser into allowing whitespace before tool call keys, which I expect to fix this issue. You could run that branch yourself via `go run . serve` if you're curious, but should have a second release candidate up soon-ish that has that patch in it, will post here when it's ready.
Author
Owner

@warabe1122 commented on GitHub (Apr 11, 2026):

Thanks, that matches what I was seeing. I'll hold off on building from the branch and wait for the next RC — happy to re-test Pattern C against it with a fresh /api/chat capture once it drops. Appreciate the quick turnaround!

<!-- gh-comment-id:4230288140 --> @warabe1122 commented on GitHub (Apr 11, 2026): Thanks, that matches what I was seeing. I'll hold off on building from the branch and wait for the next RC — happy to re-test Pattern C against it with a fresh `/api/chat` capture once it drops. Appreciate the quick turnaround!
Author
Owner

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

@warabe1122: rc1 is up at https://github.com/ollama/ollama/releases/tag/v0.20.6-rc1

<!-- gh-comment-id:4230510006 --> @drifkin commented on GitHub (Apr 12, 2026): @warabe1122: rc1 is up at https://github.com/ollama/ollama/releases/tag/v0.20.6-rc1
Author
Owner

@emansom commented on GitHub (Apr 12, 2026):

@drifkin Consider looking at LiteRT-LM and how it parses and emits Gemma 4 stuff. It is the canonical reference implementation:

https://github.com/google-ai-edge/LiteRT-LM/blob/main/runtime/conversation/model_data_processor/gemma4_data_processor.cc
https://github.com/google-ai-edge/LiteRT-LM/blob/main/runtime/conversation/model_data_processor/gemma4_data_processor_test.cc

<!-- gh-comment-id:4230935161 --> @emansom commented on GitHub (Apr 12, 2026): @drifkin Consider looking at LiteRT-LM and how it parses and emits Gemma 4 stuff. It is the canonical reference implementation: https://github.com/google-ai-edge/LiteRT-LM/blob/main/runtime/conversation/model_data_processor/gemma4_data_processor.cc https://github.com/google-ai-edge/LiteRT-LM/blob/main/runtime/conversation/model_data_processor/gemma4_data_processor_test.cc
Author
Owner

@warabe1122 commented on GitHub (Apr 12, 2026):

Tested on v0.20.6-rc1 — Pattern C is fixed!

Environment

  • Ollama v0.20.6-rc1 (binary from release assets, running via ollama serve)
  • gemma4:26b (fresh pull, ID 5571076f3d70)
  • macOS / M2 Max 64GB

Test 1 — English, short multiline edits:[...]

Sent a /api/chat request with a tool schema matching the original failure: edit tool with edits array of {newText, oldText} pairs. Model returned a well-formed tool call with two edit pairs. Parser accepted it cleanly.

Test 2 — Japanese, long multiline edits:[...]

Same structure but with Japanese markdown content, **bold** markup, and multi-line newText/oldText values containing \n — this is the closest match to the original Pattern C failure. Model returned a well-formed tool call, parser accepted it.

Server log

Zero gemma4 tool call parsing failed warnings in the server log across both tests.


Looks like #15494 resolved it. Thanks for the quick turnaround @drifkin!

<!-- gh-comment-id:4231555001 --> @warabe1122 commented on GitHub (Apr 12, 2026): Tested on `v0.20.6-rc1` — Pattern C is fixed! ## Environment - Ollama `v0.20.6-rc1` (binary from [release assets](https://github.com/ollama/ollama/releases/tag/v0.20.6-rc1), running via `ollama serve`) - `gemma4:26b` (fresh pull, ID `5571076f3d70`) - macOS / M2 Max 64GB ## Test 1 — English, short multiline `edits:[...]` Sent a `/api/chat` request with a tool schema matching the original failure: `edit` tool with `edits` array of `{newText, oldText}` pairs. Model returned a well-formed tool call with two edit pairs. Parser accepted it cleanly. ## Test 2 — Japanese, long multiline `edits:[...]` Same structure but with Japanese markdown content, `**bold**` markup, and multi-line `newText`/`oldText` values containing `\n` — this is the closest match to the original Pattern C failure. Model returned a well-formed tool call, parser accepted it. ## Server log Zero `gemma4 tool call parsing failed` warnings in the server log across both tests. --- Looks like #15494 resolved it. Thanks for the quick turnaround @drifkin!
Author
Owner

@CamJN commented on GitHub (Apr 14, 2026):

Using ollama 0.20.7:

With the js library:

const response = await ollama.chat({
    model: "gemma4:e4b",
    stream: false,
    think: false,
    keep_alive: '24h',
    options: {
      temperature: 0.8,
    },
    format: JSON.stringify(z.toJSONSchema(InvoiceSchema)),
    messages: [
      { role: "system", content: t.my_message_string, },
      { role: "user", content: t.parse_template_string.replace('{context_str}', context), },
    ]
  });

The response.message.content is "```json\n{\n "json details": "elided"\n}\n```" which has erroneous ```json and ``` markers.

<!-- gh-comment-id:4246914308 --> @CamJN commented on GitHub (Apr 14, 2026): Using ollama 0.20.7: With the js library: ```javascript const response = await ollama.chat({ model: "gemma4:e4b", stream: false, think: false, keep_alive: '24h', options: { temperature: 0.8, }, format: JSON.stringify(z.toJSONSchema(InvoiceSchema)), messages: [ { role: "system", content: t.my_message_string, }, { role: "user", content: t.parse_template_string.replace('{context_str}', context), }, ] }); ``` The `response.message.content` is <code>"\```json\n{\n "json details": "elided"\n}\n\```"</code> which has erroneous <code>\```json</code> and <code>\```</code> markers.
Author
Owner

@CamJN commented on GitHub (Apr 14, 2026):

gemma4:31b has the same issue, and additionally does not follow the schema.

<!-- gh-comment-id:4247616998 --> @CamJN commented on GitHub (Apr 14, 2026): `gemma4:31b` has the same issue, and additionally does not follow the schema.
Author
Owner

@drifkin commented on GitHub (Apr 14, 2026):

can you open a different issue for that @CamJN, I don't think you're doing anything with tool calls, right?

<!-- gh-comment-id:4247751635 --> @drifkin commented on GitHub (Apr 14, 2026): can you open a different issue for that @CamJN, I don't think you're doing anything with tool calls, right?
Author
Owner

@emansom commented on GitHub (Apr 14, 2026):

@drifkin Could you explain to me why Ollama has specific Gemma 4 workarounds and testing, when that is already in llama.cpp project? It seems like a duplication of effort to me, which could result in false positives and even harder to debug issues later.

Or is Ollama using a different parser? Different from the default PEG parser in llama.cpp?
And why?

Fix in one place, different behavior/broken in other place?
Shouldn't llama.cpp be functional by default, and Ollama acting as a gateway? Not a duct-tape?

https://github.com/search?q=repo%3Aggml-org%2Fllama.cpp+gemma4&type=code
https://github.com/ggml-org/llama.cpp/pull/21760
https://github.com/ggml-org/llama.cpp/pull/21418
https://github.com/ggml-org/llama.cpp/pull/21326

<!-- gh-comment-id:4247947597 --> @emansom commented on GitHub (Apr 14, 2026): @drifkin Could you explain to me why Ollama has specific Gemma 4 workarounds and testing, when that is already in llama.cpp project? It seems like a duplication of effort to me, which could result in false positives and even harder to debug issues later. Or is Ollama using a different parser? Different from the default PEG parser in llama.cpp? And why? Fix in one place, different behavior/broken in other place? Shouldn't llama.cpp be functional by default, and Ollama acting as a gateway? Not a duct-tape? https://github.com/search?q=repo%3Aggml-org%2Fllama.cpp+gemma4&type=code https://github.com/ggml-org/llama.cpp/pull/21760 https://github.com/ggml-org/llama.cpp/pull/21418 https://github.com/ggml-org/llama.cpp/pull/21326
Author
Owner

@drifkin commented on GitHub (Apr 14, 2026):

yup, we use our own renderers and parsers, for quite a while now. This all predates the parsers you're mentioning. We have several underlying inference engines and having the parsers at a different layer is useful to us.

<!-- gh-comment-id:4248013242 --> @drifkin commented on GitHub (Apr 14, 2026): yup, we use our own renderers and parsers, for quite a while now. This all predates the parsers you're mentioning. We have several underlying inference engines and having the parsers at a different layer is useful to us.
Author
Owner

@emansom commented on GitHub (Apr 14, 2026):

yup, we use our own renderers and parsers, for quite a while now. This all predates the parsers you're mentioning. We have several underlying inference engines and having the parsers at a different layer is useful to us.

I see. Appreciate the insight/context!

Are you aware of the LLGuidance support in llama.cpp (and other inference engines)?

https://github.com/ggml-org/llama.cpp/blob/master/docs/llguidance.md

Ollama could utilize that to achieve better performance, and enforce better parsers; while reducing code complexity (Lark syntax vs custom parsers)?

I'm positive @mmoskal can concur.

<!-- gh-comment-id:4248118032 --> @emansom commented on GitHub (Apr 14, 2026): > yup, we use our own renderers and parsers, for quite a while now. This all predates the parsers you're mentioning. We have several underlying inference engines and having the parsers at a different layer is useful to us. I see. Appreciate the insight/context! Are you aware of the LLGuidance support in llama.cpp (and [other inference engines](https://github.com/guidance-ai/llguidance?tab=readme-ov-file#integrations))? https://github.com/ggml-org/llama.cpp/blob/master/docs/llguidance.md Ollama could utilize that to achieve better performance, and enforce better parsers; while reducing code complexity ([Lark syntax](https://github.com/guidance-ai/llguidance/blob/main/docs/syntax.md) vs custom parsers)? I'm positive @mmoskal can concur.
Author
Owner

@drifkin commented on GitHub (Apr 15, 2026):

thanks for the links! We do want to do more with structured outputs/constrained decoding in the future. And particularly for tool calls.

<!-- gh-comment-id:4248546632 --> @drifkin commented on GitHub (Apr 15, 2026): thanks for the links! We do want to do more with structured outputs/constrained decoding in the future. And particularly for tool calls.
Author
Owner

@emansom commented on GitHub (Apr 15, 2026):

thanks for the links! We do want to do more with structured outputs/constrained decoding in the future. And particularly for tool calls.

I see @ParthSareen also had some thoughts about it
https://parthsareen.com/writings/sampling/

Seems it would be very beneficial for guiding tool calls:
https://github.com/guidance-ai/llguidance/blob/main/docs/toolcalls.md

<!-- gh-comment-id:4248999850 --> @emansom commented on GitHub (Apr 15, 2026): > thanks for the links! We do want to do more with structured outputs/constrained decoding in the future. And particularly for tool calls. I see @ParthSareen also had some thoughts about it https://parthsareen.com/writings/sampling/ Seems it would be very beneficial for guiding tool calls: https://github.com/guidance-ai/llguidance/blob/main/docs/toolcalls.md
Author
Owner

@jk105jk105 commented on GitHub (Apr 17, 2026):

Hi, I'm still getting msg="gemma4 tool call parsing failed".

Full error

level=WARN source=gemma4.go:306 msg="gemma4 tool call parsing failed" error="invalid character '@' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:es_search{body:{aggs:{unique_source_ips:{terms:{field:<|\"|>client_ip.keyword<|\"|>,size:1000}}},query:{bool:{must:[{match:{message:<|\"|>openvpn<|\"|>}},{term:{\"sysloghost.keyword\":\"testhost\"}}],filter:[{range:{@timestamp:{gte:<|\"|>2026-03-01T00:00:00Z<|\"|>,lt:<|\"|>2026-04-01T00:00:00Z<|\"|>}}}]}},sort:[{@timestamp:{\"order\":\"desc\"}}],size:0},path:<|\"|>logstash-2026.03.05-000059/_search<|\"|>}"

Also the invalid character is inconsistent.

Context: I'm trying to have gemma4 generate a elasticsearch query so that I can use in a search API tool call.

System
ollama: 0.21.0
os: linux
model: gemma4:eb4

Let me know if you need more information. Thanks!

<!-- gh-comment-id:4268981753 --> @jk105jk105 commented on GitHub (Apr 17, 2026): Hi, I'm still getting msg="gemma4 tool call parsing failed". Full error ``` level=WARN source=gemma4.go:306 msg="gemma4 tool call parsing failed" error="invalid character '@' looking for beginning of object key string\nrepair failed to produce valid JSON arguments" content="call:es_search{body:{aggs:{unique_source_ips:{terms:{field:<|\"|>client_ip.keyword<|\"|>,size:1000}}},query:{bool:{must:[{match:{message:<|\"|>openvpn<|\"|>}},{term:{\"sysloghost.keyword\":\"testhost\"}}],filter:[{range:{@timestamp:{gte:<|\"|>2026-03-01T00:00:00Z<|\"|>,lt:<|\"|>2026-04-01T00:00:00Z<|\"|>}}}]}},sort:[{@timestamp:{\"order\":\"desc\"}}],size:0},path:<|\"|>logstash-2026.03.05-000059/_search<|\"|>}" ``` Also the invalid character is inconsistent. Context: I'm trying to have gemma4 generate a elasticsearch query so that I can use in a search API tool call. System ollama: 0.21.0 os: linux model: gemma4:eb4 Let me know if you need more information. Thanks!
Author
Owner

@PureBlissAK commented on GitHub (Apr 18, 2026):

🤖 Automated Triage & Analysis Report

Issue: #15315
Analyzed: 2026-04-18T18:22:38.782881

Analysis

  • Type: unknown
  • Severity: medium
  • Components: unknown

Implementation Plan

  • Effort: medium
  • Steps:

This issue has been triaged and marked for implementation.

<!-- gh-comment-id:4274310364 --> @PureBlissAK commented on GitHub (Apr 18, 2026): <!-- ollama-issue-orchestrator:v1 issue:15315 --> ## 🤖 Automated Triage & Analysis Report **Issue**: #15315 **Analyzed**: 2026-04-18T18:22:38.782881 ### Analysis - **Type**: unknown - **Severity**: medium - **Components**: unknown ### Implementation Plan - **Effort**: medium - **Steps**: *This issue has been triaged and marked for implementation.*
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/ollama#35555