Skip to main content

Module constrain

Module constrain 

Source
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 as GGLIB_DISABLE_MTP and GGLIB_DISABLE_CACHE_REUSE.
MAX_GRAMMAR_TOOL_CALLS_ENV 🔒
Build the GBNF grammar for one or more dialect tool calls, from the same DialectSpec the parser reads.

Functions§

constrain_tool_calls
Originate a decode-time grammar for a demanded dialect tool call.
constrain_tool_calls_inner 🔒
constrain_tool_calls without the environment check, for tests.
demanded_names 🔒
Which names the client’s tool_choice demands a call from.
gbnf_literal_safe 🔒
Whether name can be embedded in a GBNF double-quoted literal verbatim.
gbnf_string_literal 🔒
Render s as 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_ENV is set to a truthy value.
tool_call_grammar 🔒
tool_names 🔒
The advertised function names, or None when tools is absent, empty, or not in the OpenAI function-tool shape.