API¶
1. Overview¶
The HTTP surface: a data plane with Anthropic Messages, OpenAI Chat Completions, Responses, and native Bedrock ingresses and an admin plane (health, metrics, key management). Full endpoint contract is in docs/api-reference.md.
2. Components¶
| Component | Path | Purpose |
|---|---|---|
| Data/admin mux | internal/server/server.go |
DataMux / AdminMux wiring (auth, governance, metrics) |
| Anthropic ingress | internal/server/anthropicapi/ |
/v1/messages, /v1/messages/count_tokens, /v1/models. Unknown-model 404 and disallowed-model 403 list the key's allow-filtered available models (ADR-021) |
| OpenAI ingress | internal/server/openaiapi/ |
/v1/chat/completions, /v1/models (same discoverable 404/403, ADR-021) |
| Responses ingress | internal/server/responsesapi/ |
POST /v1/responses; native Responses and supported stateless text/tool adapters, with shared admission and routing controls (ADR-044/047) |
| Bedrock ingress | internal/server/bedrockapi/ |
POST /model/{modelId}/invoke, /invoke-with-response-stream (AWS eventstream framing), /count-tokens (local HTTP-200 estimates on policy/storage refusal; invalid credentials can return 401; camelCase inputTokens) — Claude Code's native CLAUDE_CODE_USE_BEDROCK=1 + ANTHROPIC_BEDROCK_BASE_URL + CLAUDE_CODE_SKIP_BEDROCK_AUTH=1 mode; bearer credential resolves through the same KeyAuth; AWS-shaped errors ({"message"} + X-Amzn-ErrorType); bedrock-serving targets only (ADR-024) |
| Native Converse | internal/server/bedrockapi/, providers/bedrock/native_converse.go |
/model/{modelId}/converse and /converse-stream; native JSON/named AWS frames, common admission and audit; eligible Runtime targets only (ADR-052). |
| Governed comparison | internal/server/evaluation.go |
POST /admin/evaluations in mayu; full admin plus separate virtual key; non-streaming data-handler execution, semantic outcome, integer cost and uncertainty evidence (ADR-052). |
| Usage API | internal/server/usageapi/usage.go |
GET /v1/usage — data-plane, KeyAuth; the caller's own effective governance state (per-key + team budget/quota, integer µUSD, unlimited dims null); team_budget_day/key_budget_day ("window":"calendar-day") are additive — team_budget/key_budget keep their "calendar-month" window string unchanged, so existing clients are unaffected. Never echoes key_id (ADR-021) |
| Admin keys API | internal/server/adminapi/keys.go |
issue / list / revoke virtual keys (per-team entitlement + admin audit, ADR-004) |
| Admin teams/users API | internal/server/adminapi/teams.go |
GET /admin/teams (any AdminAuth identity); PUT/DELETE /admin/teams/{name} upsert/delete a team governance record (full-admin only, ADR-016 D3) — enforced dynamically in the request hot path via Governor.SetTeamLookup, no restart; GET /admin/users derived read-only owner projection (no users table, no per-user spend) |
| Whoami API | internal/server/adminapi/whoami.go |
GET /admin/whoami secret-free resolved identity (subject/teams/is_admin/auth_method) for self-service key issuance (ADR-010) |
| Admin key console | internal/server/adminui/ |
/admin/ui/ embedded static console (data-free, unauthenticated; data via /admin/keys, ADR-001). SSO login button + browser-side OAuth2 Authorization Code + PKCE when oidc.login_origins is set (ADR-026) |
| Auth config | internal/server/server.go (AuthConfigView) |
GET /admin/auth/config unauthenticated, secret-free {sso, issuer?, client_id?} the console SPA reads to decide whether to show the SSO button (ADR-026); 404 when OIDC absent, {sso:false} when login_origins unset |
| Config view/write API | internal/server/configapi/ |
GET /admin/config read-only topology (ADR-005); PUT/DELETE /admin/providers/{name} + PUT/DELETE /admin/models/{name} UI-write (ADR-008; 405 unless provider_store enabled; full-admin only — same tier as the probe, since a provider write persists the base_url live traffic sends the resolved key to, review/fable5 S2; base_url, when present, must parse as an absolute http(s) URL) — a model write's optional aliases field is validated for a within-write duplicate at parse time and for a cross-model collision at the assembly's writeMutation (ADR-021 follow-up: providerstore alias support); GET /admin/config/export secret-free Git export |
| Connection probe | internal/server/configapi/probe.go |
POST /admin/providers/test — tests a draft provider (ProviderWrite body, refs only) by resolving the ref server-side and probing the upstream via the provider's HealthChecker (ADR-014). Full-admin only; SSRF-guarded (metadata endpoint blocked at dial time, optional probe.allowed_hosts); stateless (status cached client-side in memory (no sessionStorage; data-free invariant)). 405 unless provider_store enabled. Returns {ok, latency_ms, detail} (sanitized) |
| Model catalog | internal/server/configapi/catalog.go |
GET /admin/providers/catalog?type=<t> — embedded known-model ids for the console typeahead (ADR-014); advisory (unknown type ⇒ empty, never blocks a save) |
| Provider store | internal/providerstore/ |
opt-in DB-authoritative provider/model topology (ADR-008); refs only (no secret column), durable seed marker, Postgres-portable DDL; a model's aliases are stored per model (model_aliases), not per fallback-chain target, and flow through the same Router.Canonical/RBAC-before-canonicalization path as a config-file alias (ADR-021 follow-up) |
| Audit verify API | internal/server/auditapi/ |
GET /admin/audit/verify per-sink hash-chain check (ADR-003 #2); complete-prefix, 16 MiB cap |
| Budget alerts API | internal/server/adminapi/alerts.go |
GET /admin/alerts/recent (full-admin only, D5b/ADR-017) — recent-fires ring (last 50) of the budget-alert webhook emitter (internal/alert.Notifier); per-instance state |
| Provider health API | internal/server/configapi/health.go |
GET /admin/providers/health (full-admin only, ADR-014 deferred item) — periodic background prober's current per-provider status snapshot (configapi.HealthStore); nil when provider_health_check is not configured; on-demand POST /admin/providers/test (D2) is unaffected |
| Logs + body API | internal/server/analyticsapi/logs.go, internal/server/adminapi/bodies.go |
GET /admin/logs (full-admin only, D4/ADR-018) recent request events (id-keyset paginated, body_ref marks a captured body); GET/DELETE /admin/bodies/{ref} (full-admin only) fetch/erase a captured body — GET emits body_accessed (deduped 5m/viewer), DELETE emits body_deleted, both carry record_ref never body_ref; purged/erased/undecryptable → 410 tombstone, never 500 |
| Metrics endpoint | internal/server/metricsapi.go |
unauthenticated Prometheus /metrics |
| OpenAI conversion | internal/openai/convert.go |
OpenAI ⇄ canonical request/response/chunk |
| CP usage telemetry | internal/controlplane/usage.go |
inferplaned (ADR-036): POST /v1alpha1/usage data-plane window batches (bearer; Validate→400 permanent, oversize→413, store-down→503 — the ack means stored, mayu's FIFO retries); GET /v1alpha1/usage grouped queries (team/user/model filters, group_by, integer µUSD, degraded:true on memory fallback); GET /v1alpha1/usage/export streamed CSV/JSON (required since/until, per-write deadline outlives the 30s WriteTimeout) |
| CP usage charts | internal/controlplane/usage_charts.go |
Authenticated GET /v1alpha1/usage/charts (ADR-051), required since/exclusive until, positive range ≤366 days. Integer total_micro_usd, UTC days[{day,spent_micro_usd}], top-eight models[{key,spent_micro_usd}], other_micro_usd, model_count. Retained telemetry only, no user/team identifiers or admission-credit balance. 400 invalid range; 503 cursor/store failure without a successful partial chart; no-store success. |
| CP credential broker | internal/controlplane/broker.go |
inferplaned (ADR-040): POST /v1alpha1/credentials — request {dataplane, provider:"bedrock"} (1 MiB cap), response {accessKeyId, secretAccessKey, sessionToken, expiration} + Cache-Control: no-store. Mounted ONLY when INFERPLANED_BROKER_ROLE_ARN is set (unset ⇒ 404, byte-identical default). MACHINE channel: its own INFERPLANED_BROKER_TOKEN compared constant-time, not the shared authn — a JWT-shaped bearer is 403'd unverified, so a browser SSO session can never mint AWS credentials. AssumeRole with DurationSeconds 3600 (role-chaining cap), RoleSessionName/SourceIdentity/dataplane tag = the caller's id sanitized to [\w+=,.@-]≤64; a refused SourceIdentity retries once with tags only. Empty dataplane or a non-bedrock provider → 400; STS failure or an incomplete result → 502 with a FIXED body (the STS text is logged, never returned) |
| CP policy API | internal/controlplane/policies.go |
inferplaned (ADR-038): GET /v1alpha1/policies (heartbeat bearer or console OIDC) lists the distributed document set with writable; PUT/DELETE /v1alpha1/policies/{name} persist + hot-apply (405 unless INFERPLANED_POLICY_DSN). Writes (PUT/DELETE) take a DEDICATED bearer — INFERPLANED_POLICY_WRITE_TOKEN (constant-time) or a console OIDC identity; the heartbeat INFERPLANED_TOKEN is 403'd on writes because it is deployed to every data plane (review/fable5 §08 B1). Unset write token ⇒ every static bearer is refused on writes. Every mutation logs actor/op/name/content-sha256 (SetMutationLog, default stdout). |
| CP account correlation | internal/controlplane/policies.go |
The same authenticated policy-list response optionally appends identity_bindings: [{reported_ref, account_ref, kind}] from the immutable identity declaration (ADR-050). Omitted without bindings; no raw issuer, subject or IdP groups. Cache-Control: no-store. References are authorized declaration metadata for exact financial selectors, not a user registry, identity proof or financial-account rewrite. |
| CP config export / import | internal/controlplane/export.go, import.go |
inferplaned: GET /v1alpha1/config/export — the distributed policy set as multi-doc GovernancePolicy YAML (round-trips through the policy loader; secret-free; Content-Disposition attachment). POST /v1alpha1/config/import — replace-mode restore of that format into the policy store: dry_run=true returns the per-document diff (added/changed/removed/unchanged) and result_generation; apply requires expected_generation (409 on mismatch) and allow_empty=true for an empty bundle; whole bundle validated before one policystore.Replace transaction; authnWrite (heartbeat token 403); 405 without a store; 64 MiB YAML transport cap; the worst-case sync size is measured componentwise with early exit and must fit the 8 MiB data-plane decode bound (both sync modes and request-driven authority items included; exceeding it ⇒ 400). The request-driven allowance assumes ASCII-safe/hex authority request identifiers, as real data-plane clients generate local nonces; escaping-heavy identifiers can exceed only the sending data plane’s own decode bound. Restricting authority request identifier charset in pgstore validation is a recorded follow-up. Policies only — never ledger, usage or identity config |
| CP operations console | internal/controlplane/ui/ |
inferplaned /ui/ operations dashboard: overview, precise usage/export, recent data-plane generation/rejections, reviewed policy CRUD (existing server write authority). Data-free unauthenticated shell, separate read/write credentials in JS memory only, strict self CSP; unavailable/degraded evidence is explicit. SSO login button + browser-side OAuth2 Authorization Code + PKCE when INFERPLANED_OIDC_LOGIN_ORIGINS is set (ADR-037, ported from ADR-026 — inferplaned has no config file, so the opt-in switch is fixed env vars, not a config field) |
| CP auth config | internal/controlplane/authconfig.go |
inferplaned (ADR-037): GET /ui/auth/config unauthenticated, secret-free {sso, issuer?, client_id?} — mirrors /admin/auth/config (ADR-026); mounted only when INFERPLANED_OIDC_ISSUER/INFERPLANED_OIDC_CLIENT_ID are set, else 404 |
| CLI login API | internal/server/authapi/ |
Data-plane, opt-in (oidc.cli_login.enabled, ADR-028): GET /v1/auth/config unauthenticated secret-free {cli, issuer?, client_id?}; POST /v1/auth/key (OIDC bearer, DISTINCT client_id/Verifier from the console's) mints a short-lived key — server decides expires_at/owner, never the client; per-subject mint rate limit; DELETE /v1/auth/key (ordinary KeyAuth) self-revoke, used by mayu logout. mayu login/token are the CLI counterpart |
3. Key Decisions¶
- After successful key authentication,
count_tokensreturns 200 even on routing/readiness refusals (aliases are canonicalized here too, ADR-021). Missing or invalid keys still return 401 before the count handler. - Context-window preflight failures on Messages, Chat Completions and Bedrock Invoke/Converse return HTTP 400 with
prompt is too long: N tokens > M maximuminside the ingress's own error envelope. Claude Code recognizes that wording for compaction rather than generic model fallback and can extract the overage.Nis the gateway's content-aware estimate (internal/tokenest), not an upstream tokenizer result: decoded JSON text is split into character-class runs with calibrated rates (within 3% of o200k_base for English prose, code and digit-heavy text, about 9% over o200k_base for natural Korean, under for random-looking syllables and hex), each inline image at a recognized content position counts 1,600 tokens, and other base64 payloads (documents, signatures, opaque state) count four bytes per token. Claude's tokenizer read natural Korean at about 1.7× this estimate (measured 2026-11-02); an under-estimate reaches the upstream, whose overflow is answered as the recoverable prompt-too-long error. bytes/4 over-estimated English prose by about 60%, so it refused prose above about 62% of the window (a measured request of about 97k tokens was refused against a 128k window), and under-estimated digits, hex and random-looking syllables. The same estimate is the localcount_tokensanswer (a Bedrock count estimates its decodedinput.invokeModel.body), except for a count that does not fit the in-flight byte budget, which is answered at four bytes per token (below); it is also the governance PreCheck admission estimate (TPM reservation and token-quota pre-check) and the explicitly requested Responses model's routing capacity. Monetary reservations, privacy inspection and conservative automatic/strict-target capacity checks are unchanged. Borderline requests reach the upstream, whose recognized overflow is mapped to the same recoverable error. A client's displayed window and the gateway declaration remain independent. Compaction may still fail if the remaining input does not fit; do not assume a larger model window. - Verbatim body forwarding on protocol match; canonical conversion only on mismatch. Model aliases (config
models.<name>.aliases, ADR-021) are normalized to the canonical name BEFORE RBAC/routing/audit/metrics; on the anthropic verbatim path the only body change is a cache-safe top-levelmodelrewrite (nestedcache_controlpreserved, HTML escaping off). - Errors are returned in the ingress protocol's own error shape; the unknown/disallowed-model messages append the allow-filtered available-model list (ADR-021).
- A body over
server.max_request_bytesgets HTTP 413 in the ingress's own envelope, whether or not Content-Length was declared: Anthropicrequest_too_large, Chat Completions and Responsesinvalid_request_error, BedrockValidationException(it used to be{"error":"request body too large"}, or a 400 when the length was undeclared). Count endpoints still answer 200 with a local estimate. Withserver.max_inflight_request_bytesset, a generation request that does not fit the in-flight byte budget gets 503 withRetry-After: 2in the same envelopes (Anthropicoverloaded_error, Chat Completions and Responsesserver_errorwith codegateway_overloaded, BedrockServiceUnavailableException); a count request that does not fit is answered locally (HTTP 200, four bytes per token) without buffering its body. - Model-level fallback (ADR-029, D5).
model_fallbacks(config, requested → served model, one hop) plus a default same-family heuristic (model_fallback_family, default on: an unroutedclaude-opus-5falls back to the highest configuredclaude-opus-*version below it) substitutes a served model for an unconfigured requested one BEFORE the allow-list check — a key allowed only the requested name is denied, never silently downgraded. A configured model whose upstream 404s also crosses to the fallback model within the existing priority-fallback loop; the ingress re-checks RBAC on that cross-model target (router.FilterModelAllowed) since it was appended after the original allow-list check. Either path setsx-inferplane-model-fallback: <served model>on the response (independent of the existing per-providerx-inferplane-fallback: <provider>). The unrouted-name substitution (not the upstream-404 cross-model retry, which sets only the header) is also reported by every generation ingress, Responses included: it logsrouter: model fallback source=… requested=… served=…once per distinct pair per process, countsinferplane_model_fallback_total{source,model}, and recordsrouting.reason: model_fallbackwithmodel_fallback_fromandmodel_fallback_sourcein the audit; the served model is unchanged. - Messages retries transport failures, 5xx/429 and cross-model 404 before output starts. Typed validation and other 4xx errors are terminal; a generic Bedrock
ValidationExceptioncannot prove a missing model. Every retry target is checked against its own declared context window before dispatch; the original preflight contract is preserved. A strictly recognized numeric prompt-overflow SDK validation error retains safecontext_length_exceededevidence and recognizable overflow wording; other Bedrock details remain scrubbed. Bedrock Converse (non-Claude models, on the SDK path and the native/converseingress) also recognizes the relayed serving-engine wordingInput length (N) exceeds model's maximum context length (M).(observed 2026-10-27 foropenai.gpt-oss-120b-1:0, with or without the Mantle streaming prefix) and a relayed OpenAI error whose code is exactlycontext_length_exceeded(no counts). Messages, Chat Completions and Bedrock Invoke/Converse then return the recoverableprompt is too long: N tokens > M maximumerror (counts only when the upstream reported them) in their own envelopes, instead ofbedrock upstream error (ValidationException); Responses returnserror.code: context_length_exceeded(typeinvalid_request_error) with its fixed message. - OpenAI-compatible upstream HTTP 400 errors on Messages are converted to Anthropic
invalid_request_errorenvelopes. A nestederror.code=context_length_exceededbecomes the fixed messageprompt is too long; openaicompat additionally keeps validated positive numeric counts for three exact wordings: the Anthropic one (prompt is too long: N tokens > M maximum), the Converse one (Input length (N) exceeds model's maximum context length (M).) and classic vLLM Chat Completions (verified against v0.8.5). The reported total may include requested completion tokens. Unknown, malformed or oversized error envelopes useupstream request rejected, without guessing a context error or echoing provider text. Parsing is limited to 64 KiB. This covers nonstreaming responses and streaming startup; OpenAI-ingress error bytes remain unchanged. -
Budget-tier model substitution (ADR-041,
routing.budgetTiers). A GovernancePolicyroutingsubtype (mutually exclusive withonAffinityConflictandcontext):budgetRefnames a numeric-limit budget rule in the same document; each tier'sthresholdPercent/substitutemap takes effect once that budget rule's utilization — judged GLOBALLY by the control plane from the ADR-034 lease ledger, latched monotone per budget window — crosses the threshold.router.SubstituteTierapplies it at ingress on an ALREADY-routed model (distinct from the D5 seam above, which only ever fires on an UNROUTED one, and the two compose: an unrouted model may first take amodel_fallbackstarget, then that target may itself be tier-substituted). Fires only when the original request already passesAllows, and only if the substitution TARGET also passes RBAC and is routed on this data plane — never widens access, never turns into a denial; a target that fails either check leaves the original model served. Setsx-inferplane-substituted-model: <served model>(a third, independentx-inferplane-*header) and stamps the audit record'smodel_substituted_fromwith the client's original request (model_requestedalready carries the served model, per the convention above). Apply-time validation additionally requires the substitution target to be routed+priced on the enforcing data plane (policy.Store.SetRoutedAndPriced) — an unrouted/unpriced target rejects the whole document via the existing rejection-report channel rather than silently billing 0. -
Budget windows (ADR-042). A GovernancePolicy
budgetrule carries an optionalperiod—CalendarDayorCalendarMonth; omitting it meansCalendarMonth, so every existing document keeps its meaning. It must not be combined withunlimited: true("no cap" has no window). A daily cap and a monthly cap are TWO rules inspec.rules, never two fields on one rule —hardCap/failurePolicy/lease/adminContactare per-rule, so each window carries its own.
4. Code Pointers¶
internal/server/anthropicapi/messages.go— Messages handler, streaming tee, cardinality-safe labelsinternal/server/openaiapi/chat.go— Chat Completions handlerinternal/server/auth.go—KeyAuthvirtual-key resolution
5. Cross-references¶
- Related modules:
internal/router,internal/tier,internal/governance,internal/alert,internal/bodystore,providers/ - Related ADRs: docs/decisions/ADR-016-teams-as-keystore-records.md, docs/decisions/ADR-017-budget-alert-webhooks.md, docs/decisions/ADR-018-opt-in-body-logging.md, docs/decisions/ADR-021-ticket-driven-ux-fixes.md, docs/decisions/ADR-028-cli-oidc-login-short-lived-keys.md, docs/decisions/ADR-029-model-level-fallback.md, docs/decisions/ADR-040-credential-brokering.md, docs/decisions/ADR-041-budget-tier-model-substitution.md, docs/decisions/ADR-042-budget-windows.md
- Related runbooks: docs/runbooks/, docs/runbooks/cli-login.md
Observability rule schema¶
GovernancePolicy.spec.rules[].observability selects governed OTLP export for the
subject: traces (exporter, optional samplePercent 1–100, default 100), audit
(exporter) and content (exporter), at least one of them. It requires FailOpen.
Exporter names refer to the data plane's local observability.exporters ceiling; a
data plane rejects a rule naming an undeclared exporter or a signal that exporter
does not allow, and refuses every such rule when it declares no ceiling. See
governed OTLP export.
Observability console API (mayu only)¶
GET /admin/observability, PUT /admin/observability/limits and
PUT /admin/observability/rules (internal/server/observabilityapi) are mounted
by server.WithFullAdminRoutes, so every route, reads included, needs a full
administrator. Both PUTs take dry_run and expected_revision: a dry run changes
nothing; an apply must name the revision its preview saw (400 invalid_request when
expected_revision is omitted, 409 revision_conflict when it is stale), then persists, installs and appends admin_observability_changed.
The observability routes' own errors are {"error": message, "code": code};
401 and 403 come from the shared admin middleware with its usual bodies (the
authentication_error envelope for 401, {"error":"admin only"} for 403).
/admin/capabilities adds
observability_export. Request and response shapes are in the
API reference.
Policy-routing schema and evidence (ADR-043)¶
GovernancePolicy.spec.rules[].sensitiveData has required onDetected and
onUninspectable (InternalOnly or Block) and explicit internalModels (required
for InternalOnly, no wildcards). It requires FailClosed. routing.context requires
FailOpen, fromModels, simpleModel, complexModel, positive
maxSimpleInputTokens, optional nonblank complexKeywords, and mode Shadow
(default) or Enforce. It is mutually exclusive with affinity/budgetTiers. All
matching privacy rules intersect; Block wins. See full schema contract and
example.
Provider config/admin DTOs accept data_boundary internal/external/unknown; omitted
means unknown. Model DTOs accept nonnegative context_window (zero unknown) and
capabilities from tools/vision/reasoning/structured_output. GET config, PUT
providers/models, and export preserve these fields; unknown enum values reject.
All generation ingresses consume the shared safe chain and return routing refusals
in their own error shape (403). Both count APIs return local HTTP 200 estimates
without upstream calls on privacy refusal, unready/stale governance, and body-size
failures. x-inferplane-routing-reason is bounded;
x-inferplane-routed-model advertises applied privacy/context selection. Budget's
x-inferplane-substituted-model remains earlier-stage evidence. Audit requested,
selected, proposed, and actual-attempt meanings are distinct; see the guide. This
Responses context-window preflight failures also return HTTP 400 with
error.code="context_length_exceeded", without changing admission thresholds
or bypassing authentication. The stateless bridge returns the same 400 when every
compatible target refused the request for its declared context window alone
(router reason context_window_exceeded); it used to answer 403 no
policy-permitted compatible Responses target, which Codex retried unchanged.
Every other no-route reason keeps 403.
Native Responses HTTP failures preserve the upstream status and only the exact
allowlisted recovery codes context_length_exceeded and invalid_encrypted_content.
They appear in error.code and the existing audit outcome.error; error messages,
params and unknown codes remain redacted. Error parsing is bounded to 64 KiB.
No failed request is replayed merely because it carries a recovery code.
This applies before an SSE response is committed and to non-streaming HTTP
failures. Native SSE terminal failures retain these codes through a sanitized
response.failed event, including when the failure is the first event.
Other interruptions retain the existing bounded gateway error event.
Valid native status="incomplete" output and observed usage are passed through
without retry; max_output_tokens is a normal incomplete terminal reason.
Responses admission HTTP 429 uses error.type="rate_limit_exceeded" rather than
the billing-quota type insufficient_quota.
For coded HTTP failures, error.type is invalid_request_error for the two
allowlisted recovery codes. ADR-053 adds bounded gateway stream-failure codes
such as upstream_timeout, stream_incomplete, response_incompatible and
usage_missing when a failure occurs before SSE headers are committed. These
retain the existing gateway error.type="upstream_error"; previously the same
gateway failure path omitted error.code. Clients can use the new code for
more specific diagnostics without treating the gateway failure as an invalid
request. A failure code alone does not authorize replay.
Responses now has a governed POST /v1/responses endpoint (ADR-044). Upgrade binaries/CRD before new-rule activation;
CP privacy before first sync requires require_sync.
ADR-044 adds sensitiveData.onDetected: Mask, optional context
normalModel/maxNormalInputTokens/stability, and budget-tier enforceTargets.
Strict tiers allow threshold 100; their soft budget reference is an accounting
threshold, not a second admission lease. Local session hints cannot authorize
access, and every pin/attempt is revalidated. See fields and Codex setup.
POST /v1/responses accepts the portable text/tool profile on declared Bedrock
Converse, InvokeModel and Mantle bridges as well as the existing Anthropic and
Chat Completions adapters. A neutral reasoning.effort: "none" does not demand
backend effort control. Strict-tool guarantees and opaque reasoning items
retain their native-target requirements. Stored-state references
(previous_response_id, conversation, item_reference input items) are
refused with 400 stored_reference_unsupported before routing on every target:
upstreams resolve them under the gateway's shared credential, and the gateway has
no per-user ownership evidence. Clients resend the full input instead.
The OpenAI-shaped GET /v1/models adds these client metadata fields:
| Field | Meaning |
|---|---|
capabilities |
Configured model capabilities, including tools when that deployed route is qualified for tool use. |
responses_mode |
native, bridge, or unsupported, conservatively checked across the resolved target chain. |
codex_model |
Native-only client catalog binding derived from the public model name, never from a private deployment ID. |
context_window, max_model_len |
Existing declared context bounds; omitted when undeclared. |
Discovery remains filtered by the authenticated key's model permissions. Inference rechecks capabilities, privacy, budgets and target compatibility.
Local monetary usage (ADR-054)¶
Local budget objects in GET /v1/usage optionally add reserved_usd_micros
for outstanding per-attempt liability and accounting_uncertain for frozen
accounting. spent_usd_micros remains observed cost; remaining credit includes
reservations and is zero when frozen. Missing final usage retains liability
instead of inventing zero actual cost. These counters reset on process restart.
Durable budget heartbeat (ADR-045)¶
POST /v1alpha1/sync negotiates authority.protocol: escrow-v1 when
INFERPLANED_DURABLE_BUDGETS=true. It requires the non-JWT machine bearer;
console OIDC cannot issue budget grants. The response carries a complete policy
bundle, database-owned window definitions, grants and report/meter acknowledgments.
Legacy requests receive 409; durable clients reject legacy or invalid responses.
Each ActiveTier preserves team/user scope. /readyz includes a live authority
DB check. See wire contract and
operator configuration.
Shared gateway profile (ADR-046)¶
key_store.type: postgres plus governance_store.type: postgres enables shared
identity and atomic resource admission. The heartbeat carries an authority
namespace; shared gateways verify it and request no local grants. User-scoped rate
and tokenQuota day/month rules require this profile. GET /v1/usage returns
enforcement_mode: shared and subject-scoped shared_limits. Key CLI commands
accept --config; keys import --sqlite ... --config ... preserves raw identities
and revocation tombstones. See shared governance.
Analytics evidence (ADR-053)¶
internal/server/analyticsapi/ serves full-admin summary, timeseries, logs,
health and the existing Mode-B rebuild route. internal/analytics/evidence.go
is the shared classifier for SQLite and PostgreSQL projections. Summary
Totals, team/model rows and day points add operations; events add
has_cost, outcome_state, cost_state, partial, bounded error_code
and projection_version. No route, method or authorization tier changes.
operations.completed counts observed request_completed records, including
Cost=nil. Legacy requests/tokens/amounts still include only Cost-present records.
The four outcome states and five cost states are exclusive within each class;
transport success is independent of cost evidence. The API's integer estimates
are not invoices, task-quality measures or admission credit.
Read the field contract and additive examples, operational denominator and source recovery. Mode-B migration/backfill is transactional; it does not replay source records skipped beyond old checkpoints. A zero legacy-projection count does not attest complete history. Neither profile's analytics changes financial authority.
Audit console (ADR-057)¶
Five read-only routes over the console projection of the analytics index. All
require full-admin authorization: no token gives 401 and an authenticated
non-admin receives 403. They are mounted only when the analytics index is
enabled and its store implements the console querier (SQLite Mode A and
PostgreSQL Mode B both do); otherwise they are absent, and
GET /admin/capabilities reports audit_console: false. The same endpoint
returns trace_link_template when otel.trace_link_template is set. Only GET
is accepted (405 method_not_allowed), and responses are no-store.
GET /admin/audit/events?kind=policy_denied,upstream_error&limit=100
GET /admin/audit/events/{id}
GET /admin/audit/sessions?since=2026-10-29T00:00:00Z
GET /admin/audit/sessions/{ref}?after=<event-id>
GET /admin/audit/summary
| Route | Returns |
|---|---|
/admin/audit/events |
{"events":[...],"next":"<id>"}, newest first; pass next as before for the following page. A governance denial row also carries gov_scope, gov_limit and gov_source, the names in its record's governance; other rows and denials recorded before that field omit them. |
/admin/audit/events/{id} |
{"event":{...},"record":{...},"hash":"sha256:...","waterfall":{...}}. record is the stored audit line. hash is the SHA-256 of that line, shown as the record's chain position; it is not a chain verification (use /admin/audit/verify). waterfall is the timing timeline derived from latency and attempts (rows of kind gateway, attempt, settle or denied; no rows when the record has no latency). |
/admin/audit/sessions |
{"sessions":[...],"next":"<id>"}, one row per session_ref (records without one are not sessions) with request, denial, PII, auto-mode-block and subagent counts, cost and first/last time. |
/admin/audit/sessions/{ref} |
{"events":[...],"next":"<id>"}, oldest first, for one s1: session digest; only after and limit are applied. |
/admin/audit/summary |
Counts by kind, top 10 categories, PII counts (masked, internal, blocked, shadow), top 50 denials (each with code, policy, scope, limit, source, team, key_id, count and last_ts; scope, limit and source are set only when the record names its governance limit), auto mode counts (block, allow, unknown, signatures), 12 equal time buckets and coverage_start_ms, the earliest time in the index. |
Query parameters, shared by the list routes and the summary. Every value is validated; a bad value gives the closed 400 code in the last column.
| Parameter | Rule | Code |
|---|---|---|
since, until |
RFC 3339 times. Default window is the last 24 hours; with only until, since is 24 hours earlier. since must precede until, and the window may not exceed 366 days. A + UTC offset must be sent as %2B in a raw URL, otherwise it decodes to a space. |
invalid_range |
limit |
Integer from 1 to 200 (500 on /sessions/{ref}); default 100. |
invalid_limit |
before, after |
A 26-character event ID (ULID). | invalid_cursor |
kind |
Comma-separated subset of ok, policy_denied, privacy_blocked, privacy_masked, privacy_internal, upstream_error, auto_mode_blocked. |
invalid_kind |
session |
s1: followed by 32 lowercase hex digits. |
invalid_filter |
trace_id |
32 lowercase hex digits. | invalid_filter |
code, class, category |
[a-z0-9_], 1 to 64 characters. |
invalid_filter |
team, key_id, model, provider |
At most 256 bytes. model matches the requested or the actual model. |
invalid_filter |
pii=true |
Only rows with PII evidence. | none |
auto=true |
Only rows whose auto mode verdict is block or unknown. allow verdicts are counted in the summary but never listed. |
none |
A malformed {id} or {ref} gives 400 invalid_id; an unknown event gives 404
not_found. A storage failure gives 500 query_failed with no detail. Error
bodies are {"error":"<code>"} and never echo query input.