Skip to content
Version: Current

HTTP API

2026-11-05. repository reference/snapshot reviewed 2026-11-05; verify current behavior before using as live guidance.

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.

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.

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.

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.

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.

{"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.

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.

{"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.

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.

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.

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.

The JSON form of acumen -catalog: plan, kind, eligibility, credential_path, model count and circuit per seat. Names only, never a credential value.

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.

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.

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.

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.

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 data array, 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.

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.

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.