Ringside · Migration guide

Persona · Team Lead Tanya

Updated 2026-07-18

Migrating from OpenAI Chat Completions to Ringside

Change your base_url. Add a user field. 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.

  1. 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/usage answers "what did this account cost me" in a single request, and GET /v1/customers/:id/margin?group_by=day answers "what did I make on them" next to it.
  2. 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 gets 402 customer_budget_exceeded and never reaches the provider, so the runaway stops at $50 instead of at whatever your card allows.
  3. 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.
  4. 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)

  1. Sign up at ringside.fightclub.pro/register?intent=ringside. Every signup gets $10 one-time credit, no card required.
  2. Create a server token at /ringside/app/api-keys/new. Tokens are ko_ followed by 64 hex characters, shown once. Lose it and you rotate.
  3. Point your SDK at https://api.fightclub.pro/v1 and swap the key.
  4. 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.
  5. Deploy behind a flag at whatever traffic share you're comfortable with, then watch /ringside/app fill 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

python
emb = client.embeddings.create( model="fc:openai/text-embedding-3-small", input=["hello", "world"], user="cus_42", )

Moderations (free tier)

python
mod = 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

python
resp = 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)

python
resp = 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)

python
client.chat.completions.create(model="...", messages=[...], user="cus_42")

Attribution via header (multi-tag)

python
client.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:

bash
curl -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):

bash
curl -H "Authorization: Bearer ko_..." \ "https://api.fightclub.pro/v1/customers/cus_42/margin?group_by=day"

Feature mapping

OpenAI Chat CompletionsRingside equivalent
api.openai.com/v1/chat/completionsapi.fightclub.pro/v1/chat/completions
api.openai.com/v1/embeddingsapi.fightclub.pro/v1/embeddings
api.openai.com/v1/moderationsapi.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/aFC-Property-*, FC-Tag, FC-Session-Id, X-FC-Billable-Amount headers
n/aGET /v1/customers/:id/usage + /margin
n/aPOST /v1/webhooks (34 event types)
n/aPOST /v1/customers/:id/client_tokens (frontend-safe JWTs)
n/aBudget caps: PATCH /v1/customers/:id {monthly_budget_usd: N}
n/aManaged RAG: POST /v1/vector_stores + the file_search tool
n/aPOST /v1/batches (24h completion window, batch-rate billing)
Streaming SSEIdentical chunk shape (byte-exact across providers)
Idempotency-Key headerSupported, 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 on ko_ plus 64 hex.
  • user must be a stable opaque ID, not PII. Use an internal customer UUID or tenant ID. Don't pass emails. Ringside stores user as external_id on the Customer row and you do not want PII there. Use FC-Property-Email: <email> if you want email-level tags.
  • Customer ids are cus_ plus hex. Not cust_. A cust_ id in a path returns 404, same as any other unknown id.
  • Model prefix is required. model: "gpt-4o" returns 400 invalid_request_error. Always use fc:openai/gpt-4o (or any other fc:<provider>/<model>, or a slot:<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_limit you configure) and per-Client-Token RPM. 429s include a Retry-After header.
  • 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-Id header. 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

Related migrations


Corrections or feedback: open an issue.