Admin REST API¶
Continuum Router mounts its administrative API under /admin when the binary is built with the admin feature. The default feature set includes it.
The Admin API is intended for a trusted management network. Authentication is not enabled by default. Configure admin.auth before exposing these routes beyond localhost.
Authentication¶
Bearer token¶
bearer_token must be at least 16 characters. allowed_ips is optional and accepts individual IPv4/IPv6 addresses or CIDR ranges.
HTTP Basic¶
IP allowlist¶
allowed_ips can also be combined with method: bearer or method: basic. IP checks are bypassed for Unix-socket connections because they have no peer IP address.
API key¶
# Authorization: Bearer and X-API-Key are both accepted
curl -H "Authorization: Bearer $ADMIN_API_KEY" http://127.0.0.1:8080/admin/health
curl -H "X-API-Key: $ADMIN_API_KEY" http://127.0.0.1:8080/admin/health
method: api_key validates the presented key against the same API key store used by ordinary /v1 authentication and Admin key CRUD (/admin/api-keys). The key must be enabled, unexpired, and carry the required_scope (default admin). Because the store is shared, disabling, rotating, expiring, or deleting a key stops it from authenticating against the Admin API immediately, with no restart. Missing or invalid keys return 401; a valid key without the required scope, or a request from a non-whitelisted IP, returns 403. allowed_ips can be combined with method: api_key for layered security.
Authentication configuration is captured while the Admin router is built. Restart the router after changing admin.auth itself (for example switching method or required_scope); individual key changes take effect immediately through the shared store.
Audit logging¶
admin:
audit:
enabled: true
log_level: info
include_headers: false
include_body: false
excluded_headers:
- authorization
- x-api-key
- cookie
Audit logging is enabled by default. Avoid request-body logging where requests may contain secrets or user data.
Endpoint inventory¶
All paths below are relative to the router origin.
Capabilities and health¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/capabilities |
Compile-time capabilities and runtime-enabled subsystems |
GET |
/admin/health |
Router service and backend health summary |
GET |
/admin/models |
Aggregated model catalog under Admin authentication |
POST |
/admin/models/refresh |
Refresh the aggregated model catalog |
Backend management¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/backends |
List backends and health information |
POST |
/admin/backends |
Create a backend |
GET |
/admin/backends/{name} |
Read one backend |
PUT |
/admin/backends/{name} |
Replace one backend |
DELETE |
/admin/backends/{name} |
Delete one backend |
PUT |
/admin/backends/{name}/weight |
Update backend weight |
PUT |
/admin/backends/{name}/models |
Update advertised models |
POST |
/admin/backends/{name}/models/discover |
Discover one backend's live model catalog without changing its selected models |
POST |
/admin/backends/{name}/check |
Run an on-demand connectivity check without mutating health state |
Backend mutations publish a new configuration snapshot through the configuration-update channel. Whether every field takes effect immediately depends on the subsystem; check /admin/config/hot-reload-status and restart for sections marked requires_restart.
POST /admin/backends/{name}/models/discover is read-only. It resolves exactly one configured backend by name, reuses the same backend-specific model fetcher used by normal aggregation, and returns that backend's live catalog before the configured backends[].models allowlist is applied. Use it when a management UI needs an available-model picker while keeping the selected routing allowlist unchanged.
curl -X POST http://127.0.0.1:8080/admin/backends/provider-openai/models/discover \
-H "Authorization: Bearer $ADMIN_TOKEN"
{
"backend": "provider-openai",
"object": "list",
"data": [
{
"id": "gpt-5.5",
"object": "model",
"created": 1760000000,
"owned_by": "openai"
}
]
}
The discovery response never writes configuration, invalidates the aggregated model cache, or changes backend health or circuit-breaker state. Codex OAuth backends use the loaded OAuth strategy and the account-plan filter from the normal Codex discovery path, including token refresh and chatgpt-account-id handling. Non-Codex OAuth backends, native Anthropic, and Bedrock return 501 with error.code: "model_discovery_unsupported" because the router has no provider-native catalog to query. Unknown backend names return 404 with error.code: "backend_not_found". Upstream failures are machine-readable as backend_authentication_failed, backend_discovery_timeout, backend_discovery_network_error, backend_discovery_parse_error, backend_discovery_http_error, or backend_discovery_response_too_large; response bodies do not echo access tokens, refresh tokens, token-store paths, or raw auth headers.
Circuit-breaker state¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/circuit/all |
List all circuit states |
GET |
/admin/circuit/{backend}/status |
Read a backend circuit state |
POST |
/admin/circuit/{backend}/open |
Force the circuit open |
POST |
/admin/circuit/{backend}/close |
Force the circuit closed |
POST |
/admin/circuit/{backend}/reset |
Reset the circuit |
These endpoints control the same circuit-breaker state machine that ordinary LLM proxy requests drive and consult, so they report and adjust the state used by normal proxy routing (for example, open forces a backend out of selection until it recovers).
Configuration queries¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/config |
Compact configuration summary |
GET |
/admin/config/full |
Full current configuration with sensitive values masked |
GET |
/admin/config/sections |
Supported Admin Config API sections and reload capability |
GET |
/admin/config/schema |
JSON schema for supported Admin Config API sections |
GET |
/admin/config/hot-reload-status |
Runtime hot-reload status and capability lists |
GET |
/admin/config/{section} |
One supported section with sensitive values masked |
GET /admin/config/full returns this envelope:
{
"config": {},
"hot_reload": {
"enabled": true,
"capabilities": {}
},
"metadata": {
"retrieved_at": "2026-07-19T00:00:00Z",
"version": 1
}
}
Optional sections that are not configured are omitted from config rather than present with an explicit null value. Reading one of these sections directly is unaffected: GET /admin/config/{section} for an unconfigured but recognized section (for example metrics or fallback) still answers 200 with "config": null, not 404.
The section API accepts these names:
serverbackendshealth_checksloggingretrytimeoutsrate_limitingcircuit_breakerglobal_promptsrequest_paramsadminfallbackmodel_aliasesfilesapi_keysmetricsrouting
This is a management subset, not a list of every top-level field accepted by the main configuration schema.
Configuration validation and mutation¶
| Method | Path | Purpose |
|---|---|---|
PUT |
/admin/config/{section} |
Replace a supported section |
PATCH |
/admin/config/{section} |
Recursively merge a partial section object |
POST |
/admin/config/validate |
Validate YAML, JSON, or TOML without applying it |
POST |
/admin/config/export |
Export configuration, masked by default |
POST |
/admin/config/import |
Validate and optionally publish a full configuration |
GET |
/admin/config/history |
Read up to 50 history entries with masked snapshots |
POST |
/admin/config/rollback/{version} |
Publish a configuration snapshot from history |
POST |
/admin/config/apply |
Atomically apply a full configuration candidate, or a no-op when none is supplied |
PUT and PATCH take the section value itself as the JSON body, not a { "config": ... } wrapper:
curl -X PATCH http://127.0.0.1:8080/admin/config/rate_limiting \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"enabled":true}'
A successful section mutation includes version, requires_restart, and hot_reload_capability. The API publishes an in-memory snapshot; it does not write the source YAML/TOML file.
Validation body:
{
"content": "server:\n bind_address: 127.0.0.1:8080\nbackends: []\n",
"format": "yaml",
"sections": []
}
With an empty sections array, content must be a complete configuration. For section-scoped validation, include the named top-level sections in content and list those names in sections.
Export body:
format is yaml, json, or toml. An empty sections array exports the full configuration. include_sensitive: true returns plaintext secrets and produces a security audit log entry.
format: "toml" now succeeds even when many optional sections are unset. Those sections are omitted from the serialized configuration rather than written as null, and TOML has no way to represent a null value: previously, any unset optional section made every TOML export fail with a 500 SERIALIZATION_ERROR.
Import body:
{
"content": "server:\n bind_address: 127.0.0.1:8080\nbackends: []\n",
"format": "yaml",
"apply": false,
"dry_run": true
}
Imports are limited to 1 MiB and a maximum structural nesting depth of 32. dry_run: true validates only. apply: true publishes the full candidate through the configuration-update channel and records a history version; restart-only sections still require a restart.
Masked secrets on the export to import round trip¶
Every masked value the Admin API returns, whatever the endpoint, carries the same fixed sentinel: ***CONTINUUM-MASKED:<hint>***. The hint discloses only a length: a value longer than four characters shows a two-character prefix plus the count (sk...(21 chars)), a value of four characters or fewer shows just the count ((4 chars)), and a masked boolean or number reads ***CONTINUUM-MASKED:hidden***. Earlier router builds returned the bare ***MASKED*** for any value that short, disclosing no length at all; after this change a short secret's length is visible where it previously was not, though its content never is. This is the same masking GET /admin/config/full, GET /admin/config/{section}, GET /admin/backends, and the GET /admin/config/history snapshots use, not a format specific to export.
Export masks every credential unless you pass include_sensitive: true, so the document you edit and send back normally carries these placeholders rather than secrets. Import treats each placeholder as "keep the value that is already live", splices the running secret back in, and reports every path it preserved:
{
"success": true,
"applied": true,
"version": 4,
"preserved_secret_paths": ["backends[0].api_key", "admin.auth.bearer_token"],
"validation": {
"valid": true,
"warnings": [
{
"section": "backends",
"path": "backends[0].api_key",
"message": "'backends[0].api_key' carried a masked placeholder, so the value currently in effect was kept. The submitted document did not change this secret.",
"code": "MASKED_SECRET_PRESERVED"
}
]
}
}
POST /admin/config/validate emits the same MASKED_SECRET_PRESERVED warnings, so the WebUI pre-save dry run shows what an import would leave untouched before anything is committed.
The same resolution runs on every endpoint that writes a configuration document, so what the dry run promises is what the save does: POST /admin/config/import, POST /admin/config/apply with a config candidate, and PUT/PATCH /admin/config/{section}. A section write that cannot resolve a placeholder is refused with success: false and the failing path in path/code.
A placeholder is only resolved when the running configuration still holds the value it was made from. Array entries are matched by id or name rather than by position, so reordering backends resolves correctly, while a rename does not. Entries that carry neither, such as the plain string lists guardrails.bypass_api_keys and rate_limiting.bypass_keys, have only their position to match on, so adding or removing one entry is refused rather than guessed: two keys from the same provider share a prefix and a length, and a shifted index would otherwise resolve to the neighbouring key. Edit those lists through a document exported with include_sensitive: true, or set the real values. When a placeholder cannot be matched, the whole import is refused and the failing paths are named, rather than a neighbouring secret being attached to the wrong field:
| Code | Meaning |
|---|---|
MASKED_SECRET_PRESERVED |
Warning. The live value at this path was kept; the submitted document did not change it. |
MASKED_SECRET_UNRESOLVED |
Error. The placeholder at this path matches nothing in the running configuration, usually because an entry or section was renamed, moved between documents, or removed, or because an entry was added to or removed from a list whose entries carry no id or name. Set the real value, or re-export and edit that document. |
LEGACY_MASK_PLACEHOLDER |
Error. The value has one of the placeholder shapes (***MASKED***, xy...(N chars), ${***VAR***}) emitted before the sentinel existed, typically because the document was exported before an upgrade. Those shapes are derived from the secret itself and cannot be told apart from a real credential, so they are never used to restore one. Re-export the current configuration and edit that document, or replace the flagged value with the real secret. |
MASKED_SECRET_REPORT_TRUNCATED |
Reported at most once, as a warning or an error depending on which list it summarises. The per-path lists are capped at 500 entries so a document full of placeholders cannot inflate the response; the entry states how many further paths were preserved or refused without being listed. |
A value that already equals the running one is left alone, whatever it looks like, so a configuration damaged by an older build (whose live credentials are themselves stale placeholder shapes) can still be edited section by section without retyping every secret first.
Values that are not placeholders are written through unchanged, so rotating a credential still works: replace the placeholder with the new secret and import as usual. An environment-variable reference round-trips as a reference: ${OPENAI_API_KEY} exports as ***CONTINUUM-MASKED:${OPENAI_API_KEY}*** and is restored to ${OPENAI_API_KEY}, never to the resolved value.
Apply body:
{
"hot_reload": true,
"description": "optional audit note",
"config": { "...": "a full configuration object" }
}
POST /admin/config/apply only does work when you supply a full config candidate. Section edits (PUT/PATCH), import, and rollback already publish immediately, so:
- No
config(ornull): explicit no-op. Nothing is published, no history version is recorded, and the response reportshot_reload_triggered: falsewith an emptyupdated_sections. Repeated calls never create empty history versions. configpresent,hot_reload: true: the candidate is validated, diffed against the running configuration, and, if it differs, published atomically through the configuration-update channel. Exactly one history version is recorded with the sections that changed.hot_reload_triggeredistrueonly after a successful publish;updated_sectionsandrequires_restartreflect the real diff.configpresent,hot_reload: false: preview only. The response reports theupdated_sections/requires_restartdiff, but nothing is published and no history version is recorded.
A candidate identical to the running configuration is treated as a no-op.
API keys¶
| Method | Path | Purpose |
|---|---|---|
GET, POST |
/admin/api-keys |
List or create keys |
GET, PUT, DELETE |
/admin/api-keys/{id} |
Read, update, or delete a key |
POST |
/admin/api-keys/{id}/rotate |
Rotate key material |
POST |
/admin/api-keys/{id}/enable |
Enable a key |
POST |
/admin/api-keys/{id}/disable |
Disable a key |
API-key material is returned only when created or rotated. Store it immediately.
Prompts and statistics¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/config/prompts |
List prompt files |
POST |
/admin/config/prompts/reload |
Reload prompt files |
GET, PUT |
/admin/config/prompts/{path} |
Read or update one prompt path |
GET |
/admin/stats |
Global statistics snapshot |
GET |
/admin/stats/series |
Global time series |
GET |
/admin/stats/models |
Per-model statistics |
GET |
/admin/stats/backends |
Per-backend statistics |
GET |
/admin/stats/api-keys |
API-key statistics |
GET |
/admin/stats/api-keys/{id} |
One key's statistics |
GET |
/admin/stats/api-keys/{id}/models |
One key's model statistics |
GET |
/admin/stats/api-keys/{id}/series |
One key's time series |
GET |
/admin/stats/users |
User statistics |
GET |
/admin/stats/users/{user_id} |
One user's statistics |
GET |
/admin/stats/users/{user_id}/models |
One user's model statistics |
GET |
/admin/stats/users/{user_id}/series |
One user's time series |
POST |
/admin/stats/reset |
Reset in-memory statistics |
GET |
/admin/metrics/history |
Persistent metric history; 404 when persistence is unavailable |
Routing, cache, and guardrail controls¶
| Method | Path | Purpose |
|---|---|---|
GET |
/admin/prefix-routing/stats |
Prefix-routing statistics |
GET |
/admin/response-cache/stats |
Response-cache statistics |
POST |
/admin/response-cache/invalidate |
Invalidate response-cache entries |
GET |
/admin/kv-index/stats |
KV index statistics |
GET |
/admin/kv-index/backends |
Per-backend KV index state |
POST |
/admin/kv-index/clear |
Clear the KV index |
GET |
/admin/gemini-context-cache/stats |
Gemini context-cache statistics |
POST |
/admin/gemini-context-cache/clear |
Clear Gemini context-cache state |
GET, PUT |
/admin/smart-routing/model-profiles |
List or replace model profiles |
GET |
/admin/smart-routing/model-profiles/{model} |
Read one model profile |
GET |
/admin/smart-routing/status |
Smart-routing status |
GET |
/admin/smart-routing/stats |
Smart-routing statistics |
POST |
/admin/smart-routing/classify |
Classify a request for inspection |
POST |
/admin/smart-routing/simulate |
Simulate routing |
GET, PUT |
/admin/smart-routing/policies |
Read or replace policies |
GET |
/admin/smart-routing/load-state |
Backend load state |
GET |
/admin/smart-routing/cache/stats |
Classifier-cache statistics |
POST |
/admin/smart-routing/cache/clear |
Clear the classifier cache |
GET, PATCH |
/admin/guardrails |
Read or patch guardrail policy |
PUT |
/admin/guardrails/providers/{name} |
Update a guardrail provider |
PUT, DELETE |
/admin/guardrails/routes/{route} |
Set or remove a route override |
POST |
/admin/guardrails/test |
Dry-run text through configured guardrails |
Files, ACP, and control plane¶
| Method | Path | Purpose |
|---|---|---|
GET, POST |
/admin/files |
List or upload files; 404 when Files is disabled |
GET, DELETE |
/admin/files/{file_id} |
Read metadata or delete a file |
GET |
/admin/files/{file_id}/content |
Download file content |
GET |
/admin/acp/status |
ACP service status |
GET |
/admin/acp/sessions |
Active ACP sessions |
GET |
/admin/acp/agent.json |
ACP agent descriptor |
GET |
/admin/control-plane/status |
Control-plane agent status; route exists only in a control-plane build |
Status codes¶
200: request processed; mutation responses can still contain"success": false, so inspect the body.400: malformed request or invalid input for handlers that use HTTP errors.401: missing or invalid Bearer/Basic credentials.403: IP not allowed or insufficient authorization.404: unknown resource, feature-disabled runtime service, or feature-gated route absent.413: upload exceeds the route body limit.500: internal failure or an authentication method missing required runtime state.