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)
- Sign up at
ringside.fightclub.pro/register?intent=ringside. $10 credit, no card. - Create a server token at
/ringside/app/api-keys/new. - Install the OpenAI SDK (
pip install openai/npm install openai). Leave the Anthropic SDK installed while you run split traffic. - Swap the client:
base_url="https://api.fightclub.pro/v1", model prefixfc:anthropic/. - Move
systemintomessages[0]and passuser: "<your-end-customer-id>"on every call.
Wire-shape diff, Anthropic Messages → OpenAI Chat Completions
| Anthropic Messages API | OpenAI 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 response | tool_calls on choices[0].message |
cache_control: {"type": "ephemeral"} on blocks | Passthrough 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)
pythonresp = 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
pythonresp = 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.
pythonresp = 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 feature | Ringside equivalent |
|---|---|
api.anthropic.com/v1/messages | api.fightclub.pro/v1/chat/completions + fc:anthropic/* model ref |
system: top-level | messages[0] with role: "system" |
max_tokens (required) | max_tokens (optional; a default is applied when omitted) |
| Native tool use | OpenAI-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 API | Shipped. 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 reports | GET /v1/usage, /v1/margin, /v1/customers/:id/usage |
| n/a | Customer Object + budgets + webhooks + Client Tokens |
Gotchas
- System prompt placement. Anthropic has a top-level
systemparam; 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_tokensis optional on Ringside. Anthropic requires it. If your Anthropic code always passesmax_tokens, keep passing it and you will not notice the difference.- Finish-reason renaming. Anthropic
end_turn→ OpenAIstop;tool_use→tool_calls;max_tokens→length. Ringside's C.6 finish-reason translator handles the wire; if you switch onfinish_reasonin your own code, update the case labels. - Token accounting. Ringside's
usage.cached_input_tokens=cache_creation_input_tokens + cache_read_input_tokensfrom Anthropic. If you need the split, it's in the Ringside request log (/ringside/app/logs) asraw_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 inusage.reasoning_tokens; the text itself is not returned in v1 (v2 will add athinkingresponse field). response_format: json_schemais 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 returns422 response_schema_validation_failed. Streaming plus non-native schema returns400 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
- Try in the no-signup playground.
- Compatibility matrix:
ringside.fightclub.pro/docs/compatibility. - Support: open a ticket.
Related migrations
- OpenAI Chat Completions → Ringside
- OpenAI Assistants → Ringside
- OpenRouter → Ringside
- Chatbase → Ringside
- Index
Corrections or feedback: open an issue.