Stop Sequences, Stop Reasons, and Forcing Structure
~14 min · stop-sequences, stop-reason, json, structured-output
Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete
The four stop reasons
Every response includes a stop_reason telling you why generation halted. The four you will see in production: end_turn (model finished naturally), max_tokens (you ran out of budget), stop_sequence (your custom stop string fired), tool_use (the model wants to call a tool — handle separately). A response with stop_reason='max_tokens' is truncated; treat it differently from end_turn.
Stop sequences for structured output
You can pass stop_sequences=["</answer>", "END"] and the API halts as soon as any of those strings appears. Combined with prompt design, this is a lightweight way to extract structured fragments. Heavier structure should use tool use or schema-prompted JSON, not stop sequences.
JSON output without tool use
For pure JSON output, the most reliable pattern is: tell the system prompt the exact JSON schema, prefill the assistant turn with {, and parse defensively. Prefilling the assistant message biases the model into the shape you want without any tool overhead.
Principle: Branch on stop_reason, not on response length. Truncated and complete responses look the same; only the reason tells you which is which.
Code
Branching on stop_reason·python
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": "Write 30 sentences about JSON."}],
)
match response.stop_reason:
case "end_turn":
save(response.content[0].text)
case "max_tokens":
# truncated — either raise budget or chunk the request
retry_with_higher_budget(response)
case "stop_sequence":
partial = response.content[0].text
log.info("stopped on user-defined sequence")
case "tool_use":
dispatch_tool_calls(response)
Forcing JSON via prefill·python
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
system='Respond with a single JSON object matching {"intent": str, "confidence": float}. No prose.',
messages=[
{"role": "user", "content": "User said: 'cancel my subscription'"},
{"role": "assistant", "content": "{"}, # prefill — model continues from here
],
stop_sequences=["}\n"],
)
import json
payload = json.loads("{" + response.content[0].text) # add back the prefill
print(payload)
Take an existing Claude prompt that returns JSON via prose. Convert it to (1) prefill + stop_sequences and (2) tool use with a JSON Schema. Compare parse-failure rates over 50 calls.
Hint
If the schema has more than two levels of nesting, tool use will likely win on parse rate.
Progress
Progress is local-only — sign in to sync across devices.