Skip to content

json-schema-to-grammar: empty-object schema and large maxLength both produce invalid/rejected GBNF, breaking the whole tool-call grammar #25923

Description

@blaircullen

Description

Two related bugs in common/json-schema-to-grammar.cpp cause llama-server to reject an
otherwise-valid tool-calling request with Failed to initialize samplers: failed to parse grammar
(or number of repetitions exceeds sane defaults) when the request's tools array contains a
schema shape that's uncommon but fully valid JSON Schema. Because all tool schemas are compiled into
one combined GBNF grammar, a single bad schema in the array breaks the entire request — every tool
call fails, not just the offending one.

Both were hit in the wild via Claude Code's real tool set (which includes exactly these two shapes),
not a synthetic edge case.

Bug 1 — object schema with zero properties produces invalid GBNF

A schema like:

{"type": "object", "properties": {}}

(e.g. a "placeholder"/no-op tool with no parameters) causes _build_object_rule() to fall through
both the required_props and optional_props loops (both empty), then concatenate the object rule
as:

"{" space  space "}"

Two space tokens back-to-back with nothing between them — this is invalid GBNF and fails to parse:

tool-DeferredToolPlaceholder-schema ::= "{" space  space "}"
E failed to parse grammar

Bug 2 — large maxLength produces a repetition count above the grammar engine's own sanity cap

A string schema like:

{"type": "string", "maxLength": 524288}

(a perfectly valid, if generous, maxLength) causes the minLength/maxLength branch to emit:

tool-Foo-schema-bar ::= "\"" char{0,524288} "\""

src/llama-grammar.cpp hard-caps any single repetition count at MAX_REPETITION_THRESHOLD (2000)
and throws:

parse: error parsing grammar: number of repetitions exceeds sane defaults, please reduce the number of repetitions

So a schema author who sets a large-but-reasonable maxLength (2^19 chars here, for a tool that
accepts an arbitrary script/document body) breaks grammar init entirely, with no partial/graceful
degradation.

Repro

Minimal repro against /v1/chat/completions (either bug reproduces independently):

# Bug 1
curl -s http://127.0.0.1:8095/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "qwen3-30b",
  "messages": [{"role":"user","content":"hi"}],
  "tools": [{"type":"function","function":{"name":"Placeholder","parameters":{"type":"object","properties":{}}}}]
}'
# -> 400 "Failed to initialize samplers: failed to parse grammar"

# Bug 2
curl -s http://127.0.0.1:8095/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "qwen3-30b",
  "messages": [{"role":"user","content":"hi"}],
  "tools": [{"type":"function","function":{"name":"Foo","parameters":{"type":"object","properties":{"bar":{"type":"string","maxLength":524288}},"required":["bar"]}}}]
}'
# -> 400 "number of repetitions exceeds sane defaults"

Both confirmed present as of 571d0d5 (2026-07-18).

Suggested fix

Patched locally in common/json-schema-to-grammar.cpp:

Bug 1 — in _build_object_rule(), short-circuit before the concatenation when both prop lists
are empty:

if (required_props.empty() && optional_props.empty()) {
    return "\"{\" space \"}\"";
}

Bug 2 — in the minLength/maxLength string branch, clamp max_len to "unbounded" once it
exceeds the grammar engine's own repetition threshold, rather than passing the raw value through to
build_repetition():

constexpr int GRAMMAR_MAX_REPETITION_THRESHOLD = 2000; // mirrors llama-grammar.cpp's MAX_REPETITION_THRESHOLD
if (max_len > GRAMMAR_MAX_REPETITION_THRESHOLD) {
    max_len = std::numeric_limits<int>::max();
}

This still respects minLength; it just stops trying to length-cap at the grammar level for values
the engine can't represent as a literal repetition count anyway (arguably the right behavior even
without the threshold bug, since a length this large isn't meaningfully enforceable via GBNF
repetition).

Both patches tested locally: rebuilt llama-server, verified the exact failing multi-tool request
(10 tools including both shapes above) now parses and returns a normal completion, with no change to
grammar behavior for schemas that don't hit either edge case.

Happy to open a PR with these two changes if useful — wanted to file the bug report first in case
there's a different intended approach (e.g. whether an oversized maxLength should instead be
rejected at the schema-validation layer with a clearer error, rather than silently uncapped).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions