Skip to content

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

admin:
  auth:
    method: bearer
    bearer_token: "${ADMIN_TOKEN}"
    allowed_ips: []
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://127.0.0.1:8080/admin/health

bearer_token must be at least 16 characters. allowed_ips is optional and accepts individual IPv4/IPv6 addresses or CIDR ranges.

HTTP Basic

admin:
  auth:
    method: basic
    basic_auth:
      username: admin
      password: "${ADMIN_PASSWORD}"
curl -u "admin:$ADMIN_PASSWORD" http://127.0.0.1:8080/admin/health

IP allowlist

admin:
  auth:
    method: ip_whitelist
    allowed_ips:
      - 127.0.0.1
      - 10.0.0.0/8

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

admin:
  auth:
    method: api_key
    required_scope: admin
    allowed_ips: []
# 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:

  • server
  • backends
  • health_checks
  • logging
  • retry
  • timeouts
  • rate_limiting
  • circuit_breaker
  • global_prompts
  • request_params
  • admin
  • fallback
  • model_aliases
  • files
  • api_keys
  • metrics
  • routing

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": "yaml",
  "include_sensitive": false,
  "sections": []
}

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 (or null): explicit no-op. Nothing is published, no history version is recorded, and the response reports hot_reload_triggered: false with an empty updated_sections. Repeated calls never create empty history versions.
  • config present, 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_triggered is true only after a successful publish; updated_sections and requires_restart reflect the real diff.
  • config present, hot_reload: false: preview only. The response reports the updated_sections/requires_restart diff, 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.