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