GPT-6.1 Terra, hands-on: visual reasoning, structured outputs, and tools with the Responses API
OpenAI’s GPT-6.1 Terra is the fast, cheap vision model of the GPT-6 family — $1 per million input tokens, a 1.05M-token context window, and tool calling through the Responses API. This guide makes your first call, feeds it a photo, forces structured JSON out of it, and wires up web search and your own functions — every snippet verified against the official docs.
gpt-6.1-terra) is OpenAI’s fast, efficient vision model, released October 26, 2026: $1 per million input tokens, a 1.05M-token context window, six reasoning effort levels, and tool calling through the Responses API. This guide makes a first call, sends it a photo, returns structured JSON with Pydantic, adds web search, wires up a real function, and prices the whole workflow — every snippet verified against OpenAI’s own docs.- Model:
gpt-6.1-terra, released October 26, 2026 — fast, efficient visual reasoning, precise computer use, clear writing. - Price: $1 per million input tokens, $5 per million output tokens; cached input $0.05; prompts over 272K input tokens cost 2x input and 1.5x output.
- Limits: 1,050,000-token context window, 128,000 max output tokens, June 1, 2026 knowledge cutoff; text and image in, text out.
- Tools need Responses: tool calling works only through the Responses API — Chat Completions supports Terra but without tools.

OpenAI released GPT-6.1 Terra on October 26, 2026, and it fills a gap the GPT-6 family had left open: a fast, cheap model that can actually see. The model ID is gpt-6.1-terra, it costs $1 per million input tokens and $5 per million output tokens, and it takes a 1,050,000-token context window with up to 128,000 output tokens. OpenAI positions it for visual reasoning, precise computer use, and clear, natural writing — matching product photos to catalog entries, interpreting technical drawings, acting on application screens, and drafting and revising text.
That combination matters because it changes what a cheap API call can do. The previous generation's budget tier could read text and call tools, but serious image work meant paying flagship prices. Terra brings image input, six reasoning effort levels, and the full hosted-tool set — web search, file search, code interpreter, computer use, MCP — to a dollar-per-million-token price point. This guide builds the workflow OpenAI names first: a product-photo-to-catalog matcher. By the end you will have made your first call, fed the model a photo, forced structured JSON out of it, given it web search, wired up one of your own functions, and worked out exactly what it costs to run.
What you'll need#
- An OpenAI API account with billing enabled. Terra has no free tier — the model page lists the Free tier as not supported. A ChatGPT Plus or Pro plan gives you Terra inside ChatGPT Work and Codex, but not an API key. Create the key at platform.openai.com/api-keys and add a payment method.
- Python 3.10 or later, plus the official SDK:
pip install openai pydantic. Every snippet in this guide was verified withopenai3.46.0. - Your key as an environment variable:
export OPENAI_API_KEY="sk-...". The SDK reads it automatically; never paste a key into source code. - A photo to test with. Any product photo works — a chair, a shoe, a circuit board. The examples below use a public artwork URL from OpenAI's own docs for the first image test, then a local file for the real workflow.
Step 1: Your first request#
Terra works on the Responses API, Chat Completions, and Batch — but tool calling requires the Responses API, so this guide uses Responses throughout. Start with curl to prove your key and access work before writing any Python:
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-6.1-terra",
"input": "Write a one-line commit message for a fix to a broken pagination link"
}'
A working key returns a JSON response object with an id starting resp_, a status of completed, and the message in output. An invalid_api_key error means the key is wrong; a model_not_found error means the project attached to the key has no access to Terra. The Python version is the same call through the SDK:
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
response = client.responses.create(
model="gpt-6.1-terra",
input="Write a one-line commit message for a fix to a broken pagination link",
)
print(response.output_text)
print(response.usage) # input_tokens, output_tokens, total_tokens
Two details worth knowing now. First, the model string is exactly gpt-6.1-terra — there is one snapshot and it is also the default. gpt-6.1-sol is a different model with different pricing; the names are not interchangeable. Second, responses are stored by default (store defaults to true) for at least 30 days, which is what makes multi-turn continuation with previous_response_id work. Pass store=False if you do not want a response retained.
Step 2: Send it an image#
Terra accepts text and image input and returns text — no audio or video input. Images arrive as input_image content blocks, either as a public URL or as a base64 data URL. The URL form, taken from OpenAI's images guide:
response = client.responses.create(
model="gpt-6.1-terra",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "what's in this image?"},
{
"type": "input_image",
"image_url": "https://api.nga.gov/iiif/a2e6da57-3cd1-4235-b20e-95dcaefed6c8/full/!800,800/0/default.jpg",
},
],
}
],
)
print(response.output_text)
For your own photos, base64 is the practical path — no public hosting needed:
import base64
from openai import OpenAI
client = OpenAI()
with open("chair.jpg", "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
response = client.responses.create(
model="gpt-6.1-terra",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Describe this chair in detail: material, color, style, condition."},
{"type": "input_image", "image_url": f"data:image/jpeg;base64,{image_b64}", "detail": "high"},
],
}
],
)
print(response.output_text)
The detail parameter controls preprocessing: low, high, original, or auto (the default). Use high for ordinary photo analysis and original for dense, spatially sensitive work — OCR, small-object detection, computer use — where resizing would destroy the detail you need. Supported formats are PNG, JPEG, WEBP, and non-animated GIF; a single request can carry up to 1,500 images and a 512 MB total payload. Images count as input tokens and are billed at the input rate, so a 20-photo comparison costs roughly 20 photos' worth of tokens before a word of output.

Step 3: Pick a reasoning effort#
Terra supports six reasoning effort levels: none, low, medium (the default), high, xhigh, and max. It is one of the few GPT-6 models that supports none — Astra and Sol do not. OpenAI's own guidance is to start at medium and raise the effort only when the improvement in quality is worth the added time and tokens. In practice:
| Effort | Use it for |
|---|---|
none | Instant, mechanical work: rewrites, formatting, one-line classifications |
low | Simple lookups and descriptions where speed matters more than nuance |
medium | The default. Photo matching, drafting, everyday analysis |
high | Ambiguous images, fine distinctions between similar products, multi-step checks |
xhigh / max | Hard visual reasoning — dense technical drawings, tiny differences, contested matches |
response = client.responses.create(
model="gpt-6.1-terra",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Compare this chair against the catalog entry CHR-0042. List every visible difference."},
{"type": "input_image", "image_url": f"data:image/jpeg;base64,{image_b64}", "detail": "high"},
],
}
],
reasoning={"effort": "high"},
)
print(response.output_text)
Effort and verbosity are separate knobs. reasoning.effort controls how hard the model thinks; verbosity (low, medium, high, default medium) controls how long the answer is. A high effort with low verbosity gives you careful thinking in a short answer — the right combination for a matcher that has to be both accurate and cheap to read.
Step 4: Get structured JSON back, every time#
A catalog matcher that returns prose is a demo; one that returns JSON is a system. Structured Outputs constrains the model to a JSON Schema you supply, so required keys are never omitted and enum values are never invented. The cleanest way to use it in Python is client.responses.parse with a Pydantic model — the SDK builds the schema from the class and hands you a parsed object:
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class CatalogMatch(BaseModel):
sku: str
name: str
confidence: float
match_reason: str
differences: list[str]
response = client.responses.parse(
model="gpt-6.1-terra",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Match this photo to a catalog entry. Return the SKU, name, a confidence score, why it matches, and every visible difference."},
{"type": "input_image", "image_url": f"data:image/jpeg;base64,{image_b64}", "detail": "high"},
],
}
],
text_format=CatalogMatch,
)
match = response.output_parsed
print(match.sku, match.confidence)
print(match.differences)
output_parsed is a real CatalogMatch instance, not a dict — your downstream code can trust the field types. Three things to know about this step. First, the first request you make with any schema carries extra latency while the API processes it; repeat requests with the same schema do not. Second, a response can still come back incomplete: check response.status and, if it is incomplete with reason max_output_tokens, raise your cap and retry. Third, refusals are programmatically detectable — a refusal item appears in the output instead of text — so handle that case rather than parsing an empty string. If you prefer raw JSON Schema over Pydantic, the same constraint is available through text={"format": {"type": "json_schema", "name": "catalog_match", "schema": {...}, "strict": True}} on responses.create.
Step 5: Give it web search for anything after its cutoff#
Terra's knowledge cutoff is June 1, 2026. Anything newer — a price change, a recalled batch, a model that launched last week — needs the web. The hosted web_search tool runs server-side; you declare it and the model decides when to use it:
response = client.responses.create(
model="gpt-6.1-terra",
input="Has the manufacturer recalled or repriced the CHR-0042 lounge chair since June 2026? Check current sources.",
tools=[{"type": "web_search"}],
)
print(response.output_text)
Search results come back with citations. Each output_text part carries an annotations list, and url_citation annotations hold the source URL and title — collect them and show them to the user instead of letting the model's claims stand uncited:
for item in response.output:
if item.type == "message":
for part in item.content:
if part.type == "output_text":
for ann in part.annotations:
if ann.type == "url_citation":
print(ann.title, "-", ann.url)
Web search is a hosted tool, so there is a fee per tool call on top of tokens — check the pricing page for the current rate before wiring it into a high-volume loop. The same tools list is where the other hosted tools live: file_search, code_interpreter, image_generation, hosted_shell, apply_patch, skills, computer_use, and mcp. All of them require the Responses API; none of them work through Chat Completions with this model.

Step 6: Wire up one of your own functions#
Hosted tools cover the generic cases; your own data needs your own functions. The loop has five steps: declare the tool, send the prompt, read the function_call item, run your code, and send the result back with the matching call_id. Here the model looks up a catalog price from a local dictionary:
import json
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"name": "lookup_catalog_price",
"strict": True,
"description": "Look up the catalog price of a product by SKU.",
"parameters": {
"type": "object",
"properties": {
"sku": {"type": "string", "description": "Catalog SKU, e.g. CHR-0042"},
},
"required": ["sku"],
"additionalProperties": False,
},
},
]
def lookup_catalog_price(sku):
prices = {"CHR-0042": 1299.00, "CHR-0017": 899.00}
return json.dumps({"sku": sku, "price_usd": prices.get(sku)})
input_list = [{"role": "user", "content": "What does the walnut lounge chair in this photo cost?"}]
response = client.responses.create(
model="gpt-6.1-terra",
tools=tools,
input=input_list,
)
# Keep everything the model returned, including reasoning items,
# then append the result of each function call.
input_list += response.output
for item in response.output:
if item.type == "function_call" and item.name == "lookup_catalog_price":
args = json.loads(item.arguments)
result = lookup_catalog_price(args["sku"])
input_list.append(
{
"type": "function_call_output",
"call_id": item.call_id,
"output": result,
}
)
response = client.responses.create(
model="gpt-6.1-terra",
tools=tools,
input=input_list,
)
print(response.output_text)
The line that beginners most often get wrong is input_list += response.output. For reasoning models, the reasoning items in the response must be passed back along with the tool outputs — drop them and the next call loses the thread of what the model was thinking. The same loop handles multiple calls in one turn: iterate over every function_call item, execute each, and append each result before the second request. Keep the number of functions small and their descriptions precise — function definitions count against your context and are billed as input tokens, and accuracy falls as the tool list grows. OpenAI's guidance is to stay under about 20 functions per turn and defer the rest with tool_search.
Step 7: Know what it costs before you scale#
Terra's pricing, from the official model page (standard tier, per million tokens):
| Item | Price |
|---|---|
| Input | $1.00 |
| Cached input | $0.05 |
| Cache write, 30-minute retention | $1.25 |
| Cache write, 12-hour retention | $2.50 |
| Output | $5.00 |
Three modifiers change the bill. Requests with more than 272,000 input tokens are priced at 2x input and cache rates and 1.5x output for the full request. Batch and Flex prices are 50% lower than Standard. Fast mode costs 2x Standard, and regional processing adds a 10% premium where available. A worked example for the matcher: 1,000 requests, each with 2,000 input tokens and 400 output tokens, comes to 2,000,000 input tokens ($2.00) plus 400,000 output tokens ($2.00) — $4.00 for a thousand matches. Put the repeated catalog context behind prompt caching and the input side of that bill drops to the $0.05 cached rate on repeat requests. Watch the two quiet cost drivers: image tokens, which scale with image size and detail level, and reasoning tokens, which count as output tokens — a max effort setting on a hard image can cost several times the visible answer.
Which approach should you use?#
Terra is one of four GPT-6 models, and the choice is about the job, not the leaderboard:
| Model | Reach for it when |
|---|---|
| GPT-6.1 Terra | Visual reasoning, computer use, clear writing, high volume at low cost — this guide |
| GPT-6.1 Sol | Complex coding and professional work where you want near-Astra quality at a lower price |
| GPT-6 Astra | The hardest reasoning, coding, and professional work; multi-step workflows across code, browsers, and software |
| GPT-6 Luna | Focused, high-volume tasks where speed and cost matter most |
The API choice matters as much as the model. Use the Responses API for anything involving tools, reasoning effort, or multi-turn state — it is the only interface where Terra can call tools. Use Chat Completions only for plain text or image in, text out, with no tools. If you are migrating from GPT-5.6 Terra, compare both models on representative images and workflows at the same reasoning effort, and move any Chat Completions tool calls to the Responses API — Terra does not support tool calling through Chat Completions at all.
The takeaway#
- One model string, one API.
gpt-6.1-terraon the Responses API is the whole setup — tools, reasoning, and images all live there. - Images are just content blocks.
input_imagewith a URL or a base64 data URL,detail: "high"for analysis,"original"when spatial precision matters. - Start at
mediumeffort. Raise it only when the quality gain is worth the time and the output tokens. - Force JSON with
responses.parse. A Pydantic model turns the matcher from a demo into a component your code can trust. - Pass the whole response back.
input_list += response.outputbefore appending tool results — reasoning items included. - Do the cost math first. At $1 in / $5 out, a thousand catalog matches cost about $4 — until long context,
maxeffort, or uncached images multiply it.
Sources#
- GPT-6.1 Terra model page (OpenAI) — model ID, pricing, rate limits, supported tools, reasoning effort levels, read November 7, 2026.
- Using GPT-6 (OpenAI) — Terra positioning, migration guidance from GPT-5.6 Terra, reasoning effort advice, read November 7, 2026.
- Create a model response (OpenAI API reference) — request parameters,
reasoning.effort,text.format,storedefault, read November 7, 2026. - Structured model outputs (OpenAI) —
responses.parsewith Pydantic,text.formatJSON Schema, refusal and incomplete handling, read November 7, 2026. - Images and vision (OpenAI) —
input_imageURL and base64 forms, detail levels, format and size limits, read November 7, 2026. - Function calling (OpenAI) — the five-step tool loop,
function_call_outputwithcall_id, passing reasoning items back, read November 7, 2026.