Console
For providers

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.

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

  • id is the identifier RouterPlus sends when it calls your API. It is never an alias.
  • display_name is the name buyers see.
  • A listing is text (chat completions) unless output_modality is "image" or "decisions".
  • A text listing declares streaming. Use true when your endpoint streams SSE. Use false (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_path can 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_wire on 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: true is 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.

OptionMeaning
decisions_state_per_questionYour 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_partsYour 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/models lists 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:

json
{ "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.only or provider.order), at your own price.
  • provider.priority orders the providers of one model: 0 for a first-party lab, 1 for an aggregator fallback. Routing tries the lower number first.

Endpoint

  • dialect is "openai" (Chat Completions) or "anthropic" (Messages). Buyers reach your models from both surfaces, and the gateway translates.
  • api_key_env names 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 prompt or completion rate. 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.

Endpointhttps://app.routerplus.com/v1/provider/manifest
AuthAuthorization: Bearer tm_pk_…, the provider key that RouterPlus issues you. One key speaks for one provider. Keep it secret.
MethodsGET, POST (replace), PATCH (change named fields)

What you can change:

  • provider.privacy_policy_url, provider.terms_of_service_url and provider.status_page_url.
  • For each model you list: pricing, provider_pricing, enabled, context_length and max_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": false takes 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, and max_output_tokens at most context_length.
  • For an image or video model, RouterPlus owns max_output_tokens.
  • For a decisions model, RouterPlus owns context_length and max_output_tokens.

GET

GET /v1/provider/manifest

GET returns the state in force (the RouterPlus listing with your edits on it) and these fields:

FieldMeaning
revisionThe latest revision.
in_forceThe revision in force.
frozenWhether RouterPlus froze your listing.
price_feedYour price feed, if you have one.
pendingScheduled 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_fieldsOn 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:

  1. 01
    You lower context_length to 6000.
  2. 02
    RouterPlus lowers its listing to 4000. GET shows 4000.
  3. 03
    A PATCH of a price only, PATCH {}, or a POST of your GET body keeps your 6000.
  4. 04
    RouterPlus 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 revisions lists every revision written, each with effective_from, diff, changes and reissues (the pending change it replaces). superseded lists the old pending changes. head is the latest revision: send it as the next expect_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_from moves to the first mark at or after it: 10:07:10 becomes 10:10, and 11:00 stays 11:00. The answer's effective_from gives 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_from whose 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_from is ISO 8601 with a zone, for example 2026-11-01T00:00:00Z. It must be a real time (2026-02-30 or hour 24 is 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_from that 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 shadowed ones. A write that would leave more is a 422 too_many_pending.
  • A POST with no effective_from ends 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:// or http://, at most 2,048 characters, with no spaces or control characters.
  • A price entry has only type, unit and cost_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 \ud800 without 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 (or true) checks a POST or PATCH and writes nothing. ?dry_run=0 (or false) writes. Any other value or query parameter is a 400.
  • A write answers 201 with these fields. A dry run gives the same times.
FieldMeaning
revisionThe first revision written.
headThe latest revision after the write.
revisionsEvery revision written.
diffEach price SKU of the first revision: increase, decrease or new.
changesThe other fields that changed.
supersededThe pending changes that the write replaces.
effective_fromWhen the change starts: a 5-minute mark, or the time of the write for a stop.
requested_effective_fromThe 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-Key with 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).

StatusCodeWhy
400invalid_requestThe 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
401unauthorizedThe key is missing, unknown or revoked
403forbiddenThe listing is not self-serve
403prices_from_feedRouterPlus reads your prices from your price feed
404not_foundRouterPlus has no listing for your key's provider
405method_not_allowedUse GET, POST or PATCH
409conflictexpect_revision is stale, or the Idempotency-Key was used for another body (while your listing is frozen: 423)
413payload_too_largeThe body is more than 256 KB
422manifest_refusedA rule above, or the database refused a value of the write; see below
422too_many_pendingThe write would leave more than 20 pending changes, or more than 200 scheduled revisions in all
423frozenRouterPlus froze your listing. Writes wait until it is unfrozen
429rate_limitedMore than 30 writes, or 600 requests, an hour per key
503unavailableThe 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:

  1. 01
    The fields you may change.
  2. 02
    The manifest rules.
  3. 03
    The prices.
  4. 04
    The 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, 409 conflict and 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 an Idempotency-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:

FormatShape
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. With routerplus, 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.json lists 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}:

  • type is request, prompt or completion.
  • per is minute, hour or day.
  • 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 n and a size or aspect_ratio enum, or it fails validation. quality and resolution are 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-params and 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: false stages a model: validated and conformance-testable, but not listed or routed. Set it to true when ready.
  • listed: false accepts 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 own upstream_id and 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.

CheckRequirement
C1A non-stream completion returns provider-reported usage tokens. Usage is the billable record, so a model without it cannot be listed.
C2Streamed responses are valid SSE, and every frame parses.
C3Streams end with their dialect's terminal marker ([DONE] / message_stop).
C4Usage tokens are reported in the stream.
C5An unknown model returns a parseable 4xx JSON error, never a 200 or HTML.
C6A billed response contains assistant content. RouterPlus never passes usage counters without an answer.
C7The 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.

CheckRequirement
I1A generation returns decodable base64 and provider-reported usage tokens.
I2The tokens billed for that render fit the declared per-image ceiling (max_output_tokens). An under-declared ceiling loses RouterPlus money on every request.
I3An undeclared size returns a parseable 4xx JSON error.
I4Opt-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 systemone wire: one Choice, one Score and one Noul question, and usage.input_tokens.
    • On the gliner wire: one yes/no task and one three-way task in one schema (sent with store: false). The message content must parse to one JSON object with a labelled answer under each task name, and the usage must have prompt_tokens.
    • D1 calls each model at its own decisions_path when the model declares one, else at the endpoint's.
    • With endpoint.response_envelope: "cloudflare_v4", the 200 must say success: true, and D1 reads the answer and the usage inside result.
    • With endpoint.decisions_format: "openai", D1 sends the same three questions in OpenAI's form (a choice, a score with levels and a predicate). It checks each answer under its name in its own type, with usage.input_tokens.
    • When D1 gets another status, the report shows the first 300 characters of your body.
  • 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. With endpoint.decisions_format: "openai", C5 sends the unknown model in OpenAI's form (OpenAI answers 404 model_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, weekly or monthly.

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