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

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

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
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
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.