For fourteen days one stage of our agent pipeline produced nothing at all, and logged success every morning while it did so.
The line in the log was daily_proposals_complete agents_evaluated=12 proposals_generated=0, at INFO, once a day, for two weeks. Nothing was down. The cron fired on schedule. The AI call succeeded and returned tokens. The database was healthy. Every component reported that it had done its job, and the row count went to zero and stayed there.
The cause turned out to be a property of reasoning models that is easy to miss and, once you have seen it, hard to unsee.
Hidden thinking tokens are billed against your answer budget
A reasoning model’s internal thinking tokens are charged against the same max_tokens ceiling as the visible completion. Not a separate budget. The same one.
Measured live on 2026-08-22 against gemini-2.5-flash through its OpenAI-compatible endpoint, with a deliberately trivial prompt:
completion_tokens=366, prompt_tokens=171, total_tokens=1465. The gap between 366+171 and 1465 is thinking. Measured against the live API, not inferred from documentation.If you are asking for prose, this shows up as a slightly shorter answer and you may never notice. If you are asking for structured JSON under a tight ceiling, it shows up as output that stops mid-emission — and a truncated JSON object is not a degraded result. It is an unparseable one.
How that became fourteen silent days
The failure path had four steps, and only the last one produced an observable effect:
- The model spent most of its budget thinking.
- The JSON was cut off mid-object.
- The parser raised, and the calling function returned
None. - No row was written — and the loop counted a completed cycle.
The diagnostic detail that makes this legible: 24 of the 25 logged failures carried one truncation signature. Sixteen were Unterminated string — cut mid-string. Eight were Expecting property name enclosed in double quotes — cut just after a comma. Those look like two different bugs in a triage queue. They are the same defect, observed at two different cut points.
The provider log said content was generated. That is true, and it proves only that tokens were produced — not that they parsed, not that anything downstream could use them. “The provider worked” is where this investigation nearly stopped, and it was the wrong place to stop.
We got the diagnosis wrong the first time
This is the part worth publishing, because the wrong answer was reasonable.
Three days after the proposals stopped, our AI provider hit a billing exhaustion incident. That was real, it was disruptive, and it was sitting right there as an explanation. We attributed the zeroed stage to it and moved on.
It was wrong on the dates. The proposals died on 8 August. The billing incident was on 11 August — three days later — and the model in question generated content fine at 07:31 every morning throughout. A visible outage next to a silent one is a very effective way to stop looking.
“Zero rows” has several distinct causes and the logs discriminate between them instantly, if you have named them: no work found (nothing to do), generation failed (the AI is down), parse failed (the AI is fine and the output is unusable), save failed (the database). Ours was parse — which is provably neither the billing incident nor an outage. Distinguishing these four in the log line is a small change that would have cut two weeks to an afternoon.
The root cause was a sentence in a comment
The code already had a defence. A provider-to-model override exists precisely to route structured-output calls away from reasoning models, and one provider was correctly protected by it.
What removed the protection was a comment, written during an earlier fix, asserting that the other providers’ default models were “plain chat model[s] that handle it fine.” For gemini-2.5-flash that was false — it is a reasoning model with thinking on by default. When the primary provider was switched to Gemini, the call walked straight into the wall the override had been built to prevent.
A comment encoding a capability claim about a third-party model is load-bearing code, and it rots. Model behaviour changes under a stable name. If a comment says “this model is non-reasoning,” that claim needs verifying against the live API, not against the documentation and not against the last time someone checked.
The fix, and the one we deliberately did not ship
We raised the token ceiling to 4,000. Deliberately not a new model-name override — because an unvalidated model name on the primary provider is what caused the previous incident in this same code path, and it fails 100% of calls rather than some of them. A ceiling raise cannot introduce a new failure mode: unused tokens are never billed.
The guard is a test that asserts the structured-output budget stays above the thinking-token floor, so the next person who tunes that number down has to argue with a failing test rather than with a two-week silence.
The class of bug, not the instance
The specific gotcha is worth knowing. The class is worth more.
Runs green, produces nothing is the characteristic failure of autonomous systems, and it is invisible to exactly the monitoring most teams have. Uptime checks pass. Error rates stay flat — there are no errors. The cron dashboard is green. What has actually stopped is output, and almost nobody alerts on output.
We found three more instances of the same shape in an unrelated part of this system in the same month: finished work that was produced correctly and then reached nobody, with every intermediate step reporting success. Different subsystem, identical signature.
Not “did the job run?” — you almost certainly have that one. Instead: did the job produce anything, and is that number consistent with what it produced last week? A stage that has written zero rows for three consecutive days while reporting success is not idle. It is broken, and it will stay broken for as long as nobody is counting outputs.
Related reading
10 of 122: The Monitoring Was Not Built to Watch the Evaluation as It RanAuthorization Was the Easy Part
The Six-Gate Architecture