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 2 commits 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. |
Follow-up to sqliteai#61 (see the ad-hoc validation summary and @mfethe1's proposal): the three-check oracle in `tests/serve/test_glm_upstream.py` — our `<tool_call>`/`<arg_key>`/`<arg_value>` rendering vs GLM's own `chat_template.jinja` — becomes a required, CPU-only CI job instead of an optional local check that skips without a model checkout. - Vendors `chat_template.jinja` at `tests/serve/glm_upstream/` from `zai-org/GLM-5.3-Flash` @ `eb9eb208eb0d988989d07a6a12d0fdeb5f52574a` with full provenance and license metadata in that directory's README.md. Licensing note: the template lives in the HF model repo (tagged MIT; the MIT notice is reproduced in the README) — the GitHub code repo `zai-org/GLM-5` is Apache-2.0 but does not ship the file. - Adds `requirements-test.txt` pinning `Jinja2==3.1.6`; the new `glm_upstream_oracle` CI job installs it and runs the oracle with `CI_GLM_ORACLE_STRICT=1`, which turns every skip path (missing jinja2, missing template, unresolved markers) into a failure and asserts all three checks ran — the job cannot be green because it did nothing. - Local dev is unchanged in spirit: `tests/run.sh` now defaults to the vendored template (runs out of a fresh clone) and still skips loudly if jinja2/uv is absent; `GLM_DIR` still overrides with a real release directory.
Vendored template vs pinned download
Rationale:
The vendored file is taken from zai-org/GLM-5.3-Flash @ eb9eb208eb0d988989d07a6a12d0fdeb5f52574a (Hugging Face). The directory’s README.md includes:
A one‑liner that this is used only with FakeEngine in CI; no real GLM model or weights are involved. This gives clear provenance and respects the license as stated where the file actually comes from (the model repo). Pinning Jinja2
Rationale:
Strict mode: no silent skips in CI
In this mode:
Local development behavior is intentionally softer:
This split keeps CI strict while preserving a convenient local workflow. ScopeThis is narrow infrastructure to support the tool‑protocol validation from #61. It does not:
It supports the narrower claim that the PR addresses the reported tool‑protocol behavior under tests, as per the original ad‑hoc validation. Disclaimer: GLM-5.3-flash was used to implement, perplexity.ai was used to draft this message. |
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.