Field note · agentic-daily
A pre-flight probe reported an exhausted thinking budget as an unconstrained grammar
An engine-swap safety check asked a reasoning model for a JSON verdict inside a 40-token budget. Reasoning consumed the whole budget, content came back empty, and the probe scored that as a schema that does not bind. The grammar was fine. A three-arm test isolates budget from grammar to show why.
- Hardware
- NVIDIA DGX Spark · GB10 (SM121) · aarch64
- Software
- agentic-daily preflight_engine.py · llama.cpp (--reasoning auto) · llamacpp/Qwen3.8-Flash-Next-UD-IQ4_XS
finish_reason: "length" content: "" reasoning_content: 172 chars (max_tokens=40, thinking ON)Symptom
An engine-swap pre-flight exists specifically to catch what /v1/models can't — it probes the
parameters and features the pipeline actually sends before trusting a new engine with a real
edition. On the 2026-08-26 swap to llamacpp/Qwen3.8-Flash-Next-UD-IQ4_XS, it declared the
engine NOT SAFE:
NOT SAFE — 1 problem(s):
- response_format: accepted but the reply does not match the schema — JSON-mode
beats will run unconstrained and log nothing
That is the exact wording the same tool uses for a real defect it caught on 2026-08-20 — llama.cpp
returning 500 for GBNF on the chat endpoint while silently accepting and ignoring
response_format. This time the schema bound perfectly. The probe was the problem.
The three-arm test that isolated the cause
The pre-flight's schema-binding probe asks the model to answer {"verdict": "yes"|"no"} inside a
max_tokens budget. Holding the budget and the model's thinking mode independent of each other
isolates which one actually produced the empty reply:
max_tokens | thinking | result |
|---|---|---|
| 40 | ON | finish_reason: "length", content: "", reasoning_content: 172 chars |
| 512 | ON | {"verdict": "yes"} — binds |
| 40 | OFF | {"verdict": "yes"} — binds |
Either lever alone recovers a verdict. Raise the budget with thinking still on: fine. Turn
thinking off and leave the budget at 40: also fine. The only failing cell is the one where a
model that reasons before it answers was given a budget too small to finish reasoning and still
emit anything — a fact about the budget, not about whether response_format constrains the
output.
Root cause
The probe read message.content, parsed it as JSON, and treated any exception as "accepted but
not binding":
txt = body["choices"][0]["message"]["content"]
obj = json.loads(txt)
bound = set(obj) == {"verdict"} and obj["verdict"] in ("yes", "no")
At max_tokens=40 with the model's default --reasoning auto, reasoning consumed the entire
budget before any content token was emitted. txt was "". json.loads("") raised, the
surrounding except caught it, bound stayed False, and the tool printed
ACCEPTED BUT NOT BINDING — a verdict about the schema, produced by a failure that had nothing to
do with the schema. finish_reason was present in the same response and was never read.
Fix
Two changes, in hn-briefings/tools/preflight_engine.py and hn-briefings/tests/test_preflight_engine.py:
- The probe's own
max_tokensmoved from 40 to 512 — enough room for the 94 completion tokens the identical request needed with thinking on. - An empty completion at
finish_reason: "length"is now checked for explicitly, before thejson.loadscall, and reported as its own state —NO VERDICT — thinking exhausted the probe budget— instead of being folded into the schema-binding result. It still exits 1: an unanswerable probe is a real problem, just a different one, and the failure text now says binding is unknown, not disproved.
A second test in the same file had been silently vacuous since it was written: it asserted on
max_tokens by scraping inspect.getsource starting from the response_format key, but
max_tokens is assigned earlier in the function than that key, so the scrape always read None
and the assertion could never fail. It had been passing the entire time the probe's budget was 40. It was rewritten to assert on the payload the probe actually sends.
Why the wrong verdict cost more than a false positive
The test file's own module docstring names the stakes plainly:
It runs at exactly the moment the operator is least able to debug it — right after an engine swap, before the first edition — so a broken diagnostic is worse than none.
This one didn't just misfire quietly. It reused the exact description of a real, previous outage
to explain a problem that wasn't that outage. A false alarm wearing a real name is the expensive
kind: it sends whoever is on call to re-check something that was never broken, and it spends the
credibility the same alarm needs the next time response_format genuinely doesn't bind.
Reproduction
- Serve a model with reasoning on by default (
--reasoning autoin llama.cpp, or equivalent) behind an OpenAI-compatible endpoint. - Send a chat completion with a
response_formatJSON-schema constraint and amax_tokenssmall enough that reasoning alone can exhaust it — 40 reproduced this on Qwen3.8-Flash-Next (qwen4exp). - Confirm the response shows
finish_reason: "length"with an emptycontentand a non-emptyreasoning_content. - Re-run holding one variable fixed and changing the other — a materially larger
max_tokens, or thinking disabled. Either change alone should recover a binding response, confirming the original failure was budget, not grammar. - Confirm whatever reads the response checks
finish_reasonbefore treating emptycontentas evidence about the schema.