Anatomy of a Response: The Typed output Array, Not Just Text
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:
type | What it represents | Where it's covered |
|---|---|---|
message | A chunk of text (or refusal) meant for the user | This lesson; every unit |
function_call | The model asking your code to run a specific function with specific arguments | Unit 8 |
reasoning | A reasoning model's internal reasoning summary for this turn | Unit 3, Unit 6 |
web_search_call | A built-in web search the model performed | Unit 9 |
file_search_call | A built-in file/vector-store search the model performed | Unit 9 |
computer_call | A computer-use action the model requested | Advanced / 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 atextfield with the actual string, and anannotationsfield (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 ofoutput_textwhen the model declines to answer. It has its ownrefusalfield 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 toprevious_response_idon 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 ascompletedorincomplete. Abackgroundrequest (Unit 5) may come back with a status other thancompletedif 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 amax_output_tokenslimit), 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.