Ringside · Migration guide

Persona · Teams with production Assistants code facing a hard deprecation

Updated 2026-07-18

Migrating from OpenAI Assistants API to Ringside

Your Assistants code works today and stops working on 26 August 2026. Not degrades, stops: api.openai.com/v1/assistants, /v1/threads and the run endpoints start returning 410 Gone, and the assistants, threads and runs behind them are deleted. There is no version of this where you do nothing.

The expensive path is OpenAI's own successor, the Responses API paired with Conversations. It is a reasonable API and it is not wire-compatible with what you have, so taking it means new primitives, a new run state machine and a new streaming format, which is a rewrite of every code path that touches a thread.

Ringside is the other path. Point the SDK somewhere else and every client.beta.threads.* and client.beta.assistants.* call keeps working exactly as written.

See also: Migration library index with four more step-by-step guides for migrating off OpenAI Chat, Anthropic, OpenRouter and Chatbase.


Before you worry about breaking prod

The realistic fear here is not that the migration is hard. It is that you cut over on a Thursday and something subtle breaks on Saturday. So, plainly:

  • Your prompts, tools and assistant definitions do not change. Same JSON, same function schemas, same tool_choice semantics, same streaming event names.
  • Rollback is one line. Point base_url back at api.openai.com and remove the key swap. Nothing on our side holds your app hostage, and while both are live you can flip between them per deploy.
  • You can migrate one assistant first. Create a single assistant and thread on Ringside, run your real prompts through it, and compare the responses against what OpenAI returns before you move anything else.
  • You can run both at once. Two clients, two base URLs, route a percentage of traffic. The SDK object is the only thing that differs.

The one thing you cannot defer is enumerating your thread ids. OpenAI never exposed a list-threads endpoint, so if your own database does not hold those ids, they become unrecoverable the moment the endpoints go dark. That inventory is the piece worth doing this week regardless of which path you pick.


Change two lines.

Python

python
# Before from openai import OpenAI client = OpenAI(api_key="sk-...") # After from openai import OpenAI client = OpenAI( api_key="ko_...", # <- your Ringside server token (ko_ + 64 hex) base_url="https://api.fightclub.pro/v1", # <- Ringside endpoint ) # Everything else stays the same assistant = client.beta.assistants.create(name="Support Bot", model="fc:openai/gpt-4o") thread = client.beta.threads.create() client.beta.threads.messages.create(thread_id=thread.id, role="user", content="Hello") run = client.beta.threads.runs.create_and_poll(thread_id=thread.id, assistant_id=assistant.id)

Node.js / TypeScript

javascript
// Before const openai = new OpenAI({ apiKey: "sk-..." }); // After const openai = new OpenAI({ apiKey: "ko_...", baseURL: "https://api.fightclub.pro/v1", }); // Everything else stays the same const assistant = await openai.beta.assistants.create({ name: "Support Bot", model: "fc:openai/gpt-4o" }); const thread = await openai.beta.threads.create(); await openai.beta.threads.messages.create(thread.id, { role: "user", content: "Hello" }); const run = await openai.beta.threads.runs.createAndPoll(thread.id, { assistant_id: assistant.id });

That's the migration. Every call through client.beta.threads.* continues to work. Every call through client.chat.completions.* continues to work. Ringside resolves each request against your configured model, routes to the underlying provider, bills your Ringside wallet, and logs an attributable UsageEvent.

What changes at the wire (and what doesn't): your SDK still creates thread_<hex> IDs; internally Ringside maps each one 1:1 to a conv_<hex> conversation ID via api_thread_aliases. You never see the internal ID. The SDK receives the OpenAI-shaped thread_<hex> it expects. If you stored OpenAI thread IDs in your own database, pass them in metadata.source_thread_id on threads.create for a migration audit trail.


What you gain by not just porting

Staying wire-compatible is the reason to start here. It is not the reason to stay, and the difference shows up on your first support ticket about a bill.

The Assistants API bills your project. One number, one invoice, no way to answer "which of our customers caused this month's jump" without building the attribution yourself, in your own database, from your own logs. Ringside attaches that to the primitive instead. Every call carries a user, that becomes a tracked Customer server-side, and a Customer has a spend history, a hard budget, rate limits and a webhook when it crosses a line. The four sections below are the concrete version of that.


Feature-by-feature support

Fully supported in Ringside v1

Assistants featureRingside v1
client.beta.assistants.create / retrieve / update / delete / list✅ Full
client.beta.threads.create / retrieve / update / delete✅ Full
client.beta.threads.messages.create / list✅ Full
client.beta.threads.runs.create / retrieve / cancel / list✅ Full
client.beta.threads.runs.create_and_poll✅ Full
client.beta.threads.create_and_run✅ Full
Streaming runs (stream=True, OpenAI SDK async iteration)✅ Full
Function-calling tools✅ Full passthrough
tool_choice: "auto" / "none" / "required"✅ Full
tool_choice: {type: "function", function: {name: ...}}✅ Full
response_format: {type: "json_object"}✅ Full
response_format: {type: "json_schema", json_schema: {...}}Enforced on every provider (see note below)
temperature, top_p, max_tokens, seed parameters✅ Full
metadata on assistants, threads, messages, runs✅ Full
additional_messages on run create✅ Full
additional_instructions on run create✅ Full

Retrieval, files and batches (all shipped)

The RAG half of the Assistants API is live and is the part of Ringside we have built out furthest, so if your app leans on file_search this is the section that matters.

Assistants featureRingside
Vector Stores (POST /v1/vector_stores, list, patch, delete)Shipped. Plus file batches up to 500 ids per call, a query log, and embedding-model migration with a 7-day rollback window.
file_search tool inside a runShipped. Returns chunk citations with file id, chunk index, page and similarity score. On a non-streaming run the model decides when to call it; on a streaming run (stream=True) it runs once per turn before the answer streams — see the note below.
Attachments on messagesShipped. PDF document parts route natively to models that read them.
File uploads (/v1/files)Shipped. 25+ MIME types on a plaintext store, including PDF, Word, PowerPoint, images with text and audio for transcription. purpose must be attachments, batch or vision.
Batch API (/v1/batches)Shipped. JSONL in, JSONL out, completion_window: "24h", billed at half the sync rate.

Streaming and retrieval. OpenAI's streaming run loop can pause mid-stream to service a file_search call; Ringside's streamed responses are content-only, so instead of stranding the run it retrieves before the answer streams. When a streaming run has a file_search tool and at least one attached vector store, Ringside runs one retrieval per turn using your latest user message as the query and grounds the answer on the results — the same citations you would get on a non-streaming run, minus the round-trip. The one behavioural difference from OpenAI is that streaming retrieval is always-on per turn rather than model-decided; on a non-streaming run the model still chooses when to search. This is provider-independent — the same on Claude, GPT and Gemini alike.

Two extras with no OpenAI equivalent. Setting graphrag_enabled: true on a store builds a graph of the entities in your documents during ingest, so a query can chain facts across files that no single chunk contains, and file_search returns a facts array next to the usual citations. Setting encryption: "managed" or "byok" seals the chunk text and the embedding vectors at rest under a per-store key, which is the difference between passing a security review and not. Sealed stores take text-based files only today (text, Markdown, plain text, JSON, XML, YAML, up to 25 MB), so a binary PDF or DOCX belongs on a plaintext store until you export it. The full picture is at /rag.

Genuine gaps

Assistants featureStatusWhat to do today
code_interpreter toolNot shippedUse a function-calling tool pointed at your own sandbox.
Parallel tool calls in a single runNot shippedSequential tool calling works and most apps never notice the difference.

If your app runs on code_interpreter, book a migration call and we will help you plan the bridge.


What you get beyond Assistants parity

Ringside's reason to exist isn't to be a drop-in replacement. It adds the primitives OpenAI doesn't:

1. Per-end-customer billing attribution

OpenAI Assistants bills your project. Ringside lets you tag every call with your end-customer:

python
# Same Assistants SDK call, with a customer attached run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id, extra_body={"user": "customer_42"}, # <- attributed to your customer )

Now query spend per customer:

bash
curl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/customer_42/usage?from=2026-08-01"

Returns per-day, per-model spend for that end-customer. Use it to bill your customer however you like: subscription, usage-based, flat fee. No database on your side.

2. Budget caps that actually enforce

bash
curl -X PATCH -H "Authorization: Bearer ko_..." \ -H "Content-Type: application/json" \ -d '{"monthly_budget_usd": 50}' \ "https://api.fightclub.pro/v1/customers/customer_42"

Next run that would push customer_42 over $50 for the month returns 402 customer_budget_exceeded. Your dashboard reacts; the customer's usage caps. No more runaway-loop horror stories.

3. Webhooks on operational events

OpenAI Assistants forces you to poll for run completion. Ringside pushes events:

bash
curl -X POST -H "Authorization: Bearer ko_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://mycompany.com/webhooks/ringside", "events": ["run.completed", "run.failed", "customer.budget_exceeded", "wallet.low"] }' \ "https://api.fightclub.pro/v1/webhooks"

Every event is HMAC-signed (X-FC-Signature header). Retries on a fixed schedule of 1m, 5m, 30m, 2h then 6h. Your app reacts instantly to run completions, budget hits, and wallet signals with no polling loops.

4. 19 providers behind one SDK

Ringside routes to 19 LLM providers (OpenAI, Anthropic, Google, Mistral, Cohere, Groq, Together, Fireworks, Perplexity, DeepSeek, xAI, Cerebras, SambaNova, OpenRouter, Ollama, Azure OpenAI, Bedrock, HuggingFace, Replicate). Same OpenAI SDK call, different model:

python
# Claude 3.5 Sonnet through the OpenAI SDK run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id, model="fc:anthropic/claude-3.5-sonnet", ) # Groq Llama 3.1 405B through the OpenAI SDK run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id, model="fc:groq/llama-3.1-405b-reasoning", )

Or use a slot, a named alias you can re-point without redeploying:

python
run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id, model="slot:fast", # we resolve "fast" to whichever model you configured )

5. Active-run locking (prevents thread-corruption races)

OpenAI Assistants locks threads during active runs, a subtle but important behavior. Ringside replicates this invariant: while a run is in-flight on a thread, a second concurrent run-create returns 409 conflict_error with code=conversation_locked. The lock auto-releases on completion, client disconnect, or 10-minute timeout. Messages stay in the order you sent them, not a race-condition-scrambled order.

6. Canonical token accounting across providers

Every provider reports tokens differently (OpenAI caches them, Anthropic has thinking tokens, Gemini has context-cached tokens). Ringside normalizes: your /usage reports have consistent input_tokens, output_tokens, cached_input_tokens, and reasoning_tokens fields regardless of underlying provider.

7. Run expiry + 5,000-message thread cap

Two new guardrails absent from OpenAI Assistants:

  • 10-minute default run expiry. A run that stays in_progress or requires_action beyond expires_at (default 10 min, configurable per run) is flipped to expired by the background sweeper, which also emits a run.failed webhook with reason='expired'. Prevents orphaned runs from blocking the thread forever when your tool-output handler crashes.
  • 5,000-message hard cap per thread. Ringside enforces a 5,000-row ordinal ceiling per conversation. Past that, POST /threads/:id/messages returns 409 conversation_full; start a new thread. Protects billing and latency. Most production chat threads should rotate long before this anyway.

Pricing comparison

Typical chatbot workload: 50 end-customers, avg 500 messages/day each ≈ 750,000 messages/month. Using GPT-4o-mini ($0.15 input / $0.60 output per 1M tokens) at ~1,000 input + 500 output tokens per message.

Line itemOpenAI directRingside (Pro tier)
LLM tokens~$338/mo~$338/mo
Ringside platform fee$0$99/mo (Pro base)
Ringside markup on tokens (6%)$0~$20/mo
Per-customer budget capsbuild yourself✅ Included
Conversation storagebuild yourself (Postgres + schema)✅ Included
Webhooks on eventsbuild yourself✅ Included
Per-customer usage reportsbuild yourself✅ Included
Active-run lockingbuild yourself✅ Included
19 provider optionsone19
Total$338 + the cost of building the rest~$457/mo

If the "build yourself" pieces take a full-time engineer for a month (and they will), Ringside pays for itself in the first billing cycle and every month after that is pure margin improvement.

Developer tier: $0/month + 10% markup. Good for prototypes. Every signup gets $10 one-time credit to burn through. No card required.


Cross-provider behavioral contract

When you route to a non-OpenAI provider via fc:*, Ringside guarantees the semantics, not just the wire format:

  • response_format: json_schema: Ringside validates the response against your schema server-side if the provider doesn't enforce natively. Up to 2 auto-retries with a reinforcement prompt on mismatch (configurable). Persistent failure returns 422 response_schema_validation_failed with the offending content.
  • Tool calling: tool_choice: required, forced-function, parallel declarations. All semantics preserved per OpenAI's docs across every provider. Compatibility matrix published at ringside.fightclub.pro/docs/compatibility.
  • Streaming: all 19 adapters emit OpenAI's exact streaming chunk shape. Your existing streaming code doesn't change.
  • Prompt caching: Anthropic cache_control blocks pass through verbatim. OpenAI automatic prompt caching works as-is. Cost calculator respects cache-hit pricing on providers that report it.
  • Reasoning / thinking modes: provider-specific knobs (reasoning_effort on o-series, thinking on Claude, thinking_budget on Gemini 2.5) expose canonical fields. Filter /models?supports=reasoning to find which ones.

If a provider silently breaks a guarantee, it's our bug. File an issue at github.com/fightclub/ringside-issues.


Migrating your data

If you have existing Assistants, Threads, and Messages on OpenAI you want to preserve:

1. Assistants

python
import os from openai import OpenAI old = OpenAI(api_key=os.environ["OPENAI_KEY"]) new = OpenAI(api_key=os.environ["RINGSIDE_KEY"], base_url="https://api.fightclub.pro/v1") for asst in old.beta.assistants.list(): new.beta.assistants.create( name=asst.name, instructions=asst.instructions, model=f"fc:openai/{asst.model}", # or remap to any other Ringside provider tools=[t for t in asst.tools if t.type in ("function", "file_search")], # code_interpreter is the only one to drop metadata=asst.metadata, )

2. Threads + Messages

Iterate your existing threads, create matching threads on Ringside, replay messages:

python
for thread in your_saved_thread_list: # you need your own DB of thread ids; OpenAI never listed them new_thread = new.beta.threads.create() messages = old.beta.threads.messages.list(thread_id=thread.id, order="asc") for msg in messages.data: new.beta.threads.messages.create( thread_id=new_thread.id, role=msg.role, content=msg.content[0].text.value, # text-only in v1 )

3. Tag each thread with an end-customer ID

While migrating, capture the end-customer → thread mapping you know about. Pass extra_body={"user": "customer_42"} on future run creates so Ringside attributes correctly. Unknown users auto-create as stub Customers, low-friction, no pre-registration required.

4. End-to-end migration script

A battle-tested migration script template is maintained at ringside.fightclub.pro/migrate/script. Fork, adapt to your schema, run.


Common gotchas

  • Thread IDs change. OpenAI thread_abc ≠ Ringside thread_xyz. If you store thread IDs in your own DB (most apps do), you need a migration table: (old_openai_thread_id, new_ringside_thread_id). Alternatively, pass the old ID in metadata.source_thread_id and query by that.
  • No listing of your existing threads via OpenAI's API. OpenAI never exposed a "list threads" endpoint at the project level. If you didn't track thread IDs yourself, you can't bulk-enumerate them after the Aug 26 shutdown. Act now.
  • Streaming event names. Ringside emits OpenAI's exact thread.message.delta, thread.run.completed, thread.run.failed event taxonomy. If your streaming code works against OpenAI, it works against us.
  • Model naming. Ringside prefixes model IDs with fc:<provider>/<model> or slot:<alias>. A bare gpt-4o returns 400 invalid_request_error; use fc:openai/gpt-4o.
  • Rate limits. Ringside has per-dev, per-customer, and per-client-token rate limits. If you're migrating a high-volume app, open a ticket to lift limits for the migration window.
  • Idempotency during migration. Use Idempotency-Key header on run creates during the backfill script. Prevents double-charges if your migration script crashes and you re-run.
  • Run expiry default. Runs auto-expire at 10 minutes. Long-running tool loops should submit tool outputs promptly; stalled runs get swept to expired + emit run.failed.
  • Tool type restrictions. function and file_search tools are both supported. code_interpreter returns 400 unsupported_tool_type, so swap it for a function tool against your own sandbox before you cut over.

Getting help

  • Migration call: book a 1-hour call. We migrate your app on Zoom. You walk away with a working migrated app and founding-customer pricing.
  • Support: open a ticket. Every request runs through the ticket system, so nothing gets lost in an inbox.
  • Bugs: Open an issue at github.com/fightclub/ringside-issues (public issue-tracker repo; source is closed).

Next steps

  1. Sign up for Ringside → ringside.fightclub.pro/signup. $10 one-time credit, no card required.
  2. Test the 2-line migration in the no-signup playground with one of your existing prompts. Should work unchanged.
  3. Migrate one assistant + one thread first. Verify the full lifecycle (create → message → run → response) against what OpenAI returned.
  4. Backfill-migrate the rest over a weekend. Use the migration-script template.
  5. Delete your OpenAI Assistants before the deadline: August 26, 2026.

The date

26 August 2026. After it, the Assistants endpoints return 410 Gone and the objects behind them are deleted. Step 1 of that list takes about five minutes, and step 3 is the one that tells you whether the rest is a weekend or an afternoon.


Related migrations


Questions, corrections or feature requests on this guide? Open an issue or a support ticket.