parser: don't amplify degenerate <tool> spam from too-low quants

The plaintext <tool> rescue could turn a failing 2-bit model's repetition
loop (<tool>glob</tool><tool>glob</tool>… / bare names, no args) into a flood
of bogus tool calls. Harden it: reject a batch with >6 <tool> blocks (that's
model degeneration, not many real calls) and drop any bare <tool>name</tool>
that carries no key: value argument (the spam signature). Genuine single/few
calls with arguments still parse; combined with the existing trailing-action,
declared-name, and DeepSeek-only scoping.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent e23dd2a7
...@@ -1190,15 +1190,24 @@ def parse_tool_tag_plaintext_calls(text: str, tool_names=None): ...@@ -1190,15 +1190,24 @@ def parse_tool_tag_plaintext_calls(text: str, tool_names=None):
tokens — higher-quant models emit proper formats and never reach here. tokens — higher-quant models emit proper formats and never reach here.
To avoid catching a ``<tool>`` *example* a model writes inside an explanatory To avoid catching a ``<tool>`` *example* a model writes inside an explanatory
reply, the block(s) must be the message's trailing ACTION: after the first reply, AND to avoid amplifying the degenerate ``<tool>name</tool><tool>name</tool>…``
``<tool>`` everything to end-of-text must be only ``<tool>…</tool>`` blocks and spam a too-low quant emits when it falls apart, this is deliberately strict:
whitespace. A reply with prose after/between the tags is treated as text, not a - the block(s) must be the message's trailing ACTION — after the first
call. Returns ``[(name, args), …]``.""" ``<tool>`` everything to end-of-text must be only ``<tool>…</tool>`` blocks
and whitespace (prose after/between → treated as text, not calls);
- every block must carry at least one ``key: value`` argument — a BARE
``<tool>name</tool>`` is the spam signature, never a real call here;
- the whole reply may yield at most a few distinct calls — a flood of blocks
is model degeneration, so the batch is rejected wholesale.
Returns ``[(name, args), …]``."""
if not text or '<tool' not in text.lower() or not tool_names: if not text or '<tool' not in text.lower() or not tool_names:
return [] return []
blocks = list(re.finditer(r'<tool\s*>(.*?)</tool\s*>', text, re.DOTALL | re.IGNORECASE)) blocks = list(re.finditer(r'<tool\s*>(.*?)</tool\s*>', text, re.DOTALL | re.IGNORECASE))
if not blocks: if not blocks:
return [] return []
# A flood of <tool> tags is a model falling apart, not many real calls.
if len(blocks) > 6:
return []
# Require the tag(s) to form the trailing run of the message: strip the matched # Require the tag(s) to form the trailing run of the message: strip the matched
# blocks out of the tail (from the first block on) and demand only whitespace is # blocks out of the tail (from the first block on) and demand only whitespace is
# left. Otherwise this is prose that merely mentions the <tool> syntax. # left. Otherwise this is prose that merely mentions the <tool> syntax.
...@@ -1227,6 +1236,10 @@ def parse_tool_tag_plaintext_calls(text: str, tool_names=None): ...@@ -1227,6 +1236,10 @@ def parse_tool_tag_plaintext_calls(text: str, tool_names=None):
k = k.strip() k = k.strip()
if k: if k:
args[k] = v.strip() args[k] = v.strip()
# A bare <tool>name</tool> with no arguments is the degenerate-spam shape,
# never a real call in this format — drop it.
if not args:
continue
key = (name, json.dumps(args, sort_keys=True, default=str)) key = (name, json.dumps(args, sort_keys=True, default=str))
if key in seen: if key in seen:
continue continue
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment