# API Reference

The PRIOR engine is a single native Rust server. It exposes an **OpenAI-compatible inference
endpoint** with the Tri-State WHEN gate + golden-layer steering applied in-process, plus a
governance surface under `/admin`, plus system endpoints. Default port **`8089`** (`PRIOR_PORT`).

> **One request path for proprietary controls.** All PRIOR-specific per-request options ride a
> single `prior_options` object on `POST /v1/chat/completions`. A vanilla OpenAI client that never
> sends `prior_options` is unaffected (the gate still applies).

**Base URL:** `http://<host>:8089`
**Auth:** `Authorization: Bearer <api-key>`. `/v1` user-auth is **off by default** and toggleable
live (`PATCH /admin/runtime` or `PRIOR_REQUIRE_AUTH`); `/admin` **always** requires an
Operator/Admin key.

---

## Inference

### `POST /v1/chat/completions`

OpenAI-compatible chat completion. `messages` are chat-templated **server-side** using the model's
own template. The WHEN gate routes on the combined system+user intent (so a jailbreak in the system
prompt is gated too), and steering is applied per the routing decision.

**Request.** Standard OpenAI fields plus the optional `prior_options` extension:

```json
{
  "model": "prior",
  "messages": [
    {"role": "system", "content": "You are our support assistant."},
    {"role": "user", "content": "What port does our SSH service listen on?"}
  ],
  "max_tokens": 256,
  "stream": false,
  "prior_options": {
    "ephemeral_memory": {
      "target": "6022",
      "pin_phrase": "Our SSH service listens on port 6022"
    }
  }
}
```

| Field | Type | Notes |
|---|---|---|
| `model` | string? | Tolerated/ignored; one model per container. |
| `messages` | array | `{role, content}` where role is `system` / `user` / `assistant`. |
| `max_tokens` | integer | Default `256`. |
| `temperature` | number? | Accepted for compatibility; decoding is greedy/deterministic. |
| `stream` | bool? | SSE (`chat.completion.chunk`) when `true`. |
| `prior_options` | object? | PRIOR controls (see below). |

**Response.** A standard OpenAI `chat.completion` object (or an SSE stream when `stream: true`).
PRIOR telemetry is returned in **response headers** (additive; OpenAI clients ignore them).

On a **YELLOW** route the pack's confirm notice is also **prepended to the message content** (unless
`PRIOR_YELLOW_MODE=header`), so header-blind UIs still surface it.

---

### `prior_options`

| Field | Type | Effect |
|---|---|---|
| `ephemeral_memory` | `{target, pin_phrase}` | **Stateless fact injection.** Steers the golden-layer residual toward `target` while teacher-forcing `pin_phrase`, so the model recalls a fact it otherwise wouldn't know. The **client owns RAG / user state**, so re-send each turn. Injected **only on a GREEN route**; any YELLOW/RED flag suppresses it (safety > memory). |
| `signals` | `{name: float}` | **Scores from your own detectors.** A pack declaring `external_signal: "<name>"` is scored from this map instead of from an embedding, then composes through the ordinary precedence lattice — so an in-house or third-party classifier drives PRIOR's *enforcement*, not just a label. See [Bring your own detector](#bring-your-own-detector). |
| `engage_override` | bool? | `true` = force a refusal even on GREEN/YELLOW; `false` = suppress steering (vanilla passthrough even on RED); omit / `null` = use the gate decision. |
| `steer` | object? | **Explicit low-level steering override** (gate bypassed). For calibration / efficacy harnesses. Licensed-only. Fields below. |

`steer` object (advanced; you rarely need this, since the gate does it for you):

| Field | Meaning |
|---|---|
| `engage` | apply steering this request |
| `target` | steering target token/phrase (e.g. `"cannot"`) |
| `alpha` | steering strength |
| `golden` | golden-layer index override |
| `onset_n` | number of onset tokens hard-pinned |
| `tau` | maintenance threshold |
| `recompute_every` | re-projection cadence (0 = onset-only) |
| `pin_n`, `pin_phrase` | teacher-forced pin length + phrase |
| `reactive_repin`, `repin_markers` | reactive re-pin on marker tokens |
| `logits_only` | force the backward-free steering path for this request (see below) |

**`steer.logits_only` is one-way.** A request may opt **into** logits-only, but it can never opt
*out* of it: if the engine selected logits-only for this model (because the architecture has no
usable backward pass, or VRAM is tight), a request asking for L2LM is ignored rather than crashing.
Set it globally with `PRIOR_LOGITS_ONLY=1` or live via `PATCH /admin/runtime`.

### Bring your own detector

PRIOR's cosine gate is a **reference detector**, not the product. If you have a classifier that knows
your traffic better — an in-house model trained on your own data, a commercial safety classifier, a
regex, a deny-list — you can drive PRIOR's enforcement from it directly.

Declare a pack with `external_signal`, then pass that detector's score on each request:

```jsonc
// the pack
{ "pack": "house_classifier", "external_signal": "house", "on_missing": "red",
  "tristate": { "t_low": 0.55, "t_high": 0.80 }, "enforcement": "hard", … }
```
```jsonc
// the request — you score first, then call
{ "model": "prior", "messages": [ … ],
  "prior_options": { "signals": { "house": 0.87 } } }
```

Three properties worth knowing:

- **PRIOR never calls out.** You score first and pass the result inline. There is no round-trip in
  the serving path, no second service to keep up, and no new failure mode between the request and the
  answer.
- **The score is on *your* scale.** A foreign score is not a zone. Calibrate the consuming pack's
  `t_low`/`t_high` against **your** detector's benign distribution, exactly as a cosine pack is
  calibrated against ours. PRIOR stores and enforces what you declare; it will not re-derive your
  thresholds, because re-deriving them from exemplars the pack does not have would replace your
  policy with a meaningless number.
- **It composes.** An external pack sits in the same precedence lattice as a cosine pack, so a
  classifier verdict and a membership boundary can both be live, and the more restrictive one wins.

`on_missing` decides what happens when the named signal is absent from a request — `"red"` fails
closed, which is what you want if the detector going down should stop traffic rather than open the
door.

**Trust model:** the `/v1` caller is the operator's authenticated application, not the end user. A
`signals` value is a statement by your backend, so treat the endpoint accordingly (`PRIOR_REQUIRE_AUTH=1`
whenever it is reachable beyond localhost).

---

### Response headers (`x-prior-*`)

Emitted on `/v1/chat/completions`:

| Header | Meaning |
|---|---|
| `x-prior-zone` | `GREEN` / `YELLOW` / `RED`; the WHEN-gate decision. |
| `x-prior-pack` | The winning policy pack. |
| `x-prior-injections` | Golden-layer steering steps applied (`0` = bit-for-bit vanilla). |
| `x-prior-steering` | `enabled` / `disabled` (the license gate). |
| `x-prior-decision-id` | Correlation id; matches the row in the durable safety audit. |
| `x-prior-memory` | `injected` / `suppressed` / `preempted` / `disabled`; set only when `ephemeral_memory` was sent. **`suppressed` means the call succeeded and the answer does *not* contain the fact you supplied** — branch on it if your application depends on that fact being present. |

The response exposes the *decision* (zone, winning pack, whether steering engaged) but **not** the
underlying match scores. Raw scores are deliberately kept off the caller-facing surface: exposing
them would let a caller probe the exact boundary and map the detection and steering. Scores are
retained only in the **admin-gated** safety audit, for operators tuning their own policies.

### `POST /v1/completions`

Legacy (non-chat) OpenAI completion endpoint, kept for compatibility. New integrations should use
`/v1/chat/completions`.

---

## System

| Method · Path | Purpose |
|---|---|
| `GET /health` | Liveness (answers immediately; the model loads lazily). |
| `GET /v1/models` | The loaded model id. |
| `GET /metrics` | Prometheus exposition: zone distribution, refusal counts, latency / token / injection histograms. |

---

## Governance: `/admin/*`

All `/admin` routes require an **Operator** or **Admin** key (an Observer key gets `403`). See
[Authentication & roles](#authentication--roles).

### Packs & policy

| Method · Path | Purpose |
|---|---|
| `GET /admin/packs/native` | List loaded native arbiter packs, reporting name, encoder, tier, priority, thresholds, exemplar **count**, enabled. (Exemplars themselves are never served.) |
| `POST /admin/packs/reload` | Hot-swap all packs from disk, with no restart. |
| `PATCH /admin/packs/{pack}/enabled` | Mute / unmute a pack instantly (body: `{"enabled": bool}`). |
| `PATCH /admin/packs/{pack}/priority` | Set a pack's arbitration priority (which pack wins when several match). |
| `POST /admin/packs/ingest` | **Prose → calibrated pack.** Submit a governance document; PRIOR extracts intents, generates exemplars, calibrates thresholds, and hot-reloads. See **[Policy & Packs](policy-packs.md)**. |
| `POST /admin/packs/import` | Load a **pre-built** arbiter pack verbatim, skipping ingest and calibration. For packs you built elsewhere or version-control yourself. |
| `GET /admin/packs` | List on-disk packs (metadata only). |

### Pack authoring (1.1.0)

Author and refine a pack against real evidence instead of guessing at thresholds. These are the API
behind the WebUI's pack editor.

| Method · Path | Purpose |
|---|---|
| `GET / PUT /admin/packs/{pack}` | Read a pack's editable definition (goal, rules, transitions, thresholds) and write it back. |
| `GET /admin/packs/{pack}/coverage` | **Coverage drill-down.** Per-probe results: which positives the pack *missed* and which negatives it *fired on* — the two lists you actually iterate against. Reports per-rule coverage where rules are defined. |
| `POST /admin/packs/{pack}/rules/{rule_id}/expand` | Expand one rule into additional exemplars, to lift that rule's coverage without disturbing the others. |
| `GET / POST /admin/packs/{pack}/examples` | Read and add the pack's exemplars. |
| `GET / POST /admin/packs/{pack}/dataset` | Manage the pack's test dataset and score the pack against it. |
| `POST /admin/packs/route-probe` | Score an arbitrary prompt through the **live** gate and see how it routes, and which pack claims it, without generating a completion. The fastest way to answer "why did this get through?" |
| `GET /admin/detector` | Detector state: active encoder(s) and their fingerprints. |

> A pack's **transitions** are typed — `refuse`, `sever`, `format`, `ground` — so a pack declares
> *what should happen* on a match, not just that it matched.

### Model & calibration

| Method · Path | Purpose |
|---|---|
| `GET /admin/model` | Resolved steering params (golden layer, alpha) + tier (`certified` / `beta`) + calibration status. |
| `GET /admin/models/available` | GGUF models present on disk (with sizes). |
| `POST /admin/calibrate` | Full steering calibration: golden-layer sweep + alpha (GPU). |
| `POST /admin/calibrate/alpha` | Alpha-only calibration (golden layer fixed). |
| `POST /admin/calibrate/share` | Optionally submit the calibration payload to the Eagle Logic model registry. |
| `POST /admin/model/reload` | Hot-reload the model. |

### Keys, license, config, runtime

| Method · Path | Purpose |
|---|---|
| `GET /admin/keys` | List keys (prefix + role + label; never the secret). |
| `POST /admin/keys` | Create a key; body `{"role": "OBSERVER\|OPERATOR\|ADMIN", "label"?}`. Raw key returned **once**. |
| `DELETE /admin/keys/{key}` | Revoke a key. |
| `GET /admin/license/status` | License variant, tier, capabilities, expiry, customer id. |
| `POST /admin/license/upload` | Upload a `license.bin` (hot-applied, no restart). |
| `POST /admin/license/clear` | Remove the active license. |
| `GET / PATCH /admin/config` | Read / update engine config. |
| `GET / PATCH /admin/runtime` | Live toggles (no restart): `require_auth` on `/v1`, `log_level`, `json_logs`, and **`logits_only`** (global backward-free steering mode — the path that runs on MoE). `GET` also reports `diagnostics_available`. |
| `GET /admin/logs?tail=N&level=LEVEL` | Tail the in-memory log buffer. |
| `GET /admin/whoami` | The calling key's role and label — useful for a UI deciding which controls to show. |
| `GET /admin/stats` | Aggregate engine counters. |
| `GET /admin/usage` | **Per-key usage accounting**: request and token tallies by API key. |
| `GET /admin/activity` | Recent request activity. Prompt and response **content** is included only when the flag-gated `log_content` toggle is on — off by default, and it is the one surface that can carry prompt text, so treat it as sensitive. |

### Audit

| Method · Path | Purpose |
|---|---|
| `GET /admin/audit` | Control-plane action log covering config/keys/license/model mutations, with actor + role. |
| `GET /admin/audit/safety?tail=N&zone=RED&pack=harm_veto` | **Durable** per-request gate decisions, capturing decision id, zone, pack, score, steered flag, model, prompt **hash**. Survives restarts. |

> There is also a set of low-level triple-store routes (`/admin/triples/*`) and an `/admin/ui/*`
> static-dashboard mount. The triple editor is a Admin-only advanced/legacy tool; native
> inference does not read those triples. Most operators drive PRIOR through packs (above) and the
> separate **[WebUI](webui.md)**.

---

## Authentication & roles

API keys are **SHA-256-hashed at rest**; the raw secret is shown **once** at creation and never
stored or re-served. Present a key as `Authorization: Bearer <key>`. Keys may carry a label and an
optional expiry.

| Role | `/v1` inference | `/admin` governance | Typical holder |
|---|---|---|---|
| **Observer** | ✅ | ❌ (read dashboards only where exposed) | monitoring, dashboards |
| **Operator** | ✅ | ✅ except global config write / model reload / oracle flush | day-to-day policy ops |
| **Admin** | ✅ | ✅ full: config write, licensing, calibration, lifecycle | the account owner |

- `/v1` auth is **off by default**; a fresh engine answers inference without a key. Turn it on with
  `PRIOR_REQUIRE_AUTH=1` or live via `PATCH /admin/runtime`. **Always enable it before exposing the
  engine off loopback.**
- `/admin` is **always** authenticated and requires Operator or Admin.
- Seed a first Admin key at boot with `PRIOR_API_KEY` (env-only, not persisted), then create
  scoped keys through `/admin/keys`.

---

## Errors & status codes

| Code | When |
|---|---|
| `200` | Success. |
| `400` | Malformed request, or a feature used without its prerequisite (e.g. ingest with no extractor configured, which returns a clear message and never a silent localhost dial). |
| `401` / `403` | Missing/invalid key, or an Observer key hitting `/admin`. |
| `429` | Per-caller rate limit exceeded on `/v1` (token bucket, per API key). |
| `503` | Engine still loading the model (readiness). |

---

## Statelessness

The server holds **no conversation sessions and no server-side user store**. The client re-sends the
full message history (and any `ephemeral_memory`) each turn. This is what makes horizontal scaling
and air-gap deployment straightforward: any replica can serve any request.

---

## CORS (browser clients)

To call the engine from a browser (e.g. the WebUI on a different origin), set
`PRIOR_ALLOWED_ORIGINS` to your UI's origin(s). The engine lists `Authorization` + `Content-Type`
explicitly in `Access-Control-Allow-Headers` (a `*` wildcard does **not** cover `Authorization` per
the Fetch spec, so the Bearer key would otherwise be dropped). Restrict origins in production.

---

See also: **[Concepts](concepts.md)** · **[Policy & Packs](policy-packs.md)** ·
**[Deployment](deployment.md)**.
