# Routing policies and Azure connections

A routing policy is a saved rule set for a key. It says which of your connections and which
marketplace deployments the key may use, in what order, and who pays. You can bind a policy to
an org, a workspace or a key.

## Create and bind a policy

Policy management uses the site origin, with the same session cookie, JSON content type and
same-origin write protection as [BYOK connection management](/docs/byok). Inference keys cannot
manage policies. Viewers can read them.

`POST /api/routing-policies` creates a policy revision. A revision never changes.

```json
{
  "models": ["your-exact-model-id"],
  "connections": ["FIRST_CONNECTION_UUID", "SECOND_CONNECTION_UUID"],
  "funding": "byok_only",
  "allow_fallbacks": true,
  "max_attempts": 4,
  "timeout_ms": 450000
}
```

The response is `201` with `{"policy_id":"UUID"}`. The connections must belong to your org.

| Request | Effect |
|---|---|
| `POST /api/routing-policies/{id}/bind` with `{"key_id":"KEY_UUID"}` | Binds the policy to an existing key. |
| `POST /api/routing-policies/{id}/bind` with `{"scope":"org"}` | Binds the policy to the org. |
| `GET /api/routing-policies` | Lists the newest 100 revisions. |
| `GET /api/routing-policies/{id}` | Returns one revision. |

Binding rules:

- An org policy applies to every key in the org. A key policy can narrow it.
- A key with no policy and no single-connection binding of its own uses the org policy.
- An older single-connection binding stays as an extra restriction when an org policy exists.
- Binding a policy to a key replaces the key's single-connection binding. Binding a single
  connection replaces the key's policy.
- To change a policy, create a new revision and bind it. You cannot delete a policy or unbind a
  key back to marketplace-only routing.
- Cached marketplace-only decisions can take up to 5 seconds to see a new binding. Calls already
  dispatched can finish on their pinned revision.
- Workspace bindings use the [identity API](identity.md).

## Policy fields

| Field | Behavior |
|---|---|
| `models` | 1 to 100 exact IDs. Every policy that applies must allow the requested model. |
| `connections` | Up to 8 BYOK connection UUIDs that your org owns, in order. |
| `house_deployments` | Up to 8 marketplace deployment IDs. |
| `funding: "byok_only"` | The default. Needs connections. Forbids marketplace deployments. |
| `funding: "house_only"` | Needs marketplace deployments. Forbids connections. |
| `funding: "byok_with_house_fallback"` | Needs both. Marketplace deployments are always the last phase. |
| `allow_fallbacks` | Default `true`. `false` keeps only the first eligible route, before health filtering. |
| `max_attempts` | Integer 1 to 8. Default 4. |
| `timeout_ms` | Admission and generation deadline, 1,000 to 600,000 ms. Default 450,000. |
| `regions`, `zdr`, `data_collection` | Hard restrictions, checked against provider facts that RouterPlus operators record. A route with no recorded fact is refused. |

### Fallback

- Under a policy, automatic fallback stays within the first selected **provider label**.
- You can authorize and select both OpenAI and Azure. A failed OpenAI call does not switch to
  Azure on its own.
- RouterPlus does not decide that two models are the same. It rejects
  `attest_equivalence:true`.
- Azure deployment mappings are your declarations.
- When fallbacks are off, an unavailable first route does not authorize a fallback.
- No retry crosses the [streaming commit boundary](/docs/routing#the-commit-boundary) or a
  content-policy refusal.

### How restrictions combine

- The restrictions in force are the intersection of the org policy, workspace policy, key
  policy or binding, the server catalog and the request. An empty intersection refuses the
  request. Lower limits win.
- Order: request connection preference, then provider preference, then saved order, then
  deployment ID as a stable tie-breaker.
- With mixed funding, marketplace deployments come after every BYOK route.

### Deadline

`timeout_ms` covers admission and generation after planning, including active streams. Request
upload, policy and price reads, and the final billing settlement are separate phases. A timeout
never skips billing settlement. A reservation that arrives after the deadline is released, and
RouterPlus never dispatches it.

## Request controls

The inference `provider` object accepts:

```json
{
  "only": ["openai"],
  "ignore": [],
  "order": ["openai"],
  "connections": ["CONNECTION_UUID"],
  "connection_order": ["CONNECTION_UUID"],
  "funding": ["byok"],
  "allow_fallbacks": false,
  "require_parameters": true
}
```

`only`, `ignore` and `order` name route labels: marketplace `anthropic` and `openai`, and a BYOK
connection's profile (`openai`, `anthropic`, `azure`, `bedrock`). A route label keeps that
meaning even when its route does not serve the model. Any other label matches no route. It
selects, orders or skips nothing, and an `only` list with no route label leaves no route.

On `POST /v1/decisions` the labels are the decision providers:

- `typesafe` for Jev
- `bespokelabs` for the Bespoke Nimble models
- `workers-ai` for Clef and Clef-flash
- `routerplus` for Decider 2B and Kev 4B
- `perplexity-decisions` for Perplexity Decider v1 27B
- `openai-decisions` for GPT-6 Luna
Decisions
- `levanto` for Sage
- `fastino` for GLiNER-2.5-Decide

Clef's label is `workers-ai`, not `cloudflare`: on `POST /v1/decisions`, `cloudflare` names no
provider. Perplexity Decider's label is `perplexity-decisions`, not `perplexity`. GPT-6 Luna
Decisions' label is `openai-decisions`, not `openai`, the label of the RouterPlus OpenAI chat
route.

Other controls:

- `require_parameters: true` refuses a deployment that would drop one of your request fields.
  Without it, RouterPlus calls the deployment without that field.
- `regions`, `zdr` and `data_collection` work here too, as hard restrictions.
- `sort` (`price`, `throughput`, `latency` or `quality`) and `max_price` apply to marketplace
  routes only. See [Provider routing](/docs/provider-routing). The
  [Agent integration guide](agent-integration.md) covers how to keep a session's prompt cache
  warm.
- Raw request keys and arbitrary destinations return 400.

Request preferences never grant connections, models, providers or funds that the saved policy
does not allow. You cannot write the provider facts used for region and privacy restrictions. A
restriction is not a residency or ZDR guarantee.

### Explain a route

`POST /v1/route` takes `{"model":"ID","provider":{...}}` with the inference key. It returns,
with `advisory: true`:

- the plan, catalog and policy IDs;
- the candidates in order, with their funding and pinned marketplace price IDs;
- the excluded routes, each with a reason;
- the time and source of the health data.

It makes no provider request, decryption or reservation. Health and authorization can change
before the real dispatch.

Related endpoints:

- `GET /v1/models` applies the same restrictions. An optional URL-encoded JSON `provider` query
  parameter narrows the list. `output_modalities=text,image,decisions` filters by kind.
- Token counting uses the same plan but needs an Anthropic route. It sends one count request
  and does not fall back.
- `GET /v1/generation?id=...` shows each attempt's `route_context` and fallback cause.
- Policy requests return `x-tm-route-plan-id` for correlation.

### Funding

- BYOK needs no marketplace credit.
- Marketplace fallback reserves credit only when the request reaches it, and charges under the
  standard pricing rules.
- Earlier marketplace holds stay until the request finishes, so fallback admission is
  conservative while settlements are pending.
- Each attempt pins its own funding and credential.
- RouterPlus does not know what a provider charges you on BYOK. Marketplace prices come from the
  model price list and are fixed before dispatch.

## Azure/Foundry API-key connections

Create a connection with `POST /api/connections`. In the console, open Access > Provider
connections.

```json
{
  "profile": "azure",
  "models": ["your-public-model-id"],
  "api_key": "YOUR_RESOURCE_API_KEY",
  "endpoint_config": {
    "resource": "your-resource-name",
    "host": "foundry",
    "api_version": "v1",
    "deployments": {"your-public-model-id": "your-deployment-name"}
  }
}
```

- `host` is `openai` (`{resource}.openai.azure.com`) or `foundry`
  (`{resource}.services.ai.azure.com`). RouterPlus builds the public hostname. You cannot send a
  URL.
- Every model you declare needs a deployment mapping.
- `endpoint_config` cannot change. A change needs a new connection.
- Validate the connection, then bind it to a key or include it in a policy. Rotation and
  revocation follow the [connection lifecycle](/docs/byok).

RouterPlus calls `/openai/v1/chat/completions?api-version=v1`, with the deployment name in the
model field and the native `api-key` header. Validation calls the model-list endpoint and
generates nothing. These shapes follow Microsoft's [chat reference](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/chat),
[models reference](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/models)
and [v1 lifecycle guide](https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle).

| Supported | Not supported |
|---|---|
| API-key authentication, public v1 endpoints | Entra or workload identity, legacy dated APIs, preview versions, sovereign or private endpoints, Azure capacity or residency checks |

Validation confirms endpoint authentication only. It does not confirm deployment entitlement,
model equivalence, quota or capacity.

## Limits

- Shared admission needs declared [quota pools and gateway limits](/docs/admission).
- A full quota can skip a route that the policy allows. It never adds providers or marketplace
  funding.
- Model lists and route explanations do not reserve capacity.
- The attempt record stores at most 32 exclusion details, with `excludedTotal` for the full
  count. The catalog hash and the full route explanation keep the rest of the context.
