# List your models

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 **providers@evo-hq.com** 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](#changing-your-prices-yourself)).

Validate the document against the JSON Schema before you send it: [`/docs/provider-manifest.schema.json`](/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": "billing@acme.example"
  },
  "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`.

| 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/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](#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.

| | |
|---|---|
| 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_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:

| 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:

1. You lower `context_length` to 6000.
2. RouterPlus lowers its listing to 4000. GET shows 4000.
3. A PATCH of a price only, `PATCH {}`, or a POST of your GET body keeps your 6000.
4. 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](#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.

| 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-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).

| 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:

1. The fields you may change.
2. The manifest rules.
3. The prices.
4. 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:

| 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. 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](/docs/admission)). 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](/docs/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.

| 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 `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:

```text
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 **providers@evo-hq.com** with your manifest attached. Validate it against [the schema](/docs/provider-manifest.schema.json) first. RouterPlus runs the suite against your endpoint, shares the report, stages your models and makes them live with you.
