GrepBoard/Manuals
51 manuals ·

Check stop_reason to Tell If a Claude Response Was Cut Off

·Python 3.12.3 · Claude Messages API docs as of 2026-10 · checked against sample response JSON·7 steps

If json.loads fails on a reply cut off mid-string, read stop_reason first. A Python check for max_tokens truncation, and how to raise the limit or continue.

If json.loads fails with Unterminated string starting at and the prompt looks fine, suspect a reply that ran out of room. The stop_reason field in the response JSON tells you.

[1] Look up the stop_reason value. Three values in the official table relate to truncation or a clean finish.

ValueMeaning (docs)What to do (docs)
end_turnClaude finished its response naturally.Use the response.
max_tokensThe response reached your max_tokens limit.Raise max_tokens or continue the response.
model_context_window_exceededThe response filled the model's context window.Treat the response as truncated.

max_tokens is the generation cap set in your request. The docs call it "the absolute maximum number of tokens to generate" and note the model may stop before it. At the cap, output stops mid-sentence or mid-JSON.

[2] Save a normal and a truncated response. These samples follow the response example in the docs (id, content, stop_reason, usage). They are hand-made, not real API output.

cut.json
{"id":"msg_01235","type":"message","role":"assistant","content":[{"type":"text","text":"{\"name\": \"A\", \"tags\": [\"x\", \"y"}],"stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":100,"output_tokens":10}}

ok.json has the same shape with text set to {"name": "A", "tags": ["x", "y"]} and stop_reason end_turn. cut_valid.json has text [1, 2, 3] with stop_reason max_tokens.

[3] Write the checker. It joins the text blocks, tries json.loads, and exits 1 when stop_reason is a truncation value.

check_stop.py
import json, sys TRUNCATED = {"max_tokens", "model_context_window_exceeded"} def check(path):    resp = json.load(open(path))    stop = resp.get("stop_reason")    text = "".join(b["text"] for b in resp["content"] if b["type"] == "text")    try:        json.loads(text)        parsed = "ok"    except json.JSONDecodeError as e:        parsed = f"fail ({e.msg}, char {e.pos})"    cut = stop in TRUNCATED    print(f"{path}: stop_reason={stop} output_tokens={resp['usage']['output_tokens']} json={parsed} -> {'TRUNCATED' if cut else 'complete'}")    return 1 if cut else 0 sys.exit(max(check(p) for p in sys.argv[1:]))

[4] Run it on all three files.

console
$ python3 check_stop.py ok.json cut.json cut_valid.json; echo exit=$?ok.json: stop_reason=end_turn output_tokens=18 json=ok -> completecut.json: stop_reason=max_tokens output_tokens=10 json=fail (Unterminated string starting at, char 28) -> TRUNCATEDcut_valid.json: stop_reason=max_tokens output_tokens=10 json=ok -> TRUNCATEDexit=1

cut_valid.json shows that a successful parse can still be a truncated reply. A top-level array or number can be valid JSON by accident when cut short, so check stop_reason alongside the parse.

[5] With the SDK, run the same test on the response object. This is the docs' own example.

app.py
if response.stop_reason == "max_tokens":    # Response was truncated    print("Response was cut off at token limit")    # Consider making another request to continue

[6] The simplest fix is to raise max_tokens and ask again. The docs say each model has its own maximum, so check the limit for your model in the models overview. If a tool call is cut off and the last block is an incomplete tool_use, the docs say to retry with a higher max_tokens.

[7] If the text goes to a reader, the docs also show appending a notice.

app.py
def handle_truncated_response(response):    text = next((block.text for block in response.content if block.type == "text"), "")    if response.stop_reason in ["max_tokens", "model_context_window_exceeded"]:        if response.stop_reason == "max_tokens":            note = "[Response truncated due to max_tokens limit]"        else:            note = "[Response truncated due to context window limit]"        return f"{text}\n\n{note}"    return text

Docs: Handling stop reasons · Messages API

Related: if the JSON is malformed rather than cut off, see Get JSON-only output from Claude when json.loads fails.

#claude#api#stop_reason#json