serve/glmtools.py: GLM's own tool protocol, beside kimitools - #61
serve/glmtools.py: GLM's own tool protocol, beside kimitools#61hendrikras wants to merge 1 commit into
Conversation
A container served from chat.json could declare no tools unless its
tokenizer carried Kimi K2's five control tokens, so GLM-5.3-Flash — whose
tokenizer instead carries <tool_call>, <arg_key> and <arg_value> as single
specials — answered a `tools` request with a 400 naming the field. That
refusal was correct about the declarative format and wrong about the
container: the grammar was there, in specials.json and in the release's
own chat_template.jinja, and four prefix/suffix strings were never going
to be where it lived.
So the protocol gets the same thing K2's got: a module of its own, beside
xtml.py and kimitools.py, and an oracle that diffs its rendering against
the template that defines it.
- glmtools.detect() requires all nine markers to be single tokens, the
same all-or-none gate kimitools keeps: half of this XML renders as
ordinary text, so a partial set is a different protocol, not a smaller
one.
- The grammars differ where it matters. A Kimi call is an id and a JSON
block inside a section; a GLM call is flat XML, the name after the
opening tag and one <arg_key>/<arg_value> pair per argument, no id — so
calls come back named by position. A Kimi result is a system turn named
for the tool; a GLM result is an <|observation|> turn wrapping
<tool_response> blocks, and consecutive results share one opener, which
chatfmt now emits by looking back a message.
- ChatFormat carries which protocol resolved and branches on it; the
container with neither is still refused by name, now naming both.
- PlainParser takes the tool reader from the caller, and server.new_parser
hands it glmtools.ToolParser on a GLM container. api.py maps
GlmToolError to a 400 the way it maps KimiToolError.
The security boundary survives unchanged: the tags go out as markup
segments, the signatures and argument values as plain text, so a caller's
JSON cannot forge a control token the message content is not.
tests/serve/test_glm_upstream.py is the oracle, GLM_DIR naming the
release, the way K2_DIR does for test_chatfmt_upstream; run.sh grew the
matching check. The template needs jinja2's loopcontrols extension — it
uses {% break %}.
Docs updated where they said GLM is refused: README, CLAUDE.md,
docs/SERVE.md, docs/GLM.md.
|
Independent third-party validation at the current PR head (
Scope: this is ad-hoc Tier B software validation, not canonical CI green. The oracle executes the real PR parser/renderer but uses a synthetic tokenizer/engine; no licensed real GLM model or container was run. So this supports the narrower claim that the PR addresses the reported tool-protocol behavior under tests, not end-to-end model compatibility or merge readiness. Disclosure: the validation was executed and analyzed by an AI agent under my direction; I reviewed the evidence and am sharing the scoped result. |
|
One concrete CI follow-up to the validation above, checked against the unchanged head Would you prefer a small follow-up that makes this a required, CPU-only CI check? Suggested scope:
That would preserve the existing optional local workflow while making the independent grammar comparison repeatable in CI. This is a proposal, not a claim that CI has been wired or run. The earlier result remains ad-hoc verification passed; this is not canonical suite green, and this template/FakeEngine check does not establish real-model compatibility. Happy to keep it as a narrow follow-up rather than expand this PR. Is a pinned download or a licensed vendored fixture preferable? Disclosure: AI-agent-assisted source review and drafting, posted with human authorization. |
A container served from chat.json could declare no tools unless its tokenizer carried Kimi K2's five control tokens, so GLM-5.3-Flash — whose tokenizer instead carries <tool_call>, <arg_key> and <arg_value> as single specials — answered a
toolsrequest with a 400 naming the field. That refusal was correct about the declarative format and wrong about the container: the grammar was there, in specials.json and in the release's own chat_template.jinja, and four prefix/suffix strings were never going to be where it lived.So the protocol gets the same thing K2's got: a module of its own, beside xtml.py and kimitools.py, and an oracle that diffs its rendering against the template that defines it.
The security boundary survives unchanged: the tags go out as markup segments, the signatures and argument values as plain text, so a caller's JSON cannot forge a control token the message content is not.
tests/serve/test_glm_upstream.py is the oracle, GLM_DIR naming the release, the way K2_DIR does for test_chatfmt_upstream; run.sh grew the matching check. The template needs jinja2's loopcontrols extension — it uses {% break %}.
Docs updated where they said GLM is refused: README, CLAUDE.md, docs/SERVE.md, docs/GLM.md.