# List your models

Sell your inference through the marketplace: one model document, a published
conformance bar, pass-through pricing, and automatic payment on the schedule
you choose.

Two things to know up front:

- **Pricing is pass-through.** Buyers pay your listed price; we never mark it
  up. Your price is your price.
- **Routing is earned.** Traffic flows on live health and published metrics —
  the same numbers you can see — not on negotiation.

**To apply:** email **providers@evo-hq.com** with your model document attached.
Onboarding is white-glove today: a human plus the automated suite take it from
there, usually same-week.

## The model document

Everything we need is one JSON document — your endpoint, models, prices,
capacity, and data policy. Rather than poll a `/v1/models` endpoint you host,
we accept the same information **pushed** as a versioned manifest: you send
updates when you choose, and nothing changes under you between updates. A
self-serve listing changes its own prices, limits and on/off state through the
API (see below); a new model is still added by us.

Validate against the published JSON Schema before sending:
[`/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 **exact** identifier we send when calling your API — never an
alias. `display_name` is what buyers see. A listing is text (chat completions)
unless `output_modality` says `"image"` or `"decisions"`. A text listing declares
`streaming`: `true` when your endpoint streams SSE, or `false` (OpenAI dialect only) when
it answers in one JSON body. An image listing is served
on `POST /v1/images/generations` only; it needs no `context_length` or
`streaming`, and must declare `max_output_tokens` (the per-image ceiling the
reservation is computed from) plus `supported_parameters` for `n` and a `size`
or `aspect_ratio` enum. A decisions listing (a structured decision model that returns typed
answers to questions about a state) is served on `POST /v1/decisions`; it needs a
`context_length` but no `streaming`, and your endpoint must declare `decisions_path`, the
path we POST 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 then 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: `"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; the default when the field is absent) or `"gliner"` (Fastino's GLiNER
schema, sent in an OpenAI chat-completions envelope, so `decisions_path` can be
`/chat/completions`). Buyers send questions in that format, and the gateway sends them to
you in it. A decisions listing is served only on `POST /v1/decisions`, even when its path is
a chat path: chat traffic never reaches it. Two listings of one model id must declare
the same wire. `decisions_wire` on a listing that is not `"decisions"` is refused.

If your API puts every answer and every error inside a transport envelope of its own,
declare it as `endpoint.response_envelope`, and 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": []}`, and an error is
`{"errors": [{"code": ..., "message": ...}], "success": false, "result": {}}`. A 200 whose
envelope does not say `success: true` is a failed answer, and nothing is billed for it.

If your decisions API asks System One's kinds of questions (a choice, a score, a yes/no) in a
body format of its own, declare the format as `endpoint.decisions_format`. The one value today is `"openai"`: your API speaks OpenAI's
`POST /v1/decisions` format (`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"`. Buyers still write System One, and the gateway
translates both ways: it sends you your format, and it gives the buyer your answers in
System One's shape. Only a manifest of System One decisions models may declare it. OpenAI's
GPT-6 Luna Decisions uses it.

Two more fields tell the gateway how your System One API reads a body. They are booleans on
a decisions model whose `decisions_wire` is `"systemone"` (or absent), and are refused on any
other listing. Absent means `false`.

- `decisions_state_per_question`: your API runs one prompt per question, each with the
  whole state, and bills the state once per question (Perplexity's decider does). The
  gateway then holds the state once per question when it reserves money and tokens for a
  request, so a request with many questions is held at what it will cost.
- `decisions_image_parts`: your API reads an object whose `type` is `"image_url"`, at any
  depth of the state or of a question, as an image. A decisions listing takes text and
  JSON, so the gateway refuses such a body with a typed 400 before any money is reserved,
  and it never reaches you.

A chat listing may declare `input_modalities`: what a message to the model may carry. The
list always has `"text"`, plus any of `"image"`, `"file"` (a document such as a PDF),
`"audio"` and `"video"`. Buyers see it on the model page and filter the catalog by it, and
the API lists it in `/v1/models`. The model page shows an input only when every listing of
that model declares it. The gateway sends a part only to a listing that declares its kind:
a part your listing does not declare never reaches you, and a part that no listing of the
model declares is refused with a typed 400. A declared part goes to you as sent when the
buyer calls the surface that speaks your dialect; across dialects the gateway refuses it
with a typed 400 and does not translate it. Absent means text only. An image or
decisions listing takes a text prompt, and the field is refused on it.

A chat listing on an OpenAI-dialect endpoint may declare `"chat_api": "responses"`: the
gateway then calls the model on your Responses API (`POST /responses` under `base_url`)
instead of 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: the gateway sends the
request in the Responses form and reads your answer and its usage back. Parameters with no
Responses form on these models (`stop`, `n`, `seed`, `logprobs`, `logit_bias`, the
penalties) are dropped and recorded, and an output limit under 16 tokens is sent as 16.
Absent means Chat Completions.

A chat listing may declare `"fixed_sampling": true` when 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 out on every route of the model and records the drop. Absent means you take
them.

### Serving a model the marketplace already lists

If you serve a model another provider also lists (the marketplace id
`claude-sonnet-4-5`, say), list it under **that** id and 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. Prices must be **identical** to the other providers of that id — the
catalog build rejects a mismatch (pass-through pricing means one price per
model), unless you set your own prices (see "Changing your prices yourself"),
or your listing is opt-in (`"routing": "opt_in"`): on a model that other
providers also list, an opt-in provider is served only to a request that names
it (`provider.only` or `provider.order`), at its own price. `provider.priority` orders providers of one model: `0` for a
first-party lab, `1` for an aggregator fallback; routing tries lower first.

### Endpoint

`dialect` is `"openai"` (Chat Completions) or `"anthropic"` (Messages) — one is
enough. Buyers reach your models from **both** marketplace surfaces regardless;
the gateway translates. `api_key_env` names the secret slot for the key you
issue us: it never appears in a manifest, page, or log.

### Pricing

Entries are `{type, unit, cost_usd_per_million}` with costs as decimal
**strings** in USD (never floats). Types: `prompt`, `cached_prompt`,
`cache_write`, `completion`, `internal_reasoning`; unit is `token`.

- **Declare what you charge.** A SKU you omit bills those tokens at your base
  `prompt`/`completion` rate — a missing SKU never means free, and never means
  a surprise for either side.
- **Price changes are append-only.** A new effective-dated row, never a
  rewrite. Every buyer request bills at the price in force when the request
  arrived at the gateway (see "Changing your prices yourself"), so your
  statement and their bill can never disagree about history.
- Flat per-token pricing only for now: conditional overrides, time-of-day
  windows, and per-request SKUs are not yet supported.

### Changing your prices yourself

We can enable your listing for self-serve. Then you change its prices, and
some of its limits, without waiting for a release. You use the provider key
that we issue you (`tm_pk_…`). One key speaks for one provider. Keep it secret.

The endpoint is `https://app.routerplus.com/v1/provider/manifest`. Send the key
as `Authorization: Bearer tm_pk_…`.

**What you can change.** These are the editable fields:

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

All other fields are ours. You can leave them out, or send them unchanged. If
you change one, we refuse the write and name the field. You cannot add or
remove a model. To take a model off our marketplace routes, set
`"enabled": false`: no route that we pay you for reaches it, and our public
lists hide it. A buyer who sends their own key for your service (BYOK) is not
stopped by it. A limit
(`context_length`, `max_output_tokens`) can only go down: at most the value we
listed, and `max_output_tokens` at most `context_length`. For an image or video model,
`max_output_tokens` is ours. For a decisions model, `context_length` and
`max_output_tokens` are both ours: we refuse a change and name the field (you
can send our value unchanged).

**GET** returns the full state in force. It is our listing with your edits on
it. It also returns `revision` (the latest one), `in_force`, `frozen`,
`price_feed` and `pending` (changes that are scheduled). A pending change with
`"shadowed": true` will never take effect: a later write takes effect first.
`in_force` and each pending change also show `pushed_provider_fields`: the URLs
that you set. The other URLs follow our listing, also when we change them later.

A write changes a URL or a limit only when it sends a different value from the
one that the state shows, or `null`. If you leave a field out of a PATCH, or
send the value that the state shows, we keep what you set before. This is also
true when our listing hides your value. For example: you lowered
`context_length` to 6000, and then we lowered our listing to 4000. GET shows
4000. A PATCH of a price only, `PATCH {}`, or a POST of your GET body keeps your
6000. When our listing goes back to 8192, your 6000 applies again. To follow our
listing again, send `null`. A `null` that changes only how we store the field
is still a change: its `changes` show `pushed_provider_fields` (the list before
and after) or `lowered_context_length` / `lowered_max_output_tokens` (your
lowered value before, and `null` after).

```
GET /v1/provider/manifest
```

**POST** replaces your whole editable state, from its `effective_from`. Send
every model you list. If you leave out an editable field, it goes back to our
value: no `provider_pricing` means we pay you the buyer price, no `enabled`
means on, no limit means our limit, no URL means our URL. (When the state
already shows our value for a limit or a URL, leaving it out keeps what you set
before, as above; send `null` to follow our listing.) So the safe way is: GET,
edit, POST.

A POST replaces every pending change that starts at or after its
`effective_from`. The answer 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** changes only the fields you name. Set a field to `null` to go back
to our value (`"provider_pricing": null` means we pay you the buyer price). A
field that you do not name keeps its value.

```
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` (the next 5-minute mark
if you leave it out; see the rules). It also applies to each pending change
after that time. We write each
pending change again, at its own time, with your PATCH on it. The answer lists
every revision that we write in `revisions` (each with its `effective_from`,
its `diff`, its `changes` and `reissues`: the pending change that it replaces).
`superseded` lists the old pending changes. `head` is the latest revision after
the write; send it as the next `expect_revision`.

For example: you scheduled a price increase for tomorrow. Today you PATCH
`"enabled": false` on a model. We write two revisions: the model is off now (a
stop applies at once), and tomorrow the increase starts with the model still
off. The increase keeps its time and needs no new notice.

We check each revision against what buyers would pay at its time without your
write. If we refuse one of them, we write nothing. The 422 names the time of
the revision that we refused.

**The rules.**

- **Every change starts at a 5-minute mark.** A mark is a UTC time at a whole
  5 minutes: 10:00, 10:05, 10:10. A change starts at the first mark at least 2
  minutes after your write. Written at 10:02:30, it starts at 10:05. Written at
  10:03:30, it starts at 10:10. If you set `effective_from`, we move it up 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` says when the change starts. The 2 minutes let
  every gateway load the change first, so all of them switch at the same time.
- A write cannot change what starts less than 2 minutes after it. Such a
  pending change starts as scheduled.
- **A stop applies at once.** A write with no `effective_from` (or one that is
  not in the future) whose only changes turn a model off or lower a limit
  applies when we write it. The answer says `"timing": "at_once"`. If the same
  write changes anything else (a price, what we pay you, a URL, turning a model
  on, or a pending change that a POST replaces), all of it waits for the mark.
- **Decreases need no notice. Increases need notice.** The notice period is the
  same for every provider: 1 hour, unless we tell you of a change. Set
  `effective_from` that far ahead. We check the notice against the time that
  you set; the change then starts at the first mark at or after it. We compare
  with what buyers pay at that time, and with every change you already
  scheduled.
- Turning a model off or on, and lowering a limit, need no notice. A model
  that is off has no price that buyers pay. So when you turn a model on, we
  compare its price with the price buyers last paid for it (before it went
  off; if never, the price in our listing). If it is more, that is an
  increase, and it needs notice. The price that you set while the model is off
  is not compared.
- `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. It may not be in the past, or more than 90 days ahead. Leave it
  out for the next mark.
- **Margin.** On each SKU, your price minus `provider_pricing` (what we pay
  you) may not be below zero. If our listing already accepts a negative margin
  on a SKU, you may not go below that margin.
- **Floor.** Your key, and your price feed, may not set a price below one
  tenth of the price in our listing for that SKU. A SKU that we list at 0 has
  no floor. If you need a lower price, ask us: our operators can set it.
- A SKU we list at 0 may stay 0. To make a priced SKU free, ask us first.
- The margin rule, the floor and the free-price rule apply to a SKU that your
  write changes: its price, or (for the margin) what we pay you for it. A
  write also changes a SKU when it replaces a pending change with another
  price. So you can keep a low or free price that our operators set, but you
  cannot make it last past the end that they scheduled. And a later change
  to our listing's margin does not block a write that keeps a SKU as it is.
- At most 20 pending changes (scheduled revisions that will take effect) per
  listing, and at most 200 scheduled revisions in all, also counting the ones
  that never take effect (`shadowed`). 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.
- If your write makes a pending change the same as the state just before it,
  we do not keep that pending change: the write supersedes it.
- 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 is not under this rule; the free-price rule above applies.
- 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 may not contain the character U+0000 (NUL), or a lone UTF-16
  surrogate (such as `\ud800` without its pair), in any key or string. It may
  not be nested more than 32 levels deep (the top-level object is level 1).
  We refuse such a body with a 400. We check it after your key, your request
  rate, your listing and the query, and before we check the write itself.
- **Your price is your own.** A self-serve listing is outside the
  identical-price rule above. A buyer pays the price of the provider that
  served their request.

**Answers.**

- `?dry_run=1` (or `?dry_run=true`) checks a POST or PATCH and writes
  nothing. `?dry_run=0` and `?dry_run=false` write. Any other value, and any
  other query parameter, is a 400: a mistyped dry run never writes.
- A write answers 201 with `revision` (the first revision written), `head`,
  `revisions`, `diff` (each price SKU of the first revision: `increase`,
  `decrease` or `new`), `changes` (the other fields) and `superseded` (pending
  changes it replaces). It also says when the change starts: `effective_from`
  (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) and `timing` (`"next_mark"` or
  `"at_once"`). A dry run answers the same times.
- A write that changes nothing answers 200 with `"unchanged": true`. We write
  nothing, but the write counts toward your limit.
- The same `Idempotency-Key` with the same body gives the first answer again.
  With another body it is a 409. It covers the whole write, all its revisions.
  This is also true when the first write changed nothing, and while we freeze
  your listing: a retry of a write that we accepted always gets its first
  answer. While we freeze your listing, a used key with another body is a 423,
  not a 409. A refused write (any 4xx) does not use up its key, so you can send
  the fixed write with the same key.
- `GET` lists every pending change that will take effect. It also lists the
  20 latest pending changes that never will (`shadowed: true`); when there
  are more, `shadowed_omitted` counts the rest.

**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` | We read your prices from your price feed |
| 404 | `not_found` | We have 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 we freeze your listing: 423) |
| 413 | `payload_too_large` | The body is more than 256 KB |
| 422 | `manifest_refused` | A rule above, or our 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` | We froze your listing; writes wait until we unfreeze it |
| 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 in `errors` every problem that the checks found, with a `note`
that says this. We check in stages: first the fields you may change, then the
manifest rules, then the prices, then the notice and 10× rules. A stage runs
only when the stages before it pass. So after you fix the problems in a 422, a
new write can show more problems. Use `?dry_run=1` to check a write first.
An answer lists at most 100 errors; `errors_omitted` counts the rest.

**Limits.** At most 30 writes an hour per key. These answers count as a
write:

- 201: we accepted it;
- 200 with `"unchanged": true`: it changes nothing;
- 403 `prices_from_feed`, 409 `conflict` and 422 (`manifest_refused`,
  `too_many_pending`): we refused it.

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.** Instead of writing prices, you can publish them. We set the
feed up for you in our listing (`endpoint.price_feed`). Then we read your
price list on a schedule, every hour by default. The feed must use https. We
do not follow redirects. A read stops after 20 seconds or 4 MiB.

You can use one of two formats:

- `openai_models`: an OpenAI-style model list,
  `{"data": [{"id", "pricing": {"prompt", "completion", "input_cache_read",
  "input_cache_write", "internal_reasoning"}}]}`. Prices are USD per token, as
  decimal strings. We match a model by the id you serve it under. A `"0"` on a
  cache or reasoning price means "not billed separately": that price stays as
  it is. We pay you the price you publish, so a listing with this format has no
  `provider_pricing`.
- `routerplus`: `{"models": [{"id", "pricing", "provider_pricing"}]}`, in this
  document's own price form and with our model ids. No `provider_pricing`
  means we pay you the buyer price. A model entry has only these three keys,
  and a price entry only `type`, `unit` and `cost_usd_per_million`. An entry
  with another key (a `currency`, say) is malformed.

We change prices only for the models we list. A model your feed leaves out
keeps its price. With `openai_models`, we change only the prices you publish.
With `routerplus`, an entry is the model's whole price list: a SKU it leaves
out is billed like its base (a cached prompt like the prompt, cache write like
the prompt, reasoning like the completion). If one entry for our models is
malformed, we ignore the whole read. A file that starts with a UTF-8 byte
order mark is read as usual.

- A **decrease starts at the next 5-minute mark**, at least 2 minutes after
  the read.
- An **increase starts at the first 5-minute mark after the notice period plus
  5 minutes** from the read that first saw it. Until then, the old price stays
  on the SKUs that rise.
- A later read does not move an increase that is already scheduled. A read
  with the same prices writes nothing. A decrease on another SKU or model
  starts at the next mark and does not postpone the increase.
- If your price keeps going up, we keep at most two scheduled increases per
  price: the first one that we announced, and the newest one. A read with a
  higher price replaces the increases between them: the earlier price stays
  until the newest increase starts. So a higher price only ever starts later,
  and a price that rises on every read never blocks your other changes.
  We replace only increases that your feed scheduled. An increase that you or
  our operators scheduled is never replaced, also when a feed read wrote it
  again (for example, to carry a decrease of another model). An increase that
  your feed scheduled stays the feed's when you change another field of it
  with a PATCH.
- If a later read goes back to the price in force, we cancel the increase that
  an earlier read scheduled: we write the prices in force again, from the next
  mark. So a price list that goes up and down leaves at most one scheduled
  increase.
- Your other scheduled changes stay. For example, a model that you scheduled
  to turn off is still turned off at its time.
- A lower price that our operators scheduled stays until your increase can
  start (the notice period plus 5 minutes).
- We refuse a change of more than 10x against the price in force, a price
  of zero (unless we allow free prices for your listing), or a price below the
  floor (one tenth of our listing's price). Our operators then look at it.
  The 10x rule is against the price in force only, and only for a price that
  the read changes. So if a read scheduled a wrong increase, a later read with
  the right price corrects it, also when the right price is more than 10x
  below the wrong one. And if our operators scheduled a change of more than
  10x and your feed publishes that same price, the read is not refused, also
  when that change starts after your increase could: the price starts at our
  operators' time.
- With `openai_models`, we also refuse a read while a revision in force or
  pending pays you your own cost (`provider_pricing`, from before the feed was
  set up). Our operators must clear that cost first; we never drop it without
  a decision.
- While we freeze your listing, we do not read the feed. A freeze during a
  read stops its write.

While a feed is on, your own writes cannot change prices (403
`prices_from_feed`). You can still change the other fields at any time, also
while a fed increase is pending: a PATCH keeps the increase at its time. A POST
that replaces a pending fed increase changes prices, so it is a 403.

Gateways read your revisions at least every 30 seconds, also the ones that
start later. A change starts at a mark at least 2 minutes after the write, so
every gateway has read it before it starts, and all of them switch at the mark.
`/api/models.json` lists it as scheduled within about 2.5 minutes. The public
pages show only the price in force, and switch at the mark too. Every request
is billed at the price in force when the request
arrived at the gateway. A request that arrives just before a change is billed
at the price before it, also when the gateway plans it after the change. A stop
that applies at once (a model turned off, a limit lowered) reaches every
gateway within about 30 seconds.

While we freeze your listing, buyers pay the price in our listing, and we
pay you the cost in our listing. A freeze holds back only your prices. It
keeps your model states: a model that you turned off stays off, and a limit
that you lowered stays lowered. Your key and your price feed cannot write
while it lasts. A freeze, and an unfreeze, reach every gateway within about 30
seconds; they are not held to a mark. When we unfreeze it, your revision in
force applies again, and so does each pending change that starts within the
notice period. If one
of them is more than our listing's price, our operators must confirm it
before the unfreeze is written. This includes a model that is off in one of
them: after the unfreeze, you can turn it on at the price that buyers last paid
while it was on, with no notice. If that price is more than our listing's
price, our operators confirm it at the unfreeze.

If we turn self-serve off for your listing, the gateways ignore all your
revisions: your prices, and also your model states. A model that you turned
off is on again at our listing's limits, until self-serve is on again. We
also freeze the listing. If we remove your listing, or its file in our
repository is not valid, we freeze it too. When self-serve is on again, we
unfreeze it with the same check.

### Capacity

Entries are `{type, unit, per, value}` — `request`, `prompt` or `completion`
limits per `minute`/`hour`/`day` window (`unit: "token"` is required for the two
token types). Identity is type + window; duplicates are rejected at validation.
Absent capacity means undeclared, not zero.

Capacity is information for us, not a setting. Validation checks it, but routing
and admission do not read it. The limits that admission enforces for your
deployment are a house pool that our operators set by hand from what you
declare and what you tell us (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

`supported_parameters` may be declared per model, and it is **enforced on the
image route**: a value outside a declared enum is a 400 naming 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 **every parameter it strips is recorded per-request** (visible
to the buyer in the `x-tm-dropped-params` header and their audit endpoint).
Declaring accurately now means chat enforcement picks it up when it lands.

### Data policy

`prompt_logging` is `"none"` or `"retained"`, published verbatim on your model
pages next to your privacy policy and terms URLs. Say the true thing — buyers
filter on it. The marketplace's handling of request data is described separately
in the [Data policy](/docs/data-policy).

### Lifecycle

- **`is_ready: false`** stages a model: validated, conformance-testable, listed
  nowhere, routed never. Flip it when you are ready — a model proves itself
  before going live, not after.
- **`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
  yourself, 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): past it, the model leaves the catalog
  automatically.
- **`released`** (ISO date): the day the model came out. The Models page and the
  playground list models newest first after a few featured ones, so set it when you add
  a model: the lab's release date, or the day the model first listed publicly.
- Date-pinned requests (`acme-large-1-20260901`) resolve to your listed model
  for routing and billing, and the buyer's original id reaches your API
  verbatim — you decide whether to serve the snapshot.

## The conformance bar

Our bar is **published** — seven checks, run against every model on both the
stream and non-stream paths, at listing time and on every catalog change. We
share the report with you.

- **C1** — a non-stream completion returns provider-reported usage tokens.
  Usage is the billable record; no usage, no listing.
- **C2** — streamed responses are valid SSE, every frame parseable.
- **C3** — streams end with their dialect's terminal marker (`[DONE]` /
  `message_stop`).
- **C4** — usage tokens are reported in-stream.
- **C5** — an unknown model returns a parseable 4xx JSON error, not a 200 and
  not HTML.
- **C6** — a billed response actually contains assistant content. Usage
  counters without an answer is the one failure we will never pass.
- **C7** — the response reports the model that was asked for. Resolving an
  undated id to your own dated snapshot is fine; any other equivalence must be
  declared in `resolves_to`. Silently substituting a different model is
  disqualifying.

An **image** listing (`output_modality: "image"`) is non-stream only, so C2–C4 do not
apply; it runs C5 plus three image checks, each of which buys one real render at the
model's top declared quality:

- **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 is a house loss 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 parsing to
  one JSON object with a labelled answer under each task name, and `prompt_tokens` in the
  usage. 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`), and 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 yields 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

- HTTPS endpoint speaking OpenAI Chat Completions **or** Anthropic Messages,
  with streaming. An OpenAI-dialect endpoint that answers in one JSON body even when
  asked to stream declares `streaming: false` on its text models: we ask it for JSON and
  stream the finished answer to buyers ourselves, so the buyer's first byte waits for the
  whole answer.
- Provider-reported usage tokens on both paths (C1/C4).
- Honest errors: return early 429s at capacity — queueing wrecks your published
  latency and your buyers' experience; 4xx JSON for bad requests.
- **Streaming:** response headers within 20 seconds or the attempt fails over;
  send SSE keep-alive comments through long pre-first-byte gaps, and stream
  tokens as soon as they exist. Non-stream requests are exempt — they get a
  long generation budget instead.
- After first output we never switch providers mid-answer — a stall is
  surfaced to the buyer as yours. Stream steadily.

## How uptime is counted

Exact accounting, same numbers routing uses:

```text
uptime = completed ÷ (completed + counted failures), trailing 24h
```

- **Counted against you:** provider 401/402/403, all 5xx, mid-stream deaths,
  incomplete streams.
- **Never counted:** buyer-caused requests — 400 and 413 (bad input), 429
  (their rate limit), and client cancellations mid-stream.
- **Published only after 100+ requests.** Below that the pages show a dash (–) —
  we never print a fake 100%, yours or anyone's.
- Failures open a per-deployment **health circuit**: cooldown, then exactly one
  half-open probe; recovery closes it. While everything for a model cools down
  we still send a last-resort attempt rather than strand buyers. Content-policy
  refusals are never retried and never failed over.

## Performance

We publish **TTFB p50/p95** (from merged t-digests) per model — the same
figures the router sees. Two behaviors dominate them: return early 429s instead
of queueing, and start streaming immediately.

## How you get paid

Pick a settlement mode in your manifest — both directions first-class:

- **`prepaid`** — we deposit with you ahead of traffic; usage draws it down,
  and we monitor balance against burn so routing never hits a dry account.
- **`arrears`** — we route first and pay on your interval: `half_daily`,
  `daily`, `weekly`, or `monthly`.

Your statement is generated from the **same append-only ledger that bills
buyers** — token counts and USD per period, auditable to the individual
attempt, with per-attempt byte counts recorded as 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 the day your models go live.
- A public model page per model: prices, context, your data-policy flag, live
  honest metrics.
- Per-period settlement statements that reconcile to the token.
- Degraded providers get circuits and time to recover, not silent delisting.

## Apply

Email **providers@evo-hq.com** with your manifest attached (validate it against
[the schema](/docs/provider-manifest.schema.json) first). We run the suite
against your endpoint, share the report, stage your models, and flip them live
together.
