Files
vllm-omni/benchmarks/edge_harness/android_agent/tool_parse.py
T
tzhouamandClaude Opus 5 b36a3431bd Package this fork as vllm-embedding: README, docs, roadmap, harness
Turns a branch of accumulated edge/CPU work into something someone else can
read, reproduce and continue.

README.md states what the fork is for (multimodal models on edge CPUs rather
than servers), the headline memory and speed results with their costs, and why
the numbers are worth reading -- five retracted conclusions, all measurement
errors rather than coding errors. Upstream's README is preserved at
docs/README.vllm-omni.md and NOTICE records the Apache-2.0 derivation and the
files this fork changes.

docs/edge/:
  methodology.md  the measurement discipline, which is the most transferable
                  thing here: what each of the five retractions was, and the
                  rule it left behind. Includes the two traps specific to this
                  stack -- vLLM's AOT compile cache keying on config so two
                  arms sharing an artifact load the wrong graph, and allocated
                  not being resident until mincore(2) says so.
  results.md      every measured figure with its spread and baseline, and an
                  explicit note that the four memory levers overlap and must
                  not be added.
  cpu-4bit.md     the two 4-bit paths, the packed layout as measured rather
                  than assumed, why the path choice is a config field, and the
                  GPTQ output-head trap that invalidated a whole round of W4A8
                  numbers.
  harness.md      how to run a comparison, and the two arguments that matter
                  more than they look (--context-len, --prompt-len), each of
                  which has produced a wrong conclusion by being left at its
                  default.

ROADMAP.md orders by whether the verification method is correct rather than by
expected size, lists what is done with results, what is next with expected
gains and how each would be verified, and -- at equal length -- what was
considered and rejected, including the CPU megakernel with the measurements
that rule it out for now.

benchmarks/edge_harness/ brings the tooling into the repo so the numbers are
reproducible: the grid runner, the single-run benchmarks, the memory ledger,
the fidelity metric, the Android agent suite, the quantizers, and the tests
that pin the harness's judgement. 82 tests pass from the new location.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1ax5wYLA6XbqEL1i8L9gZ
2026-09-14 13:25:32 +08:00

156 lines
5.1 KiB
Python

"""Recover a tool call from what the model actually emitted.
vLLM's stock `glm45` parser handles Spark's tool output most of the time --
4 of 5 screens in a spot check -- and then does not. On the Wi-Fi toggle
screen, the one where a switch's state decides the right action, the model
dropped out of GLM-4.5 format entirely and emitted
[tap]
{"tool": "tap", "parameters": {"index": 1}
with the JSON unterminated. `tool_calls` comes back empty, and an agent that
only reads `tool_calls` sees a step with no action at all.
For a phone agent that is the difference between finishing a task and
stopping in the middle of it, so this module tries, in order:
1. the parsed `tool_calls` (the normal path -- nothing here second-guesses it),
2. a GLM-4.5 `<tool_call>` block left in the content,
3. the bracketed `[name]` + JSON form above, repairing unbalanced braces,
4. a bare JSON object naming a tool.
Every recovery is reported, because an agent that quietly repairs its model's
output will hide a regression in that model.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
_BRACKET = re.compile(r"\[([a-z_][a-z0-9_]*)\]", re.I)
_GLM_CALL = re.compile(
r"<tool_call>\s*([a-z_][a-z0-9_]*)(.*?)</tool_call>", re.I | re.S)
_GLM_ARG = re.compile(
r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>", re.I | re.S)
_ARG_CONTAINERS = ("parameters", "arguments", "args")
_NAME_KEYS = ("tool", "name", "tool_name", "function")
@dataclass
class ParsedCall:
name: str | None
args: dict
source: str
"""Which path produced it: tool_calls | glm_block | bracketed | bare_json."""
@property
def recovered(self) -> bool:
return self.name is not None and self.source != "tool_calls"
def _coerce(value: str):
"""Tool arguments arrive as strings; indices must become integers."""
text = value.strip()
if re.fullmatch(r"-?\d+", text):
return int(text)
if text.lower() in ("true", "false"):
return text.lower() == "true"
return text
def _balance(text: str) -> str:
"""Close a JSON object the model stopped short of finishing."""
depth_curly = text.count("{") - text.count("}")
depth_square = text.count("[") - text.count("]")
if depth_curly < 0 or depth_square < 0:
return text
return text + "]" * depth_square + "}" * depth_curly
def _load(text: str) -> dict | None:
for candidate in (text, _balance(text)):
try:
value = json.loads(candidate)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
return value
return None
def _unwrap(payload: dict, fallback_name: str | None) -> tuple[str | None, dict]:
"""Pull a (name, args) pair out of assorted self-describing shapes."""
name = fallback_name
for key in _NAME_KEYS:
value = payload.get(key)
if isinstance(value, str) and value:
name = value
break
if isinstance(value, dict) and isinstance(value.get("name"), str):
name = value["name"]
payload = {**payload, **value}
break
args: dict = {}
for key in _ARG_CONTAINERS:
inner = payload.get(key)
if isinstance(inner, dict):
args = dict(inner)
break
if isinstance(inner, str):
parsed = _load(inner)
if parsed is not None:
args = parsed
break
if not args:
args = {k: v for k, v in payload.items()
if k not in _NAME_KEYS + _ARG_CONTAINERS}
return name, args
def _first_json_object(text: str) -> str | None:
start = text.find("{")
return text[start:] if start >= 0 else None
def extract_tool_call(message: dict) -> ParsedCall:
"""The one call to act on, from whichever shape the model used."""
calls = message.get("tool_calls") or []
if calls:
fn = calls[0].get("function", {}) or {}
raw = fn.get("arguments")
if isinstance(raw, dict):
args = raw
else:
args = _load(raw or "{}") or {}
if set(args) & set(_ARG_CONTAINERS):
_, args = _unwrap(args, fn.get("name"))
return ParsedCall(fn.get("name"), args, "tool_calls")
content = message.get("content") or ""
reasoning = message.get("reasoning_content") or ""
for text in (content, reasoning):
if not text.strip():
continue
if m := _GLM_CALL.search(text):
args = {k.strip(): _coerce(v) for k, v in _GLM_ARG.findall(m.group(2))}
return ParsedCall(m.group(1), args, "glm_block")
bracket = _BRACKET.search(text)
blob = _first_json_object(text)
if blob is not None:
payload = _load(blob)
if payload is not None:
name, args = _unwrap(payload, bracket.group(1) if bracket else None)
if name:
return ParsedCall(name, args, "bracketed" if bracket else "bare_json")
if bracket and not blob:
# A naked "[press_key]" with no arguments is still an instruction.
return ParsedCall(bracket.group(1), {}, "bracketed")
return ParsedCall(None, {}, "none")