Version: Current
Historical release snapshot. These docs are frozen. Read current docs.
HTTP API
acumen serves one listener, 127.0.0.1:4020. Nothing in the tree binds a
public address. Bodies are JSON, a request body is capped at 32 MiB, and a
vendor response is capped at 32 MiB.
Unknown chat fields are forwarded with the model swapped. Model names that are
not one of the six public names route as auto.
The full operation list, auth requirements, request/response schemas, streaming notes, headers, examples, and deprecations are generated from the OpenAPI contract in the API reference. This page keeps the routing and dialect semantics that the contract does not describe.
For a complete migration guide covering base URLs, key handling, model aliases, streaming, error handling, and failover, see Migrating a client to acumen.
Model doors
Section titled “Model doors”POST /v1/chat/completions
Section titled “POST /v1/chat/completions”OpenAI chat shape.
{"model":"cheap","messages":[{"role":"user","content":"ping"}]}Public model names: auto, cheap, coding, reason, vision, search.
The response includes the X-Acumen-Served-Model header (added 2026-11-04) naming
the vendor model that actually answered. See Served-Model transparency.
Streaming is stream: true; the response is text/event-stream once a provider
commits. Tools stay on the provider that accepted the first call unless that
provider’s circuit is open. A 429 or a first-byte timeout opens the circuit and
the same request continues on the next candidate. The same deployment is not
retried.
The circuit hold is the plan default: 30s for a free or subscription seat,
60s for a prepaid one, because nothing on the dispatch path parses the
vendor’s Retry-After. A vendor asking for a longer pause does not get one.
Empty content is a failure even on HTTP 200. If every candidate fails the status is 502.
POST /v1/messages
Section titled “POST /v1/messages”Anthropic message shape in, translated onto whichever provider wins the same decision path.
The response is OpenAI, never Anthropic. Non-streaming is the JSON
chat.completion object; stream: true is text/event-stream of
chat.completion.chunk frames. That holds even when the answering seat is
Anthropic: its body is converted on the way through and its SSE is converted
frame by frame.
Errors are the gateway’s {"error":…,"class":…}, not Anthropic’s
{"type":"error",…}. An Anthropic SDK expecting content[] blocks will not
understand this door’s replies.
POST /v1/responses
Section titled “POST /v1/responses”A verbatim passthrough to a seat whose wire kind is responses. The body is
forwarded as sent with only model replaced, so input, instructions,
tools, reasoning and prompt_cache_key reach the vendor untouched. The
vendor’s own payload (JSON, or SSE frames when stream: true) is copied back
unchanged.
model only picks the pool: a named catalog id pins to the seats that serve it,
otherwise every eligible responses seat is ranked. A chat-shaped reading of
instructions/input/tools is built locally for classification and bandit
learning only.
With no eligible responses seat the answer is 503 no_eligible_provider
(no responses-capable provider is configured). There is no chat fallback. The
responses seats are openai and codex; a catalog with neither has no
Responses door at all.
POST /v1/embeddings
Section titled “POST /v1/embeddings”Accepts model and input; input is required (400 otherwise). This door does
not use the bandit.
The pool is every eligible seat that advertises embeddings (kind
embeddings, or the embeddings feature flag) whose circuit is not open. A
chat-only seat is never selected, so a catalog of chat seats answers 503
no_eligible_provider (no embeddings-capable provider is available).
The pool is sorted by input price ascending, ties keeping catalog order, and the
body is forwarded verbatim. model is not rewritten, so the caller’s
embedding model name is what reaches the vendor. The first seat that answers is
returned with its status code; when every seat fails the status is 502. It needs
the embeddings scope.
POST /v1/search
Section titled “POST /v1/search”{"query":"searxng json api"}query is required and the request is classified as search. A keyed search
seat is preferred. With none available, loopback SearxNG is the fallback.
SearxNG is not billed per query; Exa, Firecrawl, You.com and Perplexity are.
Inventory and operator doors
Section titled “Inventory and operator doors”Operator endpoints, inventory, hook, stats, analytics, catalog, export, and health/metrics operations are listed with their declared scope and exposure in the generated API reference. The notes below keep behavior details that are not expressed in the OpenAPI contract.
GET /v1/models
Section titled “GET /v1/models”{"object":"list","data":[ {"id":"auto","object":"model","owned_by":"acumen"}, {"id":"cheap","object":"model","owned_by":"acumen"}, {"id":"gpt-5.3-codex","object":"model","owned_by":"codex", "context_window":400000,"max_output":16384, "pricing":{"prompt":1.25,"completion":10,"cache_read":0.125, "cache_write":0.125}}]}Two kinds of row. The six public names come first, owned_by acumen, with no
context or pricing. Then every model id of every eligible seat: owned_by is
that seat’s id, context_window and max_output are the catalog numbers, and
pricing is USD per 1M tokens (prompt, completion, cache_read, and
cache_write, which repeats the cache-read price).
context_window and max_output are omitted when zero. Empty and default ids
are dropped, and a duplicated vendor id keeps the first eligible seat. The list
is the vendor inventory acumen can actually reach, not a fixed set of names; the
row count grows with each seat that has a credential. See
Model catalogue snapshots for how to
capture a dated, redacted observation and how to treat live availability.
POST /v1/router/hook
Section titled “POST /v1/router/hook”Reports an outcome the router did not see itself: a search tool, an external
grader. It requires the header X-Acumen-Local: 1; without it the transport
guard answers 403 with the plain text forbidden before the handler runs, so a
cross-site form cannot drive it. With keys enforced it also needs the admin
scope.
| Field | Required | Meaning |
|---|---|---|
provider |
yes | catalog seat id |
model |
yes | vendor model id |
request_id |
no | echoed into the usage row |
prefix_hash |
no | echoed back |
class |
no | defaults to search |
ok |
no | success |
quality |
no | 0..1 |
latency_ms |
no | observed latency |
tokens |
no | prompt tokens |
The hook updates the bandit, the breaker and the usage log. It does not move the free-token pool: the token count is stored, not charged.
GET /v1/router/stats
Section titled “GET /v1/router/stats”JSON: per-seat enabled flag, plan, balance, spend, pressure, the credential
name (never the value), model prices, circuit state, recent decisions and
hourly rollups. hourly and bandit are present. decision_micros_last and
pricing_last_sync sit alongside the decisions array.
GET /v1/router/analytics
Section titled “GET /v1/router/analytics”Read-only hourly view over hourly_rollups: one bucket per hour, newest first,
with requests, ok, ok_ratio, tokens, cache_read_tokens, cost_usd and
failovers.
?days=N bounds the window (1..30, default 7); a non-integer or out-of-range
value is a 400 with an explicit message, and an empty window returns
"hours": []. The endpoint never rebuilds the rollups; freshness follows the
/v1/router/stats scrape.
GET /v1/router/catalog
Section titled “GET /v1/router/catalog”The JSON form of acumen -catalog: plan, kind, eligibility, credential_path,
model count and circuit per seat. Names only, never a credential value.
GET /v1/router/export
Section titled “GET /v1/router/export”Daily per-model partner aggregate for one UTC day (?date=YYYY-MM-DD, default
today). Counts, tokens, money and TTFT percentiles only. No prompts, no keys.
Health and observability
Section titled “Health and observability”GET /healthz
Section titled “GET /healthz”Liveness only. Body is the plain text ok plus a newline,
text/plain; charset=utf-8, status 200, for any method. It says nothing about
the store or the seats: a process that answers it is running, nothing more. It
is the only path exempt from key authentication.
GET /readyz
Section titled “GET /readyz”JSON readiness. Every value is a string, not a boolean.
{"ready":"true"}200 when the store is open and at least one seat is eligible: not disabled, and holding a key or marked allow-unauthenticated. Otherwise 503 with the reason:
{"ready":"false","reason":"no eligible seat"}reason is store closed or no eligible seat. It reports seat state, so it is
an operator path: with enforcement armed it needs an admin key, and on a
remote bind it needs the process token.
GET /metrics
Section titled “GET /metrics”Prometheus text. Must not contain a key, a cookie, or a file path that holds one.
acumen_requests_total, acumen_cost_usd_total, acumen_success_ratio and
acumen_circuit_state are the fastest health check. There is no
acumen_decision_* series; last-decision latency is JSON in /v1/router/stats.
Credentials the process asks for
Section titled “Credentials the process asks for”When a key is configured but the vendor’s GET /models does not return a usable
list, ProbeIntel names one environment variable:
- OpenAI-compatible seat, HTTP 401 or 403: a session cookie, in
ACUMEN_<SEAT>_INTEL. - Anthropic seat, HTTP 401 or 403: an OAuth token, same variable.
- HTTP 200 with an empty
dataarray, or any other status: a session credential, same variable.
The prompt tells the operator to set the variable. The value is never printed and
is never accepted as chat text. -catalog prints the variable name beside each
seat.
Local models
Section titled “Local models”Ollama on the same machine: set OLLAMA_LOCAL=1. The seat is
http://127.0.0.1:11434/v1 and needs no key.
A model on another host: set LOCAL_OPENAI_BASE to an http:// or https://
URL. The seat is local-openai. If LOCAL_OPENAI_KEY is empty the seat is
unauthenticated. Anything that is not an HTTP URL leaves the seat disabled.
What this API is not
Section titled “What this API is not”It is not a management API: it does not create provider accounts, mint per-agent
tokens, or store a pasted cookie. There is no /v1/credits route; that path
belongs to the Charm vendor API. Provider keys live in the environment at
startup and are never written to the database, logs or metrics.
Next steps
Section titled “Next steps”- For authentication, scopes, and failure modes, see Authentication.
- For operator procedures, use the acumen runbook.
- For generated contracts and current reference pages, see the API reference and reference index.