fix(tool_call_parsers): recover from truncated/unbalanced JSON in hermes and longcat parsers - #5315
Conversation
…mes and longcat parsers Local-model tool-call emissions (qwen3.5-122b, hermes-family, longcat) occasionally produce unbalanced JSON inside <tool_call>...</tool_call> tags: the model stops after closing the inner `arguments` object but before closing the outer function-call object, leaving one or more trailing `}` missing. Similarly, argument values containing file contents carry literal newlines inside JSON strings (strict JSON forbids this). Both cases caused `json.loads` to raise, and the bare `except Exception: return text, None` in hermes_parser and longcat_parser silently dropped the tool call, returning the raw text to the caller. In Phase 2 training loops this poisons reward signals; in deployment it surfaces to users as the model "replying with a tool-call as prose". Changes: - Add `robust_json_loads()` helper in `environments/tool_call_parsers/__init__.py` that uses `json.JSONDecoder(strict=False).raw_decode()` (tolerates literal control chars in strings) and appends 1-3 trailing `}` when the initial decode fails (recovers truncated objects). - Switch `hermes_parser` and `longcat_parser` to call the helper, drop the bare `except Exception`, and emit debug log lines instead of silently swallowing errors. - Add 16 regression tests covering: well-formed JSON, missing 1/2 close braces, literal newlines in strings, combined newline+truncation, empty/None inputs, unrecoverable garbage, non-dict top-level results, multi-line content arguments, and the unclosed-tag + unbalanced-JSON combined case. Qwen 2.5 parser inherits from hermes and benefits automatically. Other parsers (deepseek, kimi, glm, mistral) use similar patterns and may want the same treatment in follow-up PRs.
51549a9 to
772ec41
Compare
|
Closing as obsolete. The More importantly, the bug is already fixed — and more robustly — in the current production path: tool-call argument recovery now lives in
...and a few cases this PR did not cover (trailing commas, Python No port needed. Thanks! |
Problem
Local-model tool-call emissions (qwen3.5-122b, hermes-family, longcat) occasionally produce unbalanced JSON inside
<tool_call>...</tool_call>tags: the model stops after closing the innerargumentsobject but before closing the outer function-call object, leaving one or more trailing}missing. Similarly, argument values containing file contents carry literal newlines inside JSON strings, which strict JSON forbids.Both cases cause
json.loadsto raise, and the bareexcept Exception: return text, Noneinhermes_parser.pyandlongcat_parser.pysilently drops the tool call, returning the raw text to the caller.Concrete repro
Impact
/generate): silent tool-call drops poison reward signals. The model takes an action, parser returns None, loop sees no tool call, reward is miscomputed.<tool_call>...text gets sent downstream).Both are failure modes I hit with qwen3.5-122b-a10b-4bit today on a separate proxy that had the same class of bug, which is what prompted this upstream contribution.
Changes
New helper
robust_json_loads()inenvironments/tool_call_parsers/__init__.py:json.JSONDecoder(strict=False).raw_decode()to tolerate literal\n/\tinside string values}and retries (recovers truncated outer objects)Noneon unrecoverable input or non-dict top-level JSONParsers updated (
hermes_parser.py,longcat_parser.py):json.loads(raw_json)withrobust_json_loads(raw_json)namefieldexcept Exception: return text, Nonewith typed handling + debug logsTests
16 new test cases, all passing:
TestRobustJsonLoads(9 tests):TestHermesParserRobustness(5 tests):TestLongcatParserRobustness(2 tests):Coverage notes
QwenToolCallParserinherits fromHermesToolCallParser, so Qwen 2.5 benefits automatically. Other parsers (deepseek_v3,deepseek_v3_1,kimi_k2,glm45,glm47,mistral) use similarjson.loads+ bare-except patterns and may want the same treatment in follow-up PRs — out of scope here to keep the diff focused.Backwards compatibility
raw_decodeon first attempt).