List your models
Sell inference on RouterPlus.
Sell your inference on RouterPlus. You send one model document, your models pass a published conformance bar, and RouterPlus pays you on the schedule you choose.
- Pricing is pass-through. Buyers pay your listed price with no markup.
- Routing follows live health and published metrics. You see the same numbers. Routing is not negotiated.
To apply: email [email protected] with your model document attached. A person at RouterPlus and the automated suite handle onboarding. It usually completes in the same week.
The model document
The model document is one JSON document with your endpoint, models, prices, capacity and data policy, pushed to RouterPlus as a versioned manifest. RouterPlus does not poll your /v1/models, so nothing changes between your updates. RouterPlus adds new models. A self-serve listing changes its own prices, limits and on/off state through the API (see Changing your prices yourself).
Validate the document against the JSON Schema before you send it: /docs/provider-manifest.schema.json.
{
"manifest_version": "1",
"provider": {
"id": "acme",
"name": "Acme Inference",
"privacy_policy_url": "https://acme.example/privacy",
"terms_of_service_url": "https://acme.example/terms",
"status_page_url": "https://status.acme.example",
"prompt_logging": "none"
},
"endpoint": {
"base_url": "https://api.acme.example/v1",
"dialect": "openai",
"api_key_env": "ACME_API_KEY"
},
"settlement": {
"mode": "arrears",
"interval": "weekly",
"billing_contact": "[email protected]"
},
"models": [
{
"id": "acme-large-1",
"display_name": "Acme Large 1",
"context_length": 128000,
"max_output_tokens": 16384,
"streaming": true,
"is_ready": false,
"pricing": [
{ "type": "prompt", "unit": "token", "cost_usd_per_million": "0.90" },
{ "type": "completion", "unit": "token", "cost_usd_per_million": "3.60" }
],
"capacity": [
{ "type": "completion", "unit": "token", "per": "minute", "value": 2000000 }
]
}
]
}Identity
idis the identifier RouterPlus sends when it calls your API. It is never an alias.display_nameis the name buyers see.- A listing is text (chat completions) unless
output_modalityis"image"or"decisions". - A text listing declares
streaming. Usetruewhen your endpoint streams SSE. Usefalse(OpenAI dialect only) when it answers in one JSON body.
Image listings are served on POST /v1/images/generations only. They need no context_length or streaming. They must declare max_output_tokens (the per-image ceiling used for the reservation) and supported_parameters for n and a size or aspect_ratio enum.
Decisions listings return typed answers to questions about a state. They are served on POST /v1/decisions only, and need a context_length and no streaming.
Your endpoint declares decisions_path, the path RouterPlus POSTs the request to. If your API picks the model by its URL, each decisions model declares its own decisions_path instead (Cloudflare Workers AI: /clef and /clef-flash), and the endpoint needs none. The body still carries the model's upstream_id (or id) as model.
The model declares decisions_wire, the question format your API speaks. Buyers send questions in that format, and the gateway sends them to you in it:
"systemone": TypeSafe's System One API, which Jev, the Bespoke Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B and Perplexity Decider v1 27B use. This is the default when the field is absent."gliner": Fastino's GLiNER schema, sent in an OpenAI chat-completions envelope.decisions_pathcan then be/chat/completions.
Rules for decisions listings:
- Chat traffic never reaches a decisions listing, even on a chat path.
- Two listings of one model id must declare the same wire.
- RouterPlus refuses
decisions_wireon a listing that is not"decisions".
Decisions endpoint options
endpoint.response_envelope. Declare this if your API wraps every answer and error in its own transport envelope. The gateway opens it before it reads the answer. Only a manifest of decisions models may declare it. The one value today is "cloudflare_v4", Cloudflare's REST envelope:
- A 200 is
{"result": <answer>, "success": true, "errors": [], "messages": []}. - An error is
{"errors": [{"code": ..., "message": ...}], "success": false, "result": {}}. - A 200 without
success: trueis a failed answer, and RouterPlus bills nothing for it.
endpoint.decisions_format. Declare this if your API asks System One's kinds of questions (a choice, a score, a yes/no) in its own body format. The one value today is "openai": OpenAI's POST /v1/decisions format, with input and an ordered questions array of predicate, choice and score questions, answered by an ordered answers array. Your models still declare decisions_wire: "systemone", and buyers still write System One. The gateway translates both ways. Only a manifest of System One decisions models may declare it. OpenAI's GPT-6 Luna Decisions uses it.
System One body options. Two booleans for a decisions model whose decisions_wire is "systemone" (or absent). RouterPlus refuses them on any other listing. Absent means false.
| Option | Meaning |
|---|---|
decisions_state_per_question | Your API runs one prompt per question, each with the whole state, and bills the state once per question (as Perplexity's decider does). The gateway then reserves money and tokens for the state once per question. |
decisions_image_parts | Your API reads an object whose type is "image_url", at any depth of the state or a question, as an image. A decisions listing takes text and JSON, so the gateway refuses such a body with a typed 400 before it reserves money. The body never reaches you. |
Chat listing options
input_modalities lists what a message to the model may carry: always "text", plus any of "image", "file" (a document such as a PDF), "audio" and "video". Absent means text only. RouterPlus refuses the field on an image or decisions listing.
- Buyers see it on the model page and filter by it.
/v1/modelslists it. The model page shows an input only when every listing of the model declares it. - The gateway sends a part only to a listing that declares its kind. A part that no listing declares gets a typed 400.
- A declared part reaches you as sent from the surface that speaks your dialect. Across dialects, the gateway refuses it with a typed 400.
"chat_api": "responses" (OpenAI-dialect endpoints only) makes the gateway call your Responses API (POST /responses under base_url). Absent means Chat Completions. Declare it when the model takes function tools only there, as OpenAI's GPT-6 and GPT-5.6 do. Buyers still call either chat surface. Parameters with no Responses form on these models (stop, n, seed, logprobs, logit_bias, the penalties) are dropped and recorded. An output limit under 16 tokens is sent as 16.
"fixed_sampling": true declares that the model takes only its default sampling, and you refuse temperature (other than 1), top_p and top_k with a 400, as Anthropic does for the Claude 5 models and OpenAI for GPT-6 and GPT-5.6. The gateway then leaves these parameters out on every route of the model and records the drop. Absent means you take them.
Serving a model the marketplace already lists
If another provider already lists the model (for example, the marketplace id claude-sonnet-4-5), list it under that id. Set upstream_id to the name your endpoint expects:
{ "id": "claude-sonnet-4-5", "upstream_id": "anthropic/claude-sonnet-4.5", "...": "..." }- Requests to you carry
upstream_id. Conformance check C7 expects you to echo it. - Your prices must match the other providers of that id, or the catalog build rejects them. Two exceptions:
- You set your own prices (see Changing your prices yourself).
- Your listing is opt-in (
"routing": "opt_in"). Then you serve only requests that name you (provider.onlyorprovider.order), at your own price.
provider.priorityorders the providers of one model:0for a first-party lab,1for an aggregator fallback. Routing tries the lower number first.
Endpoint
dialectis"openai"(Chat Completions) or"anthropic"(Messages). Buyers reach your models from both surfaces, and the gateway translates.api_key_envnames the secret slot for the key you issue to RouterPlus. The key never appears in a manifest, page or log.
Pricing
Entries are {type, unit, cost_usd_per_million}. Costs are decimal strings in USD, never floats. Types are prompt, cached_prompt, cache_write, completion and internal_reasoning. The unit is token.
- Declare what you charge. A SKU you leave out bills its tokens at your base
promptorcompletionrate. A missing SKU is never free. - Price changes are append-only. Each change is a new effective-dated row. A request bills at the price in force when it arrived, so your statement and the buyer's bill agree.
- Pricing is flat per token. Conditional overrides, time-of-day windows and per-request SKUs are not supported yet.
Changing your prices yourself
RouterPlus can make your listing self-serve. You then change its prices and some limits through this API.
| Endpoint | https://app.routerplus.com/v1/provider/manifest |
| Auth | Authorization: Bearer tm_pk_…, the provider key that RouterPlus issues you. One key speaks for one provider. Keep it secret. |
| Methods | GET, POST (replace), PATCH (change named fields) |
What you can change:
provider.privacy_policy_url,provider.terms_of_service_urlandprovider.status_page_url.- For each model you list:
pricing,provider_pricing,enabled,context_lengthandmax_output_tokens.
RouterPlus owns all other fields. Leave them out or send them unchanged. A write that changes one is refused with the field's name.
- You cannot add or remove a model.
"enabled": falsetakes a model off the marketplace routes and the public lists. It does not stop a buyer who sends their own key for your service (BYOK).- A limit (
context_length,max_output_tokens) can only go down: at most the listed value, andmax_output_tokensat mostcontext_length. - For an image or video model, RouterPlus owns
max_output_tokens. - For a decisions model, RouterPlus owns
context_lengthandmax_output_tokens.
GET
GET /v1/provider/manifestGET returns the state in force (the RouterPlus listing with your edits on it) and these fields:
| Field | Meaning |
|---|---|
revision | The latest revision. |
in_force | The revision in force. |
frozen | Whether RouterPlus froze your listing. |
price_feed | Your price feed, if you have one. |
pending | Scheduled changes: every one that will take effect, and the 20 latest that never will. A change with "shadowed": true never takes effect, because a later write takes effect first. shadowed_omitted counts the rest. |
pushed_provider_fields | On in_force and on each pending change: the URLs that you set. The other URLs follow the RouterPlus listing, also when RouterPlus changes them later. |
How a write treats a URL or a limit. A write changes a URL or a limit only when it sends null or a value different from the state. Leaving the field out of a PATCH, or sending the value in the state, keeps your earlier value, even when the RouterPlus listing hides it. For example:
- 01You lower
context_lengthto 6000. - 02RouterPlus lowers its listing to 4000. GET shows 4000.
- 03A PATCH of a price only,
PATCH {}, or a POST of your GET body keeps your 6000. - 04RouterPlus raises its listing to 8192. Your 6000 applies again.
Send null to follow the RouterPlus listing again. This is a change even when only storage changes: its changes show pushed_provider_fields (before and after) or lowered_context_length / lowered_max_output_tokens (your value before, null after).
POST
POST replaces all of your editable state, from its effective_from. Send every model you list. An editable field you leave out goes back to the RouterPlus value:
- No
provider_pricing: RouterPlus pays you the buyer price. - No
enabled: the model is on. - No limit: the listed limit.
- No URL: the listed URL.
When the state already shows the listed value for a limit or URL, leaving it out keeps what you set before (see above). The safe sequence is GET, edit, POST. A POST replaces every pending change that starts at or after its effective_from, and lists them in superseded. To keep a pending change, use PATCH.
POST /v1/provider/manifest
Idempotency-Key: 6f1c…
{ "manifest": { "provider": { "id": "your-id" },
"models": [ { "id": "your-model",
"pricing": [ { "type": "prompt", "unit": "token", "cost_usd_per_million": "0.40" },
{ "type": "completion", "unit": "token", "cost_usd_per_million": "1.60" } ] } ] },
"effective_from": "2026-11-01T00:00:00Z", "expect_revision": 3 }PATCH
PATCH changes only the fields you name. null goes back to the RouterPlus value ("provider_pricing": null means RouterPlus pays you the buyer price).
PATCH /v1/provider/manifest
{ "models": [ { "id": "your-model", "context_length": 65536 },
{ "id": "your-old-model", "enabled": false } ] }A PATCH never cancels a change that you or your price feed scheduled.
- It applies to the state in force at its
effective_from(default: the next 5-minute mark), and to each later pending change, which RouterPlus writes again at its own time with your PATCH on it. - The answer's
revisionslists every revision written, each witheffective_from,diff,changesandreissues(the pending change it replaces).supersededlists the old pending changes.headis the latest revision: send it as the nextexpect_revision.
Example: you scheduled a price increase for tomorrow, and today you PATCH "enabled": false on a model. RouterPlus writes two revisions: the model is off now, and tomorrow the increase starts with the model still off, with no new notice.
RouterPlus checks each revision against what buyers would pay at its time without your write. If one is refused, nothing is written, and the 422 names its time.
The rules
Timing
- Every change starts at a 5-minute mark (UTC 10:00, 10:05, 10:10), the first one at least 2 minutes after your write. A write at 10:02:30 starts at 10:05, and one at 10:03:30 at 10:10.
- A set
effective_frommoves to the first mark at or after it: 10:07:10 becomes 10:10, and 11:00 stays 11:00. The answer'seffective_fromgives the start. - A write cannot change what starts less than 2 minutes after it.
- A stop applies at once. A write with no future
effective_fromwhose only changes turn a model off or lower a limit applies when written ("timing": "at_once"). If it also changes a price, what RouterPlus pays you, a URL, turns a model on, or replaces a pending change with a POST, all of it waits for the mark. effective_fromis ISO 8601 with a zone, for example2026-11-01T00:00:00Z. It must be a real time (2026-02-30or hour24is refused), not in the past, and at most 90 days ahead.
Notice
- Increases need notice: 1 hour for every provider, unless RouterPlus tells you otherwise. Set
effective_fromthat far ahead. Decreases, turning a model off or on, and lowering a limit need none. - The notice is checked against the time you set. The price is compared with what buyers pay at that time, and with every change you already scheduled.
- A model that is off has no buyer price. When you turn it on, RouterPlus compares its price with the price buyers last paid before it went off, or the listed price if it was never on. A higher price needs notice. The price you set while it is off is not compared.
Price limits
- Margin. On each SKU, your price minus
provider_pricing(what RouterPlus pays you) may not be below zero, or below the negative margin the RouterPlus listing already accepts. - Floor. Your key and feed may not set a price below one tenth of the listed price. A SKU listed at 0 has no floor. RouterPlus operators can set a lower price if you ask.
- A SKU listed at 0 may stay 0. To make a priced SKU free, ask RouterPlus first.
- These three rules apply only to SKUs that your write changes (price, or for the margin what RouterPlus pays you), including by replacing a pending change. You can keep a low or free price that operators set, but not extend it past the end they scheduled. A later change to the listing's margin does not block a write that keeps a SKU as it is.
- At most $1,000 per million tokens per SKU.
- A change of more than 10× needs
"confirm_large_change": true. A change to or from 0 falls under the free-price rule instead. - A self-serve listing is outside the identical-price rule in Serving a model the marketplace already lists. A buyer pays the price of the provider that served them.
Pending changes
- At most 20 pending changes per listing, and 200 scheduled revisions in all, counting
shadowedones. A write that would leave more is a 422too_many_pending. - A POST with no
effective_fromends every pending change from its mark on, so it is always possible. - A pending change that your write makes equal to the state before it is superseded.
Body
- A policy URL must be
https://orhttp://, at most 2,048 characters, with no spaces or control characters. - A price entry has only
type,unitandcost_usd_per_million. Another key is refused with its path. - A body is a 400 if a key or string contains U+0000 (NUL) or a lone UTF-16 surrogate (such as
\ud800without its pair), or if it is nested more than 32 levels deep (the top-level object is level 1). This check runs after the key, rate, listing and query checks, and before the write checks.
Answers
?dry_run=1(ortrue) checks a POST or PATCH and writes nothing.?dry_run=0(orfalse) writes. Any other value or query parameter is a 400.- A write answers 201 with these fields. A dry run gives the same times.
| Field | Meaning |
|---|---|
revision | The first revision written. |
head | The latest revision after the write. |
revisions | Every revision written. |
diff | Each price SKU of the first revision: increase, decrease or new. |
changes | The other fields that changed. |
superseded | The pending changes that the write replaces. |
effective_from | When the change starts: a 5-minute mark, or the time of the write for a stop. |
requested_effective_from | The time you sent, or the time of the write. |
timing | "next_mark" or "at_once". |
- A write that changes nothing answers 200 with
"unchanged": true. It counts toward your limit. - Idempotency. The same
Idempotency-Keywith the same body returns the first answer, also when that write changed nothing or the listing is frozen. With another body it is a 409 (a 423 while frozen). The key covers the whole write and all its revisions. A refused write (any 4xx) does not use up its key.
Errors
Every error has error.code, error.message and error.errors (each with its field path).
| Status | Code | Why |
|---|---|---|
| 400 | invalid_request | The body is not a write, a field has the wrong type, the body has a NUL or a lone surrogate or is nested too deep, or the query is not dry_run |
| 401 | unauthorized | The key is missing, unknown or revoked |
| 403 | forbidden | The listing is not self-serve |
| 403 | prices_from_feed | RouterPlus reads your prices from your price feed |
| 404 | not_found | RouterPlus has no listing for your key's provider |
| 405 | method_not_allowed | Use GET, POST or PATCH |
| 409 | conflict | expect_revision is stale, or the Idempotency-Key was used for another body (while your listing is frozen: 423) |
| 413 | payload_too_large | The body is more than 256 KB |
| 422 | manifest_refused | A rule above, or the database refused a value of the write; see below |
| 422 | too_many_pending | The write would leave more than 20 pending changes, or more than 200 scheduled revisions in all |
| 423 | frozen | RouterPlus froze your listing. Writes wait until it is unfrozen |
| 429 | rate_limited | More than 30 writes, or 600 requests, an hour per key |
| 503 | unavailable | The API cannot reach its database. Try again after retry-after seconds |
A 422 lists every problem found in errors, with a note. RouterPlus checks in stages:
- 01The fields you may change.
- 02The manifest rules.
- 03The prices.
- 04The notice and 10× rules.
A stage runs only when the stages before it pass, so a fixed write can show new problems. Use ?dry_run=1 first. An answer lists at most 100 errors, and errors_omitted counts the rest.
Limits
- At most 30 writes an hour per key. These answers count as a write:
- 201 (accepted)
- 200 with
"unchanged": true(no change) - 403
prices_from_feed, 409conflictand 422 (manifest_refused,too_many_pending) (refused)
- No other answer counts: not a 400, 401, 403
forbidden, 404, 405, 413, 423, 429 or 503, not a dry run, and not the replay of anIdempotency-Key. - At most 600 requests an hour per key, of every kind.
- A body is at most 256 KB.
Price feed
You can publish your prices instead of writing them. RouterPlus sets up the feed in its listing (endpoint.price_feed) and reads it every hour by default. The feed must use https, and RouterPlus does not follow redirects. A read stops after 20 seconds or 4 MiB. A UTF-8 byte order mark is fine. Two formats:
| Format | Shape |
|---|---|
openai_models | {"data": [{"id", "pricing": {"prompt", "completion", "input_cache_read", "input_cache_write", "internal_reasoning"}}]}. USD per token, as decimal strings. Models match by the id you serve them under. A "0" cache or reasoning price means "not billed separately" and leaves that price as it is. RouterPlus pays you the price you publish, so there is no provider_pricing. |
routerplus | {"models": [{"id", "pricing", "provider_pricing"}]}, in this document's price form, with RouterPlus model ids. No provider_pricing means RouterPlus pays you the buyer price. A model entry has only these three keys, and a price entry only type, unit and cost_usd_per_million. Another key (a currency, for example) makes it malformed. |
What a read changes.
- Only listed models change. A model your feed leaves out keeps its price.
- With
openai_models, only the prices you publish change. Withrouterplus, an entry is the model's full price list: a SKU it leaves out bills like its base (cached prompt and cache write like the prompt, reasoning like the completion). - One malformed entry for a listed model voids the whole read.
When a fed price starts.
- A decrease starts at the next 5-minute mark, at least 2 minutes after the read.
- An increase starts at the first mark after the notice period plus 5 minutes, counted from the read that first saw it. Until then, the old price stays on the SKUs that rise.
- A later read does not move a scheduled increase, and a read with the same prices writes nothing. A decrease on another SKU or model starts at the next mark without postponing the increase.
- For a price that keeps rising, RouterPlus keeps at most two scheduled increases: the first announced and the newest. A higher read replaces the ones between, and the earlier price stays until the newest starts. A higher price only ever starts later, and does not block your other changes.
- RouterPlus replaces only increases your feed scheduled, never ones you or operators scheduled, even if a feed read wrote them again (for example, to carry a decrease of another model). A feed increase stays the feed's when you PATCH another field of it.
- A read back at the price in force cancels the increase an earlier read scheduled, and writes the prices in force again from the next mark. A price that goes up and down leaves at most one scheduled increase.
- Your other scheduled changes stay. A model you scheduled to turn off still turns off at its time.
- A lower price that operators scheduled stays until your increase can start (the notice period plus 5 minutes).
When a read is refused.
- RouterPlus refuses a change of more than 10x against the price in force, a price of zero (unless free prices are allowed for your listing), or a price below the floor (one tenth of the listed price). Operators then look at it.
- The 10x rule compares only with the price in force, and only for prices the read changes. A later read with the right price corrects a wrong scheduled increase, even if it is more than 10x lower.
- If operators scheduled a change of more than 10x and your feed publishes that price, the read is not refused, and the price starts at the operators' time.
- With
openai_models, a read is also refused while a revision in force or pending pays you your own cost (provider_pricing, from before the feed). Operators must clear that cost first. RouterPlus never drops it without a decision. - While your listing is frozen, the feed is not read. A freeze during a read stops its write.
Your writes while a feed is on. Your writes cannot change prices (403 prices_from_feed). Other fields can change at any time. A PATCH keeps a pending fed increase at its time. A POST that replaces one changes prices, so it is a 403.
When gateways apply a change
- Gateways read your revisions, including future ones, at least every 30 seconds, so all gateways switch at the mark.
/api/models.jsonlists a scheduled change within about 2.5 minutes. The public pages show only the price in force.- A request bills at the price in force when it arrived, even if the gateway plans it after the change.
- A stop that applies at once reaches every gateway within about 30 seconds.
Freezes
While RouterPlus freezes your listing:
- Buyers pay the listed price, and RouterPlus pays you the listed cost.
- Only your prices are held back. A model you turned off stays off, and a lowered limit stays lowered.
- Your key and your price feed cannot write.
- A freeze and an unfreeze reach every gateway within about 30 seconds, without waiting for a mark.
On unfreeze, your revision in force applies again, and so does each pending change that starts within the notice period. Operators must confirm any of them above the listed price before the unfreeze, including one where a model is off: after the unfreeze you can turn it on with no notice at the price buyers last paid while it was on.
If RouterPlus turns self-serve off for your listing, the gateways ignore all your revisions, prices and model states, and a model you turned off is on again at the listed limits. RouterPlus also freezes the listing when it turns self-serve off, removes the listing, or finds the listing's file in the RouterPlus repository not valid. When self-serve is on again, RouterPlus unfreezes it with the same check.
Capacity
Entries are {type, unit, per, value}:
typeisrequest,promptorcompletion.perisminute,hourorday.unit: "token"is required for the two token types.- Identity is type plus window. Validation rejects duplicates.
- Absent capacity means undeclared. It does not mean zero.
Capacity is information for us, not a setting. Validation checks it. Routing and admission do not read it. Admission enforces a house pool for your deployment, set by hand by RouterPlus operators from what you declare and what you tell RouterPlus (see Limits and capacity). Above that pool, your own 429 is the backstop. It pauses the pool for its retry-after (1 to 60 seconds).
Parameters
You may declare supported_parameters per model.
- On the image route it is enforced. A value outside a declared enum is a 400 that names the field.
- An image listing must declare
nand asizeoraspect_ratioenum, or it fails validation.qualityandresolutionare optional, but must be enums when declared. - On chat routes, the gateway applies its own per-dialect allowlists and records every parameter it removes (the buyer sees them in
x-tm-dropped-paramsand their audit endpoint). Declare parameters accurately: chat enforcement will use them when it ships.
Data policy
prompt_logging is "none" or "retained". Your model pages publish it next to your privacy policy and terms URLs, and buyers filter on it, so it must be true. See Data policy.
Lifecycle
is_ready: falsestages a model: validated and conformance-testable, but not listed or routed. Set it totruewhen ready.listed: falseaccepts a model by name only. It is routed, priced and billed like any other model, but no list shows it (GET /v1/models, the Models page,/api/models.json, the Playground). Use it for a name that you move from model to model, such as Bespoke Labs'nimble-latest. Give it its own id, its ownupstream_idand the price you charge for that name. Two listings of one model id must agree on it.deprecation_date(ISO date): after this date the model leaves the catalog.released(ISO date): the lab's release date, or the day the model first listed publicly. The Models page and the Playground sort newest first, after a few featured models.- Date-pinned requests (
acme-large-1-20260901) resolve to your listed model for routing and billing. Your API gets the buyer's original id and decides whether to serve the snapshot.
The conformance bar
RouterPlus runs seven checks against every model, on the stream and non-stream paths, at listing time and on every catalog change. RouterPlus emails you the report.
| Check | Requirement |
|---|---|
| C1 | A non-stream completion returns provider-reported usage tokens. Usage is the billable record, so a model without it cannot be listed. |
| C2 | Streamed responses are valid SSE, and every frame parses. |
| C3 | Streams end with their dialect's terminal marker ([DONE] / message_stop). |
| C4 | Usage tokens are reported in the stream. |
| C5 | An unknown model returns a parseable 4xx JSON error, never a 200 or HTML. |
| C6 | A billed response contains assistant content. RouterPlus never passes usage counters without an answer. |
| C7 | The response reports the model that was asked for. Resolving an undated id to your own dated snapshot is fine. Declare any other equivalence in resolves_to. Substituting a different model without saying so disqualifies the listing. |
An image listing (output_modality: "image") is non-stream only, so it runs C5 and these checks instead of C2 to C4. Each check buys one real render at the model's top declared quality.
| Check | Requirement |
|---|---|
| I1 | A generation returns decodable base64 and provider-reported usage tokens. |
| I2 | The tokens billed for that render fit the declared per-image ceiling (max_output_tokens). An under-declared ceiling loses RouterPlus money on every request. |
| I3 | An undeclared size returns a parseable 4xx JSON error. |
| I4 | Opt-in (--image-ceiling): I2 at every declared size and at size: auto. |
A decisions listing (output_modality: "decisions") runs two checks:
- D1: one call with three questions comes back as typed answers, with usage.
- On the
systemonewire: one Choice, one Score and one Noul question, andusage.input_tokens. - On the
glinerwire: one yes/no task and one three-way task in one schema (sent withstore: false). The message content must parse to one JSON object with a labelled answer under each task name, and the usage must haveprompt_tokens. - D1 calls each model at its own
decisions_pathwhen the model declares one, else at the endpoint's. - With
endpoint.response_envelope: "cloudflare_v4", the 200 must saysuccess: true, and D1 reads the answer and the usage insideresult. - With
endpoint.decisions_format: "openai", D1 sends the same three questions in OpenAI's form (achoice, ascorewith levels and apredicate). It checks each answer under its name in its own type, withusage.input_tokens. - When D1 gets another status, the report shows the first 300 characters of your body.
- On the
- C5: an unknown model gets a parseable 4xx error. FastAPI's
{"detail": ...}envelope counts as a described error, and so does Cloudflare's{"errors": [{"code": ..., "message": ...}]}(or one such object in place of the list) when an entry has a message. Withendpoint.decisions_format: "openai", C5 sends the unknown model in OpenAI's form (OpenAI answers 404model_not_found).
For every model, the report also lists any Ratelimit, Ratelimit-Policy or Retry-After header your endpoint sent, or says that it sent none. These headers are reported, never judged.
Wire requirements
- An HTTPS endpoint that speaks OpenAI Chat Completions or Anthropic Messages, with streaming.
- An OpenAI-dialect endpoint that cannot stream declares
streaming: false. RouterPlus asks it for JSON and streams the finished answer, so the buyer's first byte waits for the full answer. - Provider-reported usage tokens on both paths (C1, C4).
- Errors: return 429 early when you are at capacity. Queueing raises your published latency. Return 4xx JSON for bad requests.
- Streaming: send response headers within 20 seconds, or the attempt fails over. Send SSE keep-alive comments through long gaps before the first byte, and stream tokens as soon as they exist. Non-stream requests get a long generation budget.
- After the first output, RouterPlus never switches providers, so buyers see a stall as yours.
How uptime is counted
Routing uses the same numbers:
uptime = completed ÷ (completed + counted failures), trailing 24h- Counted against you: provider 401, 402 and 403, all 5xx, streams that die mid-stream, and incomplete streams.
- Never counted: requests the buyer caused: 400 and 413 (bad input), 429 (the buyer's rate limit) and client cancellations mid-stream.
- Published after 100 or more requests. Below that, the pages show a dash.
- Failures open a per-deployment health circuit: a cooldown, then one half-open probe. Recovery closes it. While every deployment of a model cools down, RouterPlus still sends a last-resort attempt.
- Content-policy refusals are never retried and never failed over.
Performance
RouterPlus publishes TTFB p50/p95 (from merged t-digests) per model, and the router uses the same figures. Return early 429s instead of queueing, and start streaming at once.
How you get paid
Pick a settlement mode in your manifest:
prepaid: RouterPlus deposits with you ahead of traffic, and usage draws it down. RouterPlus watches balance against burn so routing never hits an empty account.arrears: RouterPlus routes first and pays on your interval:half_daily,daily,weeklyormonthly.
Your statement comes from the append-only ledger that bills buyers: token counts and USD per period, auditable to each attempt. Each attempt also records byte counts, a tokenizer-independent bound for disputes. Statements and conformance reports come by email. There is no provider dashboard yet.
What you get
- Buyer traffic from both wire surfaces on the day your models go live.
- A public page for each model: prices, context, your data-policy flag and live metrics.
- Settlement statements per period that reconcile to the token.
- Circuits and time to recover when your service degrades. RouterPlus does not delist a provider without notice.
Apply
Email [email protected] with your manifest attached. Validate it against the schema first. RouterPlus runs the suite against your endpoint, shares the report, stages your models and makes them live with you.
For agents: /docs/for-providers.md and /llms.txt