OpenAI Responses API skill for Codex
Typed output items, deliberate state, clean streams. Codex writes Responses API code that walks output items by type, keeps reasoning items when replaying history, and resends instructions on every turn. It picks one conversation-state strategy on purpose, rewrites Chat Completions shapes with a checklist, and treats truncated responses as failures instead of parsing half an answer.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- openai-responses-api Installs to ~/.agents/skills/openai-responses-api/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install openai-responses-api The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project openai-responses-api Installs to .agents/skills/openai-responses-api/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/openai-responses-api.zipunzip -o openai-responses-api.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) openai-responses-api PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $openai-responses-api, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and migrates OpenAI integrations on the Responses API (POST /v1/responses) — instructions and input items, output item parsing, previous_response_id and Conversations state, store=false with reasoning replay, built-in tools, typed streaming events and compaction. Use when writing new OpenAI API code, moving a Chat Completions integration to Responses, debugging missing context or lost reasoning between turns, or wiring streaming output into a server or UI.
Use it when
- Starting a new integration against the OpenAI API
- Porting Chat Completions code to /v1/responses
- Multi-turn context or reasoning quality degrades between turns
- Adding built-in tools such as web search or file search
Use something else when
- The app still uses the Assistants API with threads and runs: use assistants-to-responses-migration
- The work is mostly the tool-execution loop itself: use openai-function-calling
How to check the result
A fixture test covering message, reasoning, function_call and refusal items passes, and a live two-turn smoke call shows turn 2 using turn 1 context with expected input token growth.
The mistake it prevents
Assuming instructions carry over with previous_response_id; they do not, so the persona silently disappears on the second turn.
Done well means: every call goes through one thin client wrapper, conversation state is
owned on purpose (server-side or replayed, never both), the code walks output items by
type instead of assuming a single message, and streaming handlers branch on event type.
Before writing code
- Read
AGENTS.mdfor the package manager, env-var conventions and test command. - Find the current integration:
grep -rnE "chat\.completions|responses\.create|openai\(" src/ app/ lib/ 2>/dev/null. - Check the SDK version in the lockfile (
openaifor Python or Node). Responses helpers such asoutput_textandresponses.parseneed a recent SDK; upgrade first if the version predates them. Installing packages needs network, which may require approval in the Codex sandbox. - Never hard-code the model in call sites. Read it from config (
OPENAI_MODEL) and check the models page for the current recommendation before choosing a default.
Request shape
import os
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY
MODEL = os.environ["OPENAI_MODEL"] # choose from the models page; pin a snapshot in prod
resp = client.responses.create(
model=MODEL,
instructions="You are a terse release-notes writer.", # system-level guidance
input="Summarize: fixed login loop on Safari; added CSV export.",
max_output_tokens=800,
)
print(resp.output_text) # SDK helper: concatenated text of message items
import OpenAI from "openai";
const client = new OpenAI();
const resp = await client.responses.create({
model: process.env.OPENAI_MODEL!,
instructions: "You are a terse release-notes writer.",
input: "Summarize: fixed login loop on Safari; added CSV export.",
});
console.log(resp.output_text);
input is a string or a list of items: {"role": "user"|"assistant"|"developer", "content": ...},
function_call_output, images ({"type": "input_image", "image_url": ...}) and files.
Reading output
resp.output is a list of typed items. Handle at least these:
item.type | Meaning | What to do |
|---|---|---|
message | Assistant text (content parts output_text or refusal) | Render; check for refusal |
reasoning | Reasoning summary / encrypted state | Keep for replay; do not show raw |
function_call | Your tool call (name, arguments, call_id) | Execute, return function_call_output |
web_search_call, file_search_call, image_generation_call, mcp_call, … | Built-in tool activity | Log; surface citations |
Also check resp.status: incomplete with incomplete_details.reason == "max_output_tokens"
means truncated output — raise or retry with a higher limit, never parse it as complete.
State: pick exactly one strategy
| Need | Strategy | Notes |
|---|---|---|
| Simple chat, data may be stored | previous_response_id=prev.id (with store=True, the default) | Resend instructions every turn; they are not inherited. Prior tokens are still billed as input. Stored responses expire after 30 days. |
| Durable thread across devices/sessions | Conversations API: conv = client.conversations.create() then conversation=conv.id | Conversation items are not subject to the 30-day response TTL. |
| Zero data retention, compliance, or full control | store=False and replay the full output list yourself | Keep reasoning items (they carry encrypted content) or reasoning models lose their chain of thought between turns. |
history = [{"role": "user", "content": "Plan a DB migration for orders."}]
r1 = client.responses.create(model=MODEL, input=history, store=False)
history += r1.output # replay ALL items, including reasoning
history.append({"role": "user", "content": "Now add a rollback step."})
r2 = client.responses.create(model=MODEL, input=history, store=False)
In Node, convert output items before replay with the SDK helper rather than spreading raw objects if your SDK version provides one; otherwise map fields explicitly.
Never mix strategies in one thread (for example previous_response_id plus a replayed
history) — the model sees duplicated turns.
Long threads: use server-side compaction (context_management with a compaction threshold)
or the standalone compact endpoint instead of hand-truncating; check the conversation-state
guide for the current parameter shape before relying on it.
Built-in tools
resp = client.responses.create(
model=MODEL,
input="What changed in the latest PostgreSQL minor release?",
tools=[{"type": "web_search"}],
)
Built-ins include web_search, file_search (needs a vector store id), code_interpreter,
image_generation and remote MCP servers. Function tools use the flat shape
{"type": "function", "name": ..., "parameters": ..., "strict": True} — not the nested
{"function": {...}} shape from Chat Completions. For tool loops use the
openai-function-calling skill.
Streaming
stream = client.responses.create(model=MODEL, input="Write a haiku about caches.", stream=True)
for event in stream:
match event.type:
case "response.output_text.delta":
print(event.delta, end="", flush=True)
case "response.function_call_arguments.done":
pass # arguments complete for one call
case "response.completed":
final = event.response # full response with usage
case "error":
raise RuntimeError(event)
Rules: branch on event.type; ignore unknown types (new ones get added); take usage and
final status from response.completed, not from accumulated deltas; propagate client
disconnects by closing the stream so you stop paying for tokens.
Migrating from Chat Completions
-
messages→instructions(system text) +input(remaining turns) -
choices[0].message.content→resp.output_textor walkresp.output -
response_format→text={"format": {...}} - Function tools: flatten
{"type":"function","function":{...}}to{"type":"function","name":...} - Tool results:
{"role":"tool","tool_call_id":...}→{"type":"function_call_output","call_id":...,"output":...} -
max_tokens/max_completion_tokens→max_output_tokens -
nis gone: issue separate requests for multiple candidates - Stream handlers rewritten for typed events (no
choices[0].delta) - Decide
storeexplicitly (Responses defaults toTrue; Chat Completions defaultsstoreto false) -
usage.prompt_tokens→usage.input_tokens; cached tokens atusage.input_tokens_details.cached_tokens
Migrate one call site at a time behind the wrapper, compare outputs on a fixed prompt set, then delete the old path.
Gotchas
instructionsdo not carry over withprevious_response_id. Missing persona on turn 2 is almost always this.- Dropping
reasoningitems when replaying withstore=Falsedegrades multi-turn and tool-use quality silently. output_textis an SDK convenience. Raw HTTP clients must walkoutput[].content[].- A response can contain several
function_callitems; handle all before the next call. temperature/top_pmay be unsupported on reasoning models; usereasoning={"effort": ...}andtext={"verbosity": ...}where the model supports them. Check the model page.- Set
safety_identifier(a hashed user id) for end-user traffic instead of the olduserfield. - Do not log full prompts or outputs that contain user PII; log response ids and usage.
Verification
- Unit-test the wrapper with a recorded response fixture covering
message,reasoning,function_callandrefusalitems. - Run one live smoke call (needs network and a key; ask before spending tokens in CI):
python -c "from app.llm import ask; print(ask('ping'))". - For multi-turn changes, assert turn 2 references turn 1 content, and that
usage.input_tokensgrows as expected. - Grep that no old shapes remain:
grep -rn "chat.completions\|tool_call_id\|response_format" src/.
Output contract
When reporting the change, include:
**Endpoint:** Responses API, model from `OPENAI_MODEL` (currently <value>)
**State strategy:** previous_response_id | conversation | store=false replay — and why
**Call sites migrated:** <list with paths>
**Streaming:** events handled: <list>
**Verified:** <tests run, smoke call result, token usage before/after> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "OpenAI Responses API"
short_description: "Typed output items, deliberate state, clean streams"
default_prompt: "$openai-responses-api Migrate our chat.completions calls in src/llm to the Responses API" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.