Gap
The model clients never read finish_reason. A response truncated because it hit the output cap is indistinguishable from a complete one, and the recovery path makes the situation worse rather than better.
Verified on main (@8c6a2fa): grep -rn "finish_reason\|incomplete" src/four_pillars/ returns nothing. NimClient._content accepts any non-empty choices[0].message.content string, so an HTTP 200 carrying a half-written JSON object is treated as a successful generation.
The generation caps have no recorded basis
| location |
value |
nim.py:156 |
max_tokens: int = 4096 |
generation.py:35 |
max_tokens: int = 4096 |
analysis.py:159 |
max_tokens=8192 (synthesis) |
analysis.py:189 |
max_tokens=8192 (editorial repair) |
Nothing in docs/, CLAUDE.md, or AGENTS.md states where 4096 or 8192 came from — not a provider constraint, not a model catalog entry, not an administrator policy. There is no catalog of per-model output limits anywhere in the repository, so the caps cannot follow the selected model or deployment.
Why truncation gets worse on retry, not better
NimClient.generate treats a parse or schema failure as a repairable schema error (nim.py:185-208). On each round it appends the previous truncated output as an assistant message plus a repair instruction, then re-posts with the same max_tokens:
messages.extend([
{"role": "assistant", "content": raw_content}, # the truncated text
{"role": "user", "content": "Return the complete answer again ..."},
])
continue # same max_tokens
So the input grows every round while the output ceiling stays fixed. If the first attempt was cut off for lack of output budget, the retry has strictly less room to succeed. With nim_max_schema_repairs defaulting to 1 there is exactly one such retry, after which the client raises
{provider} output failed schema validation after N repair attempts: ...
which attributes an exhausted output budget to a schema defect. The operator sees a schema error, the job records a quality failure, and the real cause — the cap — is never named.
A second, quieter outcome: if a truncation happens to leave syntactically valid JSON that still satisfies the model schema, the report is accepted and delivered to the customer with content silently missing. Nothing in the pipeline can detect it, because the quality gate in quality.py checks section presence and copy, not whether generation completed.
Required behavior
- Read the provider's completion status (
finish_reason, and status/incomplete_details where the API provides them) and classify a truncated response as its own outcome, separate from a schema violation and from success.
- Do not retry a truncation at the same cap. Either raise the limit for the retry on evidence, or fail explicitly as incomplete; the current same-cap retry is guaranteed to have less room than the attempt that already failed.
- Derive the generation limit from the selected model, provider, and deployment rather than from a literal, and record the source, the computed value, and the value actually sent alongside the existing trace fields.
- Surface the distinction in
GenerationTrace and in the stored job failure reason, so "the model ran out of output budget" is never reported as "the model returned invalid JSON".
Regression cases
- a 200 response whose
finish_reason is length and whose body is valid JSON satisfying the schema must not be accepted as a complete report
- a truncated response must not be retried at the same cap, and the raised error must name the cap, not the schema
- a genuine schema violation must still take the existing repair path unchanged
- the limit must change when the selected model or deployment changes, and tests must not pin it to a literal
Scope note
src/four_pillars/nim.py and generation.py are not claimed by any open PR, but tests/test_nim.py and tests/test_nim_errors.py are owned by #39, and the routing change in #39 decides which catalog the limit should be read from. Filed rather than patched so the limit source and the routing land together.
Internal decision target: 2026-09-21. Next review: 2026-09-18.
Gap
The model clients never read
finish_reason. A response truncated because it hit the output cap is indistinguishable from a complete one, and the recovery path makes the situation worse rather than better.Verified on
main(@8c6a2fa):grep -rn "finish_reason\|incomplete" src/four_pillars/returns nothing.NimClient._contentaccepts any non-emptychoices[0].message.contentstring, so an HTTP 200 carrying a half-written JSON object is treated as a successful generation.The generation caps have no recorded basis
nim.py:156max_tokens: int = 4096generation.py:35max_tokens: int = 4096analysis.py:159max_tokens=8192(synthesis)analysis.py:189max_tokens=8192(editorial repair)Nothing in
docs/,CLAUDE.md, orAGENTS.mdstates where 4096 or 8192 came from — not a provider constraint, not a model catalog entry, not an administrator policy. There is no catalog of per-model output limits anywhere in the repository, so the caps cannot follow the selected model or deployment.Why truncation gets worse on retry, not better
NimClient.generatetreats a parse or schema failure as a repairable schema error (nim.py:185-208). On each round it appends the previous truncated output as an assistant message plus a repair instruction, then re-posts with the samemax_tokens:So the input grows every round while the output ceiling stays fixed. If the first attempt was cut off for lack of output budget, the retry has strictly less room to succeed. With
nim_max_schema_repairsdefaulting to 1 there is exactly one such retry, after which the client raiseswhich attributes an exhausted output budget to a schema defect. The operator sees a schema error, the job records a quality failure, and the real cause — the cap — is never named.
A second, quieter outcome: if a truncation happens to leave syntactically valid JSON that still satisfies the model schema, the report is accepted and delivered to the customer with content silently missing. Nothing in the pipeline can detect it, because the quality gate in
quality.pychecks section presence and copy, not whether generation completed.Required behavior
finish_reason, andstatus/incomplete_detailswhere the API provides them) and classify a truncated response as its own outcome, separate from a schema violation and from success.GenerationTraceand in the stored job failure reason, so "the model ran out of output budget" is never reported as "the model returned invalid JSON".Regression cases
finish_reasonislengthand whose body is valid JSON satisfying the schema must not be accepted as a complete reportScope note
src/four_pillars/nim.pyandgeneration.pyare not claimed by any open PR, buttests/test_nim.pyandtests/test_nim_errors.pyare owned by #39, and the routing change in #39 decides which catalog the limit should be read from. Filed rather than patched so the limit source and the routing land together.Internal decision target: 2026-09-21. Next review: 2026-09-18.