Expand description
Stage 6: decode-time enforcement of dialect tool calls.
Tier A — Compensation (ADR 0001). This stage originates a grammar
because llama.cpp builds none for dialect models. It is the clearest
deletion candidate in the tree, because upstream has the machinery
already: json_schema_to_grammar converts tool schemas to GBNF, and
lazily-triggered grammars exist for the auto case gglib cannot cover.
Deletion criterion: llama.cpp constrains dialect tool calls under both
tool_choice: "required" and "auto", with arguments conforming to the
tool’s own JSON Schema rather than merely being well-formed JSON. Note
that this stage’s grammar is weaker than that today — it constrains the
envelope, the function name, and JSON well-formedness, but admits
{"path": 42} against a schema demanding a string. So the criterion is
not “upstream matches this stage” but “upstream exceeds it”, and meeting
it deletes this stage and obviates the schema-constraint work it would
otherwise need.
Measured by scripts/experiments/lazy_grammar_conformance.py, whose
result is recorded as its own ADR rather than assumed from
RuntimeFlags::PEG_NATIVE_TOOL_CALLS.
Criterion met, stage retained (ADR 0002). On b10327 against
Qwen3.5-4B, upstream held 60/60 across auto and required under prompts
written to break types, enums, required fields and additionalProperties.
It exceeds this stage on the measured path, and the schema-constraint work
this stage would otherwise have needed is dropped rather than deferred.
The stage stays anyway, and the reason is the scope of the evidence: one
model, one build, one schema. Deleting a stage that also serves dialects
nobody has measured would trade a known cost for an unmeasured risk — the
same asymmetry RuntimeCapabilities::unknown encodes. What remains
before removal is a second dialect family measured to the same standard.
For models with a resolved DialectSpec, tool calls are free text —
the model chooses to emit OPEN{json}CLOSE markup and the proxy
parses it after the fact. Post-hoc parsing can rescue a well-formed call,
but it cannot stop a small model from producing a malformed one. This
stage can: when the client demands a tool call (tool_choice: "required" or a named function), it originates a GBNF grammar that
llama-server enforces at decode time, making an invalid envelope,
invalid JSON, or an invented tool name unrepresentable. The grammar is
generated from the same spec the parser reads, so enforcement and
parsing cannot drift.
§Why only dialect models
Models whose chat template does native tool handling are already
constrained: llama.cpp builds its own grammar from the template (eager
under required, lazily-triggered under auto) — and its OpenAI
endpoint rejects a request that combines a custom grammar with
tools (“Cannot use custom grammar constraints with tools”) unless
tool_choice is "none". Dialect models are exactly the ones that
machinery does not cover, so they are exactly where the proxy steps in.
§Why tool_choice is rewritten to "none"
That same upstream rejection is the reason the stage rewrites
tool_choice to "none" when it installs a grammar: it is the one
escape hatch llama-server leaves open for grammar + tools. The template
still renders the tool schemas into the prompt (templates never see
tool_choice), and the requirement the client expressed now lives in
the grammar itself — which is stronger than what tool_choice could
ask for on a model llama-server has no tool handling for anyway.
§Why auto is left alone
A grammar constrains from the first token, so under tool_choice: "auto" it would forbid the plain-text answers auto exists to permit.
llama.cpp solves this internally with lazily-triggered grammars, but
does not expose lazy triggers as request fields — so auto keeps
today’s behaviour: unconstrained decode, post-hoc parsing.
Constants§
- DISABLE_
GRAMMAR_ ENV - Environment kill switch. Truthy values (case-insensitive
1,true,yes,on) disable grammar origination entirely — the same contract asGGLIB_DISABLE_MTPandGGLIB_DISABLE_CACHE_REUSE. - MAX_
GRAMMAR_ 🔒TOOL_ CALLS_ ENV - Build the GBNF grammar for one or more dialect tool calls, from the
same
DialectSpecthe parser reads.
Functions§
- constrain_
tool_ calls - Originate a decode-time grammar for a demanded dialect tool call.
- constrain_
tool_ 🔒calls_ inner constrain_tool_callswithout the environment check, for tests.- demanded_
names 🔒 - Which names the client’s
tool_choicedemands a call from. - gbnf_
literal_ 🔒safe - Whether
namecan be embedded in a GBNF double-quoted literal verbatim. - gbnf_
string_ 🔒literal - Render
sas a GBNF double-quoted literal, escaping"and\. - grammar_
call_ 🔒limit - How many tool calls the originated grammar may express in one response.
- grammar_
disabled_ 🔒via_ env - Whether
DISABLE_GRAMMAR_ENVis set to a truthy value. - tool_
call_ 🔒grammar - tool_
names 🔒 - The advertised function names, or
Nonewhentoolsis absent, empty, or not in theOpenAIfunction-tool shape.