"""End-to-end checks of the fail-closed parser on real GLM tool-call text, fed once whole (a non-streaming response) and once token by token (streaming). The model writes arguments as / pairs, which the engine turns into JSON; validating the raw text refused every streamed call. Needs the checkpoint's tokenizer at /model and the plugin at /tmp/plugin.py. """ import json from transformers import AutoTokenizer from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest from vllm.tool_parsers.abstract_tool_parser import ToolParserManager as M M.import_tool_parser("/tmp/plugin.py") Parser = M.get_tool_parser("glm47_failclosed") tok = AutoTokenizer.from_pretrained("/model", trust_remote_code=True) tools = [ {"type": "function", "function": {"name": "bash", "parameters": {"type": "object", "properties": { "command": {"type": "string"}, "timeout": {"type": "integer"}}, "required": ["command"]}}}, {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": { "path": {"type": "string"}}}}}, ] req = ChatCompletionRequest(model="glm53", messages=[{"role": "user", "content": "x"}], tools=tools) R = "_rejected_by_server" CASES = [ # (label, model output, [(name, argument keys or R)]) ("good", "Listing.bashcommandls /tmp", [("bash", ["command"])]), ("two args", "bashcommandsleep 1" "timeout30", [("bash", ["command", "timeout"])]), ("key not in schema", "bashcmdls", [("bash", R)]), ("good then bad", "read_filepath/etc/hosts" "bashcmdls", [("read_file", ["path"]), ("bash", R)]), ("markup after name", "bashcommandpwd" "", [("bash", ["command"])]), ("tool not offered", "curlurlx", [("curl", R)]), ] def whole(text): info = Parser(tok, req.tools).extract_tool_calls(text, req) return [(c.function.name, c.function.arguments) for c in info.tool_calls] def streamed(text): p = Parser(tok, req.tools) ids = tok.encode(text, add_special_tokens=False) names, args, prev_text = {}, {}, "" for i in range(len(ids)): cur = tok.decode(ids[: i + 1]) d = p.extract_tool_calls_streaming(prev_text, cur, cur[len(prev_text):], ids[:i], ids[: i + 1], [ids[i]], req) for tc in (d.tool_calls or []) if d else []: if tc.function and tc.function.name: names[tc.index] = tc.function.name if tc.function and tc.function.arguments: args[tc.index] = args.get(tc.index, "") + tc.function.arguments prev_text = cur return [(names.get(k), args.get(k, "")) for k in sorted(set(names) | set(args))] def shape(calls): out = [] for name, arguments in calls: parsed = json.loads(arguments or "{}") out.append((name, R if R in parsed else sorted(parsed))) return out for label, text, want in CASES: for mode, run in (("whole", whole), ("streamed", streamed)): got = shape(run(text)) print(f" {label:<18} {mode:<9} {'ok' if got == want else 'WANTED ' + str(want) + ' GOT'} {got}")