Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8e85133
feat(api): add GET /livez as a process-alive probe
RaviTharuma Aug 20, 2026
a352c23
fix(chatgpt-web): refresh current model catalog (#10637)
backryun Aug 20, 2026
5e50814
perf(electron): defer hidden-start renderer creation (#10327)
backryun Aug 20, 2026
8122f6b
perf(electron): optionally unload renderer on close (4/8) (#10328)
backryun Aug 20, 2026
80a59c0
fix(images): route bare dall-e-3 to OpenAI (#10847)
RaviTharuma Aug 20, 2026
84d7e33
feat(resilience): warn on slow /healthz event-loop lag (#10827)
RaviTharuma Aug 20, 2026
7ac6bbb
docs(backend): document memory/skills/token-refresh event-loop cost (…
RaviTharuma Aug 20, 2026
6767f27
docs(db): document throttled pre-write SQLite backups (#10824)
RaviTharuma Aug 20, 2026
8b52596
docs(auth): distinguish access tokens, API keys, and management crede…
RaviTharuma Aug 20, 2026
6f08a08
feat(speech): accept response_format=ogg as an opus alias (#10822)
RaviTharuma Aug 20, 2026
77d7502
fix(opencode-plugin): keep bare combo ids unprefixed (#10821)
RaviTharuma Aug 20, 2026
2f73158
fix(auto): log empty auto-family pools once per process (#10820)
RaviTharuma Aug 20, 2026
56b9d00
fix(docker): warn when OMNIROUTE_MEMORY_MB disagrees with NODE_OPTION…
RaviTharuma Aug 20, 2026
7c6bf32
docs(docker): document SQLite single-replica HA limits (#10817)
RaviTharuma Aug 20, 2026
9eddaff
fix(admission): reserve Responses and Messages bodies before clone (#…
RaviTharuma Aug 20, 2026
9d2240e
fix(search): name /v1/search 502 provider and cause (#10756)
RaviTharuma Aug 20, 2026
c537f64
Merge commit 'refs/pull/10819/head' of https://github.com/diegosouzap…
hartmark Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions @omniroute/opencode-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,10 +1294,15 @@ export function mapRawModelToModelV2(
// `(providerID, modelID)`. If the raw id is already provider-prefixed
// (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or
// `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave
// it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with
// the resolved `providerId` so a bare key like `claude-opus-4` parses as
// `(omniroute, claude-opus-4)` and the credentials resolve correctly.
id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`,
// it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids
// (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed:
// OpenCode looks up `-m <plugin>/<combo>` as model id `<combo>` under
// the plugin provider (#10345). Other bare ids still prefix with
// `providerId` so credentials resolve as `(omniroute, model)`.
id:
raw.id.includes("/") || raw.owned_by === "combo"
? raw.id
: `${ctx.providerId}/${raw.id}`,
/**
* Display name. Falls back to raw.id when no enrichment is available;
* the caller (`createOmniRouteProviderHook`) overlays
Expand Down
34 changes: 34 additions & 0 deletions @omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";

import { mapRawModelToModelV2 } from "../src/index.ts";

test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => {
const combo = mapRawModelToModelV2(
{
id: "gpt-5.6-sol",
owned_by: "combo",
context_length: 272000,
max_output_tokens: 8192,
},
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(combo.id, "gpt-5.6-sol");
assert.equal(combo.providerID, "omniroute");

const slashed = mapRawModelToModelV2(
{
id: "cx/gpt-5.6-sol",
owned_by: "combo",
context_length: 272000,
},
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(slashed.id, "cx/gpt-5.6-sol");

const ordinary = mapRawModelToModelV2(
{ id: "claude-primary", context_length: 200000 },
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(ordinary.id, "omniroute/claude-primary");
});
1 change: 1 addition & 0 deletions changelog.d/features/10303-healthz-event-loop-lag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303))
1 change: 1 addition & 0 deletions changelog.d/features/10316-livez-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316))
1 change: 1 addition & 0 deletions changelog.d/features/10587-ogg-speech-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587))
1 change: 1 addition & 0 deletions changelog.d/fixes/10345-bare-combo-opencode-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345))
1 change: 1 addition & 0 deletions changelog.d/fixes/10346-empty-pool-warn-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(backend):** log `auto/<family> matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346))
1 change: 1 addition & 0 deletions changelog.d/fixes/10353-memory-heap-conflict-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353))
1 change: 1 addition & 0 deletions changelog.d/fixes/10735-search-provider-named-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))
1 change: 1 addition & 0 deletions changelog.d/fixes/10832-unprefixed-dalle3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351))
1 change: 1 addition & 0 deletions changelog.d/maintenance/7786-management-auth-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs(auth):** distinguish dashboard sessions, `oma_live_…` Access Tokens, manage-scoped API keys, and inference keys ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
33 changes: 29 additions & 4 deletions docs/guides/DOCKER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ lastUpdated: 2026-06-28
- [Docker Compose with Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls)
- [Cloudflare Quick Tunnel](#cloudflare-quick-tunnel)
- [Image Tags](#image-tags)
- [Availability: default SQLite is single-replica](#availability-default-sqlite-is-single-replica)
- [Important Notes](#important-notes)

---
Expand Down Expand Up @@ -341,13 +342,15 @@ For orchestrators (Kubernetes, Nomad, etc.):

| Probe | Prefer | Avoid |
| --- | --- | --- |
| Liveness | TCP on the main port (`PORT`, default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` as liveness |
| Liveness | HTTP `GET /livez`, or TCP on the main port (`PORT`, default `20128`) | `/api/monitoring/health` as liveness |
| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
| Deep / blackbox | `/api/monitoring/health` | — |

`/healthz` only reports process lifecycle (`ok` / `starting` / `stopping`). It still
runs on the same Node event loop as request handling, so CPU-bound catalog or
compression work can delay it — busy ≠ dead. Full probe guidance:
`/healthz` reports process lifecycle (`ok` / `starting` / `stopping`). `/livez` is
process-alive only (200 whenever the handler can run; it does not wait for
readiness). Both still run on the same Node event loop as request handling, so
CPU-bound catalog or compression work can delay them — busy ≠ dead. Prefer TCP
liveness if HTTP probes time out. Full probe guidance:
[Monitoring guide — Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations).

## Docker Compose with Caddy (HTTPS Auto-TLS)
Expand Down Expand Up @@ -465,6 +468,28 @@ docker compose up -d

A release-branch build can never move `latest`; only an eligible stable semantic version may promote the stable pointer. The `next` images retain the release image inspection and blocking CRITICAL-vulnerability gate.

## Availability: default SQLite is single-replica

Stock Docker / Kubernetes OmniRoute is **one Node process + one SQLite writer**. High availability is **not supported** on that topology.

| Constraint | Consequence |
| --- | --- |
| Single writer | Do **not** run multiple replicas against the same SQLite file. That corrupts the DB. |
| Recreate / restart / HEALTHCHECK kill | **Full outage** of in-flight SSE, dashboard sessions, and in-memory state. Every connected client drops. |
| Same event loop as `/healthz` | A busy catalog or compression tick can delay probes; a short timeout then restarts the **only** replica. |

**Probe matrix** (see also [Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations)):

| Probe | Target | Do not use |
| --- | --- | --- |
| Liveness | TCP on `PORT` (default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` |
| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
| Deep / humans | `/api/monitoring/health` | Automated kubelet liveness |

**Upgrades:** expect every session to drop. Drain clients if you can; there is no rolling update on default SQLite. Compose `restart: unless-stopped` plus Docker `HEALTHCHECK` will also replace the only process when the container is Unhealthy — same blast radius.

External Postgres / multi-writer HA is **not** a documented stock path. If you need HA, keep a single replica or run a topology the project has tested and documented separately.

## Important Notes

- **SQLite WAL Mode:** `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40`.
Expand Down
164 changes: 138 additions & 26 deletions docs/guides/MANAGEMENT-AUTH.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,159 @@
---
title: "Management Authentication"
version: 3.8.50
lastUpdated: 2026-08-05
lastUpdated: 2026-08-20
---

# Management Authentication

OmniRoute uses four distinct credential families for management access. This guide
distinguishes them by purpose, scope, and locality.
OmniRoute has **four credential families** that can authorize management routes.
They are not interchangeable. Inference API keys (`sk-…`) do **not** manage the
server unless they were explicitly granted `manage` or `admin` scope.

| Credential | Scope | Locality | Use Case |
|-------------------------|--------------------|---------------|-----------------------------------|
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
| Manage-scope API key | `manage` scope | External | Management API calls |
Canonical implementation: `src/lib/api/requireManagementAuth.ts`.

## Dashboard JWT Session
| Credential | Typical form | Created where | Intended use | Management capability |
|---|---|---|---|---|
| Dashboard session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules |
| Local CLI machine token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only |
| Scoped Access Token | `oma_live_…` | **Settings → Access Tokens** or `omniroute connect` | Remote CLI and management API | Must satisfy the route's required `read`, `write`, or `admin` scope |
| Inference API key | `sk-…` (and other API-key prefixes) | **API Manager / API Keys** | `/v1/*` inference | **None** unless the key metadata includes `manage` or `admin` |

Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
Valid for the session duration. Cannot be used from external hosts.
`oma_` credentials are management/CLI credentials. They are **not** inference API keys.

## CLI Machine-ID Token
If login/API-key auth is disabled for the server, some management routes may
accept unauthenticated calls. Local-only and always-protected routes still apply
their own rules. Presenting one of these credentials is therefore not universally
mandatory, and possessing one is not universally sufficient without the required
scope and route locality.

Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
Used by the CLI for all management operations. Tied to the machine identity.
Related: [Remote Mode](./REMOTE-MODE.md) (how `oma_live_…` is minted for a remote CLI).

## Scoped `oma_` Access Token
---

## Scope matrices

These two scope vocabularies are **different**. Do not mix them.

### Access Token scopes (`oma_live_…`)

| Scope | Typical operations |
|---|---|
| `read` | List/status GETs that the token is allowed to see |
| `write` | Mutations (create/update/delete) below admin |
| `admin` | Full remote CLI / connect token (password bootstrap defaults here) |

A token with `read` cannot call a `write` route. Runtime message shape:
`Access token scope '<have>' is insufficient; '<need>' required.`

### API-key management scopes

| Scope | Meaning |
|---|---|
| (none) | Inference only. Management routes return 403. |
| `manage` | Management API (same gate as `requireManagementAuth` API-key branch) |
| `admin` | Also satisfies `hasManageScope` (treated as management-capable) |

Enable `manage` on the key in the API Keys / API Manager UI. Do not reuse a
chat client key for automation unless you deliberately granted that scope.

---

## How to create and revoke

### Dashboard session

1. Open `/login`, sign in with the management password (`INITIAL_PASSWORD` on first boot).
2. Cookie `auth_token` is HttpOnly. Browser dashboard uses it automatically.
3. Log out via `/api/auth/logout`. There is no long-lived secret to copy.

### Local CLI machine token

Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
1. Run `omniroute` on the **same host** as the server (loopback).
2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600).
3. This does **not** work from another machine. Use an Access Token for remote CLI.

## Manage-Scope API Key
### Scoped Access Token (`oma_live_…`)

Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
Used for management API calls from external hosts.
1. Dashboard: **Settings → Access Tokens** → create (name + scope). **The secret is shown once.**
2. Or CLI: `omniroute connect <host>` (password → token). See [Remote Mode](./REMOTE-MODE.md).
3. Header: `Authorization: Bearer oma_live_…`
4. Revoke from the same Access Tokens page (or delete the CLI context).
5. Server stores only a hash. Treat the plaintext like a password.

## Header Examples
### Manage-scoped API key

1. Dashboard: **API Manager / API Keys** → create or edit a key → enable `manage` (or `admin`).
2. Header: `Authorization: Bearer sk-…` (the key's actual prefix).
3. Revoke or strip `manage` in the same UI.
4. Least privilege for automation that is not the CLI: prefer a `read` Access Token for GET-only jobs; use `manage` on an API key only when the caller must also speak `/v1` and management.

---

## Header format

```http
Authorization: Bearer oma_live_<secret>
Authorization: Bearer sk-<secret>
Cookie: auth_token=<dashboard-jwt>
```
Authorization: Bearer oma_abc123def456
Authorization: Bearer <standard-api-key-with-manage-scope>
Cookie: omniroute_session=<jwt-token>

Do not put management credentials in the URL path or query string. Management
auth is header/cookie only.

---

## Copy-paste examples

Read-only (list providers). Use a `read` Access Token:

```bash
curl -sS "$OMNIROUTE_URL/api/providers" \
-H "Authorization: Bearer oma_live_<read-token>"
```

See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
Modifying (create a provider connection). Use `write`/`admin` Access Token or a
manage-scoped API key:

```bash
curl -sS -X POST "$OMNIROUTE_URL/api/providers" \
-H "Authorization: Bearer oma_live_<write-or-admin-token>" \
-H "Content-Type: application/json" \
-d '{"provider":"openai","apiKey":"<upstream-key>"}'
```

Inference (not management). Ordinary API key, no `manage` required:

```bash
curl -sS "$OMNIROUTE_URL/v1/models" \
-H "Authorization: Bearer sk-<inference-key>"
```

---

## Current runtime errors (do not echo secrets)

| Situation | Typical status | Message (sanitized) |
|---|---|---|
| No credential | 401 | `Authentication required` |
| Invalid/expired `oma_live_…` | 401 | `Invalid or expired access token` |
| Valid API key without `manage`/`admin` | 403 | `API key lacks 'manage' scope. Enable it in the API Keys dashboard.` |
| Invalid ordinary API key on a management route | 403 | `Invalid management token` |
| Access Token scope too low | 403 | `Access token scope '<have>' is insufficient; '<need>' required.` |

"Invalid management token" means the bearer was **not** accepted as a management
credential. It does **not** tell you which family to mint. Use the table above:
inference keys need `manage` scope; remote CLI needs `oma_live_…`; the dashboard
uses the session cookie.

---

## Recommended least-privilege choice

| Caller | Use |
|---|---|
| Browser | Dashboard session |
| CLI on the server host | Machine token |
| CLI on a laptop talking to a remote server | `oma_live_…` from `omniroute connect` |
| CI / scripts (management only) | `oma_live_…` with the smallest scope that works |
| CI that must call both `/v1` and `/api` | API key with `manage` **or** two credentials |
10 changes: 8 additions & 2 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7329,12 +7329,18 @@ components:
BearerAuth:
type: http
scheme: bearer
description: API key obtained from the OmniRoute dashboard
description: >
Two bearer families are accepted. Inference API keys (typically `sk-…`)
authorize `/v1/*`. Management routes also accept `oma_live_…` Access Tokens
(Settings → Access Tokens / `omniroute connect`) and API keys whose metadata
includes `manage` or `admin` scope. See docs/guides/MANAGEMENT-AUTH.md.
Bearer credentials are accepted on management routes that use this scheme;
they are not rejected solely for being Bearer.
ManagementSessionAuth:
type: apiKey
in: cookie
name: auth_token
description: Dashboard management session cookie for protected management routes
description: Dashboard management session cookie (auth_token) for protected management routes. Distinct from Bearer Access Tokens and API keys. See docs/guides/MANAGEMENT-AUTH.md.

parameters:
ResourceId:
Expand Down
Loading
Loading