/ docs

API reference.
Protocol spec.

REST endpoints, proxy headers, schema definitions and the gateway error contracts — the whole surface, on one page.

For agents →

Point Claude, Cursor or any MCP client at ACE so it answers from this reference rather than guessing header names and error types.

Authentication & Credentials

Identity Model

ACE uses two credentials that do two different jobs. Conflating them is the most common integration error.

CredentialHeaderIdentifiesRequired For
ACE developer keyAuthorization: Bearer ace_dev_...your org (tenant) to ACEevery authenticated endpoint
Provider key (pass-through)x-ace-openai-key / x-ace-anthropic-key / x-ace-google-keywhich upstream LLM account to billreal (non-echo) inference, unless you have stored a vendor key
Provider key resolution.
Pass-through header on this request → your tenant's stored vendor key → reject. Pass-through keys are used for that one call and never persisted or logged (zero-knowledge mode), so there is nothing to onboard if you always send one.
Key prefixes.
Two prefixes count as an ACE key: ace_dev_ (what the dashboard mints, and what every key you hold looks like) and ace_tok_ (env-seeded deployment keys). A credential starting with neither is treated as a provider key, leaving the request with no ACE identity — which is a 401.
Vendor-shaped surfaces.
The vendor-shaped surfaces accept the ACE dev key on the header their SDK already sends, which is what makes the drop-in one line: /anthropic/v1/messages reads x-api-key, /azure/openai/… (chat completions and /azure/openai/v1/responses alike) reads api-key, and /v1/responses reads Authorization: Bearer exactly as /v1/chat/completions does. An ACE key prefix there is your ACE identity; any other value is treated as a pass-through provider key.
Precedence.
An Authorization: Bearer header you set yourself always wins. Sending both is the normal pass-through shape — bearer carries your ACE identity, x-api-key / api-key carries the provider credential. A synthesized value only ever fills a gap; nothing overwrites a header you set.

Proxy Header Specification

HTTP Protocol Spec

ACE acts as an intelligent proxy gateway. Requests authenticate using your ACE Developer Key, while upstream provider credentials can be passed dynamically via headers (Zero-Trust Mode) or attached server-side via the Cloud Provider Vault.

Header NameRequiredDescriptionSample Value
AuthorizationYesACE developer key — identity and telemetry attribution for your org.Bearer ace_dev_9a8b...
x-ace-openai-keyZero-TrustUpstream OpenAI key. Used for this call only, never persisted or logged.sk-proj-8f2e...
x-ace-anthropic-keyZero-TrustUpstream Anthropic key. Used for this call only, held in memory.sk-ant-api03-...
x-ace-google-keyZero-TrustUpstream Google AI key, for the Gemini-shaped surface.AIza...
x-ace-azure-keyAzure ModeUpstream Azure OpenAI deployment key.7a4b9c1d...
x-ace-azure-endpointAzure ModeYour Azure OpenAI resource endpoint.https://<resource>.openai.azure.com
x-ace-vertex-keyVertex ModeGCP service-account JSON (raw or Base64) for Vertex. The project is auto-extracted from the JSON if not given separately.<service-account-json>
x-ace-vertex-tokenVertex ModeDirect OAuth bearer token (ya29…) for Google Vertex AI. Alternative to x-ace-vertex-key.ya29.a0AfH6SM...
x-ace-google-projectVertex ModeGoogle Cloud Project ID for Vertex AI (auto-extracted if using Service Account JSON).my-gcp-project-123
x-ace-google-regionOptionalGoogle Cloud region for Vertex AI regional publisher endpoints (defaults to us-central1). `global` is addressed at the unprefixed `aiplatform.googleapis.com` host.us-central1
x-ace-bedrock-api-keyBedrock ModeAn Amazon Bedrock API key (long-term `ABSK…` or short-term), the alternative to the access-key pair below: ACE sends it upstream as `Authorization: Bearer` and signs nothing, so no AWS secret or STS token leaves your side. Its own header — an `ABSK…` value on `Authorization` is an unknown ACE key and a 401. `x-ace-aws-region` is required with it (a short-term key is bound to the region it was minted in, so ACE refuses to default one). Sent beside x-ace-bedrock-key or x-ace-aws-secret-access-key it is a 400 naming both. Used for this call only, never persisted or logged.ABSK...
x-ace-bedrock-keyBedrock ModeAWS access key id — half of the SigV4 pair, the alternative to x-ace-bedrock-api-key. ACE computes the SigV4 signature per request, so no local AWS CLI or stored role is needed.AKIAIOSFODNN7EXAMPLE
x-ace-aws-secret-access-keyBedrock ModeAWS secret access key paired with x-ace-bedrock-key. Never alongside x-ace-bedrock-api-key: the two can belong to different IAM principals, so ACE refuses the combination rather than pick one.wJalrXUtnFEMI/K7MDENG/...
x-ace-aws-regionBedrock ModeAWS region the Bedrock model is served from. Required with x-ace-bedrock-api-key — a missing region is a 400, never a defaulted host; the SigV4 pair alone defaults to us-east-1.us-east-1
x-ace-aws-session-tokenOptionalAWS session token, for temporary STS credentials. Omit it when signing with a long-lived access key pair.IQoJb3JpZ2luX2Vj...
x-ace-openai-base-urlOptionalSend the `openai` channel to another OpenAI-compatible host — OpenRouter, Together, Groq, a self-hosted vLLM or SGLang — with that host's key on x-ace-openai-key and its model slug sent verbatim. A trailing `/v1` (the SDK convention) is accepted and not doubled. Every channel has the same spelling (x-ace-anthropic-base-url, x-ace-google-base-url, …), and x-ace-base-url is the provider-agnostic form; a stored endpoint on the vendor key is the per-tenant alternative.https://openrouter.ai/api/v1
x-ace-base-urlOptionalProvider-agnostic spelling of the per-channel base-URL override above; the channel's own header wins when both are sent.https://openrouter.ai/api/v1
x-ace-vertex-projectVertex ModeAlias of x-ace-google-project. Google Cloud project for Vertex AI, on either surface.my-gcp-project-123
x-ace-vertex-regionOptionalAlias of x-ace-google-region. `global` is addressed at the unprefixed Vertex host.europe-west1
x-ace-providerOptionalPin the upstream provider explicitly rather than inferring it from the model.azure
x-ace-tenant-idOptionalExplicit tenant namespace override for multi-tenant isolation.tenant-prod-us
x-ace-route-toOptionalForce a specific provider:model pair, overriding engine routing.anthropic:claude-sonnet-4-5
x-ace-modeOptionalWhich terminal serves this request. `echo` (synonyms `sandbox`, `test`) answers from the echo terminal — no upstream call, nothing billed; `live` (synonyms `prod`, `production`) forces a real provider call. The per-request value wins over the key's stored echo flag; absent or unrecognised, the key's flag decides, and the default is live. Send `live` on production traffic so a key left in echo cannot serve a synthetic 200. `echo` on a deployment without an echo terminal is a 400, never a silent real call.echo
x-ace-cache-partitionOptionalNamespace this request's semantic-cache reads and writes to a partition of your org's pool that nothing else shares (`[A-Za-z0-9_-]{1,64}`). It only ever narrows — a partition cannot reach the org-wide pool, another partition or another tenant — so benchmark and load-test traffic can run against a production tenant without writing into the pool that serves real users. A present-but-malformed value fails closed to a namespace unique to the request (always MISS) rather than falling through to the shared pool. The response confirms the scope with `x-ace-cache-scope`.loadtest-2026-09
x-ace-sessionOptionalAn opaque conversation handle minted by your side (`[A-Za-z0-9_-]{1,64}`; a malformed value reads as absent). On `/v1/execute` the same handle is `attribution.session`, and agent-trajectory compaction also accepts a body `session_id`. Three things read it: telemetry, where it is the `session_id` on the request row; agent-trajectory compaction, which keys its checkpoint on it and refuses to fold a `cache_control`-bearing request that has none (`skipped: cache_control_without_session`); and, on a deployment running the semantic cache in session-isolation mode (public demos, not the production default), the cache namespace — there a request without one is isolated to itself and always misses. It is a reporting label: it never selects the tenant, the key or a rate-limit bucket.sess_7f3a9c2e
x-ace-teamOptionalAttribution tag: the `team` label on this request's telemetry row, and the `team` dimension of `GET /api/v1/usage`. Absent, the row carries your key's public stable id instead, so untagged traffic still groups by key. On `/v1/execute` the same label is `attribution.tenant`. It keys the per-team budget window, and nothing else: it does not move the semantic-cache namespace, the virtual-key id or the rate-limit bucket, and a label naming another tenant on a dedicated deployment is a 403, not a relabel.payments-platform
x-ace-use-caseOptionalAttribution tag: the `feature` dimension of `GET /api/v1/usage` (`use_case` on the row; `attribution.use_case` on `/v1/execute`). The one attribution field with a serving consequence — it is also the key into your tenant's stored fallback chain, so a tagged request has a fallback where an untagged twin has none. Send one per product surface (`support-bot`, `code-review`) and the usage rollup separates their spend without a second key per feature.support-bot
x-ace-skillsOptionalPer-request skill overrides on every vendor-shaped surface (and, beneath its own `skill_overrides` field, on `/v1/execute`): comma-separated `skill_id=mode` pairs, `mode` one of `off` / `shadow` / `prod` — the same catalogue, vocabulary and validator as the native override, strict and case-sensitive. `canary:N` and `rolled_back` are not per-request modes: a canary is a fraction of a key's traffic, so it is set on the key through the lifecycle route (see the endpoint reference). The override beats your stored mode in either direction; nothing is persisted. An unknown skill id or mode is a 400 in the surface's own envelope naming the pair, refused before dispatch; an override on a skill your org has locked is a 403 `skill_locked` projected per surface (Anthropic `permission_error`, Gemini `PERMISSION_DENIED`, Bedrock `AccessDeniedException`) with the locked ids in the message. On the OpenAI shim a skill named in `extra_body.ace.skill_overrides` beats the header's pair for that skill. The response's `x-ace-skills-applied` echoes the pairs that actually ran. **The valid ids are exactly the `universal` list `GET /api/v1/skills` returns** (every skill whose `scope` is `SKILL_SCOPE_UNIVERSAL` and is not deprecated — the same set `POST /api/v1/dev_key/skills` accepts); it includes `skill_knowledge_graph`, `output_budget` and `reasoning_effort`, and it grows as skills ship, so **a hand-maintained off-list goes stale**: a control arm that names the seven skills it knew about leaves the eighth running and measures a system with one lever still pulled. Use the wildcard instead: `*=off` (or `*=shadow` / `*=prod`) stands for every universal skill the gateway registers, resolved per request from its registry, never from a list; the same `"*": {"mode": "off"}` entry is accepted in `skill_overrides`. A pair naming a skill explicitly in the same header or body beats the wildcard for that skill (`*=off,injection_guard=prod`), whichever side each came from. `*` with no mode, or a mode outside the vocabulary, is the same 400. A skill your org has locked is skipped by the wildcard rather than refused — you did not name it — and reported as `<skill>=locked` in `x-ace-skills-applied`; naming a locked skill beside `*` is still the 403. There is no `all=` alias.*=off,injection_guard=prod
Cache-ControlOptional`no-cache` (exactly that value): this request is never served from the semantic cache. The provider still answers and the answer is still stored for later hits; only the read is skipped. Use it on a call whose answer must be fresh — a grounded search, a tool-bearing turn. A request with `temperature > 0.8` is not served from the cache either. On `/v1/execute`, `execution.no_cache` is the same switch, and an explicit `false` there overrides a stale header a client library set once.no-cache
x-request-idOptionalYour own id for this request. It becomes the gateway's request id — the `id` of a `/v1/execute` response, the `request_id` in the request log, and the tag on every log line for the call — in place of a generated `req-…`. It is not echoed as a response header on the vendor-shaped surfaces.order-7f3a-retry-2

Response Header Specification

Telemetry Contract

The response body is byte-shape unmodified — a standard OpenAI chat.completion object — so existing SDKs parse it with zero changes. Everything ACE-specific rides on headers.

HeaderMeaning
x-ace-cache`HIT` / `MISS` / `BYPASS` — semantic cache result. `BYPASS` says the lookup was not allowed to serve and `x-ace-cache-bypass-reason` names the gate that refused it: `skill_off`, `shadow_mode`, `no_cache_requested` (`execution.no_cache`), `cache_control_no_cache` (the header), `high_temperature` (`temperature > 0.8`), `stateful_unpartitioned` (the `strict` key policy refusing an unpartitioned stateful request), `provider_evaluated_extension` (a `guardrailConfig` / `safetySettings` that must run at the provider), or `tool_result_turn` — the live turn is a tool result, an agent step. Under the `read_set` and `strict` key policies that step's key carries its own tool-result digest and the session, so nothing but a byte-identical re-send can match it, and that re-send is served from the exact-prompt table without an embed: a client retry is still a `HIT` at ~0ms, and no `MISS` is reported because the embed and vector query that would have produced it never run — nor the shadow lookup, nor the vector write. `skill_params.semantic_cache.lookup_on_tool_results: true` restores the approximate tier on such steps; under the `text` key policy it runs regardless. `x-ace-cache-shadow` carries what the lookup would have done (`WOULD_HIT` / `MISS` / `PENDING` — `PENDING` means the shadow lookup had not answered within the join grace, so count it as unmeasured, never as a miss). With it: `-similarity` (how close the best candidate came, on a miss too), `-threshold` (the floor this lookup actually applied — adaptive thresholds raise it to `0.98` on an entity-dense prompt, so it is not always the cache's base setting) and, on a `MISS`, `-miss-reason`: `no_candidate` (nothing stored in this namespace yet), `below_threshold`, `identifier_mismatch` (the numeric guard: an identifier token differs between the two prompts, so a 0.98 that missed is the guard refusing a different thing, not the cache failing. Four shapes make a token an identifier — it carries a digit (a ticket number, a hex id, a version); it carries an underscore, which ordinary English does not (`test_staging` vs `test_production`); it is upper-case and written after `ticket` / `case` / `order` / `id` / …; or it is written after a word naming a SLOT rather than a code — `database`, `user`, `branch`, `service`, `instance`, `cluster`, … — where the next token is the name whatever its case, which is what separates `user alice` from `user bob` and `service payment-gateway` from `service auth-gateway`. A bare hyphen does NOT make an identifier, so `read-only` still matches `read only`), `polarity_mismatch` (the two prompts name OPPOSITE sides of one action while both stay affirmative — `allow` vs `block`, `enable` vs `disable`, `grant` vs `revoke` — a pair the embedder puts ~0.98 apart because every other word matches. It fires only when each side carries one pole and not the other, so a sentence naming both, the right way to start and stop the cluster, is ordinary phrasing rather than a conflict), `negation_mismatch` (a negator present on one side only, or — both sides negating — one side's negators a proper subset of the other's, the extra negator stacked on a negation they share: is it NOT possible to avoid downtime against is it possible to avoid downtime. Two disjoint negators are left alone, being two ways to say one negative), `word_order`, `store_error` (the vector store or the embedder failed; the trace stage's `store_error` names the exception class), and the three the skill latency ceiling reports — `ceiling_exceeded`, `overloaded`, `loop_stalled` (the lookup did not answer inside its per-request ceiling and the request went upstream rather than wait; see **The skill latency ceiling** below).
x-ace-cache-usage / x-ace-cache-entry-age-s / x-ace-cache-key-policyOn a `HIT`, `x-ace-cache-usage: replayed` says the body is the STORED completion served verbatim — its `id` and its `usage` are the FIRST call's. They are deliberately not rewritten (the surface contract is a vendor-shaped body, and callers parse it), so a hit can show a vendor `cache_creation_input_tokens` beside `x-ace-cost-usd: 0.000000` on a request that never reached the vendor. Reconcile spend against `x-ace-cost-usd`, never a replayed `usage`, and drop replayed responses before measuring anything about the vendor's own prompt cache; the same header rides a completion replay (`x-ace-replay: HIT`). `-entry-age-s` is how old the served entry was. `-key-policy` is the key policy this lookup ran under (`read_set` is the default, with a session partition).
x-ace-cache-scopePresent only when this request did NOT share the org-wide cache pool: `partition` (a valid `x-ace-cache-partition` was sent), `session` (a deployment in session-isolation mode), `isolated` (fail-closed — alone in its namespace, always MISS), `bypass-unpartitioned` (the strict cache-key policy refused to serve an unpartitioned stateful request). Absent on an ordinary production request.
x-ace-cache-entityA digest of the identifier tokens the gateway found in this prompt, present only when it found any and `ACE_CACHE_ENTITY_PARTITION` is on (default off). It names a namespace the GATEWAY imposed: two prompts whose identifier sets differ carry different digests and cannot read each other's entries at any similarity. It is NOT `x-ace-cache-scope`, which reports the isolation the CALLER asked for (`session` / `partition` / `isolated`); this one reports isolation applied on top of whatever the caller asked for, and both can appear on the same response. The partition is keyed on the same tokens `identifier_mismatch` compares, so it separates only the pairs that guard was already refusing — no hit served today is lost — and it reaches the ones the guard could not: the index is queried one candidate deep, so a correct entry sitting behind a same-shape-different-id neighbour was unreachable, and inside its own partition it is first. What the header adds is legibility. Without it, a probe that missed because it named a different entity reports `no_candidate` at similarity 0.0, which a reader cannot tell apart from a cold cache; with it, the digest says which namespace the lookup actually ran in. Turning it on re-namespaces every stored entry whose prompt contains an identifier token — a partial cold start, silent by construction. It is gated in lockstep with the identifier guard, so an operator who turned that guard off does not still get identifier separation imposed structurally.
x-ace-served-byDestination id that actually served this request. `echo` means no real upstream is configured; `direct_passthrough` means the optimization pipeline was bypassed.
x-ace-destinationWhich destination or provider actually answered: a fleet destination id on a dispatched request, the provider name (`anthropic`, `azure`, `bedrock`, …) on a relayed one, `echo` on an echo-served one. On the relay path `x-ace-served-by` names only the MECHANISM — every relayed response reports `direct_passthrough` whichever provider answered — so this is the field that tells two relayed responses apart.
x-ace-cost-usdActual (or, when streaming, estimated) cost of this request.
x-ace-route-model / x-ace-route-tier / x-ace-route-reasonWhich model and tier the router picked, and why. Present when a router is configured. A router that ran and routed nothing says why in `-reason`: `no_candidates`, `no_tier_choice`, `router_failed`, or `ceiling_exceeded` / `overloaded` / `loop_stalled` when the skill latency ceiling shed the stage and the caller's model passed through.
x-ace-route-escalated`true` if a cheap-tier answer failed validation and escalated to a flagship model.
x-ace-warningsComma-separated codes, each defined in the table below: echo_fallback, model_auto_selected, provider_key_pass_through, provider_key_ignored, extensions_omitted, optimization_suppressed, pipeline_bypassed. Absent when there is nothing to warn about — an ordinary relay is not a warning.
x-ace-budget-remaining-dailyRemaining daily team budget, when a finite cap exists.
x-ace-errorOn every error ACE originated, on every surface: the ACE taxonomy type (`invalid_developer_key`, `budget_exhausted`, `request_error`, `skill_locked`, …) whatever envelope the body wears. Absent on a vendor error relayed from upstream — that absence is the signal a retry classifier branches on. Values in the errors section.
x-ace-skills-appliedThe `skill_id=mode` pairs from the request's `x-ace-skills` header that this request actually ran with (`semantic_cache=off,prompt_compaction=shadow`). Only the pairs that applied: one the body's `extra_body.ace.skill_overrides` beat, or that `compute_status: passthrough_only` forced to shadow after it was accepted, is absent — as is the header when nothing was sent or nothing applied. The per-skill headers (`x-ace-cache`, `x-ace-compaction-*`, `x-ace-skill-modes`) report exactly as before. A `*` wildcard is echoed **expanded** — one pair per registered universal skill it resolved to, never `*` itself — and a locked skill a wildcard skipped is reported as `<skill>=locked`, which is not a mode, so a job asserting `pii_ner=off` fails on it rather than reading a lock as an off.
x-ace-skill-modesEvery skill that ran on this request and the mode it ran under, as sorted `skill_id=mode` pairs (`llm_router=shadow,semantic_cache=prod`) — whatever set the mode: the key's stored lifecycle, the deployment default or a per-request override. One header rather than a flag per skill because the question is asked about the request as a whole: which of these numbers am I allowed to believe changed something. A skill that was off is absent, and so is a retired skill id still stored on the key with no module behind it. Distinct from `x-ace-skills-applied`, which echoes only the pairs you sent in `x-ace-skills` that took effect; the two never disagree, because both report the intent each skill ran under.
anthropic-ratelimit-* / x-ratelimit-* / retry-after / request-idThe vendor's own rate-limit headers, relayed verbatim on a response the vendor produced, buffered or streamed. See **Vendor rate-limit headers** below for the exact list and when they are absent.
x-ace-guardrail / -stages / -modeThe injection guard on a request it did NOT refuse — a refusal is a 400 with `x-ace-error: guardrail_violation`, so `x-ace-guardrail` only ever reads `clean`. `-stages` says what looked: `regex` (the pattern firewall) or `regex+model` (plus the learned classifier). `-mode` is the CLASSIFIER's stage mode, `enforce` or `shadow`, present only when there is a classifier; it is not the skill's mode. `x-ace-skill-modes: injection_guard=prod` with `x-ace-guardrail-mode: shadow` is the usual production reading, not a contradiction: the firewall refuses, the classifier scores alongside it and records to telemetry — the deployment is measuring its false-positive rate before letting it refuse anyone. A key running the whole skill in shadow reports `shadow` here whatever the classifier's own setting. No score in either mode: a per-request P(injection) returned to the sender is a tuning oracle.
x-ace-pii-redactedCount of redacted entities, when PII redaction is on. `x-ace-pii-kinds` names them (`EMAIL=1,PERSON=2`); `x-ace-pii-stages` says which stages ran (`ner_regex`, `ner_model`, `ner_regex+ner_model`). In shadow, `x-ace-pii-would-redact` carries the count instead and `x-ace-pii-redacted` is `0`.
x-ace-pii-skipped`ner_model=budget_exceeded`: the learned stage ran past its per-request budget (`ACE_PII_NER_BUDGET_MS`, default 50) and the request was served with the pattern redactions applied and the model's applied only to the turns it finished. `ner_model=ceiling_exceeded` / `overloaded` / `loop_stalled`: the whole scan did not come back inside the skill latency ceiling and the request was served on patterns alone. `ner_model=<reason>,ner_regex=<reason>` (with `x-ace-pii-stages: none`): not even the patterns pass answered inside the ceiling and the request went upstream **unredacted** — named, so a `0` count cannot read as "clean". Every one of these is a verdict on THIS request's wall clock, not a quota: the next request starts from zero. See **PII redaction and long prompts** and **The skill latency ceiling** below.
x-ace-compaction-tokens-before / -after / -savedPrompt compaction ran; what went in and what went upstream. `x-ace-compaction: refused` + `-reason` when an interlock declined. `x-ace-compaction: skipped` + `-reason` (`cache_hit`, `replay_hit`) when the skill is on but a stored answer was served above it, so there was no upstream prompt to shrink, or (`ceiling_exceeded`, `overloaded`, `loop_stalled`) when scoring did not finish inside the skill latency ceiling and the prompt went upstream as received — all distinct from absent, which means the skill is **off** for this key. In shadow, `-saved` is `0` and `-tokens-would-save` carries the counterfactual. Under a declared prompt-cache breakpoint the skill runs by the **prefix-stable rule** and says so with `x-ace-compaction-prefix: stable`: every rewritable span is pruned by the same pure function of its own bytes on every request (history from a per-process memo, at no compute), so the history goes upstream byte-identical and the first differing byte stays in the live turn. There is no "protect the last N turns" window — a span pruned later than it arrived would move the prefix on every request. Stable is **priced**: the span the provider has actually cached is rewritten only where the later cache reads pay for the re-write over the task's horizon. Held, `x-ace-compaction-prefix: protected` with `x-ace-compaction-prefix-reason` `cache_breakpoint_not_worth_it` or `auto_cache_not_worth_it`; priced and pruned anyway, `stable` with `-reason: pays_back`. `x-ace-compaction-spans: prefix=<n>,user=<n>,tool_results=<n>` says where the saving came from. `x-ace-compaction-partial: <reason>` says part of the prompt went upstream as received although the skill ran, one reason in precedence order: a ceiling shed (`ceiling_exceeded` / `overloaded` / `loop_stalled` — the shed span is **pinned** as a no-op in the memo so later steps read it at no compute instead of re-scoring it into the same ceiling) outranks `span_too_large` (over `skill_params.prompt_compaction.max_span_tokens`, default 24000) outranks `constraints_not_preserved` outranks `tool_yield_zero` (a tool whose last `tool_yield_window` scored results never pruned is skipped; the spans header then carries a trailing `tool_results_skipped=<n>`).
x-ace-trajectory-compactionTrajectory compaction's verdict on this request: `compacted`, `would_compact` (shadow), `no_op` (+ `-reason`: `first_epoch`, `nothing_sealable`, `single_turn`), `refused` / `skipped` (+ `-reason`: `cache_control_without_session`, `tool_loop`, `unwritable_body`), or `failed` (fail-open). With it: `-mode`, `-strategy` (`epoch`, the grid fold; `sliding_window` only for a compactor deployed in that mode), `-turns` (`sealed/total` conversational turns), `-messages-before` / `-after`, `-tokens-before` / `-after` / `-saved` (shadow: `-saved` is `0` + `-tokens-would-save`), and `-checkpoint` — a 12-hex digest of the summary text that changes exactly when the folded prefix changes, once per epoch, for reconciling against the provider's `cache_creation_input_tokens`. A cache-warm caller (declared `cache_control`) with no `x-ace-session` is `skipped`, never folded. Absent when the skill is off.
x-ace-stage-durations / x-ace-pre-inference-ms / x-ace-full-duration-msWhere the wall clock went. `x-ace-stage-durations` names each stage that completed and, on a `504 gateway_timeout`, the one still running — `byok_store=30000` is the identity store, `upstream=…` is the vendor, `queue_wait=…` is time on the loop before the engine reached the request. Read it FIRST when a request was slow: it separates ACE's own pre-inference work from the provider's, which no other signal does. `x-ace-pre-inference-ms` is everything ACE did before the upstream call and `x-ace-full-duration-ms` the whole request, so the difference is the vendor.

The x-ace-warnings codes:

CodeMeaning
echo_fallbackServed by the echo terminal — a synthetic reply, no upstream call, $0. Your key is in echo mode or the request sent `x-ace-mode: echo`.
model_auto_selectedNo `model` was sent and the router chose one; `x-ace-route-model` says which.
provider_key_pass_throughThe provider key on the request was used for the upstream call (zero-trust), not a stored one.
provider_key_ignoredA provider key on the request was for a channel other than the one that served, and a stored key served instead. The key you send is meant to be the key that serves you, so a silent substitution is named.
extensions_omittedThe request carried a provider extension the terminal that served it cannot honor, and the REPLY omits it. This covers `thinking` (a budget, or a replayed signed `thinking` block) and any other signed or provider-scoped root field. It is emitted from exactly two places — the dispatch path (the echo terminal, a fleet destination; neither can produce a thinking block or a signature) and a semantic-cache hit, where the stored reply predates this request's thinking — and never on a relay to the vendor: a relayed request goes upstream as sent, `thinking` and signatures included, and comes back as the vendor sent it. So on a live key it appears only on a cache hit; on an echo key it appears on every request carrying `thinking`, which is the echo terminal saying it could not fake extended thinking, not the gateway saying it stripped yours.
optimization_suppressedEvery lever was forced to shadow by `compute_status`.
pipeline_bypassedA fail-open after an internal fault: the request was relayed with no lever applied.

The skill latency ceiling. Every heavy stage on the request path — pii_ner, semantic_cache (the embed and the vector-store query), prompt_compaction, llm_router and the injection guard's learned stage — runs under a wall-clock ceiling measured by the request itself: 250 ms for a stage the key runs in prod, 50 ms for one it runs in shadow. A stage that has not answered by then is shed: the request is served without it, the response says so on the header that stage already reports through, and the trace stage carries reason, waited_ms and ceiling_ms. It is a bound on one request's wait, not a quota. Nothing accumulates across calls, there is nothing to reset, and the next request is judged on its own clock.

ReasonMeaning
ceiling_exceededthe stage did not answer inside its ceiling; served without it.
overloadedthe skill worker queue was already deeper than one ceiling can drain; the stage was not attempted rather than queued to time out later.
loop_stalledthe ceiling's timer fired late: the gateway's event loop was held by something else, and the stage may not have run at all. Read this as a gateway fault, not a slow skill — the trace's late_ms is how long the loop did not run.

GET /api/v1/settings reports the effective ceilings under skill_ceiling (per skill, acting and shadow, with the worker pool that runs them); GET /healthz reports the pool as it stands — what is queued, what is running and for how long, and everything shed so far by reason. A gateway shedding at ceiling_exceeded with nothing queued and a job running for minutes has a wedged worker; one shedding at loop_stalled has a held loop; neither is load, and neither is your key.

This header set is frozen legacy: nothing in it is going away without notice, and nothing new is being added. A flat string cannot carry what a shadow skill measured, and on a streamed request x-ace-cost-usd is written before the first token exists — an estimate reported as fact. Both are why /v1/execute exists; new skills report on its trace.

REST Endpoint Dictionary

SDK & HTTP Specs
Drop-in replacement for OpenAI SDKs. Change base_url to https://engine.acefleet.dev/v1. All optimization skills (vector cache, compaction, PII NER guardrails) run automatically inline.
cURL / HTTP
curl -X POST https://engine.acefleet.dev/v1/chat/completions \
  -H "Authorization: Bearer ace_dev_<your-dev-key>" \
  -H "x-ace-openai-key: sk-proj-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{ "role": "user", "content": "Analyze cluster telemetry." }]
  }'
Python (OpenAI SDK)
import openai

client = openai.OpenAI(
    base_url="https://engine.acefleet.dev/v1",
    api_key="ace_dev_<your-dev-key>",
    default_headers={
        "x-ace-openai-key": "sk-proj-...",  # Zero-Trust header mode
    },
)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Analyze cluster telemetry."}],
)
print(response.choices[0].message.content)

Provider Surfaces & the Responses API

Both Directions, Verbatim

Every vendor-shaped surface — /v1/chat/completions, /v1/responses, /anthropic/v1/messages, /azure/openai/…, /gemini/v1beta/…, /bedrock/converse — is a compatibility shim over the same engine, and each one makes the same promises.

Request.
The body you send is the body the provider receives, byte for byte except inside the text spans a lever rewrote. Thinking configuration and signed thinking blocks, `cache_control` breakpoints, images, documents, tool definitions, tool results, Gemini `thoughtSignature` parts and Bedrock `reasoningContent` all travel as you wrote them. Nothing on this path is parked, projected into another vendor's schema, or silently dropped — and the levers that would invalidate a signature or a cache prefix stand down when one is present.
Response.
The provider's answer comes back as the provider sent it, buffered or streamed. On Anthropic that is the full event vocabulary — `thinking_delta`, `signature_delta`, `redacted_thinking`, `ping`, a mid-stream `error`, the cache counters on `message_start` and the vendor's own `msg_` id. On Gemini it is every `thought` part and `thoughtSignature`, `thoughtsTokenCount`, `cachedContentTokenCount`, `groundingMetadata` and `responseId`. On Bedrock it is the signed `reasoningContent`, the cache counters and `metrics`. Usage is metered from a copy of the stream, so accounting can never alter it. A response the vendor produced also carries the vendor's own rate-limit headers verbatim — `anthropic-ratelimit-*`, `x-ratelimit-*`, `retry-after` and its request id — buffered or streamed; see Response Headers.
Attachments.
Images, PDFs and audio reach the provider, whichever provider that is. On a provider surface the part arrives by construction, because the body is relayed as written. When ACE translates — an OpenAI-shaped /v1/chat/completions or /v1/execute request served by Anthropic, Gemini, Vertex or Bedrock — every attachment is rewritten into the destination's schema: `image_url` becomes an Anthropic `image`, a Gemini `inlineData` or a Converse `image`; a `file` part (a PDF, base64 or `file_url`) becomes an Anthropic `document`, a Gemini `inlineData` or a Converse `document`; `input_audio` becomes a Gemini `inlineData`. What the destination cannot receive — audio to Anthropic or Bedrock, a URL image to Bedrock (Converse takes bytes, and ACE does not fetch URLs for you), a document type the provider does not read, an OpenAI `file_id` sent anywhere but OpenAI — is a 400 naming the part and the provider, raised before anything goes upstream. An attachment is never dropped silently. The one place one legitimately leaves a request is trajectory compaction, which replaces whole earlier turns with a summary; the current and retained turns keep theirs.
No cross-provider failover.
A request written in one vendor's format is only ever served by that vendor's channel. It never fails forward to another provider, even when your vault holds keys for two channels that serve the same model — an Anthropic Messages body is never posted to Bedrock Converse on an Anthropic 503.
The Responses API.
The OpenAI Responses API is a surface of its own: `POST /v1/responses`, and for Azure `/azure/openai/v1/responses`, `/azure/openai/responses?api-version=…` and `/openai/v1/responses` (the Azure SDKs have used all three). It is what an OpenAI or Azure client posts for GPT-5 — `client.responses.create(...)`, or the AI SDK's `@ai-sdk/openai` / `@ai-sdk/azure` providers, which default to it — and chat completions cannot carry it: the body is item-based, `reasoning.summary` has no chat equivalent, and `store: false` + `include: ["reasoning.encrypted_content"]` is how a Zero-Data-Retention org keeps reasoning continuity across turns. The `input[]` items (`reasoning` items with `encrypted_content`, `function_call` / `function_call_output`, `input_image` and `input_file` data URLs), `tools`, `tool_choice`, `reasoning`, `store`, `include`, `max_output_tokens` and `stream` go upstream as sent; the Responses object comes back verbatim, and `stream: true` relays the provider's own `response.*` event stream. A Responses body can only be served by OpenAI or Azure. Pinned to any other provider (`x-ace-provider: anthropic`, say) it is refused with a 400 naming the reason, in OpenAI's error envelope — never rewritten into chat completions you did not write. A request the semantic cache or an in-fleet model answers is projected into a Responses object (`output[]` of `message` and `function_call` items) so the SDK still parses it; those paths never produce a `reasoning` item.
Anthropic count_tokens.
`POST /anthropic/v1/messages/count_tokens` (and `/v1/messages/count_tokens`) relays the body to Anthropic with the credential the call carries — the zero-trust header, a non-ACE `x-api-key`, or your stored Anthropic key — and returns the vendor's count, status included, so a vendor 400 naming a bad block reaches you as such. With no Anthropic credential anywhere the local estimate answers, which cannot count an image, a PDF or a tool schema the way the model's tokenizer does. Echo is decided first: a key in echo mode, or a request sending `x-ace-mode: echo`, is never relayed whatever credential it carries — the estimate answers with `x-ace-served-by: echo` and `x-ace-warnings: echo_fallback`, so a placeholder key sent while testing cannot reach the vendor.
The one exception.
POST /v1/messages is the exception, and it is not the recommended proxy: it exists for parity testing, not production traffic. Being a relay, it preserves however you presented your credential and never rewrites it — and it forwards the body unmodified, so no ACE lever runs on it. Production Anthropic traffic belongs on /anthropic/v1/messages, which accepts the same SDK, carries thinking, tools, images and cache_control as sent, returns Anthropic's own answer, and gets the full lever set.

Skill Documentation Directory

Ordered as in Settings

Individual technical specifications for each optimization skill, in the bands and the exact order they appear on the Optimization Skills page. Bands whose skills have shipped are open below; the ones still to be built fold.

Circuit breaker
The "Broken Machine" Detector — stops sending traffic to a provider that has completely crashed.
Adaptive concurrency
The "Traffic Jam" Preventer — stops a sluggish provider from causing a system-wide backlog.
Outlier ejection
The "Weakest Link" Remover — benches the worst-performing server in a group of identical servers.
PII protection
Unified perimeter protection combining fast regex rules with ONNX token classification.
Semantic cache
Sub-20ms exact and vector-similarity cache reuse for prompt completions.
Model router
Intent and complexity-based model selection to optimize quality versus token cost.
Prompt pruning
Entropy-scored context trimming ahead of outbound model calls.
Agent trajectory compaction
Folds old agent turns into a checkpoint on a fixed grid, so a long tool loop stops re-sending its whole history — and the bytes it does send stay cache-stable between folds.
Injection guard
Perimeter prompt-injection defense combining fast regex filters and DeBERTa classification.
Local SLM fallback
Co-located quantized Llama-3-8B emergency backup for zero-downtime cloud provider failover.
Feedback distillation ring
Turns the answers your callers accepted into a cheaper model that can serve them.
Knowledge graph
Extracts reusable workflows from successful agent traces, collapsing search loops into deterministic procedural execution.
Output budget
A per-key ceiling on the completion budget a request declares. Only lowers; shadow reports.
Reasoning effort
A per-key ceiling on reasoning: a level on OpenAI, a thinking budget on Anthropic, Bedrock and Gemini. Never on, never up.
Geo-fence compliance
Residency as a hard gate — evaluated before price, not alongside it.
Multi-agent guard
Halts a runaway agent loop before it is paid for — a depth cap and a repeat detector.

Onboarding a Skill · Shadow → Prod

Rollout Protocol

Each skill runs in one of three modes per developer key, set on the Optimization Skills page. Onboard in shadow first, promote once the numbers justify it.

ModeEffect on your requestWhere the numbers land
offNot in the data path.Absent from telemetry.
shadowNone — decides, records, forwards untouched. No risk.shadow block — counterfactual only.
prodActs: cache hit served, prompt pruned, request rerouted, redacted, or refused.Realized totals — saved, blocked, cache_hit.

Counterfactual metrics

A shadow skill computes its full decision — cache lookup, routing pick, compaction plan, classifier score — then discards it, and the gateway stores the delta: tokens that would have been pruned, requests that would have hit cache, spend avoided, turns refused. Those dollars were still spent, so the figures project what promotion would save, never what was banked — they never join the realized totals.

Onboarding a skill

  1. Set it to shadow on your key and save — nothing changes.
  2. Let traffic accumulate.
  3. Read the projection on Telemetry, or per request in trace.stages[].
  4. Flip to prod — the skill starts acting.

Shadow is unlocked on every tier; prod on some skills requires Pro/Team.

Gateway Status Codes & Error Contracts

Error Matrix

Errors share one envelope:

{"error": {"message": "...", "type": "invalid_developer_key", "code": 401}}
HTTPTypeMeaning
401invalid_developer_keyMissing, invalid, revoked or expired ACE developer key. There is no partial-credit anonymous fallback — a header that does not resolve to a known key fails the same way as no header at all.
402onboarding_incompleteAuthenticated, but there is nothing to serve the request with: no provider key (stored or pass-through), or no model given and no router configured.
403model_not_in_scopeThe requested model is not enabled for this tenant.
403invalid_admin_keyAn admin-only endpoint was called with a key that is not an admin key.
429rate_limitedPer-tenant RPS cap exceeded.
402budget_exhaustedThe request would breach a real-time spend cap. The cap, what was consumed and when it resets ride on `x-ace-budget-cap-usd`, `x-ace-budget-consumed-usd` and `x-ace-budget-resets-at` as well as in the body.
400guardrail_violationA prompt-injection or jailbreak guardrail tripped. Carries a `detected` field naming what fired.
507adapter_unavailableA LoRA adapter cannot fit even when loaded alone.
503overloadedACE shed the request on its own capacity, never the provider's. Three places decide it: the transport, before the body is read (declared size against what is in flight, `ACE_INGRESS_BUDGET`, off unless set; or a heavy body while the event loop has been stalled past `ACE_LOOP_STALL_SHED_SHARE` of the last `ACE_LOOP_STALL_WINDOW_S`, on by default at 0.5 over 10s — light bodies are still served); engine entry, when the request sat past `ACE_QUEUE_WAIT_MAX_S` (10s) before the engine reached it; and dispatch, when every destination was at its concurrency limit. Always carries `Retry-After`, computed from what this process has actually drained lately (1–30s). The body wears the surface's own overload envelope (`overloaded_error` on the Anthropic surfaces). Back off and retry, or send direct to the provider — neither the key nor the provider is involved. `GET /healthz` reports the gauge under `load`.
502upstream_truncatedThe provider answered 2xx with a body that does not parse as JSON — a gzip stream or an EOF-framed body cut short between the provider's origin and ACE, which the HTTP client decodes without error. It is never relayed as the 200 it arrived with. Typed because its remedy differs from every other 502: the provider generated and billed this turn once, so a retry pays for the answer twice. On the relays only (`/v1/messages`, and every surface relaying a pass-through vendor key); the engine's routed path already falls forward on the same failure.
504store_timeoutACE's own durable store did not answer inside its bound — a read behind a control-plane route (`GET /api/v1/tenant/requests`; retry with a narrower window or filters), or a BYOK store call on the request path that did not come back inside `ACE_BYOK_STORE_TIMEOUT_S` (default 30s, worker wait included). Carries `Retry-After: 5` — the identity read timed out, so retry once the store answers.
504gateway_timeoutThe request deadline: the handler had not started its response `ACE_REQUEST_DEADLINE_S` (default 600s) after the transport accepted the request, and the gateway cancelled it and answered in its place. Nothing had been sent, so a retry is safe. The body wears the surface's envelope (`api_error` on the Anthropic surfaces, `DEADLINE_EXCEEDED` on Gemini, `ModelTimeoutException` on Bedrock), and `x-ace-stage-durations` beside it names the stages that completed and the one still running — `byok_store=30000` is the store, `upstream=…` is the vendor. Once the status line has left no header can change, so a **stream** silent for `ACE_STREAM_IDLE_S` (default 300s) or open past `ACE_STREAM_DEADLINE_S` (default 3600s) is closed with one terminal `error` event carrying `"type": "gateway_timeout"` instead.

/v1/execute refuses differently. A validation failure is {"detail": {"error": "...", "field": "input.messages"}} and a locked-skill override is {"detail": {"error": "skill_locked", "locked": [...]}}. Both still carry x-ace-error (request_error and skill_locked respectively).

Guardrail detection values. A guardrail_violation carries a detected field naming what fired — instruction_override, role_reassignment, secret_exfiltration, system_prompt_probe from the always-on regex stage, and learned_injection from the optional classifier. The four regex values come from the always-on stage. `learned_injection` comes from the optional classifier and only appears on a deployment that has moved it out of its default shadow mode (ACE_GUARDRAILS_MODEL_MODE=enforce). In shadow — the default — the classifier never produces a 400 and never alters the response.

ACE never blocks traffic on its own accounting. No spend threshold, quota or internal limit turns a request into an error. Whenever ACE cannot compute — a metering ceiling, a telemetry blip, a skill that fails to evaluate — it degrades to direct passthrough: relayed straight to your provider, pipeline bypassed, normal 200, upstream body untouched. Errors originate at the edge (auth) or upstream (provider), never from the optimization layer.

Resilience & Fallbacks: Direct Pass-Through Architecture

ACE enforces a strict Fail-Open Architectural Guarantee. If processing latency exceeds the 50ms skill budget ceiling, or an internal component (Qdrant vector store, Redis cluster, or PII NER detector) raises an uncaught exception, ACE instantly bypasses all optimization layers and relays raw HTTP bytes directly to the primary downstream provider. The response header x-ace-served-by: direct_passthrough is attached so your APM can track every bypass.

Trigger Conditions
  • Skill latency ceiling exceeded — a stage that has not answered inside its per-request wall-clock ceiling (250 ms acting, 50 ms shadow) is shed and the response names it: `ceiling_exceeded`, `overloaded` or `loop_stalled` on that stage's header and trace. Per request, never cumulative; nothing to reset.
  • Gateway panic / uncaught 500
  • Redis, Qdrant or TimescaleDB unreachable
APM Telemetry Headers
  • x-ace-served-by: direct_passthrough
  • x-ace-warnings: <reason>_passthrough
  • Logged to request_log.error_detail

Prometheus Exporter Specification

Exporter Protocol

ACE exposes standard Prometheus text exposition format (v0.0.4) metrics at /metrics (HTTP GET). This endpoint streams real-time counters and gauges for time-series scraping into Prometheus, Grafana Alloy, OpenTelemetry Collector, VictoriaMetrics, and Datadog. For complete metric dictionary and interactive inspector, see the Prometheus Metrics Console.

Need assistance with enterprise cluster deployment or custom headers? Contact engineering at contact@acefleet.dev