Admin Ai
GET /api/admin/ai-settings
Section titled “GET /api/admin/ai-settings”Get Platform Ai Settings
Current platform AI scalars. Null fields defer to the env defaults.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlatformAISettingsResponse |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/admin/ai-settings
Section titled “PATCH /api/admin/ai-settings”Update Platform Ai Settings
Update platform AI scalars.
Values land in platform_settings and are re-read per run, so a change
here reaches running workers within their catalog/settings TTL — no deploy.
The default model is checked against the catalog before it is stored: it is what every org without a default of its own runs on, so a typo’d or retired id here would silently break all of them at the next turn instead of here, now, with a list of the ids that would have worked.
Feature models are checked the same way and against a harder deadline: an uncatalogued id there is not a broken call, it is a working one that bills nothing while the platform pays the provider, and nothing downstream would report it.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| compaction_enabled | boolean | no | |
| compaction_threshold | number | no | |
| default_model | PlatformDefaultModelInput | no | |
| default_reasoning_effort | string | no | |
| feature_models | object | no | |
| max_reasoning_effort | string | no | |
| mode_high_effort | ModePresetInput | no | |
| mode_normal | ModePresetInput | no | |
| mode_trivial | ModePresetInput | no | |
| thinking_enabled | boolean | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlatformAISettingsResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/ai/route-keys
Section titled “GET /api/admin/ai/route-keys”Get Platform Route Keys
Which key each inference route would spend, masked, with its provenance.
Note is_set and source can disagree: a stored route key that no
longer decrypts reads as set and falls through to legacy/env,
which is exactly the state a rotated ENCRYPTION_KEY produces.
Its own path rather than a field on GET /ai/routes — the route list is
not a secret-adjacent read, and keeping them apart means a client that only
renders routes never asks for key state at all.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlatformRouteKeysResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/admin/ai/route-keys/{route_id}
Section titled “PUT /api/admin/ai/route-keys/{route_id}”Set Platform Route Key
Store or clear one route’s platform key.
Stored encrypted, in a row created on first write — which is what makes this surface open-ended: a route created in the admin panel has a working key slot immediately, with no code change, no def and no migration. Clearing falls back down the ladder (legacy provider key, then the env var) rather than to nothing.
Refused for a route that spends another route’s key: the whole point of
credential_route_id is that the secret is stored once, and accepting a
write here would create the second copy it exists to prevent. The error names
the route to edit instead.
The cache is dropped after the commit, so a rotation reaches the API and every agent-runner immediately instead of within the cache TTL.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| api_key | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | PlatformRouteKeysResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/ai/routes
Section titled “GET /api/admin/ai/routes”List Inference Routes
Every route, enabled or not, in display order.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | InferenceRouteResponse[] |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/ai/routes
Section titled “POST /api/admin/ai/routes”Create Inference Route
Add a route. slug must be unique — it’s what catalog rows will store.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| base_url | string | no | |
| credential_route_id | string (uuid) | no | |
| dialect | "anthropic" | "openai" | "voyage" | yes | |
| enabled | boolean | no | |
| gateway_provider_slug | string | no | |
| label | string | yes | |
| slug | string | yes | |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | InferenceRouteResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/ai/routes/{route_id}
Section titled “GET /api/admin/ai/routes/{route_id}”Get Inference Route
One route by id.
Exists so a detail view gets a real 404 for a deleted or mistyped id, rather than deriving “not found” from a successful list response — which reports the same condition as an empty filter, and cannot distinguish “this route is gone” from “the list call failed and returned nothing”.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | InferenceRouteResponse |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/admin/ai/routes/{route_id}
Section titled “PATCH /api/admin/ai/routes/{route_id}”Update Inference Route
Update a route.
Switching enabled off is refused with 409 while anything is still
pinned to it, naming what — there is no lazy retirement for routes, so a
disabled route is a hard fail at dispatch rather than a redirect, and the
blast radius of getting it wrong is a mode preset taking down every
trivial-effort surface at once. POST /ai/routes/{id}/repoint is what
makes the refusal satisfiable; GET /ai/routes/{id}/pins is the same
answer without attempting the write.
Which half of the enabled asymmetry this extends. Route enabled
gates dispatch through a route and deliberately not credential
resolution: resolve_credential_route walks through disabled rows so
that switching anthropic-direct off to push everyone onto
anthropic-cf does not also strip the gateway route of the key it spends.
This guard is on the dispatch side — it counts what would stop
dispatching, and asks nothing about who spends whose key. The credential
side already has its own, separate refusal: _ensure_usable_credential_route
rejects a disabled route as somebody’s credential_route_id, and the
delete path refuses while dependants exist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| base_url | string | no | |
| credential_route_id | string (uuid) | no | |
| dialect | "anthropic" | "openai" | "voyage" | no | |
| enabled | boolean | no | |
| gateway_provider_slug | string | no | |
| label | string | no | |
| sort_order | integer | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | InferenceRouteResponse |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/admin/ai/routes/{route_id}
Section titled “DELETE /api/admin/ai/routes/{route_id}”Delete Inference Route
Remove a route.
Refused with 409 while another route spends this one’s key. Not a cascade
and not a null-out: promoting anthropic-cf to “spends its own key”
because anthropic-direct was deleted would send requests authenticated
with a key nobody ever configured, and the first anyone hears of it is a
provider 401 on live traffic. Repoint the dependants first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | Successful Response | |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/ai/routes/{route_id}/pins
Section titled “GET /api/admin/ai/routes/{route_id}/pins”Get Inference Route Pins
What is pinned to this route, without attempting a write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | RoutePinsResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/ai/routes/{route_id}/repoint
Section titled “POST /api/admin/ai/routes/{route_id}/repoint”Repoint Inference Route
Move everything pinned to this route onto another one, atomically.
The bulk repoint, and the only supported way a model changes route. All five stores move in one transaction — catalog rows, agents, prompt templates, mode presets, feature-model settings — because a partial repoint is worse than none: a mode preset left naming a route that is then disabled takes down titles, summaries, extraction and compaction simultaneously.
Afterwards the source route disables cleanly — unless an org has its own
offering on it. Those are moved by nobody but the org: they are the record
of where that tenant’s traffic goes and what it costs, so an admin tidying
routes must not relocate them as a side effect. GET …/pins reports them
separately and answers repoint_clears: false when they are there, which
is the case where running this action first would be wasted.
Refusals, all 422 except the missing target:
- the target does not exist (404) or is the source itself;
- the target is disabled — repointing onto a route nothing may dispatch through just relocates the outage;
- the target’s dialect cannot serve some row’s
kind. Checked per row through_ensure_dialect_matches_kind, the same predicatecreateand the per-row repoint go through, because “a row lands on a route” has to have exactly one choke point — a chat model onvoyage-directwould otherwise be reachable in bulk while being refused one at a time.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| route_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| target_route_id | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | RepointRouteResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/models
Section titled “GET /api/admin/models”List Catalog Models
Every catalog row, enabled or not, in display order.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ModelCatalogEntryResponse[] |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/models
Section titled “POST /api/admin/models”Create Catalog Model
Add a model on a route. (key, route) must be unique.
A key may appear on two routes — that is the point of the pair — but only one of them may be enabled at a time, so the conflict below covers both the same-pair collision and the second-enabled-row one.
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| allowed_efforts | string[] | no | |
| cache_write_price_per_1m | number | string | no | |
| cached_input_price_per_1m | number | string | no | |
| context_window | integer | no | |
| cost_tier | string | no | |
| credit_multiplier | number | string | no | |
| enabled | boolean | no | |
| fallback_credits_per_1k_tokens | number | string | no | |
| input_price_per_1m | number | string | no | |
| key | string | yes | |
| kind | string | no | |
| label | string | yes | |
| max_output_tokens | integer | no | |
| output_price_per_1m | number | string | no | |
| provider | string | yes | |
| remote_model_id | string | no | |
| replacement_model_id | string (uuid) | no | |
| route_id | string (uuid) | yes | |
| sort_order | integer | no | |
| supports_reasoning | boolean | no | |
| supports_vision | boolean | no |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | ModelCatalogEntryResponse |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/admin/models/{entry_id}
Section titled “PATCH /api/admin/models/{entry_id}”Update Catalog Model
Update a model. Switching enabled off takes effect platform-wide.
Runs already pinned to a disabled model are not rewritten: they resolve to
replacement_model_id (or the platform default) lazily at dispatch.
Renaming key is refused with 409 while any live config still names
the old one, listing what to repoint first — a rename is not a relabel, it
changes the id sent on the wire and every consumer stores that id by value.
Nothing cascades and no historical row is touched. To follow a vendor’s own
id change, add a second row and point the old one’s replacement_model_id
at it; that is what lazy retirement is for.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| entry_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| allowed_efforts | string[] | no | |
| cache_write_price_per_1m | number | string | no | |
| cached_input_price_per_1m | number | string | no | |
| context_window | integer | no | |
| cost_tier | string | no | |
| credit_multiplier | number | string | no | |
| deprecated | boolean | no | |
| enabled | boolean | no | |
| fallback_credits_per_1k_tokens | number | string | no | |
| input_price_per_1m | number | string | no | |
| key | string | no | |
| kind | string | no | |
| label | string | no | |
| max_output_tokens | integer | no | |
| output_price_per_1m | number | string | no | |
| provider | string | no | |
| remote_model_id | string | no | |
| replacement_model_id | string (uuid) | no | |
| route_id | string (uuid) | no | |
| sort_order | integer | no | |
| supports_reasoning | boolean | no | |
| supports_vision | boolean | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ModelCatalogEntryResponse |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/admin/models/{entry_id}
Section titled “DELETE /api/admin/models/{entry_id}”Delete Catalog Model
Remove a model from the catalog.
Prefer disabling: a deleted row loses its replacement pointer, so anything still pinned to the key falls all the way through to the platform default.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| entry_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | Successful Response | |
| 422 | Validation Error | HTTPValidationError |
POST /api/admin/models/{entry_id}/sync-price
Section titled “POST /api/admin/models/{entry_id}/sync-price”Sync Catalog Model Price
Re-price one model from the community LiteLLM dataset.
A convenience for identifying prices, not a runtime dependency: runs bill from the catalog, and this is one way to fill it. Deliberate per-row action, so it does overwrite what’s there — the boot-time sweep is the one that only fills blanks, precisely so a hand-typed price survives it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| entry_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ModelCatalogEntryResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/admin/orgs/{org_id}/ai-settings
Section titled “GET /api/admin/orgs/{org_id}/ai-settings”Get Org Ai Settings
One org’s runtime overrides, plus the platform values behind its nulls.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | OrgRuntimeSettingsResponse |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/admin/orgs/{org_id}/ai-settings
Section titled “PATCH /api/admin/orgs/{org_id}/ai-settings”Update Org Ai Settings
Set (or clear) one org’s runtime overrides on its behalf.
Writes the same rows the org’s own settings page writes, so support flipping a single org and that org flipping itself are the same operation — and the platform tier stays where everyone else reads it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| compaction_enabled | boolean | no | |
| compaction_threshold | number | no | |
| thinking_enabled | boolean | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | OrgRuntimeSettingsResponse |
| 422 | Validation Error | HTTPValidationError |