API Reference¶
inferplane exposes two HTTP planes: the data plane (:8080, client traffic) and
the admin plane (:9090, operations). Generation and model discovery authenticate
with a virtual key (ik_...). Count and opt-in login bootstrap endpoints have the
specific exceptions documented below.
Data Plane (:8080)¶
Authentication¶
Send the virtual key the way the client protocol expects:
- Anthropic ingress: x-api-key: ik_... (Claude Code sends this when ANTHROPIC_API_KEY=ik_...).
- OpenAI ingress: Authorization: Bearer ik_....
The gateway resolves the key to a Principal (team + allowed models) and never forwards
the client key upstream; it injects the upstream provider credential itself.
Anthropic ingress¶
| Endpoint | Notes |
|---|---|
POST /v1/messages |
Messages API; streaming via SSE when "stream": true. Body forwarded verbatim to an Anthropic-protocol upstream. |
POST /v1/messages/count_tokens |
Token counting. Returns 200 after successful key authentication; routing, policy, readiness and storage refusals use local counting. Missing or invalid keys return 401. |
GET /v1/models |
Lists models the principal may use (negotiated by anthropic-version). |
OpenAI ingress¶
| Endpoint | Notes |
|---|---|
POST /v1/chat/completions |
Chat Completions; streaming via SSE when "stream": true. Converted via the canonical schema when the upstream protocol differs. |
POST /v1/responses |
Native Responses or supported stateless text/tool bridging; see Responses and adaptive routing below. |
GET /v1/models |
Lists models the principal may use. |
Bedrock-shaped ingress¶
POST /model/{modelId}/invoke
POST /model/{modelId}/invoke-with-response-stream
POST /model/{modelId}/converse
POST /model/{modelId}/converse-stream
POST /model/{modelId}/count-tokens
These gateway routes use virtual-key authentication (x-api-key or Bearer),
not client AWS SigV4 identity. URL-encode the configured model identifier.
Generation uses the common admission/routing controls; streaming uses the
Bedrock response shape. Count requests retain the local HTTP-200 contract
when readiness, routing or storage prevents generation. Missing or invalid
virtual keys can still return 401.
Provider credentials and AWS signing are applied separately on egress.
Native Converse preserves JSON bytes and named AWS eventstream frames, including trailing usage metadata. It does not wrap Converse events as InvokeModel chunks or apply the Anthropic adapter's parameter rewrites. Mandatory guardrail insertion and verified policy masking remain exceptions. A conflicting guardrail is refused. Only configured Bedrock Runtime targets qualify; Mantle and SDK endpoint overrides do not qualify. FIPS and dualstack selections are respected. Non-default service tiers and optimized latency are rejected until separately priced. Unknown cache TTL and unqualified OpenAI cached-input accounting remain uncertain. See the native contract and evaluation guide.
Usage¶
GET /v1/usage returns the authenticated principal's usage view. Shared mode
includes enforcement_mode: shared and subject-scoped shared_limits; used
includes reservations/uncertainty, and reserved identifies the retained
portion. This is separate from control-plane telemetry ingestion.
CLI login (opt-in, ADR-028)¶
Only mounted when oidc.cli_login.enabled is set — 404 otherwise. Lets
mayu login mint a short-lived virtual key instead of a human copying
one out of the console. See docs/runbooks/cli-login.md.
GET /v1/auth/config # unauthenticated; {cli, issuer?, client_id?}
POST /v1/auth/key # Authorization: Bearer <IdP ID token>; {"team"?: "..."} -> {key, key_id, team, expires_at}
DELETE /v1/auth/key # x-api-key: <the minted key>; self-revoke, used by `mayu logout`
expires_at/owner are always server-decided — a client cannot request a
longer-lived key.
Admin Plane (:9090)¶
Unauthenticated¶
GET /healthz # liveness
GET /readyz # readiness
GET /metrics # Prometheus exposition (no secret/key_id labels)
Token-authenticated (/admin/keys)¶
Authenticate with the admin token (Authorization: Bearer <INFERPLANE_ADMIN_TOKEN>).
POST /admin/keys # issue a virtual key (plaintext returned once)
GET /admin/keys # list key metadata (never plaintext)
DELETE /admin/keys/{id} # revoke a key
Analytics outcome and cost evidence (ADR-053)¶
These existing routes require full-admin authorization. No token gives 401; an authenticated team-mapped non-admin receives 403. The static console remains data-free and public. When analytics is disabled, these routes are unavailable.
GET /admin/analytics/summary?since=2026-10-21&until=2026-10-21
GET /admin/analytics/timeseries?days=30
GET /admin/logs?limit=200&before=<event-id>
GET /admin/analytics/health
POST /admin/analytics/rebuild
Summary accepts inclusive since/until dates in YYYY-MM-DD form. Invalid
or reversed dates and endpoint differences greater than 366 days return 400;
a difference of 366 days includes 367 dates. Omitted since defaults to UTC
today minus 30 days, which includes 31 dates through today. When until is
omitted, validation uses UTC today but the store receives an empty upper bound,
so no upper date cutoff is applied. Supply both dates for a reproducible
bounded report; for 30 included dates ending 2026-10-21, use
since=2026-09-22&until=2026-10-21.
Timeseries reads only days; since and until are ignored. Missing, invalid
or nonpositive days defaults to 30; values above 366 are clamped to 366.
It applies an inclusive cutoff of today minus days, orders existing dates
newest first, and returns at most days rows. There is no upper date cutoff
or zero-filling: sparse history can include the extra cutoff date. Mode A's
cutoff uses UTC; Mode B uses database current_date, whose date depends on
the database session timezone. days=30 therefore does not promise exactly
the same period as an explicit 30-date summary.
Logs keep the existing newest-first ID keyset order and maximum page size of 200.
They now include every observed request_completed, including records with no Cost.
Rebuild is supported in PostgreSQL Mode B: 204 acknowledges the derived-state
reset; the aggregator then replays retained source segments. Mode A returns 405.
See recovery procedures.
operations is additive in summary totals, team/model rows and each day point.
Its unit is an observed completion record, not all traffic, unique user
requests, sessions or model-task quality. request_started refusals,
unauthenticated 401s, admin events and requests never sent by the client are
excluded. Retry/fallback record granularity can differ by ingress.
Outcome precedence is: Partial=true → partial; missing Outcome or status outside
200–599 → unknown; valid status with a nonempty Outcome.Error or status ≥300 →
failed; remaining 2xx → succeeded. Partial is counted once, separately from
failed. The observed failure proportion is (failed + partial) / completed;
show — for completed=0 and disclose unknown outcomes beside the ratio.
| Cost state (precedence order) | Meaning |
|---|---|
unavailable |
Cost=nil, including old costless source records; no amount is known |
unpriced |
Cost.PricingMissing; a zero numeric field is not a zero-price estimate |
legacy_unknown |
Cost exists but Usage or Usage.Evidence is missing |
uncertain |
Partial, Usage.Estimated, a false InputKnown/OutputKnown/InputTotalKnown/CacheWriteTTLKnown flag, AccountingUncertain, or a negative amount |
estimated |
Remaining records with usage/pricing evidence; zero is a valid estimate |
Cost state is independent of outcome: HTTP 400 may have an estimate. Even
estimated is a rate-based estimate, not the supplier's final invoice.
estimated_cost_micros sums estimated records;
uncertain_recorded_cost_micros sums uncertain + legacy_unknown recorded amounts.
All amounts remain integer microUSD. Existing requests, token and cost_micros
totals retain their Cost-present (has_cost=true) membership and arithmetic;
new operational counts do not redefine chargeback or budget settlement.
The eight-record acceptance fixture has the following totals excerpt. The same
operations object appears in its one team row, model row and day point.
{
"requests": 6,
"cost_micros": 3350,
"operations": {
"completed": 8,
"succeeded": 4,
"failed": 2,
"partial": 1,
"unknown": 1,
"estimated_requests": 3,
"uncertain_requests": 1,
"unpriced_requests": 1,
"unavailable_requests": 2,
"legacy_unknown_requests": 1,
"estimated_cost_micros": 1300,
"uncertain_recorded_cost_micros": 2050,
"legacy_projection_records": 0
}
}
completed equals both the sum of the four outcome counts and the sum of the five
cost-state counts. Costless-only teams/models/days can have legacy requests and
amounts of zero while their operations are nonzero. legacy_projection_records
counts version-0 rows awaiting source replay. Zero does not prove all historical
records were collected: retention, queue drops, ingestion lag and missing
completion records still limit coverage.
GET /admin/logs returns {"events":[...]}. Existing event fields keep their
names; has_cost, outcome_state, cost_state, partial, error_code and
projection_version are additive. Empty error_code is omitted. This event
excerpt represents an HTTP-200 partial response:
{
"status": 200,
"cost_micros": 2000,
"has_cost": true,
"outcome_state": "partial",
"cost_state": "uncertain",
"partial": true,
"error_code": "stream_incomplete",
"projection_version": 1
}
For unavailable/unpriced records, do not render the numeric zero as a known dollar
amount. Missing operations or cost_state from an older API means evidence is
unavailable, rather than success or zero cost. Bounded stream reasons and
coverage limits
apply; raw error text is not added to this API.
Governed model comparison (mayu only, ADR-052)¶
POST /admin/evaluations requires full administrator authorization and a separate
X-Inferplane-Virtual-Key. Its JSON body is
{"protocol":"bedrock-converse","model":"configured-name","body":{"messages":[...]}}.
Protocols are bedrock-converse, anthropic, openai, and responses; for the
last three, body.model must equal model. No URL or provider credential is accepted.
The bridge makes one non-streaming request through the ordinary data-plane handler,
including virtual-key authorization, routing, admission and audit. Stream/background
requests are rejected. Request bodies use the configured gateway limit; the bridge
itself is bounded at 64 MiB and browser-visible responses at 4 MiB.
The outer HTTP response describes bridge authorization/validation. A successful
bridge returns the inference http_status, semantic outcome, response_success,
stop_reason, duration_ms, output, usage evidence, cost/pricing version,
accounting_known, attempts and unsettled_attempts. ttft_ms is null:
non-streaming E2E output tokens/second is not decode throughput. Refusals and
truncations can have inference HTTP 200 but response_success:false.
Known accounting requires one attempt and one complete settlement with sufficient
usage and a configured price; it does not mean an AWS invoice was reconciled.
After fallback, a reported final settlement is not a known total for all attempts.
Responses are no-store. The control-plane binary never handles these inferences.
Audit console (ADR-057)¶
These read-only routes require full-admin authorization (401 without a
token, 403 for a non-admin) and are mounted only when the analytics index is
enabled; GET /admin/capabilities reports audit_console. Only GET is accepted.
GET /admin/audit/events?kind=policy_denied&limit=100&before=<event-id>
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
List routes take since/until (RFC 3339, default last 24 hours, at most 366
days; encode a + offset as %2B), limit (1-200, default 100), the
cursors before/after, and the filters kind, team, key_id, model,
provider, code, class, category, session, trace_id, pii=true and
auto=true (block and unknown auto mode verdicts only). Errors are
{"error":"<code>"} with 400 invalid_range, invalid_limit, invalid_cursor,
invalid_kind, invalid_filter or invalid_id, 404 not_found, 405
method_not_allowed or 500 query_failed. The event detail response includes
hash, the SHA-256 of the stored audit line, which shows the record's chain
position and is not a verification. See the
field contract and
record fields.
Observability console (mayu only)¶
Full administrators only, reads included (team-mapped identities get 403 and an
admin_denied record). Counters are per process since counters_since.
instance names the answering process with the audit chain's instance id:
runtime limits, console rules and counters belong to that process, so with
several replicas each one answers for itself
(scope).
GET /admin/observability:
{
"configured": true,
"instance": "mayu-0-01JC8X3V4N5Q6R7S8T9V0WXYZA",
"policy_source": "files",
"service_name": "inferplane",
"counters_since": "2026-10-29T10:00:00Z",
"limits": {"editable": true, "revision": "sha256:4f8b…"},
"rules": {"editable": true, "revision": "sha256:5f1c…", "content_rules": false, "console": [
{"name": "console-eng", "subject": {"team": "engineering"}, "audit": {"exporter": "corp"}}
]},
"exporters": [{
"name": "corp", "endpoint": "otel-collector.internal:4318", "protocol": "http", "tls": true,
"header_names": ["authorization"], "signals": ["audit", "traces"], "effective_signals": ["audit", "traces"],
"queue_limit_bytes": 67108864, "queued_bytes": 0, "limit": {},
"stats": [{"signal": "audit", "outcomes": {"sent": 12, "dropped_queue_full": 0, "dropped_oversize": 0,
"dropped_export_error": 0, "withheld_limit": 0, "sampled_out": 0}, "last_success_at": "2026-10-29T10:05:00Z"},
{"signal": "traces", "outcomes": {"sent": 30, "dropped_queue_full": 0, "dropped_oversize": 0,
"dropped_export_error": 0, "withheld_limit": 0, "sampled_out": 9}, "last_success_at": "2026-10-29T10:05:01Z"}]
}],
"decisions": [{"signal": "traces", "decisions": {"selected": 39, "no_rule": 3, "content_conflict": 0, "withheld_limit": 0}},
{"signal": "audit", "decisions": {"selected": 12, "no_rule": 30, "content_conflict": 0, "withheld_limit": 0}},
{"signal": "content", "decisions": {"selected": 0, "no_rule": 42, "content_conflict": 0, "withheld_limit": 0}}],
"selections": [{"policy": "console-eng", "rule": "export", "source": "console", "subject": {"team": "engineering"},
"targets": [{"signal": "audit", "exporter": "corp", "exported": true}]},
{"policy": "engineering-observability", "rule": "export", "source": "files", "subject": {"team": "engineering"},
"targets": [{"signal": "audit", "exporter": "corp", "exported": true}]}]
}
PUT /admin/observability/limits replaces the runtime limits. An exporter left
out has no limit; signals lists the declared signals to keep; max_content_bytes
and max_sample_percent only lower the bootstrap values.
{
"dry_run": true,
"current_revision": "sha256:4f8b…",
"result_revision": "sha256:9a1c…",
"changes": [{"exporter": "corp", "field": "paused", "before": false, "after": true}],
"effects": [{"policy": "engineering-observability", "rule": "export", "signal": "audit", "exporter": "corp",
"before": {"exported": true}, "after": {"exported": false}}]
}
PUT /admin/observability/rules replaces the console-owned rules, the
console-… documents in observability.console.policy_file:
{"dry_run": true, "expected_revision": "sha256:5f1c…", "rules": [
{"name": "console-eng", "subject": {"team": "engineering"}, "audit": {"exporter": "corp"}},
{"name": "console-finance", "subject": {"team": "finance"},
"traces": {"exporter": "corp", "sample_percent": 20}, "audit": {"exporter": "corp"}}]}
{
"dry_run": true,
"current_revision": "sha256:5f1c…",
"result_revision": "sha256:7c2d…",
"changes": [{"name": "console-finance", "action": "added"}],
"summary": {"added": 1, "changed": 0, "removed": 0, "unchanged": 1},
"decisions": [{"subject": {"team": "finance"},
"before": {"traces": [], "audit": []},
"after": {"traces": [{"exporter": "corp", "sample_percent": 20}], "audit": ["corp"]}}]
}
Send the same body with "dry_run": false to apply. Errors:
| Status | code |
Meaning |
|---|---|---|
| 400 | invalid_request |
The body is not one JSON object with known fields, or an apply has no expected_revision |
| 413 | invalid_request |
The body is larger than the limit |
| 400 | invalid_limits / invalid_rules |
Outside the bootstrap ceiling, or a rule the policy loader refuses |
| 400 | content_not_allowed |
A console rule adds or changes content export without observability.console.content_rules |
| 409 | read_only |
Not editable here (reason: not_configured, no_state_file, no_policy_file, control_plane) |
| 409 | revision_conflict |
The state changed since it was read (current_revision is returned) |
| 409 | limits_file_changed |
The state file was edited on disk; restart mayu to load it |
| 409 | console_file_foreign |
The console policy file holds documents the console does not manage |
| 409 | policy_reload_failed |
The policy set did not reload; the console file was restored, so nothing changed |
| 503 | audit_unavailable |
The audit chain is not accepting records, or the audit identity configuration cannot be resolved; nothing changed |
| 500 | persist_failed / read_failed |
A state file could not be written or read, or the limits could not be installed and the state file was restored; nothing changed |
| 500 | restore_failed |
The change was not installed and its file could not be put back, so the file may still hold it: a restart would install unapplied limits, and the policy watcher may install unapplied rules later without a change record |
Error Codes¶
| Code | Meaning |
|---|---|
| 400 | Bad request (malformed body) |
| 401 | Missing/invalid virtual key (data plane) or admin token (admin plane) |
| 402 | Monetary authority exhausted in durable/shared admission |
| 403 | Access, privacy, region or policy refusal; some legacy governance denials |
| 404 | No eligible route after configured model resolution/fallback |
| 429 | Rate/token quota exhausted in the enforcing profile |
| 503 | Required policy/identity readiness, authority or shared store unavailable |
| 5xx | Upstream provider error (teed through) or gateway failure |
Errors are returned in the shape of the ingress protocol (Anthropic error object on the Anthropic ingress; OpenAI error object on the OpenAI ingress).
CLI¶
mayu serve --config <path>
mayu keys create --team <t> --models <csv> --store <path>
mayu keys list --store <path>
mayu keys revoke --id <key_id> --store <path>
mayu keys create --team <t> --models <csv> --config <path> # select the configured backend
mayu keys list --config <path>
mayu keys revoke --id <key_id> --config <path>
mayu keys import --config <shared-config> --sqlite <old-db>
mayu audit verify --file <path>
mayu report --file <path> --by team,model
mayu pricing check --config <path> # ADR-030, CI guard: exit 1 if any route has no rate
mayu login --gateway <url> [--team <t>] [--id-token-command <cmd>] # ADR-028
mayu token [--export] [--raw] # ADR-028, meant to run as apiKeyHelper
mayu logout # ADR-028
Policy-aware request routing (ADR-043)¶
Anthropic Messages, OpenAI Chat Completions and native Bedrock generation use the same privacy-constrained attempt chain; security refusals return an ingress-shaped 403. Anthropic and Bedrock token-count endpoints instead return local HTTP-200 estimates with zero upstream calls on routing refusal, unready/stale governance, or oversized/unreadable bodies. Responses support was added separately in ADR-044.
x-inferplane-routing-reason is a bounded decision reason and
x-inferplane-routed-model identifies applied privacy/context selection. Context
Shadow still enforces privacy. Budget's earlier x-inferplane-substituted-model
may differ from final selection. The audit request.routing object records
requested (resolved pre-tier), selected, proposed and actual-attempt evidence
separately, including provider/boundary. See schema, upgrade requirements and
limits before enabling rules.
Responses and adaptive routing (ADR-044)¶
Tool-free Codex compaction requests may set parallel_tool_calls: false. The stateless bridge accepts this neutral preference only when tools is absent, null or an empty array. With any offered tool, serialized execution remains unsupported and the request is rejected. This does not add support for remote conversation state or encrypted compaction items. See measured client compatibility.
POST /v1/responses accepts authenticated HTTP Responses requests. Native
openai_responses targets retain their wire; supported stateless text/tool
requests can use canonical adapters. Streaming emits the Responses event
lifecycle and settles observed usage, including interrupted streams.
Unsupported cross-protocol state is rejected before egress. The endpoint
shares body/readiness, RBAC, region, PII, strict budget and total admission gates.
New policy fields are sensitiveData.onDetected: Mask, context
normalModel, maxNormalInputTokens, stability, and budget-tier
enforceTargets. See adaptive configuration and limits.