The first mistake in most structured output failures is that the retry loop fires before anyone sees what came back. If your code goes straight from the API response into json.loads(), the exception message ("Expecting value: line 1 column 1") tells you almost nothing. It cannot distinguish a markdown fence from an HTML error page from a truncated string.
Start by writing the unmodified payload to a log or a file, keyed by request ID. That means the full completion text, the finish reason, the token usage counts and the model identifier that actually served the request. Keep the raw bytes even when parsing succeeds, because a field can be silently wrong while the JSON is syntactically fine.
raw = resp.choices[0].message.content
log.debug("raw_len=%d finish=%s raw=%r",
len(raw or ""), resp.choices[0].finish_reason, raw)
Two fields on that line carry most of the diagnostic weight. finish_reason tells you whether the model stopped naturally, hit a token limit, or was filtered. A length value there is your truncation signal before you even look at the content. The %r repr matters too: it makes leading and trailing whitespace, fence characters and control characters visible in a way a plain print often hides.
This habit is the foundation of everything else. Once you can reproduce the exact bytes, every later step is a controlled experiment rather than a guess. A practitioner who skipped this step ends up changing three things at once: the prompt, the schema and the retry count, then has no idea which one fixed it, if any did.



