Skip to content

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

POST /v1/messages
POST /v1/messages/count_tokens
GET  /v1/models
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

POST /v1/chat/completions
POST /v1/responses
GET  /v1/models
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, "expected_revision": "sha256:4f8b…", "exporters": {"corp": {"paused": true}}}
{
  "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.