Migrating from OpenAI Chat Completions to Ringside
Change your
base_url. Add auserfield. That gets you per-end-customer billing, hard budget caps, webhooks, Client Tokens, margin reporting and 19 LLM providers behind one SDK. Your existing OpenAI SDK stays exactly as it is.
Status: v1 (2026-04-20).
See also: Migration library index.
The three-line diff
Python
python# Before from openai import OpenAI client = OpenAI(api_key="sk-...") resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], ) # After from openai import OpenAI client = OpenAI( api_key="ko_...", # <- your Ringside server token base_url="https://api.fightclub.pro/v1", # <- Ringside endpoint ) resp = client.chat.completions.create( model="fc:openai/gpt-4o-mini", # <- fc:<provider>/<model> ref messages=[{"role": "user", "content": "Hello"}], user="cus_42", # <- your end-customer ID (attribution) )
Node.js / TypeScript
javascript// Before import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const resp = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Hello" }], }); // After const openai = new OpenAI({ apiKey: process.env.RINGSIDE_API_KEY, baseURL: "https://api.fightclub.pro/v1", }); const resp = await openai.chat.completions.create({ model: "fc:openai/gpt-4o-mini", messages: [{ role: "user", content: "Hello" }], user: "cus_42", });
That's the whole migration for the basic flow. Streaming, tool calls, JSON mode, vision, embeddings and moderations all keep working unchanged.
Read this before you worry about breaking prod
You are pointing an HTTP client at a different host. That is the entire blast radius, and it is worth being blunt about what stays put.
Your SDK doesn't change. Same openai package, same version, same chat.completions.create call, same choices[0].message.content, same usage object, same streaming chunk shape down to the [DONE] sentinel. Ringside emits byte-exact OpenAI chat-completion-chunk frames across all 19 providers, so your SSE parser is untouched even after you switch a request to Claude.
Your errors don't change shape. Everything comes back as {error: {code, message, type, param}}, which is what the OpenAI SDK's error parser already expects.
There is no data to migrate. Chat Completions is stateless. There is no thread store, no assistant object and no history sitting on OpenAI's side that has to be moved. The messages array you build on every request is the only state, and it lives in your database already.
Rollback is one line. Point base_url back at https://api.openai.com/v1 and put the sk- key back in the env var. Drop the fc: prefix from the model ref and you are exactly where you started, on the same SDK, with the same code paths. Nothing about your data or your users is one-way.
You can send a fraction of traffic first. Because rollback is a config value, the safe move is to route 5% of requests through Ringside behind a flag, watch the per-customer numbers appear, then ratchet up. Plenty of teams sit at 10% for a fortnight before going all in.
Why migrate
The invoice lands on the first of the month and it is one number. Say it's $4,180. Somewhere inside that number is a single customer whose nightly sync went haywire and burned $900 in nine hours while everyone was asleep, but the bill does not know that, because OpenAI has no idea your customers exist. It sees one API key. So when that customer opens a ticket asking why you raised their plan, you open a spreadsheet, join your own request logs against a token count you estimated and hope nobody checks your arithmetic.
Ringside sits on the same wire and adds one thing to it. Every call carries a user field, which the OpenAI SDK has had for years and which OpenAI treats as a logging string. Ringside treats it as an identity. The first call with user="cus_42" creates a Customer server-side, every subsequent call attaches usage to that Customer at the token level, and GET /v1/customers/cus_42/usage gives you the number you were previously reconstructing by hand.
The models are the same models. What changes is that the bill arrives already split by who caused it.
Four things follow from that, and they are the reasons people actually switch.
- One bill covering every customer, with no way to split it. OpenAI bills the key, not the tenant. Ringside bills the key and attributes the tenant, so
GET /v1/customers/:id/usageanswers "what did this account cost me" in a single request, andGET /v1/customers/:id/margin?group_by=dayanswers "what did I make on them" next to it. - No hard ceiling when an agent loop runs away overnight. OpenAI's usage limits are account-wide and soft.
PATCH /v1/customers/cus_42 {"monthly_budget_usd": 50}is per-tenant and hard. The call that would cross $50 gets402 customer_budget_exceededand never reaches the provider, so the runaway stops at $50 instead of at whatever your card allows. - No answer when a customer disputes their usage. Every response carries
X-Request-Id. Every charge is an attributed usage event with a model, a token count and a timestamp. When someone says "we never ran that", you have the row. - One provider, one outage. 19 providers sit behind the same URL: OpenAI, Anthropic, Google, Mistral, Groq, Together, Fireworks, Perplexity, DeepSeek, xAI, Cerebras, SambaNova, Cohere, OpenRouter, Ollama, Azure OpenAI, Bedrock, HuggingFace and Replicate. You change the model ref, not the SDK, and with
slot:<alias>you change it without a deploy.
On top of those, 34 webhook event types cover the operational surface (customer.budget_exceeded, wallet.low, run.failed, moderation.flagged and the rest), HMAC-signed and retried, so the $900 night pages you at $60 rather than showing up on next month's invoice.
Step-by-step (5 minutes)
- Sign up at
ringside.fightclub.pro/register?intent=ringside. Every signup gets $10 one-time credit, no card required. - Create a server token at
/ringside/app/api-keys/new. Tokens areko_followed by 64 hex characters, shown once. Lose it and you rotate. - Point your SDK at
https://api.fightclub.pro/v1and swap the key. - Pass
user: "<your-end-customer-id>"on every call. Any stable opaque string works (UUID, internal ID, tenant ID). Ringside auto-creates the Customer on first pass. - Deploy behind a flag at whatever traffic share you're comfortable with, then watch
/ringside/appfill in per Customer in real time.
Drop-in code diffs
Streaming chat completions
python# Works identically to OpenAI. Only the base_url + model ref change. stream = client.chat.completions.create( model="fc:openai/gpt-4o-mini", messages=[{"role": "user", "content": "Write me a haiku"}], stream=True, user="cus_42", ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="", flush=True)
Ringside emits byte-exact OpenAI chat-completion-chunk shape across all 19 providers (see the C.6 canonical streaming layer). Your existing SSE parser doesn't change when you switch providers.
Embeddings
pythonemb = client.embeddings.create( model="fc:openai/text-embedding-3-small", input=["hello", "world"], user="cus_42", )
Moderations (free tier)
pythonmod = client.moderations.create( model="fc:openai/omni-moderation-latest", input="Check this text", ) # Ringside routes moderations through its platform OpenAI key at-cost (no markup on Pro+).
Tool calling
pythonresp = client.chat.completions.create( model="fc:openai/gpt-4o", messages=[{"role": "user", "content": "What's the weather in SF?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"], }, }, }], user="cus_42", )
Tools pass through verbatim on OpenAI-compatible providers. On Anthropic, Google, Cohere and Bedrock, Ringside's C.6 canonical tool-call translator normalizes the wire format.
JSON schema (response_format)
pythonresp = client.chat.completions.create( model="fc:anthropic/claude-haiku-4-5", # non-native json_schema provider messages=[{"role": "user", "content": "Generate a user record"}], response_format={ "type": "json_schema", "json_schema": { "name": "user", "schema": { "type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["name", "age"], }, }, }, user="cus_42", )
Ringside's enforcer (C.6) validates the response against your schema server-side. Up to 2 auto-retries with a reinforcement prompt on mismatch. Persistent failure returns 422 response_schema_validation_failed.
Using Customer attribution
Attribution via body field (default, OpenAI-compat)
pythonclient.chat.completions.create(model="...", messages=[...], user="cus_42")
Attribution via header (multi-tag)
pythonclient.chat.completions.create( model="fc:openai/gpt-4o-mini", messages=[...], user="cus_42", extra_headers={ "FC-Tag": "chat-support", "FC-Property-Plan": "enterprise", "FC-Session-Id": "workflow_abc", "X-FC-Billable-Amount": "0.25", # optional: what you're charging for this call }, )
Then query per-Customer spend:
bashcurl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/cus_42/usage?from=2026-08-01"
Or margin (your charge vs. Ringside's cost):
bashcurl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day"
Feature mapping
| OpenAI Chat Completions | Ringside equivalent |
|---|---|
api.openai.com/v1/chat/completions | api.fightclub.pro/v1/chat/completions |
api.openai.com/v1/embeddings | api.fightclub.pro/v1/embeddings |
api.openai.com/v1/moderations | api.fightclub.pro/v1/moderations (free at-cost on Pro+) |
model: "gpt-4o-mini" | model: "fc:openai/gpt-4o-mini" |
user: "<id>" (logging only) | user: "<id>" auto-creates tracked Customer + attribution |
| n/a | FC-Property-*, FC-Tag, FC-Session-Id, X-FC-Billable-Amount headers |
| n/a | GET /v1/customers/:id/usage + /margin |
| n/a | POST /v1/webhooks (34 event types) |
| n/a | POST /v1/customers/:id/client_tokens (frontend-safe JWTs) |
| n/a | Budget caps: PATCH /v1/customers/:id {monthly_budget_usd: N} |
| n/a | Managed RAG: POST /v1/vector_stores + the file_search tool |
| n/a | POST /v1/batches (24h completion window, batch-rate billing) |
| Streaming SSE | Identical chunk shape (byte-exact across providers) |
| Idempotency-Key header | Supported, 24-hour per-dev dedup window |
Gotchas
- Key format. A Ringside server token is
ko_followed by 64 hex characters. There is no_live_or_test_segment. If you're pattern-matching keys in a secret scanner, match onko_plus 64 hex. usermust be a stable opaque ID, not PII. Use an internal customer UUID or tenant ID. Don't pass emails. Ringside storesuserasexternal_idon the Customer row and you do not want PII there. UseFC-Property-Email: <email>if you want email-level tags.- Customer ids are
cus_plus hex. Notcust_. Acust_id in a path returns 404, same as any other unknown id. - Model prefix is required.
model: "gpt-4o"returns400 invalid_request_error. Always usefc:openai/gpt-4o(or any otherfc:<provider>/<model>, or aslot:<alias>). Bare model names are reserved for a possible OpenAI-only passthrough mode in v2. - Error envelope shape. Ringside wraps all errors as
{error: {code, message, type, param}}, compatible with the OpenAI SDK's error parser. Upstream provider errors are normalized to Ringside codes (upstream_unavailable,upstream_quota_exceeded,upstream_auth_failed,upstream_invalid_request,upstream_context_length_exceeded,upstream_content_filter) so you can write one error handler. - Rate limits. Per-dev, per-Customer (
rpm_limit/tpm_limityou configure) and per-Client-Token RPM. 429s include aRetry-Afterheader. - Moderations pricing. On the Developer tier, moderations pass through at cost plus 10%. On Pro+, moderations run through Ringside's platform OpenAI key at cost with no markup, effectively free.
- Request IDs. Every response carries an
X-Request-Idheader. Log it. When you file a support ticket, include the request ID and we'll find the trace instantly.
FAQ
Q: What about latency? A: Ringside adds roughly 10-30ms of edge routing plus attribution overhead on top of the upstream provider. Streaming first-byte latency is unaffected, because we proxy the upstream SSE directly.
Q: How do I migrate existing data? A: There isn't any. Chat Completions is stateless, so nothing lives on OpenAI's side to move. Your first call with user="cus_42" creates the Customer and subsequent calls attach to it. Pre-create with POST /v1/customers only if you want a budget or metadata set before the first request.
Q: A customer hit their cap mid-month and I need to unblock them. A: PATCH /v1/customers/cus_42 {"monthly_budget_usd": 200}. The field is monthly_budget_usd, not budget_usd. The rate-limit fields on the same body are rpm_limit and tpm_limit.
Q: GDPR / CCPA hard-delete? A: DELETE /v1/customers/:id?hard=true purges the Customer row and anonymizes all associated UsageEvents (user_external_id nulled, metadata scrubbed). Returns 204.
Q: Failover if OpenAI is down? A: Use slot:<alias> refs. One admin-side PATCH /admin/ringside/slots/chat-cheap re-points slot:chat-cheap from fc:openai/gpt-4o-mini to fc:anthropic/claude-haiku-4-5 with zero app redeploys.
Q: SLA? A: Pro tier is 99.9% monthly uptime. Enterprise is 99.95% with runbook access.
Next steps
- Try it in the no-signup playground. Paste your existing request body, hit run.
- Full API reference:
ringside.fightclub.pro/docs. - Managed RAG, if retrieval is next on your list:
ringside.fightclub.pro/rag. - Support: open a ticket.
Related migrations
- OpenAI Assistants → Ringside
- Anthropic Messages → Ringside
- OpenRouter → Ringside
- Chatbase → Ringside
- Index
Corrections or feedback: open an issue.