Problem
When the native tool-call parser fails on a model completion with slightly malformed JSON inside a parameter value (a real but minor model generation defect — e.g. a bracket mismatch), omlx/api/tool_calling.py's XML fallback parsers recover the tool call itself but silently keep the malformed parameter as a raw string instead of the array/object it's supposed to be. Downstream clients that validate tool-call arguments against the declared JSON schema then reject the call:
Validation failed for tool "edit":
- edits.0: must be object
Received arguments: {"edits": "[{...}]", ...}
i.e. the array-typed edits parameter arrives JSON-encoded as a string instead of a native array.
Diagnosis
Root cause — one pattern duplicated across 3 branches
The same defective pattern — parse each parameter/arg value independently with a bare try: json.loads(val) except: val, no knowledge of the tool's declared schema — is copy-pasted across three separate parser branches:
# omlx/api/tool_calling.py, _parse_xml_tool_calls(), Qwen/Llama branch, ~line 183
for pm in re.finditer(
r"<parameter=(\w+)>\s*(.*?)\s*</parameter>", params_text, re.DOTALL
):
key = pm.group(1)
val = pm.group(2).strip()
try:
arguments[key] = json.loads(val)
except (json.JSONDecodeError, ValueError):
arguments[key] = val # <-- kept as a raw string, no repair, no schema
The identical pattern also appears in:
_parse_xml_tool_calls()'s GLM branch (<arg_key>/<arg_value>, ~line 213)
_parse_namespaced_tool_calls() (MiniMax <invoke name=...><parameter name=...>, ~line 268)
A fourth, related-but-different spot, _parse_bracket_tool_calls() ([Calling tool: name(args)] format, ~line 400), wraps a parse failure in {"raw": args_str} instead of mistyping a specific field — still not schema-aware, but a different failure mode; flagging it, not claiming it's the same bug.
Contrast with parse_tool_calls() (the primary path all of these fall back from), which does thread tools through to mlx_lm's native parser so it can correctly type-coerce declared array/object parameters. These three fallbacks take no tools parameter at all, so a value that's almost valid JSON (just malformed) degrades silently into a string instead of being repaired or rejected with a clear error.
Gemma 4 has its own separate, more sophisticated repair chain for its call:name{key:val} syntax (_gemma4_args_to_json_robust/ _gemma4_transcode_to_json) that doesn't have this defect — which is presumably why it doesn't show this corruption in my data (see below); it goes through a different code path entirely.
Minimal repro (no model or server needed — pure function call against the currently-installed package):
from omlx.api.tool_calling import _parse_xml_tool_calls
import json
malformed = '''<tool_call>
<function=edit>
<parameter=path>
config.py
</parameter>
<parameter=edits>
[{"range": ["3#8C", "3#8C"], "lines": [" value: 100,"}]}
</parameter>
</function>
</tool_call>'''
_, calls = _parse_xml_tool_calls(malformed)
parsed = json.loads(calls[0].function.arguments)
print(type(parsed["edits"])) # <class 'str'>, should be <class 'list'>
print(parsed["edits"]) # '[{"range": ["3#8C", "3#8C"], "lines": [" value: 100,"}]}'
Confirmed the identical result calling the GLM branch and _parse_namespaced_tool_calls() directly with equivalent malformed input — all three branches reproduce the same corruption via the same root cause. The malformed input itself (a real }/] mismatch) is exactly the shape observed live from actual model output, not a contrived edge case.
Real-world evidence
~/.omlx/logs/server.log* already logs this failure mode at WARNING level. Across 4 days of logs I found 23 total occurrences, splitting cleanly by tool-call format:
- 19/19
Native tool parser failed (...), recovered via XML fallback entries use the <function=name><parameter=key>... format (this issue), all from Qwen3.5/3.6-family models I was running (prism-ml/Ternary-Bonsai-27B-mlx-2bit, mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit). Every one shows this exact corruption — not hypothetical.
- 4/4
... and XML fallback could not recover. Dropping match entries use Gemma 4's call:name{key:val} format — a different symptom (dropped entirely, not type-corrupted) via Gemma 4's separate repair chain; different bug, out of scope here.
Real agentic sessions doing file edits (longer content, more escaping, multi-line strings) give the model's baseline rate of small JSON mistakes enough chances to fire that this becomes a real reliability problem, while short/simple test prompts rarely trigger it.
Environment
- oMLX 0.5.3 (also present in 0.5.1; unchanged across that range)
- Models observed triggering this:
prism-ml/Ternary-Bonsai-27B-mlx-2bit, mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit
- Client: Pi (
@earendil-works/pi-coding-agent) via the OpenAI-compatible /v1/chat/completions endpoint — but this is a server-side bug, reproducible with no client at all (see repro above)
Suggested fix
Since the pattern is duplicated three times, a shared helper seems better than three separate patches:
- Thread
tools through _parse_xml_tool_calls() and _parse_namespaced_tool_calls() (mirroring what parse_tool_calls()
already does for the native parser) so they can type-coerce per the declared schema.
- Attempt a real repair pass (e.g. a JSON-repair library) on parameter values that fail strict
json.loads(), instead of falling back to the verbatim string.
- At minimum, don't silently succeed with a wrong type — if a value looks like it was meant to be an array/object but fails to parse, surface a clear parse error instead of a validation failure two layers away.
Related
Happy to help test a fix — have real session logs and standalone repros against all three affected branches if useful.
Problem
When the native tool-call parser fails on a model completion with slightly malformed JSON inside a parameter value (a real but minor model generation defect — e.g. a bracket mismatch),
omlx/api/tool_calling.py's XML fallback parsers recover the tool call itself but silently keep the malformed parameter as a raw string instead of the array/object it's supposed to be. Downstream clients that validate tool-call arguments against the declared JSON schema then reject the call:i.e. the array-typed
editsparameter arrives JSON-encoded as a string instead of a native array.Diagnosis
Root cause — one pattern duplicated across 3 branches
The same defective pattern — parse each parameter/arg value independently with a bare
try: json.loads(val) except: val, no knowledge of the tool's declared schema — is copy-pasted across three separate parser branches:The identical pattern also appears in:
_parse_xml_tool_calls()'s GLM branch (<arg_key>/<arg_value>, ~line 213)_parse_namespaced_tool_calls()(MiniMax<invoke name=...><parameter name=...>, ~line 268)A fourth, related-but-different spot,
_parse_bracket_tool_calls()([Calling tool: name(args)]format, ~line 400), wraps a parse failure in{"raw": args_str}instead of mistyping a specific field — still not schema-aware, but a different failure mode; flagging it, not claiming it's the same bug.Contrast with
parse_tool_calls()(the primary path all of these fall back from), which does threadtoolsthrough tomlx_lm's native parser so it can correctly type-coerce declared array/object parameters. These three fallbacks take notoolsparameter at all, so a value that's almost valid JSON (just malformed) degrades silently into a string instead of being repaired or rejected with a clear error.Gemma 4 has its own separate, more sophisticated repair chain for its
call:name{key:val}syntax (_gemma4_args_to_json_robust/_gemma4_transcode_to_json) that doesn't have this defect — which is presumably why it doesn't show this corruption in my data (see below); it goes through a different code path entirely.Minimal repro (no model or server needed — pure function call against the currently-installed package):
Confirmed the identical result calling the GLM branch and
_parse_namespaced_tool_calls()directly with equivalent malformed input — all three branches reproduce the same corruption via the same root cause. The malformed input itself (a real}/]mismatch) is exactly the shape observed live from actual model output, not a contrived edge case.Real-world evidence
~/.omlx/logs/server.log*already logs this failure mode at WARNING level. Across 4 days of logs I found 23 total occurrences, splitting cleanly by tool-call format:Native tool parser failed (...), recovered via XML fallbackentries use the<function=name><parameter=key>...format (this issue), all from Qwen3.5/3.6-family models I was running (prism-ml/Ternary-Bonsai-27B-mlx-2bit,mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit). Every one shows this exact corruption — not hypothetical.... and XML fallback could not recover. Dropping matchentries use Gemma 4'scall:name{key:val}format — a different symptom (dropped entirely, not type-corrupted) via Gemma 4's separate repair chain; different bug, out of scope here.Real agentic sessions doing file edits (longer content, more escaping, multi-line strings) give the model's baseline rate of small JSON mistakes enough chances to fire that this becomes a real reliability problem, while short/simple test prompts rarely trigger it.
Environment
prism-ml/Ternary-Bonsai-27B-mlx-2bit,mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit@earendil-works/pi-coding-agent) via the OpenAI-compatible/v1/chat/completionsendpoint — but this is a server-side bug, reproducible with no client at all (see repro above)Suggested fix
Since the pattern is duplicated three times, a shared helper seems better than three separate patches:
toolsthrough_parse_xml_tool_calls()and_parse_namespaced_tool_calls()(mirroring whatparse_tool_calls()already does for the native parser) so they can type-coerce per the declared schema.
json.loads(), instead of falling back to the verbatim string.Related
64a1b8c. That fix onlytouches the generic
{"name": ..., "arguments": ...}top-level JSON branch, not the three branches this issue is about.Happy to help test a fix — have real session logs and standalone repros against all three affected branches if useful.