Skip to content

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_tokens returns 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 maximum inside the ingress's own error envelope. Claude Code recognizes that wording for compaction rather than generic model fallback and can extract the overage. N is 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 local count_tokens answer (a Bedrock count estimates its decoded input.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-level model rewrite (nested cache_control preserved, 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_bytes gets HTTP 413 in the ingress's own envelope, whether or not Content-Length was declared: Anthropic request_too_large, Chat Completions and Responses invalid_request_error, Bedrock ValidationException (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. With server.max_inflight_request_bytes set, a generation request that does not fit the in-flight byte budget gets 503 with Retry-After: 2 in the same envelopes (Anthropic overloaded_error, Chat Completions and Responses server_error with code gateway_overloaded, Bedrock ServiceUnavailableException); 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 unrouted claude-opus-5 falls back to the highest configured claude-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 sets x-inferplane-model-fallback: <served model> on the response (independent of the existing per-provider x-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 logs router: model fallback source=… requested=… served=… once per distinct pair per process, counts inferplane_model_fallback_total{source,model}, and records routing.reason: model_fallback with model_fallback_from and model_fallback_source in 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 ValidationException cannot 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 safe context_length_exceeded evidence and recognizable overflow wording; other Bedrock details remain scrubbed. Bedrock Converse (non-Claude models, on the SDK path and the native /converse ingress) also recognizes the relayed serving-engine wording Input length (N) exceeds model's maximum context length (M). (observed 2026-10-27 for openai.gpt-oss-120b-1:0, with or without the Mantle streaming prefix) and a relayed OpenAI error whose code is exactly context_length_exceeded (no counts). Messages, Chat Completions and Bedrock Invoke/Converse then return the recoverable prompt is too long: N tokens > M maximum error (counts only when the upstream reported them) in their own envelopes, instead of bedrock upstream error (ValidationException); Responses returns error.code: context_length_exceeded (type invalid_request_error) with its fixed message.
  • OpenAI-compatible upstream HTTP 400 errors on Messages are converted to Anthropic invalid_request_error envelopes. A nested error.code=context_length_exceeded becomes the fixed message prompt 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 use upstream 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 GovernancePolicy routing subtype (mutually exclusive with onAffinityConflict and context): budgetRef names a numeric-limit budget rule in the same document; each tier's thresholdPercent/substitute map 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.SubstituteTier applies 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 a model_fallbacks target, then that target may itself be tier-substituted). Fires only when the original request already passes Allows, 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. Sets x-inferplane-substituted-model: <served model> (a third, independent x-inferplane-* header) and stamps the audit record's model_substituted_from with the client's original request (model_requested already 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 budget rule carries an optional period — CalendarDay or CalendarMonth; omitting it means CalendarMonth, so every existing document keeps its meaning. It must not be combined with unlimited: true ("no cap" has no window). A daily cap and a monthly cap are TWO rules in spec.rules, never two fields on one rule — hardCap/failurePolicy/lease/adminContact are per-rule, so each window carries its own.

4. Code Pointers

  • internal/server/anthropicapi/messages.go — Messages handler, streaming tee, cardinality-safe labels
  • internal/server/openaiapi/chat.go — Chat Completions handler
  • internal/server/auth.go — KeyAuth virtual-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.