Anatomy of a Response: The Typed output Array, Not Just Text

Ma Mahalakshmi V Updated 13 Sep 2026
7 min read ·Lesson 8 of 10

The Shortcut You've Already Been Using

Since Unit 1, every example in this course has read a model's reply the same way:

response = client.responses.create(
    model="gpt-5.6-luna",
    input="Name one moon of Saturn.",
)

print(response.output_text)

That output_text property is a convenience — a shortcut the SDK provides so you don't have to write boilerplate for the common case of "the model just replied with text." It works, and for a large share of simple applications, it's all you'll ever need. But it hides real structure underneath it, and the moment you start using tools (Unit 8 and 9), reasoning models, or anything beyond plain text-in-text-out, you need to understand what output_text is actually a shortcut for — because that's where the real data lives.

A Response Is Not a String — It's an Object With an output Array

When you call client.responses.create(...), what comes back is a Response object with several fields. The one that matters most for this lesson is output — a list, not a string:

response = client.responses.create(
    model="gpt-5.6-luna",
    input="Name one moon of Saturn.",
)

print(type(response.output))   # <class 'list'>
print(len(response.output))    # typically 1, for a simple text reply
print(response.output[0])

That last line prints something with a structure roughly like this (simplified for readability):

ResponseOutputMessage(
    id='msg_...',
    role='assistant',
    status='completed',
    type='message',
    content=[
        ResponseOutputText(
            type='output_text',
            text='Titan is one of Saturn\'s moons.',
            annotations=[]
        )
    ]
)

This is the core idea of this lesson: the output array is a list of typed items, and a plain text reply is just one particular kind of item — a message item — containing, in turn, a list of content pieces, one of which is a piece of output_text. response.output_text (no underscore-free typo — it really is one property name that reads like two words) is the SDK doing the work of digging through that structure, finding all the output_text content across every message item, and concatenating it into one plain string for you.

Why an Array, and Why Typed Items

This design exists directly because of the limitation covered in Lesson 1: models don't just produce text anymore. In a single turn, a model might reason internally, call a tool, wait for that tool's result, call a second tool, and then produce a text answer. Chat Completions had one message with fields bolted onto it to approximate this. The Responses API instead represents the entire sequence of things the model did during this turn as an ordered list of typed items. Each item declares its own type, and that type tells you how to interpret the rest of its fields.

The item types you'll encounter across this course include:

typeWhat it representsWhere it's covered
messageA chunk of text (or refusal) meant for the userThis lesson; every unit
function_callThe model asking your code to run a specific function with specific argumentsUnit 8
reasoningA reasoning model's internal reasoning summary for this turnUnit 3, Unit 6
web_search_callA built-in web search the model performedUnit 9
file_search_callA built-in file/vector-store search the model performedUnit 9
computer_callA computer-use action the model requestedAdvanced / later units

You will not see all of these in every response — a simple text-only call like the example above produces an output array with exactly one message item in it. But as soon as you give a model tools (Unit 8), you should expect to iterate over output and check each item's type, rather than assuming index 0 is always the text you want.

Inside a message Item: The content Array

A message-type item itself contains a content list, because even a single "message" can, in principle, mix different kinds of content. The two content types you'll meet most:

  • output_text — the normal case. Has a text field with the actual string, and an annotations field (a list, usually empty for plain text — it's used for citations when a model references files or web search results, covered in Unit 9).
  • refusal — what appears instead of output_text when the model declines to answer. It has its own refusal field explaining, in the model's words, why it's declining, rather than a normal answer.

This is worth sitting with for a second, because it explains something output_text conveniently hides from you: if a model refuses a request, response.output_text may come back empty, not because nothing happened, but because the reply was a refusal content item, not an output_text one, and the shortcut only concatenates the latter. If your code ever needs to distinguish "the model didn't answer" from "the model gave an empty answer," you have to look past output_text and inspect output directly.

Walking the Array Manually

Here's the pattern for handling a response that might contain more than one type of item — the pattern you'll build on heavily starting in Unit 8:

response = client.responses.create(
    model="gpt-5.6-luna",
    input="Name one moon of Saturn.",
)

for item in response.output:
    if item.type == "message":
        for content_piece in item.content:
            if content_piece.type == "output_text":
                print("Text:", content_piece.text)
            elif content_piece.type == "refusal":
                print("Refused:", content_piece.refusal)
    elif item.type == "function_call":
        print("Model wants to call:", item.name, "with args:", item.arguments)
    elif item.type == "reasoning":
        print("Reasoning item present (summary may be empty by default)")

This is more code than print(response.output_text), and for a simple text-only call, it's genuinely overkill — use the shortcut when a plain string is all you need. The point of this lesson isn't "always write the long form." It's that you now know what the short form is doing, so when it stops giving you what you expect — an empty string, a missing tool call, a reasoning-model response that looks incomplete — you know exactly where to look instead of guessing.

The Other Top-Level Fields Worth Knowing

output is the field this lesson focuses on, but a Response object carries several other top-level fields that matter in later units:

  • id — a unique identifier for this response (e.g. resp_...). This is what you pass to previous_response_id on a later call to chain conversations (Unit 4).
  • model — confirms which model actually processed the request. Useful for logging, and occasionally different from what you requested if OpenAI routed you to a specific dated snapshot behind an alias (Lesson 5).
  • status — the processing state of the response, such as completed or incomplete. A background request (Unit 5) may come back with a status other than completed if you check on it before it's finished.
  • usage — token accounting: how many input tokens, output tokens, and (when relevant) cached tokens this call used. This is what you'd read to track cost programmatically, tying directly back to the per-token pricing covered in Unit 1.
  • incomplete_details — populated when a response was cut off before finishing (for example, hitting a max_output_tokens limit), explaining why.
  • error — populated if something went wrong at the response level.

A Concrete Example: Inspecting usage

response = client.responses.create(
    model="gpt-5.6-luna",
    input="Explain what a REST API is in one paragraph.",
)

print(response.output_text)
print("---")
print("Input tokens:", response.usage.input_tokens)
print("Output tokens:", response.usage.output_tokens)
print("Total tokens:", response.usage.total_tokens)

Expected output (exact token counts will vary slightly based on the model's phrasing):

A REST API is a way for two pieces of software to communicate over
HTTP using a small, standard set of verbs...
---
Input tokens: 14
Output tokens: 58
Total tokens: 72

That usage object is where you'd hook in cost tracking for a real application — multiplying input_tokens and output_tokens by the per-model rates from the pricing table in Unit 1 gives you the exact cost of that one call.

Common Mistakes

Assuming response.output[0] is always the text reply. Once tools are involved, the first item in output might be a reasoning item or a function_call, not a message. Always check .type rather than assuming position.

Treating an empty output_text as "the call failed." It might mean the model refused (check for a refusal content item), or that the entire turn was tool calls with no accompanying text yet (common in multi-step tool-calling flows, covered in Unit 8) — not that anything is broken.

Confusing response.output (the array) with response.output_text (the derived string). They serve different purposes: output is the ground truth; output_text is a lossy convenience view over it that only captures text content.

Best Practices

Use response.output_text freely for simple, text-only, tool-free calls — it's there precisely so you don't have to hand-roll the same three-line loop in every project. But once your application uses tools, reasoning models, or needs to detect refusals reliably, switch to iterating response.output directly and branching on item.type, since that's the only place the full picture — every action the model took during that turn — actually lives.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Ask about this post
AI Ask about this post

Ask questions about Anatomy of a Response: The Typed output Array, Not Just Text and get answers drawn from it.

Signed-in readers only.