fix: validate GLM tool names against the provided tools list - #114
Merged
Conversation
vLLM 0.24 re-aliased "glm45" from the lenient Glm4MoeModelToolParser to Glm47MoeModelToolParser, whose engine runs with validate_tool_names=True (vllm/parser/glm47_moe.py:171): a completed <tool_call> block whose name isn't in the request's tool list is silently dropped — no tool call is emitted and the response reads as a voluntary stop. parse_glm stayed lenient, so train-side parsing (renderer client) disagreed with what an OpenAI chat-completions eval client sees from the engine: unknown-name and missing-<arg_key> blocks kept episodes alive in train but killed them in eval. Mirror the >=0.24 semantics: when tools is passed, a parsed call whose name isn't declared gets the new status UNKNOWN_TOOL instead of OK. The attempt stays visible on parsed.tool_calls (unlike vLLM's silent drop) for verifier / RL-loss consumers, but the client-side stop->tool_calls finish-reason promotion no longer fires, matching the engine. Without tools there is no validation, exactly like vLLM's ParserEngine._is_valid_tool_name on tool-less requests. Cross-validated end-to-end against the real vLLM v0.23.0 and v0.24.0 parsers (vendored from the release tags) on identical completions: before, the renderer matched 0.23's leniency and diverged from 0.24 on unknown-name and missing-arg_key blocks; after, all cases match 0.24. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ApprovabilityVerdict: Needs human review This PR introduces a runtime behavior change to GLM parsing: tool calls with unrecognized names will now receive UNKNOWN_TOOL status instead of OK when tools are provided. While well-scoped and tested, this changes how downstream consumers interpret parsing results and warrants human review. You can customize Macroscope's approvability policy. Learn more. |
hallerite
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
glm45tool parser from the lenientGlm4MoeModelToolParsertoGlm47MoeModelToolParser, whose engine runs withvalidate_tool_names=True(vllm/parser/glm47_moe.py:171): a completed<tool_call>block whose name isn't in the request's tool list is silently dropped — no tool call is emitted,tools_called=False, and the response reads as a voluntary stop.parse_glmstayed lenient, so train-side parsing (renderer client) disagreed with what an OpenAI chat-completions eval client sees from the same engine.parse_glmnow mirrors the ≥ 0.24 semantics whentoolsis passed: a parsed call whose name isn't declared gets the newToolCallParseStatus.UNKNOWN_TOOLinstead ofOK. The attempt stays visible onparsed.tool_calls(unlike vLLM's silent drop) for verifier / RL-loss consumers, but the client-sidestop→tool_callsfinish-reason promotion (renderers/client.py) no longer fires — so both sides agree on "no tool was called". This also covers the missing-<arg_key>shape (<tool_call>bash\n<arg_value>…</arg_value></tool_call>): both parsers resolve the whole block as the name, which then fails validation.tools, no validation happens — matching vLLM (ParserEngine._is_valid_tool_namereturnsTrueon tool-less requests) and keeping all existing tools-lessparse_responsecallers untouched.GLM45RendererandGLM5Renderer(sharedparse_glm; vLLM'sglm45andglm47aliases both resolve to the strict parser in ≥ 0.24).tests/test_glm_tool_name_validation.pypins the semantics for GLM-4.5-Air and GLM-5.Verification
Cross-validated end-to-end against the real vLLM
v0.23.0andv0.24.0parser sources (vendored from the release tags, serving-layer imports stubbed) on identical completions — token ids to the renderer, decoded text to vLLM, as in production. Comparison is at the harness-visible level: dispatched(name, args)pairs plus whether the episode continues (finish_reason == "tool_calls").Before (renderer tracks 0.23, diverges from 0.24):
After: all six cases MATCH vLLM 0.24 (unknown-name attempts surface as
unknown_tool, valid calls still parse with schema coercion, plain stops unchanged).Full suite: 2573 passed, 169 skipped, 1 xfailed. ruff + ty clean.
Breaking
parse_glm/GLM45Renderer.parse_response/GLM5Renderer.parse_response: whentoolsis passed, calls naming an undeclared tool now returnstatus=UNKNOWN_TOOLinstead ofOK. Downstream OK-filters (including the renderer client's finish-reason promotion) stop treating them as tool turns — this is the intended parity with vLLM ≥ 0.24. Migration: callers that want the old lenient behavior can omittoolsat parse time or additionally acceptUNKNOWN_TOOLwhen filtering.ToolCallParseStatusgains a member (UNKNOWN_TOOL = "unknown_tool"); exhaustive matches over the enum need a new arm.🤖 Generated with Claude Code
Note
Medium Risk
Behavior change when
parse_response(..., tools=...)is used: undeclared names no longer count as OK, affecting finish-reason promotion and any exhaustiveToolCallParseStatusmatches; tools-less parsing is unchanged.Overview
Aligns GLM train-side parsing with vLLM ≥ 0.24, where undeclared tool names are no longer treated as successful calls.
Adds
ToolCallParseStatus.UNKNOWN_TOOLand_extract_tool_namessoparse_glm(GLM-4.5 / GLM-5) checks function names against the passedtoolslist when it is non-empty. Unknown names getUNKNOWN_TOOLinstead ofOK, sorenderers/client.pystop→tool_callsfinish-reason promotion matches an OpenAI client on the same engine, while the parse attempt stays onparsed.tool_callsfor verifiers/RL. Notools⇒ no validation (unchanged). OpenAIfunctionenvelopes and parameter-less tools count as known names.New
tests/test_glm_tool_name_validation.pycovers known/unknown/mixed calls, missing-<arg_key>shapes, and tools-less backward compatibility.Reviewed by Cursor Bugbot for commit 6629992. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Validate GLM tool call names against the provided tools list
UNKNOWN_TOOLto theToolCallParseStatusenum in renderers/base.py to represent parsed calls whose name is not in the declared tools list._extract_tool_nameshelper in renderers/parsing.py to derive the set of valid tool names from the tools list, supporting both flatToolSpecdicts and OpenAI-style{'type': 'function', 'function': {...}}envelopes._parse_glm_tool_callsnow assignsstatus=UNKNOWN_TOOL(instead ofOK) for calls whose names are not declared; name and arguments are still populated. When no tools list is provided, behavior is unchanged.ParsedToolCall.statuswill now seeUNKNOWN_TOOLfor unrecognized tool names when a tools list is passed to the GLM parser.Macroscope summarized 6629992.