_glm47_failclosed_stream_test.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. """End-to-end checks of the fail-closed parser on real GLM tool-call text, fed
  2. once whole (a non-streaming response) and once token by token (streaming).
  3. The model writes arguments as <arg_key>/<arg_value> pairs, which the engine
  4. turns into JSON; validating the raw text refused every streamed call. Needs the
  5. checkpoint's tokenizer at /model and the plugin at /tmp/plugin.py.
  6. """
  7. import json
  8. from transformers import AutoTokenizer
  9. from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
  10. from vllm.tool_parsers.abstract_tool_parser import ToolParserManager as M
  11. M.import_tool_parser("/tmp/plugin.py")
  12. Parser = M.get_tool_parser("glm47_failclosed")
  13. tok = AutoTokenizer.from_pretrained("/model", trust_remote_code=True)
  14. tools = [
  15. {"type": "function", "function": {"name": "bash", "parameters": {"type": "object", "properties": {
  16. "command": {"type": "string"}, "timeout": {"type": "integer"}}, "required": ["command"]}}},
  17. {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {
  18. "path": {"type": "string"}}}}},
  19. ]
  20. req = ChatCompletionRequest(model="glm53", messages=[{"role": "user", "content": "x"}], tools=tools)
  21. R = "_rejected_by_server"
  22. CASES = [ # (label, model output, [(name, argument keys or R)])
  23. ("good", "</think>Listing.<tool_call>bash<arg_key>command</arg_key><arg_value>ls /tmp</arg_value></tool_call>",
  24. [("bash", ["command"])]),
  25. ("two args", "</think><tool_call>bash<arg_key>command</arg_key><arg_value>sleep 1</arg_value>"
  26. "<arg_key>timeout</arg_key><arg_value>30</arg_value></tool_call>", [("bash", ["command", "timeout"])]),
  27. ("key not in schema", "</think><tool_call>bash<arg_key>cmd</arg_key><arg_value>ls</arg_value></tool_call>",
  28. [("bash", R)]),
  29. ("good then bad", "</think><tool_call>read_file<arg_key>path</arg_key><arg_value>/etc/hosts</arg_value>"
  30. "</tool_call><tool_call>bash<arg_key>cmd</arg_key><arg_value>ls</arg_value></tool_call>",
  31. [("read_file", ["path"]), ("bash", R)]),
  32. ("markup after name", "</think><tool_call>bash</arg_key><arg_key>command</arg_key><arg_value>pwd"
  33. "</arg_value></tool_call>", [("bash", ["command"])]),
  34. ("tool not offered", "</think><tool_call>curl<arg_key>url</arg_key><arg_value>x</arg_value></tool_call>",
  35. [("curl", R)]),
  36. ]
  37. def whole(text):
  38. info = Parser(tok, req.tools).extract_tool_calls(text, req)
  39. return [(c.function.name, c.function.arguments) for c in info.tool_calls]
  40. def streamed(text):
  41. p = Parser(tok, req.tools)
  42. ids = tok.encode(text, add_special_tokens=False)
  43. names, args, prev_text = {}, {}, ""
  44. for i in range(len(ids)):
  45. cur = tok.decode(ids[: i + 1])
  46. d = p.extract_tool_calls_streaming(prev_text, cur, cur[len(prev_text):], ids[:i], ids[: i + 1], [ids[i]], req)
  47. for tc in (d.tool_calls or []) if d else []:
  48. if tc.function and tc.function.name:
  49. names[tc.index] = tc.function.name
  50. if tc.function and tc.function.arguments:
  51. args[tc.index] = args.get(tc.index, "") + tc.function.arguments
  52. prev_text = cur
  53. return [(names.get(k), args.get(k, "")) for k in sorted(set(names) | set(args))]
  54. def shape(calls):
  55. out = []
  56. for name, arguments in calls:
  57. parsed = json.loads(arguments or "{}")
  58. out.append((name, R if R in parsed else sorted(parsed)))
  59. return out
  60. for label, text, want in CASES:
  61. for mode, run in (("whole", whole), ("streamed", streamed)):
  62. got = shape(run(text))
  63. print(f" {label:<18} {mode:<9} {'ok' if got == want else 'WANTED ' + str(want) + ' GOT'} {got}")