> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Route Updates

> PUT /api/v1/routes/{id} changes a route's enabled flag, its balancing strategy and its target list with weights in one transaction; a field you leave out keeps its stored value.

`PUT /api/v1/routes/{id}` edits a route that already exists. It changes any of
three things: whether the route is served, its balancing strategy, and its
targets with their weights. The route's public name, parameters and advanced
settings have endpoints of their own (`/params`, `/advanced`, `/complexity`).

Every field is optional. A field you leave out, or send as `null`, keeps the
value the route already has, so a client that only sends `enabled` never resets
the strategy or the targets. An empty body changes nothing and returns the
route as it is.

| Field      | Type    | Meaning                                                                                            |
| ---------- | ------- | -------------------------------------------------------------------------------------------------- |
| `enabled`  | boolean | `false` takes the route out of service without deleting it                                         |
| `strategy` | string  | one of the strategies route creation accepts, listed in [Load balancing](/concepts/load-balancing) |
| `targets`  | array   | the route's complete target list; see below                                                        |

Each entry of `targets` has the same shape as the body of
`POST /api/v1/routes/{route_id}/targets`:

| Field            | Type    | Meaning                                                                                                                                           |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider_id`    | uuid    | a provider of the route's organization (required)                                                                                                 |
| `upstream_model` | string  | the model name sent upstream; omitted, `null` or blank forwards the route's name                                                                  |
| `weight`         | integer | the target's share for strategies that read weights; at least 1, defaults to 1                                                                    |
| `takes_over`     | array   | the failure classes the target is a fallback for; omitted or empty for an ordinary target (see [Fallback targets](/reliability/fallback-targets)) |

An entry carries only these four fields. A target's `id` is not one of them,
and an entry that sends it is refused like any other unknown field.

## How a target list is applied

`targets` is the whole list, not a patch. Rolter matches each entry to a stored
target by **provider and upstream model**, the same key a `rolter.toml` import
matches on:

- an entry that matches a stored target keeps that target, with its `id`, and
  takes the entry's weight and `takes_over`;
- a stored target that no entry matches is removed;
- an entry that matches no stored target is added.

So changing a weight edits the target in place, while changing a target's
provider or upstream model replaces it with a new one under a new `id`. Each
entry claims at most one stored target: list the same provider and upstream
model twice and the route keeps two targets for it.

The strategy, the enabled flag and the whole target list change in one
database transaction. Gateways pick the change up on their next config poll
and see the route either as it was or as it is now, never with half a target
list. A refused update changes nothing.

## Example

Move a route to weighted routing and send three quarters of its traffic to the
first provider:

```bash
curl -s -X PUT https://control.example.com/api/v1/routes/$ROUTE_ID \
  -H "Authorization: Bearer $ROLTER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "weighted",
    "targets": [
      {"provider_id": "'"$PRIMARY_ID"'", "weight": 3},
      {"provider_id": "'"$SECONDARY_ID"'", "upstream_model": "gpt-4o-mini", "weight": 1}
    ]
  }'
```

The response is the route row, as `GET /api/v1/projects/{project_id}/routes`
lists it. List the targets with `GET /api/v1/routes/{route_id}/targets`.

## Reading a project's routes with their targets

`GET /api/v1/projects/{project_id}/routes` lists the project's routes. A screen
that also shows each route's targets or complexity policy does not need one
request per route: `include` embeds them in the same response.

```bash
curl -s "https://control.example.com/api/v1/projects/$PROJECT_ID/routes?include=targets,complexity" \
  -H "Authorization: Bearer $ROLTER_TOKEN"
```

| `include` value | adds to each route                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| `targets`       | `targets`: the list `GET /api/v1/routes/{route_id}/targets` returns, empty when the route has none      |
| `variants`      | `variants`: the list `GET /api/v1/routes/{id}/variants` returns, empty when the route has none          |
| `complexity`    | `complexity`: the policy `GET /api/v1/routes/{id}/complexity` returns, `{"tiers": []}` when none is set |

Values are comma-separated and each is optional. Without `include` the list is
the route rows alone, as before; a value other than the three above is a `400`
with the `invalid_field` code. The embedded fields need the same `route:read`
permission as the list, and a route an access profile hides from the caller is
left out of the list together with its targets. The per-route endpoints keep
working.

## Weighted variants

A route defined through the API can carry the same weighted
[variants](/concepts/providers-and-routes#variants) as a `[[routes.variants]]`
entry in `rolter.toml`: named buckets with a traffic weight, their own ordered
targets and optional param defaults. Each request samples one variant by weight
and falls back through the others in the order they are listed. While a route
has variants, its own target list is not used.

`GET /api/v1/routes/{id}/variants` lists them in that order, each with its
targets in routing order. `PUT /api/v1/routes/{id}/variants` replaces the whole
set; send an empty list to remove the variants and put the route back on its
own targets.

```bash
curl -s -X PUT "https://control.example.com/api/v1/routes/$ROUTE_ID/variants" \
  -H "Authorization: Bearer $ROLTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "variants": [
      {"name": "control", "weight": 9,
       "targets": [{"provider_id": "'"$OPENAI_ID"'", "upstream_model": "gpt-4o"}]},
      {"name": "canary", "weight": 1, "params": {"temperature": 0.7},
       "targets": [
         {"provider_id": "'"$ANTHROPIC_ID"'", "upstream_model": "claude-sonnet-4-20250514"},
         {"provider_id": "'"$OPENAI_ID"'", "upstream_model": "gpt-4o"}
       ]}
    ]
  }'
```

| field                      | meaning                                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `name`                     | unique in the route, not blank; request logs and metrics carry it in their `variant` field                              |
| `weight`                   | the variant's share of traffic, `1` or more (default `1`)                                                               |
| `params`                   | default inference params for this variant, layered over the route's own; omitted is none                                |
| `targets`                  | 1 to 32 targets in the order they are tried                                                                             |
| `targets[].provider_id`    | a provider of the route's organization that the route's project may use                                                 |
| `targets[].upstream_model` | the model name sent upstream; omitted, `null` or blank forwards the route's public name                                 |
| `targets[].weight`         | `0` or more (default `1`); at least one target of each variant needs a weight above `0`                                 |
| `targets[].takes_over`     | the failures the target is a [fallback](/reliability/fallback-targets) for; omitted or `[]` makes it an ordinary target |

A route takes at most 32 variants. A variant weight of `0` is refused for now:
it would read as "fallback only", but the gateway samples a `0` as if it were
`1`, so such a variant still takes a share of first attempts
([#3341](https://github.com/rolter-ai/rolter/issues/3341)). A `rolter.toml`
variant with weight `0` still imports and keeps that sampled-as-`1` meaning;
the dashboard's split preview shows the share it actually gets, and asks for
`1` or more before it saves the route's variants.

The response is the stored list. A set equal to the stored one changes nothing
and is not audited; any other is recorded as `route.set_variants` with each
variant's name, weight and target count, and reaches the gateways with the next
snapshot. A provider that a variant still targets cannot be deleted, the same as
one a route target names. `rolter-seed --import` stores a route's
`[[routes.variants]]` and `rolter config export` writes them back.

## Disabling complexity routing

`DELETE /api/v1/routes/{id}/complexity` removes the route's complexity policy
and returns the updated route row. It needs `route:update`, the same permission
as configuring tiers. No request body is needed.

Only `_rolter_complexity` is removed from `params`. Other inference parameters,
parameter locks, targets and the route itself stay unchanged. After the next
snapshot reload, requests use the original route and its balancing strategy
instead of being redirected to a tier. Calling it again is safe.

In **Complexity Router**, choose **Disable policy** on the route and confirm.
The dialog names the route and explains the rollback; a refused request leaves
it open with the error. Reconfigure a policy through **No policy yet** to enable
tiering again. The audit action is `route.set_complexity` with `complexity: null`.

## Models and their targets

`GET /api/v1/models` returns one entry per public model name the gateway
serves. Each entry carries `route_targets`, the route's targets as
`{provider, model, weight}` where `provider` is the provider's name and `model`
is the upstream model (`null` forwards the public name), and `targets`, their
count. A route created through the API also carries `org_id` and `project_id`;
a route from `rolter.toml` has neither. Two organizations that serve the same
model name are two entries, told apart by `org_id`.

The list is scoped to the caller. A member sees the routes of the organizations
they hold a role in, plus the routes of a single project they hold a role in;
another organization's models, targets, `org_id` and `project_id` are not
listed. A superadmin, the admin token and open mode see every route, and so does
anyone for routes from `rolter.toml`, which belong to the deployment. A
deployment with one organization lists what it listed before.

## Who may call it, and what it refuses

Changing a route takes the **admin** role on its project (or on the project's
team or organization), the same as creating one or adding a target. Each target
entry is checked the way adding a single target is checked.

| status | when                                                                                                                                                                                                                                               |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | saved                                                                                                                                                                                                                                              |
| `400`  | an unknown `strategy`, an empty `targets` list, a `weight` below 1, an unknown or repeated `takes_over` class, a list whose every entry sets `takes_over` (`all_targets_restricted`), or a field the endpoint does not take                        |
| `400`  | for variants: a variant weight below 1, a negative target weight, a blank or repeated name, no target, an unknown or repeated `takes_over` class, or a set that leaves the route only restricted targets (`all_targets_restricted`, on `variants`) |
| `401`  | no valid session or admin token                                                                                                                                                                                                                    |
| `403`  | the caller is not an admin of the route's project                                                                                                                                                                                                  |
| `404`  | no such route, or a `provider_id` that is not a provider of the route's organization                                                                                                                                                               |
| `409`  | `scope_mismatch`: a provider scoped to a different project                                                                                                                                                                                         |

A refused weight or an empty list answers with the `invalid_field` code and the
field's path, such as `targets[1].weight` or `variants[0].targets[1].weight`, so
a client can point at the entry. Too many variants names `variants`.
Variants take the same `route:update` permission as the rest of the route.
An empty list is refused because it would leave the route nothing to serve;
set `enabled` to `false` or delete the route instead. A list of only restricted
targets is refused for the same reason, on the `targets` field: such a route
could never dial anything. `POST /api/v1/routes/{route_id}/targets` refuses a
restricted target that would be the route's only kind, on `takes_over`, and
`DELETE /api/v1/route-targets/{id}` answers `409` rather than remove the last
ordinary target of a route that has restricted ones.

The rule counts the route's own targets and every variant's together, the way
the gateway validates a route. A route whose variants have an ordinary target
may keep only restricted targets of its own, and a variant set that would leave
the route with only restricted targets, its own and the variants' together, is
refused on `variants`, an empty set included.

## Audit log

An update that sends only `enabled` is recorded as `route.set_enabled`, as it
always was. Any other update is recorded as `route.update`, with the new and
previous strategy when it changed, and the targets that were added, reweighted
(with the previous weight and `takes_over`) and removed.
