Assistants to Responses skill for Codex

Get off the shut-down Assistants API safely. Codex inventories every beta.assistants and threads call, moves assistant instructions and tools into versioned code, and rewrites runs and polling as responses.create with a capped function_call loop. Legacy threads are backfilled into Conversations from your own stored history after you confirm the plan, streaming handlers and citation annotations are remapped, and a replay set proves the new path behaves like the old one.

  • AI & LLM Engineering
  • $assistants-to-responses-migration
  • 161 lines
  • 3 files
  • ~2,150 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- assistants-to-responses-migration

Installs to ~/.agents/skills/assistants-to-responses-migration/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $assistants-to-responses-migration, 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

Migrates OpenAI Assistants API applications to the Responses API and Conversations - assistants to instructions and tools in code or prompt objects, threads to conversations with backfilled items, runs and polling to responses.create, requires_action tool loops to function_call and function_call_output items, file_search and code_interpreter tool configuration, streaming event mapping and citation annotations. Use when code still calls beta.assistants, beta.threads or runs (the Assistants API was shut down on 26 August 2026), when an assistant-based app is failing, or when planning the data and cutover for a migration.

Use it when

  • Code still calls beta.assistants, beta.threads or threads.runs
  • An assistant-based chatbot broke after the August 2026 shutdown
  • Planning data backfill and cutover from threads to Conversations
  • Converting Assistants streaming handlers or file_search citations

Use something else when

How to check the result

A search for Assistants call sites returns nothing, and a replay of real conversations through the new path matches or beats the old answers with tool calls and citations intact.

The mistake it prevents

Assuming thread history can still be fetched from the API after shutdown, or sending instructions only on the first turn when Responses applies them per request.

~/.agents/skills/assistants-to-responses-migration/SKILL.md

--- name: assistants-to-responses-migration description: Migrates OpenAI Assistants API applications to the Responses API and Conversations - assistants to instructions and tools in code or prompt objects,… license: MIT ---

The OpenAI Assistants API was deprecated in August 2025 and shut down on 26 August 2026. Any code path still calling beta.assistants, beta.threads or threads.runs is broken or about to be. Migration is done when no Assistants calls remain, every existing user conversation either continues in a Conversation or is explicitly archived, tool loops run in your code with caps, and behaviour on a replay set matches the old app.

Concept map

Assistants APIResponses APIWhat changes for you
Assistant (model, instructions, tools)instructions + tools in code, or a Prompt object (prompt={"id": ...})Config moves into version control (or a dashboard prompt)
ThreadConversation (conversations.create)Holds items: messages, tool calls, tool outputs
MessageItem (message with input_text / output_text content)Typed content parts
Run + pollingresponses.create (synchronous, streamed, or background=True)No polling loop for normal calls
Run stepOutput items (function_call, file_search_call, code_interpreter_call, message)Inspect response.output by type
requires_action / submit_tool_outputsfunction_call items answered by function_call_output items in the next requestYour code owns the loop
tool_resources on assistant/threadvector_store_ids on the file_search tool; container on code_interpreterPer request
additional_instructionsinstructions on that requestNot inherited across turns
response_formattext={"format": ...}Structured outputs syntax
truncation_strategytruncation="auto" or compactionContext management

Full parameter, event and annotation mapping: read references/mapping.md when converting a specific call site or streaming handler.

Inventory before changing code

  1. Read AGENTS.md, then find every call site: rg -n "beta\.(assistants|threads)|threads\.runs|submit_tool_outputs|create_and_poll|assistant_id|thread_id" .
  2. List each assistant the code references (ids in env vars or the DB) with model, instructions, tools, vector stores and files. The dashboard may still show them for reference even though the API is gone; copy the instructions and tool schemas into the repo now.
  3. Find where thread ids are stored (users table, sessions). That is your mapping table.
  4. Find your own copy of message history. Since shutdown, thread messages can no longer be listed through the API, so history exists only if your app stored it (or an export was taken before shutdown). Tell the user plainly if it does not exist.
  5. Vector stores and uploaded files are separate resources used by Responses file_search; confirm they still list (client.vector_stores.list()) and reuse them.

Decisions

QuestionDefaultAlternative
Where do instructions and tools live?In code, versioned in git, one module per former assistantDashboard Prompt objects when non-engineers edit prompts; check OpenAI’s deprecation notes for reusable prompts before depending on them
How is state kept?Conversations (conversation=conv_id) for user-visible chatsprevious_response_id chaining for short flows; full client-side history with store=False for zero-retention requirements
Old historyBackfill into a new Conversation per threadArchive read-only in your DB and start fresh conversations
Long-running runsSynchronous or streamed responses.createbackground=True and poll responses.retrieve(id) for tasks over a few minutes

Code: before and after

# BEFORE (no longer works)
run = client.beta.threads.runs.create_and_poll(thread_id=thread_id, assistant_id=ASSISTANT_ID)
if run.status == "requires_action":
    outputs = [{"tool_call_id": tc.id, "output": run_tool(tc)}
               for tc in run.required_action.submit_tool_outputs.tool_calls]
    client.beta.threads.runs.submit_tool_outputs_and_poll(
        thread_id=thread_id, run_id=run.id, tool_outputs=outputs)
# AFTER
import json
from openai import OpenAI
client = OpenAI()

SUPPORT = {                                  # was assistant asst_...
    "model": "gpt-5-mini",                   # pick and evaluate; do not assume the old model
    "instructions": open("prompts/support.md").read(),
    "tools": [
        {"type": "file_search", "vector_store_ids": ["vs_abc123"]},
        {"type": "function", "name": "get_order", "description": "Look up an order by id.",
         "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
                        "required": ["order_id"], "additionalProperties": False},
         "strict": True},
    ],
}

def reply(conversation_id: str, user_text: str) -> str:
    resp = client.responses.create(**SUPPORT, conversation=conversation_id,
                                   input=[{"role": "user", "content": user_text}])
    for _ in range(6):                                       # cap tool rounds
        calls = [o for o in resp.output if o.type == "function_call"]
        if not calls:
            return resp.output_text
        outputs = [{"type": "function_call_output", "call_id": c.call_id,
                    "output": json.dumps(run_tool(c.name, json.loads(c.arguments)))}
                   for c in calls]
        resp = client.responses.create(**SUPPORT, conversation=conversation_id, input=outputs)
    raise RuntimeError("tool loop did not finish")

instructions apply to one request only; send them on every call (the module dict does this). Tool calls and outputs are appended to the conversation automatically when you pass conversation.

Backfilling threads into conversations

def to_items(messages):          # from your own stored history, oldest first
    items = []
    for m in messages:
        part = "input_text" if m.role == "user" else "output_text"
        content = [{"type": part, "text": m.text}]
        if m.role == "user" and m.image_url:
            content.append({"type": "input_image", "image_url": m.image_url, "detail": "auto"})
        items.append({"type": "message", "role": m.role, "content": content})
    return items

conv = client.conversations.create(items=to_items(history[-40:]),
                                   metadata={"legacy_thread_id": thread_id, "user_id": uid})
db.update_user_session(uid, conversation_id=conv.id)
  • Backfill only recent turns (or a summary plus recent turns); full histories cost tokens on every future request.
  • Run the backfill as an idempotent job keyed on the legacy thread id; it can be resumed.
  • Writing to production data and creating conversations for every user is a bulk operation: show the plan and counts to the user and get confirmation before running it.

Cutover checklist

  • Inventory call sites, assistants, vector stores, thread id storage, history source
  • Copy instructions and tool schemas into the repo
  • Build the Responses wrapper with tool loop, caps and logging
  • Map streaming handlers to Responses events (references/mapping.md)
  • Render file_citation / url_citation annotations from output_text parts
  • Backfill or archive old threads (after user confirmation); store conversation ids
  • Replay 30-100 real conversations through old transcripts vs new path and compare
  • Remove all beta.assistants / beta.threads code and dead env vars
  • Deploy behind a flag; confirm with the user before the production rollout

Verification

  • rg for Assistants call sites returns nothing.
  • Replay set: answers are judged equivalent or better (use llm-evals graders), tool calls hit the same functions with valid arguments, citations render.
  • Token and cost per conversation are logged; compare against the old app’s usage export.
  • A migrated user opens an old chat and the next answer uses the backfilled context.

Gotchas

  • previous_response_id and conversation are alternatives; using both errors or duplicates context.
  • Responses default to store=True. If the old app relied on zero-retention settings, set store=False and replay context yourself (reasoning items need include=["reasoning.encrypted_content"]).
  • Function call ids: answer with call_id, not the item id.
  • Code interpreter files are now container files; download them through the containers API before the container expires, not through the old file ids.
  • Model behaviour changes with the model, not the API. Evaluate the model choice separately from the plumbing migration.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Assistants to Responses"
  short_description: "Get off the shut-down Assistants API safely"
  default_prompt: "$assistants-to-responses-migration Our app still uses the OpenAI Assistants API and it stopped working, migrate it to Responses"
references/mapping.md96 lines, 5.3 KB
# Assistants to Responses: detailed mapping

Use this when converting a specific call site. Confirm exact fields against the current
OpenAI API reference if a call fails; the SDK's type hints are the fastest check.

## Calls

| Assistants call | Responses replacement |
|---|---|
| `beta.assistants.create/update` | A config module in code (`model`, `instructions`, `tools`) or a dashboard Prompt |
| `beta.assistants.retrieve` | Import the config module |
| `beta.threads.create(messages=..., metadata=...)` | `conversations.create(items=..., metadata=...)` |
| `beta.threads.retrieve/update/delete` | `conversations.retrieve/update/delete` |
| `beta.threads.messages.create` | Pass the message as `input` on the next `responses.create`, or `conversations.items.create` to append without generating |
| `beta.threads.messages.list` | `conversations.items.list(conversation_id)` for new conversations; your own DB for legacy threads |
| `beta.threads.runs.create` / `create_and_poll` | `responses.create(..., conversation=conv_id)` |
| `beta.threads.runs.stream` | `responses.create(..., stream=True)` or `responses.stream(...)` |
| `beta.threads.create_and_run` | `conversations.create` then `responses.create` |
| `runs.submit_tool_outputs(_and_poll)` | Next `responses.create` with `function_call_output` items as `input` |
| `runs.cancel` | Abort the HTTP request; for `background=True`, `responses.cancel(response_id)` |
| `runs.steps.list` | Iterate `response.output` items |

## Run parameters

| Run / assistant field | Responses field |
|---|---|
| `assistant_id` | Spread your config (`model`, `instructions`, `tools`) or `prompt={"id": ..., "version": ...}` |
| `instructions` (override) | `instructions` |
| `additional_instructions` | Append to `instructions` for this request |
| `additional_messages` | Extra items at the start of `input` |
| `tools` | `tools` (same function schema, flattened: `name`, `description`, `parameters` at top level, plus `"strict": true`) |
| `tool_choice` | `tool_choice` |
| `parallel_tool_calls` | `parallel_tool_calls` |
| `temperature`, `top_p` | Same names, where the model supports them (reasoning models ignore or reject them) |
| `max_completion_tokens` | `max_output_tokens` |
| `max_prompt_tokens`, `truncation_strategy` | `truncation="auto"`; compaction for long conversations |
| `response_format` (json_schema) | `text={"format": {"type": "json_schema", "name": ..., "schema": ..., "strict": True}}` |
| `metadata` | `metadata` |
| `tool_resources.file_search.vector_store_ids` | `{"type": "file_search", "vector_store_ids": [...]}` in `tools` |
| `tool_resources.code_interpreter.file_ids` | `{"type": "code_interpreter", "container": {"type": "auto", "file_ids": [...]}}` |

## Function tool definition

```json
// Assistants (nested)
{"type": "function", "function": {"name": "get_order", "description": "...", "parameters": {...}}}

// Responses (flat)
{"type": "function", "name": "get_order", "description": "...", "parameters": {...}, "strict": true}
```

With `strict: true`, every property must be listed in `required` and
`additionalProperties` must be `false`; model optional fields as nullable types.

## Run status to Responses handling

| Run status | Responses equivalent |
|---|---|
| `queued`, `in_progress` | Request in flight; with `background=True`, `status` on `responses.retrieve` |
| `requires_action` | `response.output` contains `function_call` items |
| `completed` | `status == "completed"` with no pending function calls |
| `incomplete` | `status == "incomplete"`; check `incomplete_details.reason` (e.g. `max_output_tokens`) |
| `failed` | Exception from the SDK, or `status == "failed"` with `error` |
| `cancelled`, `expired` | Aborted request; background responses can be cancelled |

## Streaming events

| Assistants event | Responses event |
|---|---|
| `thread.run.created` | `response.created` |
| `thread.message.delta` | `response.output_text.delta` (`event.delta` is the text) |
| `thread.message.completed` | `response.output_text.done` / `response.output_item.done` |
| `thread.run.step.delta` (tool call args) | `response.function_call_arguments.delta` / `.done` |
| `thread.run.requires_action` | `response.output_item.done` with a `function_call` item, then `response.completed` |
| `thread.run.completed` | `response.completed` (carries `usage`) |
| `thread.run.failed` | `response.failed` / `error` |

Branch on `event.type`; ignore event types you do not handle so new ones do not break
the parser.

## Annotations and files

- File search citations arrive as `annotations` on `output_text` content parts with type
  `file_citation` (`file_id`, `filename`, `index`). Map `file_id` to your document titles.
- Web search citations use `url_citation` (`url`, `title`, start and end indexes).
- Code interpreter outputs are container files, cited with `container_file_citation`.
  Download them via the containers file-content endpoint while the container is alive.
- To get file search results themselves (not only citations), request
  `include=["file_search_call.results"]`.

## Usage and cost

Responses report `usage.input_tokens`, `usage.output_tokens`,
`usage.input_tokens_details.cached_tokens` and `usage.output_tokens_details.reasoning_tokens`.
Conversations replay their items as input on every turn, so long conversations grow
input cost; cap backfilled history and use truncation or compaction.

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.

esc