Reading the Response: Content Blocks, stop_reason and Usage

A
Author
30 Oct, 2026 4 min read 0 views

A Claude response is a Message object with an ID, the model name, a list of content blocks, a stop_reason and a usage record. Your program should read all of these, not just the first line of text.

The shape of a response

import anthropic

client = anthropic.Anthropic()
message = client.messages.create(
    model="claude-haiku-5-5",
    max_tokens=300,
    messages=[{"role": "user", "content": "Name three HTTP methods."}],
)

print(message.id)            # unique message ID
print(message.model)         # model that answered
print(message.role)          # always "assistant"
print(message.stop_reason)   # why generation ended
print(message.usage)         # token counts
for block in message.content:
    print(block.type)        # "text" for a simple answer

Content blocks

content is a list because a single reply can contain different kinds of output. A plain answer is one block with type == "text". With thinking enabled you may also get thinking blocks. With tools you get tool-use blocks. Always branch on block.type and ignore types you do not handle.

def extract_text(message) -> str:
    return "".join(b.text for b in message.content if b.type == "text")

stop_reason values

The stop reason tells you whether the answer is complete. Treat it as part of the result.

  • end_turn: the model finished naturally. This is the normal case.
  • max_tokens: the output hit your limit and is truncated. Raise the limit or ask for a shorter answer.
  • stop_sequence: a stop sequence you supplied was produced. The stop_sequence field says which one.
  • tool_use: the model wants you to run a tool and send back the result.
  • refusal: the model declined for safety reasons on models that report it. The response may carry details about the category.
  • pause_turn: a long server-side turn paused and can be resumed.

Usage

usage.input_tokens counts the tokens you sent that were not served from cache. usage.output_tokens counts what the model generated. When you use prompt caching, cache_creation_input_tokens and cache_read_input_tokens appear as well. Log these on every call. They are the raw data behind your bill.

A defensive reader

This helper returns text and raises when the result is unusable. It is a good starting point for scripts.

def read_reply(message) -> str:
    if message.stop_reason == "max_tokens":
        raise RuntimeError("Reply truncated: raise max_tokens")
    if message.stop_reason == "refusal":
        raise RuntimeError("Model declined this request")
    text = "".join(b.text for b in message.content if b.type == "text")
    if not text:
        raise RuntimeError("No text in reply")
    return text

print(read_reply(message))

Running it on the earlier example prints a short list of HTTP methods. If you lower max_tokens to 5, it raises the truncation error instead of quietly returning half an answer.

Why the response shape is designed this way

The API returns a list of blocks rather than a string because model output is no longer just text. A reply might include reasoning that you can inspect, a request to call one of your functions, or results from a server-side tool. Giving each piece a type means new features can be added without breaking old code, as long as your code ignores block types it does not understand. This is why the defensive pattern of filtering by type is worth adopting on day one.

Using the ID and model fields

The message id is useful in logs. When you contact support about a strange result, quoting the ID lets them find the exact request. The model field tells you which model actually answered, which matters if you ever use an alias that can move to a newer snapshot. Store both next to your own request ID so that you can trace a bad answer back to the prompt, the model and the time it happened.

Turning the response into your own result type

Rather than passing raw SDK objects around your application, convert them into a small structure of your own at the edge. That keeps SDK details in one place and makes it easy to add logging, cost tracking and checks in a single function.

from dataclasses import dataclass

@dataclass
class LLMResult:
    text: str
    input_tokens: int
    output_tokens: int
    stop_reason: str
    message_id: str

def to_result(message) -> LLMResult:
    text = "".join(b.text for b in message.content if b.type == "text")
    return LLMResult(
        text=text,
        input_tokens=message.usage.input_tokens,
        output_tokens=message.usage.output_tokens,
        stop_reason=message.stop_reason,
        message_id=message.id,
    )

Now the rest of your program only deals with LLMResult, and moving to another provider later means rewriting one function instead of hunting through the codebase.

Mistakes to avoid

  • Assuming content[0] is text. It may be a thinking or tool block.
  • Ignoring stop_reason and shipping truncated output to users.
  • Not logging usage, then being surprised by the invoice.
  • Parsing the ID or model name for logic. Use them for logging only.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.