Model references and resolution
Every Ringside model ref carries a prefix that says how the string becomes a concrete model: fc: pins one exact model, match: picks the newest in a family and rolls forward, slot: is an alias you repoint without shipping code and dyn: chooses a model per request from a profile. This page covers the four kinds, the @region residency suffix on fc: refs, the response headers that tell you what resolved and the errors you get when a ref is malformed.
Most LLM gateways take a bare model name (gpt-4o) and route it. Ringside does not. The model field you send is a typed reference whose prefix declares its resolution strategy, and Ringside rejects a bare name with 400 invalid_model_ref rather than guess what you meant. The point of the prefix is to make late binding explicit: a request can pin one exact build, or it can hand the choice to Ringside and let the resolved model change over time (a family rolling to a newer version, an alias you repoint, a per-request decision) without you touching the call site.
Resolution runs server-side on every metered call (chat, runs, assistants, conversations). A dyn: ref is expanded first, then the result flows through the same router that handles fc:, match: and slot:. Whatever the input ref, resolution always lands on a concrete fc:<provider>/<model> id before a provider is called, and that concrete id (not your selector) is what gets metered, cached and capability-checked. The headers documented below report what the string actually resolved to so you are never guessing.
The four ref kinds
| Prefix | Form | Resolves to | Rolls forward? |
|---|---|---|---|
fc: | fc:<provider>/<model> | Exactly that model, no indirection | No (pinned) |
match: | match:<provider>/<family>[>=N | =N] | Newest catalog model in the family that satisfies the constraint | Yes |
slot: | slot:<alias> | Whatever fc: ref the alias points at right now | Only when you repoint it |
dyn: | dyn:<name> | A model picked at request time from a profile pool | Per request |
fc: (pinned)
A fc: ref is the literal model. The string after fc: is passed through to the upstream catalog unchanged, so fc:openai/gpt-4o-mini is OpenAI gpt-4o-mini and nothing else. Pin with fc: when you have qualified a specific build and want byte-for-byte stable behaviour across a deploy, an eval suite or a regression baseline. An empty ref (fc: with nothing after it) is 400 invalid_model_ref.
{ "model": "fc:openai/gpt-4o-mini", "messages": [ ... ] }match: (newest in a family)
A match: ref names a provider and a family and resolves to the newest catalog model in that family, with an optional version floor or pin. The grammar is match:<provider>/<family> for the bare newest, match:<provider>/<family>>=N for the newest at major version N or above (this is the one that rolls forward as new majors ship) and match:<provider>/<family>=N to stay within the v{N}.x line. The resolver sorts family members newest-first by major then minor and returns the top candidate that satisfies your constraint.
Family selection is the right default when you want the best current model in a line and you trust Ringside to carry you onto its successor. match:anthropic/sonnet>=4 keeps serving the newest Sonnet at v4 or later: when Sonnet 4.5 lands it picks that, when a v5 ships it rolls to v5, and you change nothing. The bare match:anthropic/sonnet never errors because it always rolls forward to whatever exists. A constraint can age out: if no catalog model satisfies your floor or pin the call returns 400 model_family_eol and the message names the newest model that does exist, so you can update the ref.
match: resolves Anthropic families only for now (sonnet, opus, haiku), against the post-v4 id shape anthropic/claude-{family}-{major}[-{minor}]. Older ids like claude-3-5-sonnet are not selectable through match: and must be pinned with fc:anthropic/claude-3-5-sonnet. Other providers are added one family at a time as their version semantics are pinned down. An unsupported provider returns unsupported_provider; an unknown family returns model_family_unknown.
slot: (an alias you repoint)
A slot: ref is a named pointer you define on the platform side that resolves to a fc: ref. The alias is indirection you control out of band: change what slot:default-chat maps to in platform settings and every caller using that slot picks up the new target on the next request, with zero code changes and no redeploy. Slots can chain (a slot pointing at another slot) and the resolver walks them to a maximum depth of 8 before bailing with slot_cycle to stop an alias loop. An unknown alias is unknown_slot; an empty alias (slot: with nothing after it) is invalid_model_ref.
Ringside keys the response cache on the resolved concrete model id, never on your slot:/match:/dyn: selector. So after you repoint a slot, you get fresh generations from the new model rather than a stale body that was produced by the old one and would now be billed at the new model's price.
dyn: (a profile picked at request time)
A dyn: ref names a dynamic profile you have saved on your developer account. The other three kinds map to one fixed target. A dyn: ref does not. The profile holds a pool of concrete refs arranged as a ladder (a floor, a ceiling and a starting rung) and Ringside picks a rung per request from the request shape. The router reads the live turn (token count, whether tools or vision or a json_schema are in play and a difficulty estimate from the prompt) and chooses a model from the pool. Profiles in dispatcher mode also let a cheap model classify the demand first. That classify call is timeout-bounded and never throws, so a slow or failed classification falls back to the heuristic instead of erroring the request.
Because the choice is per request, two identical-looking calls to the same dyn: ref can resolve to different models. Read the X-Ringside-Dynamic-Profile and X-Ringside-Dynamic-Reason headers to see which profile ran and the one-line reason it picked the rung it did. A dyn: ref whose profile does not exist on your account, or whose stored profile is misconfigured (empty pool, floor above ceiling, a starting rung outside the ladder), is rejected as 400 invalid_model_ref with a message describing the fault.
Why bare names are rejected
A bare name like gpt-4o carries no resolution strategy. It could mean a pinned build, the newest in a family or an alias you control, and Ringside will not guess between them, so it returns 400 invalid_model_ref instead. The message names the three routable prefixes (the ref must start with fc:, slot: or match:). A dyn: ref is valid too but is expanded one layer earlier, so it never reaches this router branch. This is the most common error when migrating an existing OpenAI integration. Point the SDK at the Ringside base URL and the only required code change is prefixing your model strings with fc: (or one of the other kinds).
One more rejected shape worth calling out: user: refs (bring-your-own-key models from the Knockout surface) are not accepted by the Ringside API and return 400 byok_not_supported. Use fc: or slot: instead.
// 400 invalid_request_error / invalid_model_ref
{
"error": {
"type": "invalid_request_error",
"code": "invalid_model_ref",
"message": "Model ref must start with 'fc:', 'slot:', or 'match:'. Got: gpt-4o",
"param": "model"
}
}Data residency: the @region suffix
You pin a call to a cloud and region by appending an @<region> suffix to the model id. The suffix attaches to fc: refs only. It is opt-in and never changes traffic that omits it (refs without a suffix route normally through the platform path with no residency guarantee). The canonical model id stays the billing model, so residency changes where the call runs, not what it costs.
| Suffix | Cloud + region | Valid for |
|---|---|---|
@eu | OpenAI direct, EU (eu.api.openai.com) | openai/* models |
@us | OpenAI direct, US (api.openai.com) | openai/* models |
@eu-bedrock | Claude via AWS Bedrock, eu-west-1 | Region-routable Claude models |
@us-bedrock | Claude via AWS Bedrock, us-east-1 | Region-routable Claude models |
@eu-vertex | Claude via Google Vertex, europe-west1 | Region-routable Claude models |
@us-vertex | Claude via Google Vertex, us-east5 | Region-routable Claude models |
The suffix is split off the last @ in the id (canonical catalog ids never contain @, so a present @ is always a region tag). An unknown suffix like @apac returns 400 invalid_model_ref and the message lists the supported suffixes. A mismatch returns 400 unsupported_provider: an OpenAI suffix (@eu / @us) on a non-OpenAI base is rejected and the message points you at @eu-bedrock/@eu-vertex for Claude, and a Claude bedrock/vertex suffix on a model that is not region-routable is rejected with the list of models that are.
The response cache key includes the region tag, so fc:openai/gpt-4o@eu and fc:openai/gpt-4o@us (and the unpinned fc:openai/gpt-4o) never share a cached body even for an identical prompt. A response generated under EU residency is never served to a US-pinned request, or the reverse. Ringside spans 19 providers in total.
curl https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "fc:anthropic/claude-opus-4.6@eu-bedrock",
"messages": [{"role":"user","content":"Summarise this contract clause."}]
}'Resolution response headers
Every chat response carries headers that report how your ref resolved. You do not have to parse the model out of the body. Read these to confirm the concrete model, to detect when a newer model exists behind your match: constraint and to see why a dyn: profile chose what it chose.
| Header | Set when | Carries |
|---|---|---|
X-Request-Id | Always | The req_ id for this request; quote it in support tickets. |
X-Ringside-Model-Resolved | A match: ref resolved | The concrete model id the match: selector resolved to. slot: and dyn: do not set this header; for dyn: read the X-Ringside-Dynamic-* headers instead. |
X-Ringside-Model-Newer-Available | A match: constraint excluded a newer model | The newer catalog id you are not getting (e.g. a =4 pin while v5 exists). |
X-Ringside-Dynamic-Profile | The ref was dyn:<name> | The profile name that ran. |
X-Ringside-Dynamic-Reason | The ref was dyn:<name> | One-line reason the rung was chosen. |
X-Ringside-Model-Used | On a model:[] cost cascade | The winning model after walking the chain. |
X-Ringside-Models-Tried / -Fallback-Triggered | On a model:[] cost cascade | The full chain tried and whether fallback fired. |
When you pin a major (match:anthropic/sonnet=4) and a newer major exists in the catalog, Ringside still serves your pinned line and sets X-Ringside-Model-Newer-Available to the id you are passing over. It is a nudge, not a reroute: nothing changes until you relax the constraint. A >=N ref that is already on the newest model does not set the header.
Worked example: a match: ref rolling forward
Send a family ref with a version floor and let Ringside pick the newest qualifying Sonnet. The body stays the same call you would make with any model.
- 1Send the match: ref
Ask for the newest Sonnet at major 4 or above, attributed to a customer.
curl -i https://api.fightclub.pro/v1/chat/completions \ -H "Authorization: Bearer $FC_API_KEY" \ -H "FC-Customer: cus_42" \ -H "Content-Type: application/json" \ -d '{ "model": "match:anthropic/sonnet>=4", "messages": [{"role":"user","content":"Draft a release note."}] }' - 2Read the resolution headers
The response tells you the concrete model that ran. Here the catalog's newest Sonnet at the time was 4.6.
HTTP/1.1 200 OK X-Request-Id: req_8sKQ2v... X-Ringside-Model-Resolved: anthropic/claude-sonnet-4-6 X-Ringside-Wallet-Deducted: 0.000412 X-RateLimit-Remaining: 59
- 3Ship a newer model, change nothing
When Sonnet 4.7 (or a v5) enters the catalog, the same ref resolves to it on the next request and X-Ringside-Model-Resolved updates accordingly. Your call site is untouched.
from openai import OpenAI
client = OpenAI(
base_url="https://api.fightclub.pro/v1",
api_key=os.environ["FC_API_KEY"],
)
resp = client.chat.completions.create(
model="match:anthropic/sonnet>=4",
messages=[{"role": "user", "content": "Draft a release note."}],
extra_headers={"FC-Customer": "cus_42"},
)
# the resolved concrete model is on the raw response headers
resolved = resp.response.headers.get("x-ringside-model-resolved")
newer = resp.response.headers.get("x-ringside-model-newer-available")
print(resolved, newer)Worked example: a dyn: profile
A dyn: ref hands the model choice to a profile. A short, simple prompt may land on a budget rung; a long prompt with tools and a strict json_schema pushes the router up the ladder. The reason header tells you which signal moved it.
POST /v1/chat/completions
{ "model": "dyn:cost-aware", "messages": [ ...short prompt... ] }
HTTP/1.1 200 OK
X-Ringside-Dynamic-Profile: cost-aware
X-Ringside-Dynamic-Reason: low difficulty, no tools, budget rung
X-Request-Id: req_8sKQ2v...Errors and edge cases
| Status | code | Cause |
|---|---|---|
| 400 | invalid_model_ref | No prefix (a bare name), an unknown prefix, an empty fc:/slot: body, an unknown @region suffix or a dyn: profile that is missing or misconfigured. |
| 400 | byok_not_supported | A user: BYOK ref. Not accepted by the Ringside API. |
| 400 | unknown_slot | A slot: alias not defined in platform settings. |
| 400 | slot_cycle | A slot alias loop, or chained slots deeper than 8 levels. |
| 400 | unsupported_provider | A match: provider not yet supported, or an @region suffix on a base that does not support it. |
| 400 | model_family_unknown | A match: family the resolver does not know (or no catalog members for it). |
| 400 | model_family_eol | A match: floor or pin that no catalog model satisfies. The message names the newest model that exists. |
| 400 | invalid_model_for_chat | The ref resolved to a non-chat model (image / audio / embedding) on /v1/chat/completions. Use the matching endpoint. |
Two operational edges to plan for. First, match: and slot: depend on a warm catalog: if the platform catalog has not been synced the resolver returns model_family_unknown rather than a stale guess. Second, resolution interacts with the cost cascade. When model is an array (a fallback chain), Ringside resolves each entry, walks the chain on a 5xx or a schema-validation failure and reports the winner via X-Ringside-Model-Used. A cascade cannot combine with stream:true (400 fallback_with_stream_unsupported) or with an idempotency-key (400 fallback_with_idempotency_unsupported).
Use fc: for anything you measure or regression-test so the result is reproducible, and use match:, slot: or dyn: on the production hot path where you want Ringside to carry you onto newer or cheaper models without a redeploy. Watch X-Ringside-Model-Resolved in your logs to catch a roll-forward the moment it happens.