Ringside · Migration guide

Persona · Team Lead Tanya

Updated 2026-07-18

Migrating from Anthropic Messages API to Ringside

A customer emails to dispute their bill. They say they barely touched the AI features last month. You open the Anthropic console and it shows you one number for the whole account, so the only way to answer them is to go back through your own application logs and hope you wrote down enough at the time. Every question that starts with "which of my customers" has to be answered somewhere other than the API.

Ringside sits in front of Claude and answers those questions at the API. Same model, same prompts, same prompt caching, same latency to within tens of milliseconds. You pass a user field on the call, the spend lands against a Customer record, and you can query it or cap it.

Status: v1 (2026-04-20). See also: Migration library index.


Read this before you worry about breaking prod

Your prompts do not change. Not one character. The system string moves from a top-level parameter into messages[0], and that is the only edit to the content you send.

Your model does not change. fc:anthropic/claude-sonnet-4-5 dispatches to the same Anthropic model you call today, on the same Anthropic infrastructure.

Your prompt caching does not change. cache_control: {"type": "ephemeral"} blocks pass through verbatim, so your cache hit rate carries over on the first call rather than warming up again.

There is nothing to migrate. Chat Completions is stateless. Every call carries its full message array, so no conversation history lives in your Anthropic account, there is no export step and there is no data sitting behind you that has to be moved before you can cut over.

You can run a fraction of traffic first. The Anthropic SDK and the OpenAI SDK construct fine in the same process. Put 5% of live calls behind one, compare latency and output on real requests for a week, then move the rest when you're satisfied.

Rollback is the flag you already added. Set that percentage back to zero and every call goes to api.anthropic.com again. Ringside holds no state your app needs for a plain chat call, so there's nothing to unwind and nothing to restore.


The whole change, in one diff

python
# Before (Anthropic SDK) import anthropic client = anthropic.Anthropic(api_key="sk-ant-...") msg = client.messages.create( model="claude-haiku-4-5", max_tokens=1024, system="You are a helpful assistant.", messages=[{"role": "user", "content": "Hello"}], ) print(msg.content[0].text) # After (OpenAI SDK → Ringside) from openai import OpenAI client = OpenAI( api_key="ko_...", base_url="https://api.fightclub.pro/v1", ) msg = client.chat.completions.create( model="fc:anthropic/claude-haiku-4-5", messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"}, ], user="cus_42", ) print(msg.choices[0].message.content)

Ringside server tokens are ko_ followed by 64 hex characters. There is no _live_ or _test_ segment.


Why move

One bill for all your customers. The Anthropic console gives you a monthly total for the account. It has no concept of which of your end-customers caused which dollar, so if you want that answer you build it: a usage table, a write on every call, a rollup job and a dashboard to read it back. On Ringside you send user: "cus_42" and then GET /v1/customers/cus_42/usage returns that customer's tokens and cost. The Customer is created lazily the first time you name one, so there is no provisioning call to add.

Nothing caps a runaway loop. An agent that gets stuck re-calling a tool will spend until a human notices, and the Anthropic API gives you an account-wide limit as the only brake, which means the choice is between blowing the budget and cutting off every customer at once. PATCH /v1/customers/cus_42 {"monthly_budget_usd": 50} gives that one customer a ceiling. Past it, their calls return 402 customer_budget_exceeded and everybody else keeps working.

Polling instead of webhooks. Anthropic ships no webhook surface, so anything you want to react to becomes a cron job that re-reads state on a timer. Ringside registers 34 event types, including customer.budget_exceeded, wallet.low and moderation.flagged. Deliveries are HMAC-signed over t.body with a 5-minute tolerance and retried at 1m, 5m, 30m, 2h and 6h. A few of the 34 are registered ahead of being emitted; the taxonomy is the full registered set.

Browser calls without standing up a proxy. Client Tokens are short-lived Ed25519-signed JWTs pinned to a single Customer, with an optional origin allowlist and optional IP binding, so a frontend can call chat/completions directly. Today that traffic goes through a backend route you wrote purely to keep the key secret.

One error taxonomy and one SSE shape across 19 providers. Whether the call lands on Anthropic, OpenAI, Bedrock or Groq, the error envelope carries the same type / code / message, and the stream carries OpenAI chat-completion-chunk frames. Adding a second provider later stops being a parser rewrite.


Step-by-step (5 minutes)

  1. Sign up at ringside.fightclub.pro/register?intent=ringside. $10 credit, no card.
  2. Create a server token at /ringside/app/api-keys/new.
  3. Install the OpenAI SDK (pip install openai / npm install openai). Leave the Anthropic SDK installed while you run split traffic.
  4. Swap the client: base_url="https://api.fightclub.pro/v1", model prefix fc:anthropic/.
  5. Move system into messages[0] and pass user: "<your-end-customer-id>" on every call.

Wire-shape diff, Anthropic Messages → OpenAI Chat Completions

Anthropic Messages APIOpenAI Chat Completions (Ringside)
system: "..." (top-level string)messages[0] = {"role": "system", "content": "..."}
messages: [{"role": "user", "content": "hi"}]Same shape; content may be string or content-blocks array
Content blocks: [{"type": "text", "text": "hi"}]Same array shape supported (OpenAI SDK accepts it)
max_tokens: 1024 (REQUIRED)max_tokens (optional; Ringside applies a default if you omit it)
Tool use: tools: [{name, description, input_schema}]tools: [{"type": "function", "function": {name, parameters, description}}]
tool_use content blocks on responsetool_calls on choices[0].message
cache_control: {"type": "ephemeral"} on blocksPassthrough on fc:anthropic/* refs (C.6 canonical)
Vision: image content block with source{"type": "image_url", "image_url": {"url": "..."}} part
PDF: document content block{"type": "file", "file": {"file_data": "data:application/pdf;base64,..."}} part
stream: true (native Anthropic SSE events)stream: true (OpenAI chat-completion-chunk shape, byte-exact)
stop_reason: "end_turn"finish_reason: "stop"
stop_reason: "tool_use"finish_reason: "tool_calls"
stop_reason: "max_tokens"finish_reason: "length"
usage: {input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}usage: {prompt_tokens, completion_tokens, cached_input_tokens} (Ringside canonical sums both cache fields)

Full matrix with per-feature status: ringside.fightclub.pro/docs/compatibility.


Drop-in code diffs

Streaming

python
# Before (Anthropic SDK) with client.messages.stream( model="claude-haiku-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Count to 10"}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) # After (OpenAI SDK → Ringside, Anthropic under the hood) stream = client.chat.completions.create( model="fc:anthropic/claude-haiku-4-5", messages=[{"role": "user", "content": "Count to 10"}], stream=True, user="cus_42", ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="", flush=True)

Ringside's canonical SSE layer translates Anthropic's message_start / content_block_delta / message_delta events into OpenAI chat-completion-chunk format on the wire. An existing OpenAI-style SSE parser works unchanged.

Tool use

python
# After (Ringside, Anthropic tools via OpenAI SDK shape) resp = client.chat.completions.create( model="fc:anthropic/claude-sonnet-4-5", messages=[{"role": "user", "content": "What's the weather in SF?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a location", "parameters": { "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"], }, }, }], user="cus_42", ) # resp.choices[0].message.tool_calls is OpenAI-shape regardless of underlying provider

Ringside's C.6 canonical tool-call translator rewrites Anthropic tool_use content blocks → OpenAI tool_calls on the way out, and your tool_calls replies → Anthropic tool_result content blocks on the way back in.

Prompt caching (Anthropic-specific, works via passthrough)

python
resp = client.chat.completions.create( model="fc:anthropic/claude-sonnet-4-5", messages=[ { "role": "system", "content": [ { "type": "text", "text": "<very long instructions 10k tokens...>", "cache_control": {"type": "ephemeral"}, # <- passes through verbatim } ], }, {"role": "user", "content": "Question?"}, ], user="cus_42", ) print(resp.usage) # Ringside canonical: cached_input_tokens = cache_read + cache_creation (summed)

Vision

python
resp = client.chat.completions.create( model="fc:anthropic/claude-sonnet-4-5", messages=[{ "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, {"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}}, ], }], user="cus_42", )

Ringside rewrites image_url parts into Anthropic's image content-block shape before dispatch.

PDF

python
resp = client.chat.completions.create( model="fc:anthropic/claude-sonnet-4-5", messages=[{ "role": "user", "content": [ {"type": "text", "text": "Summarise the termination clause."}, {"type": "file", "file": { "filename": "msa.pdf", "file_data": "data:application/pdf;base64,JVBERi0xLjQK...", }}, ], }], user="cus_42", )

file_data also accepts an https URL or an fc_file:<id> reference to a file you already uploaded through /v1/files. Ringside maps the part onto an Anthropic document block. PDF is the only MIME accepted for file parts.


Feature mapping

Anthropic featureRingside equivalent
api.anthropic.com/v1/messagesapi.fightclub.pro/v1/chat/completions + fc:anthropic/* model ref
system: top-levelmessages[0] with role: "system"
max_tokens (required)max_tokens (optional; a default is applied when omitted)
Native tool useOpenAI-shape tools + tool_calls (C.6 canonical translator)
cache_control (prompt caching)Passthrough (C.6 canonical usage aggregation)
Extended thinking (thinking: {type: "enabled"})extra_body={"thinking": {...}} passthrough
Vision (image content blocks)image_url content parts (auto-translated)
PDF (document content blocks)Shipped. {"type": "file", "file": {...}} content parts, mapped to Anthropic document blocks
Batch APIShipped. POST /v1/batches over a JSONL file uploaded with purpose=batch, completion_window: "24h"; children bill at 0.5x the sync markup from 10 requests up
Console usage reportsGET /v1/usage, /v1/margin, /v1/customers/:id/usage
n/aCustomer Object + budgets + webhooks + Client Tokens

Gotchas

  • System prompt placement. Anthropic has a top-level system param; OpenAI puts it as the first message. Move it. Forgetting is the number one migration bug and it fails quietly, because the model still answers, just without your instructions.
  • max_tokens is optional on Ringside. Anthropic requires it. If your Anthropic code always passes max_tokens, keep passing it and you will not notice the difference.
  • Finish-reason renaming. Anthropic end_turn → OpenAI stop; tool_usetool_calls; max_tokenslength. Ringside's C.6 finish-reason translator handles the wire; if you switch on finish_reason in your own code, update the case labels.
  • Token accounting. Ringside's usage.cached_input_tokens = cache_creation_input_tokens + cache_read_input_tokens from Anthropic. If you need the split, it's in the Ringside request log (/ringside/app/logs) as raw_upstream_usage.
  • Streaming chunk shape. You receive OpenAI's chat-completion-chunk format even though the upstream is Anthropic. If you previously consumed Anthropic's raw event stream (message_start, content_block_delta), switch to a delta-based parser. Most OpenAI SDKs do this for you.
  • Extended thinking. Pass via extra_body={"thinking": {"type": "enabled", "budget_tokens": 4000}}. The thinking output is surfaced in usage.reasoning_tokens; the text itself is not returned in v1 (v2 will add a thinking response field).
  • response_format: json_schema is fallback-mode on Anthropic. Anthropic has no native JSON schema support. Ringside's C.6 enforcer reinforces the schema in the system prompt and validates the response with Ajv. Up to 2 retries; persistent failure returns 422 response_schema_validation_failed. Streaming plus non-native schema returns 400 streaming_with_schema_fallback_unsupported.
  • Batch discount has a floor. The 0.5x multiplier applies from 10 requests in a batch. Smaller batches bill at the normal sync rate, so a one-row "batch" buys you nothing.

FAQ

Q: Does this cost more than Anthropic direct? A: Ringside Pro is $99/mo base plus 6% markup on tokens. Above roughly $1.5k/month of token spend, the built-in Customer billing, budgets, webhooks and margin reports replace an engineer-month of internal plumbing. See /pricing.

Q: Latency? A: Roughly 10-30ms of edge overhead. Streaming first-byte is unaffected.

Q: Can I still reach Anthropic-specific fields? A: Yes. extra_body={"thinking": ..., "tool_choice": {"type": "tool", "name": "..."}} passes arbitrary JSON through, and the raw upstream response is kept in the request log for debugging.

Q: GDPR / CCPA hard-delete? A: DELETE /v1/customers/:id?hard=true purges the Customer and anonymizes UsageEvents.

Q: What happens if Anthropic is down? A: Use slot:<alias> refs. Re-point the slot (via /admin/ringside/slots or PATCH /v1/account/slots/:alias) to fc:openai/gpt-4o-mini or another provider with zero app redeploys. We're building automated failover in v2.


Next steps

Related migrations


Corrections or feedback: open an issue.