--- title: "API Reference" version: 3.8.52 lastUpdated: 2026-10-05 --- # API Reference 🌐 **Languages:** 🇺🇸 [English](./API_REFERENCE.md) | 🇪🇹 [አማርኛ](../i18n/am/docs/reference/API_REFERENCE.md) | 🇸🇦 [العربية](../i18n/ar/docs/reference/API_REFERENCE.md) | 🇦🇿 [Azərbaycan dili](../i18n/az/docs/reference/API_REFERENCE.md) | 🇧🇬 [Български](../i18n/bg/docs/reference/API_REFERENCE.md) | 🇧🇩 [বাংলা](../i18n/bn/docs/reference/API_REFERENCE.md) | 🇧🇦 [Bosanski](../i18n/bs/docs/reference/API_REFERENCE.md) | 🇨🇿 [Čeština](../i18n/cs/docs/reference/API_REFERENCE.md) | 🇩🇰 [Dansk](../i18n/da/docs/reference/API_REFERENCE.md) | 🇩🇪 [Deutsch](../i18n/de/docs/reference/API_REFERENCE.md) | 🇬🇷 [Ελληνικά](../i18n/el/docs/reference/API_REFERENCE.md) | 🇪🇸 [Español](../i18n/es/docs/reference/API_REFERENCE.md) | 🇪🇪 [Eesti](../i18n/et/docs/reference/API_REFERENCE.md) | 🇮🇷 [فارسی](../i18n/fa/docs/reference/API_REFERENCE.md) | 🇫🇮 [Suomi](../i18n/fi/docs/reference/API_REFERENCE.md) | 🇫🇷 [Français](../i18n/fr/docs/reference/API_REFERENCE.md) | 🇮🇪 [Gaeilge](../i18n/ga/docs/reference/API_REFERENCE.md) | 🇮🇳 [ગુજરાતી](../i18n/gu/docs/reference/API_REFERENCE.md) | 🇳🇬 [Hausa](../i18n/ha/docs/reference/API_REFERENCE.md) | 🇮🇱 [עברית](../i18n/he/docs/reference/API_REFERENCE.md) | 🇮🇳 [हिन्दी](../i18n/hi/docs/reference/API_REFERENCE.md) | 🇭🇷 [Hrvatski](../i18n/hr/docs/reference/API_REFERENCE.md) | 🇭🇺 [Magyar](../i18n/hu/docs/reference/API_REFERENCE.md) | 🇦🇲 [Հայերեն](../i18n/hy/docs/reference/API_REFERENCE.md) | 🇮🇩 [Bahasa Indonesia](../i18n/id/docs/reference/API_REFERENCE.md) | 🇳🇬 [Igbo](../i18n/ig/docs/reference/API_REFERENCE.md) | 🇮🇹 [Italiano](../i18n/it/docs/reference/API_REFERENCE.md) | 🇯🇵 [日本語](../i18n/ja/docs/reference/API_REFERENCE.md) | 🇬🇪 [ქართული](../i18n/ka/docs/reference/API_REFERENCE.md) | 🇰🇭 [ខ្មែរ](../i18n/km/docs/reference/API_REFERENCE.md) | 🇮🇳 [ಕನ್ನಡ](../i18n/kn/docs/reference/API_REFERENCE.md) | 🇰🇷 [한국어](../i18n/ko/docs/reference/API_REFERENCE.md) | 🇱🇹 [Lietuvių](../i18n/lt/docs/reference/API_REFERENCE.md) | 🇱🇻 [Latviešu](../i18n/lv/docs/reference/API_REFERENCE.md) | 🇮🇳 [മലയാളം](../i18n/ml/docs/reference/API_REFERENCE.md) | 🇮🇳 [मराठी](../i18n/mr/docs/reference/API_REFERENCE.md) | 🇲🇾 [Bahasa Melayu](../i18n/ms/docs/reference/API_REFERENCE.md) | 🇲🇹 [Malti](../i18n/mt/docs/reference/API_REFERENCE.md) | 🇲🇲 [မြန်မာ](../i18n/my/docs/reference/API_REFERENCE.md) | 🇳🇵 [नेपाली](../i18n/ne/docs/reference/API_REFERENCE.md) | 🇳🇱 [Nederlands](../i18n/nl/docs/reference/API_REFERENCE.md) | 🇳🇴 [Norsk](../i18n/no/docs/reference/API_REFERENCE.md) | 🇮🇳 [ଓଡ଼ିଆ](../i18n/or/docs/reference/API_REFERENCE.md) | 🇮🇳 [ਪੰਜਾਬੀ](../i18n/pa/docs/reference/API_REFERENCE.md) | 🇵🇭 [Filipino](../i18n/phi/docs/reference/API_REFERENCE.md) | 🇵🇱 [Polski](../i18n/pl/docs/reference/API_REFERENCE.md) | 🇵🇹 [Português (Portugal)](../i18n/pt/docs/reference/API_REFERENCE.md) | 🇧🇷 [Português (Brasil)](../i18n/pt-BR/docs/reference/API_REFERENCE.md) | 🇷🇴 [Română](../i18n/ro/docs/reference/API_REFERENCE.md) | 🇷🇺 [Русский](../i18n/ru/docs/reference/API_REFERENCE.md) | 🇱🇰 [සිංහල](../i18n/si/docs/reference/API_REFERENCE.md) | 🇸🇰 [Slovenčina](../i18n/sk/docs/reference/API_REFERENCE.md) | 🇸🇮 [Slovenščina](../i18n/sl/docs/reference/API_REFERENCE.md) | 🇷🇸 [Српски](../i18n/sr/docs/reference/API_REFERENCE.md) | 🇸🇪 [Svenska](../i18n/sv/docs/reference/API_REFERENCE.md) | 🇰🇪 [Kiswahili](../i18n/sw/docs/reference/API_REFERENCE.md) | 🇮🇳 [தமிழ்](../i18n/ta/docs/reference/API_REFERENCE.md) | 🇮🇳 [తెలుగు](../i18n/te/docs/reference/API_REFERENCE.md) | 🇹🇭 [ไทย](../i18n/th/docs/reference/API_REFERENCE.md) | 🇹🇷 [Türkçe](../i18n/tr/docs/reference/API_REFERENCE.md) | 🇺🇦 [Українська](../i18n/uk-UA/docs/reference/API_REFERENCE.md) | 🇵🇰 [اردو](../i18n/ur/docs/reference/API_REFERENCE.md) | 🇺🇿 [Oʻzbekcha](../i18n/uz/docs/reference/API_REFERENCE.md) | 🇻🇳 [Tiếng Việt](../i18n/vi/docs/reference/API_REFERENCE.md) | 🇳🇬 [Yorùbá](../i18n/yo/docs/reference/API_REFERENCE.md) | 🇨🇳 [中文 (简体)](../i18n/zh-CN/docs/reference/API_REFERENCE.md) | 🇹🇼 [中文 (繁體)](../i18n/zh-TW/docs/reference/API_REFERENCE.md) Core reference for the OmniRoute API. It covers the public `/v1` surface and the most-used management endpoints; the machine-readable [`docs/openapi.yaml`](../openapi.yaml) and the route tree under `src/app/api/` are the exhaustive sources. For the focused OpenAI-compatible protocol and provider capability matrix, see [`OPENAI_COMPATIBILITY.md`](./OPENAI_COMPATIBILITY.md). --- ## Table of Contents - [Chat Completions](#chat-completions) - [Exclusive Managed Session Leases](#exclusive-managed-session-leases) - [Embeddings](#embeddings) - [Image Generation](#image-generation) - [Document OCR](#document-ocr) - [List Models](#list-models) - [Provider Plugin Manifest](#provider-plugin-manifest) - [Compatibility Endpoints](#compatibility-endpoints) - [Files API](#files-api) - [Batches API](#batches-api) - [Search API](#search-api) - [WebSocket Streaming](#websocket-streaming) - [Quotas & Issues Reporting](#quotas--issues-reporting) - [Semantic Cache](#semantic-cache) - [Dashboard & Management](#dashboard--management) - [Combo Management](#combo-management) - [Webhooks](#webhooks) - [Registered Keys (Auto-Management)](#registered-keys-auto-management) - [Agents Protocol](#agents-protocol) - [Management Proxies](#management-proxies) - [Resilience (extended)](#resilience-extended) - [Skills](#skills) - [Memory](#memory) - [MCP Server](#mcp-server) - [A2A Server](#a2a-server) - [Cloud, Evals & Assess](#cloud-evals--assess) - [Request Processing](#request-processing) - [Authentication](#authentication) --- ## Chat Completions ```bash POST /v1/chat/completions Authorization: Bearer your-api-key Content-Type: application/json { "model": "cc/claude-opus-4-6", "messages": [ {"role": "user", "content": "Write a function to..."} ], "stream": true } ``` ### Custom Headers | Header | Direction | Description | | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X-OmniRoute-No-Cache` | Request | Set to `true` to bypass cache | | `x-omniroute-no-memory` | Request | Set to `true` to skip memory + skills injection for this request (mirrors no-cache; avoids the per-call token/cost overhead) | | `X-OmniRoute-Progress` | Request | Set to `true` for progress events | | `X-Session-Id` | Request | Sticky session key for external session affinity | | `x_session_id` | Request | Underscore variant also accepted (direct HTTP) | | `X-OmniRoute-Session-Id` | Request | Caller-supplied session/conversation tag (also feeds memory). When present, persisted verbatim to `call_logs.session_tag` for per-session cost attribution (#8249) — never synthesized when absent | | `Idempotency-Key` | Request | Dedup key (5s window) | | `X-Request-Id` | Request | Alternative dedup key | | `x-omniroute-connection` | Request | Pin a single-model request to one provider connection (account) by its id — see [Account pinning per request](#account-pinning-per-request) | | `X-OmniRoute-Cache` | Response | `HIT` or `MISS` (non-streaming) | | `X-OmniRoute-Idempotent` | Response | `true` if deduplicated | | `X-OmniRoute-Progress` | Response | `enabled` if progress tracking on | | `X-OmniRoute-Session-Id` | Response | Effective session ID used by OmniRoute | | `X-OmniRoute-Request-Id` | Response | Request correlation id (when known) | | `X-OmniRoute-Version` | Response | OmniRoute build version (always present) | | `X-OmniRoute-Cost-Saved` | Response | USD the cache avoided on a HIT (cache hits only) | | `X-OmniRoute-Decision` | Response | Routing trace: `strategy=; provider=; latency_ms=` (`` is the combo strategy, or `single` for a non-combo request) — always present on completion responses | > Nginx note: if you rely on underscore headers (for example `x_session_id`), enable `underscores_in_headers on;`. #### Account pinning per request When a provider has several connections (accounts or keys), OmniRoute normally picks one through its account-selection strategy. A request can instead pin one specific connection with the `x-omniroute-connection` header: ```bash curl http://localhost:20128/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "x-omniroute-connection: " \ -d '{"model": "codex/gpt-5.5", "messages": [{"role": "user", "content": "hi"}]}' ``` - **Where to find the id.** Open the provider page in the dashboard (**Dashboard → Providers → **) and click the `xxxxxxxx…` id chip on a connection row — it copies the full connection id. - **Which requests honor it.** Single-model requests (`provider/model`) on `/v1/chat/completions`, `/v1/messages` and `/v1/responses`. Combos ignore the header: pin an account per combo step through the step's own connection instead. - **The pin is strict, not a preference.** The pinned connection is used exclusively. A failure inside the request records the account's cooldown but does **not** rotate to a sibling account of the same provider. - **Cooldown and quota release the pin.** While the pinned connection is cooling down, quota-exhausted or blocked by a quota policy, later requests fall back to the provider's other connections, exactly like unpinned traffic. - **Unknown or disabled ids fail.** An id that is not in the provider's active pool (deleted, deactivated, or belonging to another provider) leaves no eligible account, so the request fails instead of silently routing elsewhere. Claude Code sends custom headers through `ANTHROPIC_CUSTOM_HEADERS`. Put it in the `env` block of `~/.claude/settings.json`, or in a project-level `.claude/settings.json` to pin a different account per project: ```jsonc { "env": { "ANTHROPIC_BASE_URL": "http://:20128", "ANTHROPIC_AUTH_TOKEN": "", "ANTHROPIC_CUSTOM_HEADERS": "x-omniroute-connection: ", }, } ``` Source: `src/sse/handlers/chat.ts` (header read), `src/sse/services/auth.ts` and `src/sse/services/sessionAffinityPin.ts` (pin release and the missing-from-pool check). > **Cost telemetry headers:** non-streaming success responses also carry the `X-OmniRoute-*` cost-telemetry set — `X-OmniRoute-Response-Cost` (USD, fixed 10 decimals; `0.0000000000` for free/unpriced), `X-OmniRoute-Tokens-In` / `X-OmniRoute-Tokens-Out`, `X-OmniRoute-Model`, `X-OmniRoute-Provider`, `X-OmniRoute-Latency-Ms`, `X-OmniRoute-Cache-Hit`, and `X-OmniRoute-Fallback-Attempts` (only when > 0), plus `X-OmniRoute-Request-Id` and `X-OmniRoute-Version`. These are emitted by chat completions, `/v1/responses`, `/v1/messages`, **and the media endpoints** — `/v1/embeddings`, `/v1/images/generations`, `/v1/audio/speech`, `/v1/audio/transcriptions`, `/v1/rerank`, `/v1/videos/generations`, `/v1/music/generations`, and `/v1/moderations` (always cost `0`). Media cost is computed per modality (per-image, per-second, per-character, per search-unit) when pricing is available, otherwise `0` (fail-open). > **Cache-hit cost semantics:** on a semantic-cache HIT (`X-OmniRoute-Cache-Hit: true`) no upstream call is made, so `X-OmniRoute-Response-Cost` is `0.0000000000` (the **incremental** cost of serving the hit). The original/would-have-been cost is reported separately in `X-OmniRoute-Cost-Saved`. Billing consumers should sum `X-OmniRoute-Response-Cost` (hits cost nothing); cache analytics can aggregate `X-OmniRoute-Cost-Saved`. ## Exclusive Managed Session Leases Exclusive managed session leasing is an opt-in, client-neutral routing contract: one active owner holds one eligible OmniRoute connection. It does not lease a model, require OAuth, identify a particular client, or require a particular provider. The authenticating API key must have scope `lease:exclusive` and an explicit non-empty `allowedConnections` list. The database mutation boundary enforces both fields together on key creation and partial updates. ```http POST /api/v1/session-leases Authorization: Bearer Content-Type: application/json X-OmniRoute-Lease-Owner: vlo_<43-base64url-characters> {"action":"acquire","model":"glm/glm-4.6"} ``` Successful acquire, renew, and release responses expose timestamps, `state`, and the exact positive `generation`, but never the selected connection or credentials. Renew and release supply the generation in the JSON body: ```json { "action": "renew", "generation": 1 } ``` ```json { "action": "release", "generation": 1, "reason": "OWNER_EXIT" } ``` An active lease owner can explicitly request privacy-safe display metadata for its current binding: ```json { "action": "status", "generation": 1 } ``` ```json { "state": "ACTIVE", "generation": 1, "acquiredAt": "2026-08-28T12:00:00.000Z", "renewedAt": "2026-08-28T12:00:30.000Z", "expiresAt": "2026-08-28T12:02:30.000Z", "connection": { "displayName": "Primary Codex", "provider": "codex" } } ``` This opt-in status action is fenced by the opaque owner, authenticated managed API key, and exact active generation in one database transaction. `displayName` is only the trimmed configured connection name; it is `null` when no safe configured name exists. OmniRoute never substitutes an email or generated account identity. The provider value is a non-sensitive display label and never a generated compatible-provider identifier. Credentials, tokens, cookies, raw connection or API key ids, owner hashes, fencing secrets, and internal routing data are excluded. Wrong-key, wrong-owner, stale-generation, missing, expired, released, and invalidated lookups all return the same `409 LEASE_FENCE_STALE` error without connection metadata. A client that received the capacity-wait response has no active binding to inspect. When routing transitions an active lease, the same generation remains valid and status atomically returns the new binding, never the old one. Existing clients remain unchanged because acquire, renew, release, and waiting responses retain their previous shapes. This server contract does not change stock OpenAI Codex `/status`. Stock Codex currently reports its model provider and built-in authentication/account state but does not render arbitrary custom provider account metadata; a later client integration must call this action and decide how to display `connection.displayName`. Every managed inference request then supplies both control headers: ```http X-OmniRoute-Lease-Owner: vlo_<43-base64url-characters> X-OmniRoute-Lease-Generation: 1 ``` The exact owner, generation, active connection, and authenticated API key are fenced immediately before each supported upstream attempt. Replaying owner and generation with another key fails even when that key permits the same connection. Raw owners are not persisted, logged, retained in the request snapshot, or forwarded upstream. Temporary contention returns HTTP `429` with `Retry-After` and: ```json { "state": "WAITING_FOR_CAPACITY", "error": { "type": "lease_error", "code": "LEASE_CAPACITY_UNAVAILABLE" }, "reason": "NO_FREE_ELIGIBLE_CONNECTION", "retryAfter": 30 } ``` This response only means that the ordinary eligible set was non-empty and every free candidate was held by a foreign active lease. Unsupported models/providers, policy mismatch, cooldown, quota, health, and other ordinary eligibility failures retain their existing OmniRoute responses. ### `x-omniroute-compression` Per-request override of the compression plan. Highest precedence — beats the routing-combo override, the active profile, auto-trigger, and the panel Default. Values: | Value | Effect | | ------------- | ------------------------------------------------------------------------------------------- | | `off` | No compression for this request. | | `default` | The panel-derived Default profile (ignores the active profile). Lossy engines are left off. | | `safe` | Dedup and whitespace folding only. | | `allow-lossy` | Keep the operator plan for this request, including summaries and style rewrites. | | `engine:` | A single engine when enabled, e.g. `engine:rtk`. Per-request opt-in for that engine. | | `` | A named combo, matched by name (case-insensitive) first, then by id. | Notes: - Unknown values are ignored (the request is never rejected); resolution falls through to the normal operator precedence. - If multiple combos share a name, pass the combo **id** for a deterministic match. - A combo whose name is `off` or `default` cannot be selected by name (those keywords are interpreted first); reference such a combo by its id. - The master compression switch is a hard gate: when compression is disabled globally, this header cannot enable it. The applied plan is echoed back in the response header: ``` X-OmniRoute-Compression: ; source= ``` where `` is one of `request-header`, `routing-override`, `active-profile`, `auto-trigger`, `default`, or `off`. --- ## Embeddings ```bash POST /v1/embeddings Authorization: Bearer your-api-key Content-Type: application/json { "model": "nebius/Qwen/Qwen3-Embedding-8B", "input": "The food was delicious" } ``` Available providers: Nebius, OpenAI, Mistral, Together AI, Fireworks, NVIDIA, **OpenRouter**, Jina AI. Catalog ids are `provider/model` (example: `jina-ai/jina-embeddings-v5-omni-small`). Bare Jina model ids that appear in the registry (for example `jina-embeddings-v5-text-small`, `jina-reranker-v3.5`) also resolve. Jina embed/rerank/classify/segment use dashboard `jina-ai` credentials first; `JINA_AI_API_KEY` is a fallback only when no dashboard key exists. The `jina-reader` card is Reader / `r.jina.ai` only (`POST /v1/web/fetch`) and never serves embeddings or rerank. Registry models that advertise multimodal support also accept up to 32 provider-neutral structured items. Media item types are `text`, `image`, `audio`, `video`, and `document`. Their media `source` is either `{"type":"url","url":"https://..."}` or `{"type":"base64","data":"...","media_type":"..."}`. Jina v5 Omni (`jina-ai/jina-embeddings-v5-omni-small`, `jina-ai/jina-embeddings-v5-omni-nano`, and the family alias `jina-ai/jina-embeddings-v5-omni` → omni-small) also accepts Jina's native EmbeddingsV5Request docs and **forwards them intact** to `https://api.jina.ai/v1/embeddings`: ```json { "model": "jina-ai/jina-embeddings-v5-omni-small", "task": "retrieval.query", "normalized": true, "input": [ { "text": "a red bicycle" }, { "image": "https://example.com/bike.png" }, { "content": [{ "text": "caption" }, { "image": "data:image/png;base64,..." }] } ] } ``` Native `{ image | audio | video | pdf }` values may be a public HTTPS URL, a `data:` URI, or raw base64. OmniRoute does not stringify those objects or fetch native image URLs — Jina retrieves public media itself. Extra Jina fields (`task`, `normalized`, `truncate`, `embedding_type`) are forwarded. Text-only Jina SKUs still reject non-text docs. Security and transport bounds: - Remote media URLs must be public HTTPS. Canonical `{type,source:url}` items are fetched server-side (redirect revalidation, timeout, size limits, public DNS, connection pinning) and inlined before the provider call. Jina-native `{image:"https://..."}` items are forwarded as-is after the same public-HTTPS check; Jina fetches the URL. - Inline base64 media is limited to 8 MiB decoded per item and 16 MiB decoded across the request. Provider translation (canonical items are never forwarded unchanged): - Jina multimodal models: each top-level item becomes one modality-keyed object (`text` / `image` / `audio` / `video` / `pdf`) using data URIs for inline media; one vector per top-level item. - Gemini Embedding 2 family: one top-level array becomes a single native `models/{model}:embedContent` request with `content.parts` (`text` or `inline_data`). - llama.cpp (`llama-cpp/`, any model the local server loaded): canonical `text` items become plain strings, and `image` / `audio` / `video` become one `{"content": [part]}` object each, using llama-server's chat content parts (`image_url`, `input_audio` with format `wav` / `mp3` / `flac`, `input_video`) with inline data; one vector per top-level item. The server must run with `--embedding --mmproj …`; without a projector it rejects media itself. `document` is not supported. - Unknown/dynamic models without explicit modality metadata reject structured input with HTTP 400. ```json { "model": "jina-ai/jina-embeddings-v5-omni-small", "input": [ { "type": "text", "text": "A red bicycle" }, { "type": "image", "source": { "type": "url", "url": "https://example.com/bicycle.png" } } ], "dimensions": 512, "encoding_format": "float" } ``` Unsupported model/modality combinations return HTTP 400 rather than coercing the item. Non-input extension fields on legacy string/token requests continue to pass through unchanged. ```bash # List all embedding models GET /v1/embeddings ``` --- ## Image Generation ```bash POST /v1/images/generations Authorization: Bearer your-api-key Content-Type: application/json { "model": "openai/gpt-image-2", "prompt": "A beautiful sunset over mountains", "size": "1024x1024" } ``` Available providers include OpenAI (GPT Image 2), xAI (Grok Image), Together AI (FLUX), Fireworks AI, Nebius (FLUX), Hyperbolic, NanoBanana, **OpenRouter**, **ZenMux**, SD WebUI (local), ComfyUI (local). `n` returns `n` images in `data[]`. Providers whose upstream API returns one image per call (Black Forest Labs, Stability AI, Leonardo, Ideogram, Haiper, Hyperbolic, Magnific/Freepik, NVIDIA NIM, Cloudflare Workers AI) get `n` separate upstream calls, at most 4 in flight. A fixed `seed` is offset by call index (`seed`, `seed + 1`, …), so the images differ but stay reproducible. `n` is capped at 10. The request fails as a whole if any call fails. The list of fan-out formats is `SINGLE_IMAGE_FANOUT_FORMATS` in `open-sse/handlers/imageGeneration/fanOut.ts`. ZenMux reuses the existing API-key connection and accepts `zenmux/` or `zm/` prefixes: - `zenmux/openai/gpt-image-2` uses ZenMux's OpenAI Images API. Options include `size`, `quality`, `n`, `output_format`, `output_compression`, `background`, and `response_format`. - Other publishers, such as `zm/meta/muse-image-1.0`, use ZenMux's Vertex AI `:predict` endpoint. `n` maps to `sampleCount`, `aspect_ratio` to `aspectRatio`, and `image_size` (`1K`, `2K`, `4K`) to `sampleImageSize`. A pixel `size` supplies only an aspect ratio, not guaranteed pixel dimensions. Supported ratios, resolutions, and counts vary by model. - `zm/inclusionai/ming-image-0.1-design` chooses its own dimensions. Omit `size`, `aspect_ratio`, and `image_size`; explicit values return HTTP 400. PNG, JPEG, and WebP can be requested with `output_format`. This integration supports text-to-image generation, not reference-image editing. Vertex output is normalized to `data[].b64_json`; `response_format: "url"` returns an upstream HTTPS URL or a base64 data URL when only image bytes are available. Empty/filtered outputs return an error rather than an empty success. Model access depends on the ZenMux account. See [ZenMux's Vertex API](https://docs.zenmux.ai/api/vertexai/generate-images) and [OpenAI Images API](https://docs.zenmux.ai/api/openai/generate-an-image). ```bash # List all image models GET /v1/images/generations ``` --- ## Document OCR ```bash POST /v1/ocr Authorization: Bearer your-api-key Content-Type: application/json { "model": "mistral/mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://example.com/invoice.pdf" } } ``` `model` selects the OCR provider via a `provider/model` prefix; a bare model id (e.g. `mistral-ocr-latest`) resolves to its registered provider, and an omitted `model` defaults to Mistral (`mistral-ocr-latest`). Registered providers (`open-sse/config/ocrRegistry.ts`): | Provider id | Model id | `model` value | Notes | | ----------------------------- | -------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `mistral` | `mistral-ocr-latest` | `mistral/mistral-ocr-latest` (or bare `mistral-ocr-latest`) | Synchronous — the response is returned directly from the single upstream call. | | `azure-document-intelligence` | `prebuilt-read` | `azure-document-intelligence/prebuilt-read` | Asynchronous upstream (`analyze` + poll) — see below. | | `vertex-deepseek-ocr` | `deepseek-ocr-maas` | `vertex-deepseek-ocr/deepseek-ocr-maas` | Synchronous, via Vertex AI's `openapi/chat/completions` partner endpoint — see below for auth/URL. | All three providers respond in the same Mistral-shaped body: ```json { "pages": [{ "index": 0, "markdown": "# Extracted text..." }], "model": "mistral-ocr-latest", "usage_info": { "pages_processed": 1 } } ``` ### Azure Document Intelligence poll flow Azure Document Intelligence's `analyze` API is asynchronous: the initial request returns an `Operation-Location` header instead of a body, and the result must be polled for. The handler (`open-sse/handlers/ocr.ts`) polls that URL every second for up to 30 attempts, fails fast (does not keep polling) on a non-`ok` poll response or a `"failed"` status, and returns `504` if the operation is still running after the attempt budget is exhausted. The final Azure response is normalized into the same `pages`/`markdown` shape used by Mistral before being returned to the caller, so client code does not need to special-case the provider. ### Vertex AI DeepSeek OCR auth and endpoint resolution `vertex-deepseek-ocr` reuses the same Vertex AI authentication OmniRoute already supports for chat/image traffic (`open-sse/executors/vertex.ts`): the connection's API key is either a Service Account JSON credential (exchanged for a short-lived OAuth access token via the JWT-bearer flow) or an already-minted OAuth access token used as-is. The upstream endpoint URL is Vertex's generic `openapi/chat/completions` partner endpoint, built from the connection's project and region — an explicit `providerSpecificData.project`/`providerSpecificData.region` always wins; otherwise the project is derived from the Service Account JSON's `project_id` and the region defaults to `us-central1`. Both resolutions happen in `open-sse/handlers/ocr.ts` (`resolveVertexOcrAccessToken`, `resolveVertexOcrBaseUrl`), consumed by `src/app/api/v1/ocr/route.ts` before dispatching to `handleOcr`. --- ## List Models ```bash GET /v1/models Authorization: Bearer your-api-key → Returns all chat, embedding, and image models + combos in OpenAI format ``` ### Model id prefixes (`?prefix=`) Most models are advertised under a **provider prefix**. Which prefix you get is controlled by the `MODELS_CATALOG_PREFIX_MODE` feature flag, and can be overridden **per request** with a query parameter — useful for a client that wants a clean list without changing the server-wide setting for everyone else: ```bash GET /v1/models?prefix=alias # one id per model — the short alias prefix GET /v1/models?prefix=dual # both forms (server default) GET /v1/models?prefix=canonical # only the full provider-id prefix ``` | Mode | Emits | Notes | | ----------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dual` | `cc/claude-sonnet-4-6` **and** `claude/claude-sonnet-4-6` | **Default.** Both ids route to the same model; kept so client configs that hardcoded either form keep working. Roughly doubles the catalog. | | `alias` | `cc/claude-sonnet-4-6` | One entry per model. Providers without a distinct alias still emit their entry, so nothing is lost. | | `canonical` | `claude/claude-sonnet-4-6` | One entry per model under the full provider-id prefix. Providers without a distinct alias (e.g. `antigravity/…`, `agy/…`) emit their single id here too, so nothing is lost. | A `dual`-mode mirror can also be recognised without the query parameter: it carries a `parent` field pointing at the primary id. Clients that render a model picker should request `?prefix=alias` — this is what the [OmniCopilot VS Code extension](../guides/VSCODE-COPILOT.md) does. ### Discovery filters (`?capability=`, `?min_context=`, `?kind=`, `?q=`) `GET /v1/models` accepts opt-in discovery filters. Without any of them the response is unchanged. They narrow the catalog the caller already sees (API-key permissions apply first), and they run before the OpenAI-style `limit` / `after` paging: ```bash GET /v1/models?capability=vision&capability=tool_calling # every listed capability must be true GET /v1/models?capability=vision,reasoning&min_context=200000 GET /v1/models?kind=embedding # catalog `type`; rows without one are `chat` GET /v1/models?q=sonnet&limit=10 # case-insensitive match on id or name ``` | Param | Values | | ------------- | --------------------------------------------------------------------------------------------------- | | `capability` | `attachment`, `reasoning`, `structured_output`, `temperature`, `thinking`, `tool_calling`, `vision` | | `min_context` | non-negative integer, compared with `context_length` | | `kind` | `chat`, `audio`, `embedding`, `image`, `moderation`, `music`, `rerank`, `video` | | `q` | 1–200 characters | An unknown value, or a `min_context` that is not a non-negative integer (for example `1.5`), returns HTTP `400` and the message lists the valid values. Implementation: `src/app/api/v1/models/catalogFilters.ts`. ### Model shortlist (`POST /v1/models/query`) `POST /v1/models/query` returns a short, deterministic list from the same catalog, with the same auth and per-key visibility as `GET /v1/models`: ```bash POST /v1/models/query Authorization: Bearer your-api-key Content-Type: application/json { "capabilities": ["vision", "tool_calling"], "min_context": 128000, "max_output": 16000, "kind": "chat", "q": "gpt", "owned_by": "openai", "exclude": ["openai/gpt-4o"], "limit": 5 } → { "object": "list", "data": [ ...models ] } ``` Every field is optional. `capabilities` and `exclude` take an array or a comma-separated string; `max_output` compares with `max_output_tokens`. Results are sorted by `owned_by`, then `id`. `limit` defaults to `20` and values above `100` are capped at `100`. Invalid JSON, an unknown field or an invalid value returns HTTP `400`. ### Individually hidden chat models A model marked **Hidden** on its provider page is excluded from the catalog and rejected with HTTP `404` / `model_not_found` when requested explicitly. The check uses the resolved provider and model, including provider aliases, compatible-provider node prefixes, and connection defaults. A combo skips hidden targets and can use a visible sibling; when no executable target remains it returns the same error code. Unhiding takes effect on the next request. Image-only visibility overrides do not hide the chat model with the same ID. This individual model setting is separate from the [model exposure allow/deny lists](../routing/MODEL_EXPOSURE_LIST.md), which filter catalog advertisement and auto-routing candidates while retaining explicit dispatch. API-key model permissions continue to apply independently. The default catalog prefix mode remains `dual`. ### No-thinking model variants For thinking-capable Claude models, `/v1/models` also advertises a **no-thinking** variant whose id is prefixed with `claude-3-omniroute-no-thinking/`: ``` claude-3-omniroute-no-thinking// ``` Selecting this id (e.g. in a Claude Code config that always attaches a `thinking` block) resolves back to the real `/` with reasoning suppressed — `thinking:{type:"disabled"}` on the `/v1/messages` path, or the `reasoning`/`reasoning_effort` fields dropped on the `/v1/chat/completions` path. The variant is only listed for Claude-family models that support thinking **and** honor `disabled` (so e.g. adaptive-only models that reject `disabled` are excluded). Operators can force the variant on or off per model via `ModelSpec.noThinkingAlias`. --- ## Provider Plugin Manifest ```bash GET /api/v1/provider-plugin-manifest ``` Returns the JSON-safe provider plugin manifest used by Bifrost, CLIProxyAPI, and future sidecar routers. The response is generated from the TypeScript provider registry and intentionally excludes OAuth client secrets, runtime environment resolution, executor functions, request headers, and account data. Use this endpoint when a sidecar runs out-of-process and cannot import `open-sse/config/providerPluginManifestRegistry.ts` directly. --- ## Compatibility Endpoints | Method | Path | Format | | ------ | ----------------------------------------- | ---------------------------------- | | POST | `/v1/chat/completions` | OpenAI | | POST | `/v1/messages` | Anthropic | | POST | `/v1/responses` | OpenAI Responses | | POST | `/v1/embeddings` | OpenAI | | POST | `/v1/images/generations` | OpenAI Images | | POST | `/v1/images/edits` | OpenAI Images (edit/inpaint) | | POST | `/v1/videos/generations` | OpenAI-style video generation | | POST | `/v1/music/generations` | OpenAI-style music generation | | POST | `/v1/audio/transcriptions` | OpenAI Audio (STT) | | POST | `/v1/audio/speech` | OpenAI TTS (returns audio body) | | POST | `/v1/rerank` | Cohere/Voyage-style rerank | | POST | `/v1/classify` | Jina classify (`api.jina.ai`) | | POST | `/v1/segment` | Jina segmenter (`segment.jina.ai`) | | POST | `/v1/moderations` | OpenAI Moderations | | GET | `/v1/models` | OpenAI | | POST | `/v1/models/query` | Filtered model shortlist | | POST | `/v1/messages/count_tokens` | Anthropic | | GET | `/v1beta/models` | Gemini | | POST | `/v1beta/models/{...path}` | Gemini generateContent | | POST | `/v1/api/chat` | Ollama | | GET | `/api/v1/vscode/{token}/` | OpenAI catalog alias | | GET | `/api/v1/vscode/{token}/models` | OpenAI models alias | | POST | `/api/v1/vscode/{token}/chat/completions` | OpenAI tokenized alias | | POST | `/api/v1/vscode/{token}/responses` | OpenAI Responses tokenized alias | | POST | `/api/v1/vscode/{token}/api/chat` | Ollama tokenized alias | | GET | `/api/v1/vscode/{token}/api/tags` | Ollama tags tokenized alias | All POST routes follow the same shape: `Bearer your-api-key` + Zod-validated JSON body (`v1RerankSchema`, `v1ModerationSchema`, `v1AudioSpeechSchema`, etc., see `src/shared/validation/schemas.ts`). 4xx is returned on schema failure. For clients that cannot attach `Authorization: Bearer ...`, OmniRoute also accepts API keys in the URL via either query-string compatibility (`?token=...`, `?apiKey=...`, `?api_key=...`, `?key=...`) or the dedicated `/api/v1/vscode/{token}/...` endpoints documented below. ```bash # Rerank (cloud registry provider, or an OpenAI-compatible provider node as "/") POST /v1/rerank { "model": "jina-ai/jina-reranker-v3.5", "query": "...", "documents": ["..."] } # Jina classify (Foundation API credentials) POST /v1/classify { "model": "jina-embeddings-v5-text-small", "input": ["..."], "labels": ["a", "b"] } # Jina segmenter POST /v1/segment { "content": "...", "return_chunks": true } # Jina search (s.jina.ai; provider aliases: jina-search, jina-ai, jina) POST /v1/search { "query": "...", "provider": "jina-search" } # Moderations POST /v1/moderations { "model": "omni-moderation-latest", "input": "..." } # TTS — returns audio/mpeg (or requested format) body POST /v1/audio/speech { "model": "openai/tts-1", "input": "Hello", "voice": "alloy" } # Soniox TTS requires a language and a voice: `language` defaults to "en"; a missing # voice or an OpenAI stock voice name (alloy, nova, …) becomes "Adrian" POST /v1/audio/speech { "model": "soniox/tts-rt-v1", "input": "Xin chào", "voice": "Adrian", "language": "vi" } # Image edit (multipart) POST /v1/images/edits -F image=@input.png -F prompt="..." -F mask=@mask.png # Video / music generation (provider-prefixed model id) POST /v1/videos/generations { "model": "runway/gen-3", "prompt": "..." } POST /v1/music/generations { "model": "kie/suno-v4.0", "prompt": "..." } ``` > **Rerank provider nodes:** `POST /v1/rerank` also routes to OpenAI-compatible provider nodes > (oMLX, vLLM, Infinity, TEI behind a gateway, …) addressed as `/`. Loopback > nodes (`localhost`, `127.0.0.1`, `172.16.0.0/12`) are always eligible, and so are hostnames the > operator lists in `OMNIROUTE_LOCAL_PROVIDER_NODE_HOSTS` (e.g. a Docker/Compose service name such > as `http://reranker:8080/v1`; these are called directly, never through `HTTP(S)_PROXY` or a > connection's pinned proxy). Nodes on any other > host — a LAN box or Tailscale peer — are eligible only when the operator enables the > `RERANK_REMOTE_PROVIDER_NODES` feature flag **and** the node's base URL passes the provider > outbound URL policy (`OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` / `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`). > The memory engine's rerank step calls this route over > loopback, so the same rule governs `rerankProviderModel` in the Memory settings. > > **Local server shapes:** the node is called at `/v1/rerank` and, on 404, at `/rerank` > (Infinity, TEI). The upstream body carries both the Cohere/OpenAI spelling (`documents`, > `return_documents`) and the TEI spelling (`texts`, `return_text`), and the upstream response is > normalized to the Cohere envelope: TEI's bare `[{index, score, text}]`, `{results: [{index, score}]}` > from thin gateways, and Voyage-style `{data: [...]}` all come back to the client as > `{results: [{index, relevance_score, document?}]}`, sorted by score and capped at `top_n`. > **Provider-node discovery:** models on an OpenAI-compatible provider node appear in `GET /v1/models` > under the node prefix. Rows that carry no endpoint metadata (typical for local `/v1/models` listings) > inherit the node's `apiType`, so an `embeddings` node's models are `type: "embedding"` and a > `rerank` node's models are `type: "rerank"` instead of defaulting to chat; an explicit > `supportedEndpoints` on a synced or manually added row still takes precedence. ### Dedicated Provider Routes ```bash POST /v1/providers/{provider}/chat/completions POST /v1/providers/{provider}/embeddings POST /v1/providers/{provider}/images/generations ``` The provider prefix is auto-added if missing. Mismatched models return `400`. --- ## Files API OpenAI-compatible files endpoint for batch input/output and file-purpose uploads. | Method | Path | Description | | ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------- | | POST | `/v1/files` | Upload a file (multipart: `file`, `purpose`, `expires_after[anchor]`, `expires_after[seconds]`) — 512 MiB max | | GET | `/v1/files` | List files for the authenticated API key | | GET | `/v1/files/[id]` | Retrieve a file's metadata | | DELETE | `/v1/files/[id]` | Delete a file | | GET | `/v1/files/[id]/content` | Stream the raw file body back | **Auth:** Bearer API key — files are scoped per-API-key via `getApiKeyRequestScope`. A key sees, downloads and deletes its own files only; a dashboard session without a key reads the whole instance; a file with no owner (anonymous or dashboard-session upload) is denied to every non-session caller. `GET /v1/files` rejects an anonymous caller — and a presented key that does not resolve — with `401` even when `REQUIRE_API_KEY=false`, instead of listing every tenant's files (GHSA-m3hp-hq9g-fpmv, GHSA-2jm2-mpx8-6523). --- ## Batches API OpenAI-compatible batch processing. | Method | Path | Description | | ------ | ------------------------- | --------------------------------------------------------------------------------------------------------- | | POST | `/v1/batches` | Create batch — body validated by `v1BatchCreateSchema` (`input_file_id`, `endpoint`, `completion_window`) | | GET | `/v1/batches` | List batches | | GET | `/v1/batches/[id]` | Retrieve batch status + `request_counts` | | DELETE | `/v1/batches/[id]` | Delete a finished/failed batch | | POST | `/v1/batches/[id]/cancel` | Cancel an in-progress batch | **Auth:** Bearer API key. Batches are scoped per-API-key under the same three-way rule as files: own key only, dashboard session instance-wide, null-owner records denied to every non-session caller (retrieve, delete, cancel, and the `input_file_id` check on create). `GET /v1/batches` rejects an anonymous caller with `401` even when `REQUIRE_API_KEY=false`. --- ## Search API Web/search provider abstraction (Tavily, Brave, Exa, Serper, etc.). | Method | Path | Description | | ------ | ---------------------- | ------------------------------------------------------------------------------------ | | GET | `/v1/search` | List configured search providers + capabilities | | POST | `/v1/search` | Run a search query — body validated by `v1SearchSchema`, supports caching/coalescing | | GET | `/v1/search/analytics` | Per-provider hit/latency/cache stats | **Auth:** Bearer API key (`extractApiKey` + `isValidApiKey`). Search policy enforced via `enforceApiKeyPolicy`. ### Exa and Tavily `provider_options` Exa and Tavily read provider-specific fields from a namespaced bag: `provider_options.exa` (or `provider_options["exa-search"]`) and `provider_options.tavily` (or `provider_options["tavily-search"]`). Only the keys below are forwarded. Each one is checked against its own Zod schema in `open-sse/handlers/search/providerOptionsPassthrough.ts`, and a key with the wrong type is dropped. Any other key is ignored, including auth, base URL, `stream` and `outputSchema`. The flat keys used by other providers (`cx`, `depth`, ...) work as before. | Provider | Top-level fields | Nested | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Exa | `type`, `category`, `userLocation`, `moderation`, `additionalQueries`, `objective`, `systemPrompt`, `compliance` (flat primitives), `startPublishedDate`, `endPublishedDate` | `contents.{text, highlights, summary, extras, subpages, subpageTarget, livecrawlTimeout, maxAgeHours}` merged over the default `{ text: true, highlights: true }` | | Tavily | `search_depth`, `chunks_per_source`, `time_range`, `start_date`, `end_date`, `include_published_date`, `filter_by_published_date`, `include_answer`, `include_raw_content`, `include_images`, `include_image_descriptions`, `include_favicon`, `include_domains_mode`, `language`, `filter_by_language`, `auto_parameters`, `exact_match`, `include_usage`, `safe_search` | — | The generic fields always win: `query`, `max_results`, `filters` (domains), `country` and the topic derived from `search_type`. On Exa, `search_type: "news"` keeps `category: "news"`. Tavily also accepts `search_type: "finance"` (`topic: "finance"`). The generic `time_range` now reaches both providers: Exa gets `startPublishedDate` (ISO timestamp) and Tavily gets `start_date` (`YYYY-MM-DD`). An explicit date in `provider_options` overrides the mapped one. Exa results keep `id`, `highlight_scores`, `summary`, `subpages`, `entities` and `extras` in `provider_raw`. Tavily results keep `images` and `favicon`. Cost accounting is still the flat registry `costPerQuery`. Tavily bills `search_depth: "advanced"` at more credits per call, and that difference is not reflected in `usage.search_cost_usd`. --- ## Web Fetch API Extract content from a URL via a configured web-fetch provider (Firecrawl, Jina Reader, Tavily Extract, TinyFish Fetch, Nimble Extract). | Method | Path | Description | | ------ | --------------- | --------------------------------------------------------- | | POST | `/v1/web/fetch` | Fetch/scrape a URL — body validated by `v1WebFetchSchema` | **Auth:** Bearer API key (`extractApiKey` + `isValidApiKey`). Policy enforced via `enforceApiKeyPolicy`. **Quota-aware fallback (#8297):** when no explicit `provider` is given, the pool (`firecrawl` → `jina-reader` → `tavily-search` → `tinyfish` → `nimble-search`) is walked in fixed priority order (fill-first) — a rate-limited-but-configured provider is skipped instead of short-circuiting the request, and a retryable/quota upstream failure (HTTP 429 always; 402/403 for Firecrawl/Tavily/TinyFish quota-style free tiers — not for Jina Reader, and never for a plain 400 bad request) falls through to the next untried credentialed provider at request time. When every provider in the pool is exhausted, the endpoint returns a single `429` (with a `Retry-After` header) instead of the previous generic `400`. When an explicit `provider` is requested, there is **no** silent fallback — a rate-limited or failing explicit provider surfaces its own error (`429` if rate-limited, otherwise the upstream status). --- ## WebSocket Streaming ```bash GET /v1/ws?handshake=1 ``` Validates a WebSocket upgrade handshake and returns the wire protocol example messages (`request`, `cancel`). Actual WS frames are handled by the bundled WS server outside the Next.js route table. **Auth:** Bearer API key during handshake. ### Responses API over WebSocket (codex only) ```bash # Same host:port as the HTTP API (default 20128); upgrade the connection: wscat -c "ws://localhost:20128/v1/responses?api_key=" # (or: -H "Authorization: Bearer ") # First frame MUST be response.create: { "type": "response.create", "model": "gpt-5.5", "input": [ { "role": "user", "content": "hi" } ] } ``` A Responses-API-over-WebSocket proxy is wired **exclusively to `codex`** (ChatGPT backend). It listens on the same port as the API/dashboard at paths `/v1/responses`, `/responses`, and `/api/v1/responses`. On the first `response.create` frame it authenticates + prepares via the internal `codex-responses-ws` bridge, selects a codex OAuth connection, and tunnels to `wss://chatgpt.com/backend-api/codex/responses` via the `wreq-js` transport. **Non-codex models are rejected** (`codex_ws_provider_required`). For quota-share routing use `model: "qtSd//codex/"`. Implemented in `app/server-ws.mjs` + `scripts/dev/responses-ws-proxy.mjs` + `src/app/api/internal/codex-responses-ws/route.ts`. **Auth:** Bearer API key during handshake. The bundled HTTP server (`server-ws.mjs`) must be the active entrypoint (it is, by default, when `app/server-ws.mjs` exists). #### Model id: use the bare ChatGPT id (no `codex/` prefix) The OpenAI **Codex CLI** validates the model name client-side when `supports_websockets = true` and **rejects provider-prefixed ids** like `codex/gpt-5.5` (`The 'codex/gpt-5.5' model is not supported when using Codex with a ChatGPT account`). Send the **bare** id (e.g. `gpt-5.5`). OmniRoute's bridge is codex-only, so it re-resolves a bare id as a codex model (`resolveCodexWsModelInfo`) before tunneling upstream — even though a bare `gpt-5.5` would otherwise route to another provider over HTTP. #### Configuring the OpenAI Codex CLI Point the Codex CLI at OmniRoute by adding a custom provider with WebSocket support to `~/.codex/config.toml` (use a separate `CODEX_HOME` to avoid touching an existing config): ```toml model = "gpt-5.5" # bare id — NOT "codex/gpt-5.5" model_provider = "omniroute" [model_providers.omniroute] name = "OmniRoute (WS)" base_url = "http://localhost:20128/v1" # no trailing slash; the WS URL is derived (use https/wss in production) wire_api = "responses" # only supported value since Feb 2026 supports_websockets = true # enables the Responses-over-WS transport env_key = "OMNIROUTE_API_KEY" # holds the OmniRoute API key (Bearer) ``` ```bash export OMNIROUTE_API_KEY=sk-... # an OmniRoute API key (any key if REQUIRE_API_KEY=false) codex exec "Responda apenas: PONG" ``` The CLI upgrades `base_url + /responses` to a WebSocket and OmniRoute tunnels it to the selected codex OAuth connection. Validated end-to-end against the local server: ChatGPT returns `codex.rate_limits` + `response.created` and streams the completion. --- ## Quotas & Issues Reporting | Method | Path | Description | | ------ | ------------------- | ------------------------------------------------------------------------------------- | | GET | `/v1/quotas/check` | Pre-validate quota for a `provider` + `accountId` before issuing a registered key | | POST | `/v1/issues/report` | Report a quota/key issuance failure to GitHub (requires `GITHUB_ISSUES_REPO` + token) | **Auth:** Bearer API key (`isAuthenticated`). --- ## Self-service usage (`/api/usage/om-usage`) Any API key can read **its own** usage and quotas — no management auth. This is the endpoint a client (CLI, the OmniCopilot panel) uses to show a key holder their spend. ```bash # Text form (the historical contract — plain text for a terminal) curl -H "Authorization: Bearer " \ http://localhost:20128/api/usage/om-usage # Structured form — what a UI consumes curl -H "Authorization: Bearer " \ "http://localhost:20128/api/usage/om-usage?format=json" ``` The key must have **`allowUsageCommand`** enabled (off by default — the dashboard's API-key manager toggles it per key). Without it the endpoint answers `403`. `?format=json` returns a discriminated shape so a caller never reads a data field off a refusal. On success: ```jsonc { "allowed": true, // present only when the key opted into per-key usage limits (daily/weekly USD): "personal": { "dailySpentUsd": 1.25, "dailyLimitUsd": 5, "dailyResetAtIso": "…", "weeklySpentUsd": 8, "weeklyLimitUsd": 20, "weeklyResetAtIso": "…" /* … */, }, // the selected provider quota snapshot, or null when nothing is cached yet: "provider": { "connectionId": "…", "provider": "claude", "plan": "…", "quotas": {/* … */} }, // every connection's snapshot, so a UI can render several providers side by side: "providers": [ { "connectionId": "…", "provider": "claude" /* … */ }, { "provider": "codex" /* … */ }, ], } ``` On refusal (`401` bad key / `403` not allowed) the same route returns `{ "allowed": false, "error": { "message": "…" } }` — a present-but-empty `personal`/`provider` (key allowed, nothing learned yet) is a different state from a refusal, and only the JSON form distinguishes them. **Auth:** the caller's own Bearer API key, validated with `isValidApiKey` — this is _not_ the management surface (`/api/keys/…`), which stays behind `requireManagementAuth`. ### Raw vs. Cutoff-Adjusted Quota Percentages When consuming `/api/usage/om-usage`, callers should note the distinction between the percentages presented by the **plain-text output** and the structured **JSON output**: | Aspect | Plain-text format (`/api/usage/om-usage` / `@@om-usage`) | JSON format (`?format=json`) | | :------------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | | **Output field** | `NN% left` (e.g., `20% left`) | `provider.quotas` / `providers[].quotas` | | **Measurement** | **Cutoff-adjusted usable allowance**: remaining capacity above the cutoff threshold | **Raw upstream quota**: unadjusted provider measurement | | **Cutoff scaling** | Scaled via `effectiveRemainingPercent()` so the protected reserve reads as `0% left` | None (raw snapshot values as reported by the provider) | | **Threshold data** | Incorporated into the displayed percentage | Stored in `quotaWindowThresholds` per connection (or server defaults) | | **Primary consumer** | Interactive CLIs, terminal prompts, human operators | Programmatic integrations, dashboards, analytics, billing trackers | #### The Cutoff Adjustment Formula OmniRoute supports proactive quota cutoffs to prevent upstream exhaustion (configured via connection `quotaWindowThresholds`, provider defaults, or global resilience settings). When a cutoff threshold (e.g., `cutoff = 25%`) is active on a quota window, the text renderer calculates remaining allowance as: - If `remaining <= cutoff`: returns `0%`. - If `remaining > cutoff`: returns `((remaining - cutoff) / (100 - cutoff)) * 100`. The formatted text output rounds this to the nearest integer (`Math.round(...)`). #### Illustrative Example Consider a provider window with **60% raw usage** (hence **40% raw remaining quota**) and a configured **25% remaining-quota cutoff** threshold: | Representation | Calculation | Remaining | Implied Used | | :---------------------------- | :------------------------------------------------- | :-------: | :----------------------------------------: | | **Raw JSON (`?format=json`)** | Upstream snapshot (`quotas[window]`) | **40%** | **60%** | | **Text format (`NN% left`)** | `((40 - 25) / (100 - 25)) * 100 = (15 / 75) * 100` | **20%** | **80%** (if calculating `100 - remaining`) | - **Why text shows 20% left:** Out of the 40% physical quota left on the account, 25% is reserved by the cutoff policy. The caller has only 15% out of 75% usable headroom remaining before the router stops routing to this connection. Scaling ensures the connection signals exhaustion (`0% left`) exactly when routing will cease. - **Why JSON preserves raw values:** The JSON payload returns the exact provider snapshot (`UsageSnapshot`) so external tools have access to ground-truth upstream utilization and connection-level `quotaWindowThresholds` without server-side lossy scaling. #### Consumer Migration Guidance If an external integration (such as an editor plugin or agent extension) previously scraped the text endpoint (`NN% left`) and computed used percentage as `100 - NN`, migrating to `?format=json` means: - Reading raw JSON values will reflect **actual upstream consumption** (60% used in the example above). - If the integration wishes to preserve the text endpoint's usable-allowance semantics, it can evaluate the effective remaining formula client-side using `quotaWindowThresholds` for connection-level overrides. --- ## Semantic Cache ```bash # Get cache stats GET /api/cache/stats # Clear all caches DELETE /api/cache/stats ``` Response example: ```json { "semanticCache": { "memorySize": 42, "memoryMaxSize": 500, "dbSize": 128, "hitRate": 0.65 }, "idempotency": { "activeKeys": 3, "windowMs": 5000 } } ``` ### Latency impact A semantic cache HIT serves the response from cache **without an upstream call**, so the reported `X-OmniRoute-Response-Latency` is near-zero (regardless of the original upstream latency). Latency-sensitive clients (benchmarking, p50/p99 monitoring) should check the `X-OmniRoute-Cache-Latency` response header: | Value | Meaning | | ----------- | ------------------------------------------------------------- | | `synthetic` | Response served from cache; latency is not real upstream time | | _(absent)_ | Response from real upstream call | ### Per-key cache bypass API keys can opt out of semantic cache reads via `cacheDefaultMode`: | Value | Behavior | | -------- | ----------------------------------------------- | | `legacy` | Normal cache behavior (default) | | `bypass` | Skip cache lookup entirely; always hit upstream | Set at key creation (`POST /api/keys`) or update (`PATCH /api/keys/[id]`): ```json { "cacheDefaultMode": "bypass" } ``` ### Per-request bypass Any request can bypass the cache regardless of key settings: ``` X-OmniRoute-No-Cache: true ``` --- ## Dashboard & Management Management routes (`/api/*` except public auth/login) are **not** authorized by ordinary inference API keys. Credential families, scopes, and curl examples: [Management Authentication](../guides/MANAGEMENT-AUTH.md). ### Authentication | Endpoint | Method | Description | | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/auth/login` | POST | Login | | `/api/auth/logout` | POST | Logout | | `/api/settings/require-login` | GET/POST | Login status (GET, public). POST sets the password / toggles login: operator-only once a password exists (dashboard session, local CLI, or login disabled — API keys and `oma_` tokens get `403`); a dashboard session sends `currentPassword` to replace the password | ### Provider Management | Endpoint | Method | Description | | --------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/providers` | GET/POST | List / create providers | | `/api/providers/[id]` | GET/PUT/DELETE | Manage a provider | | `/api/providers/[id]/test` | POST | Test provider connection | | `/api/providers/[id]/models` | GET | List provider models | | `/api/providers/validate` | POST | Validate provider config | | `/api/providers/bulk` | POST | Bulk-add API keys for ONE provider | | `/api/providers/import` | POST | Import a heterogeneous provider LIST from a parsed CSV/JSON file (#6836); per-row partial-failure results | | `/api/provider-nodes*` | Various | Provider node management | | `/api/provider-models` | GET/POST/PUT/PATCH/DELETE | Custom models and per-model overrides (add, update, hide/show, delete) | | `/api/provider-models/validate-and-add` | POST | Management-authenticated, opt-in strict-connection validation and atomic custom-model registration; see [Model validation](../guides/MODEL-VALIDATION.md) | For synced/imported models, `PUT /api/provider-models` accepts `provider`, `modelId`, and `maxOutputTokenOverride`: a positive integer sets the manual output-token cap, and `null` clears it to restore the default. `GET /api/provider-models?provider=` returns these values in `modelOutputOverrides`, including models without a custom-model row. The override uses the runtime `max_output_tokens` capability and survives a model re-sync. The OpenAI-compatible provider page offers the same edit/clear controls and marks models with explicit vision support. Custom Chat Completions nodes adapt explicit reasoning opt-outs to the upstream backend. A successful connection test automatically selects chat-template controls for each exact model ID whose `/models` entry proves a recognized `owned_by` value: `vllm`, `sglang`, or `llamacpp`. Transparent OpenAI-compatible wrappers may preserve the original model entry inside a nested `openai` object; detection follows up to three such envelopes. Models with missing, unknown, or conflicting ownership keep ordinary OpenAI behavior. Detection reuses the existing catalog request, generates no completion tokens, and is invalidated when the connection endpoint changes. To pin the behavior for a backend that does not expose that metadata, use the existing partial provider update API: ```json { "providerSpecificData": { "reasoningControl": "chat-template" } } ``` Send that body with `PUT /api/providers/`. On that connection, an explicit reasoning effort of `none` is sent as `chat_template_kwargs.thinking=false` and `chat_template_kwargs.enable_thinking=false`. Explicit native template values remain authoritative unless a server-side reasoning rule forces an effort. The setting applies only when a custom OpenAI-compatible connection dispatches a Chat Completions body; Responses requests and ordinary providers keep their native request shape. Set `reasoningControl` to `openai` to force ordinary OpenAI `reasoning_effort` passthrough, or omit it/set it to `null` to use automatic detection. Claude Code auto-mode classifier requests default native thinking to disabled when they contain no explicit reasoning controls. Detection uses the classifier's system marker in Claude-format requests, not model names or completion limits. Explicit body controls, supported effort/thinking headers, routing rules, and resolved model effort keep their existing priority. Both classifier stages retain their prompts, completion limits, stop sequences, and real upstream permission verdicts; the second stage can still produce its requested visible reasoning as ordinary text. ### OAuth Flows | Endpoint | Method | Description | | -------------------------------- | ------- | ----------------------- | | `/api/oauth/[provider]/[action]` | Various | Provider-specific OAuth | ### Routing & Config | Endpoint | Method | Description | | --------------------- | -------- | ----------------------------- | | `/api/models/alias` | GET/POST | Model aliases | | `/api/models/catalog` | GET | All models by provider + type | | `/api/combos*` | Various | Combo management | | `/api/keys*` | Various | API key management | | `/api/pricing` | GET | Model pricing | ### Usage & Analytics | Endpoint | Method | Description | | -------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/usage/history` | GET | Usage history | | `/api/usage/logs` | GET | Usage logs | | `/api/usage/request-logs` | GET | Request-level logs | | `/api/usage/[connectionId]` | GET | Per-connection usage | | `/api/usage/token-limits` | GET/POST/DELETE | Per-API-key token-limit budgets | | `/api/usage/model-latency-stats` | GET | Rolling per-provider/model latency aggregate (avg/p50/p95/p99, success rate); filters: `windowHours`/`minSamples`/`maxRows`/`provider`/`model` (#6873) | | `/api/usage/cache-health` | GET | Prompt-cache health summary over `call_logs` — write/read ratio, p50/p90/p99 write-size distribution, heavy-write concentration, per-model split, and a `healthy`/`degraded`/`thrash`/`no-data` verdict; query params `range` (`1h`\|`24h`\|`7d`\|`30d`, default `24h`) and optional `model` (#8827) | ### API key permissions `PATCH /api/keys/{id}` updates an existing key's permissions. Like every `/api/keys*` route it needs management authorization (see [Management Authentication](../guides/MANAGEMENT-AUTH.md)), not an inference key. Send only the fields you want to change; a request with none of them is rejected with `No valid fields to update`. The accepted fields are defined by `updateKeyPermissionsSchema` in `src/shared/validation/schemas/keys.ts`. | Field | Type | Notes | | ------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `name` | string, 1-200 chars | | | `isActive` | boolean | | | `isBanned` | boolean | | | `expiresAt` | ISO 8601 datetime or `null` | `null` clears the expiry | | `modelAccessMode` | `all` \| `restricted` | `allowedModels` must be empty when the mode is `all` | | `allowedModels`, `blockedModels` | array of strings, up to 1000 | | | `allowedCombos` | array of strings, up to 500 | Gates which combos the key may call; direct models are governed by `modelAccessMode` / `allowedModels` | | `connectionAccessMode` | `all` \| `restricted` | `allowedConnections` must be non-empty when `restricted` and empty when `all` | | `allowedConnections` | array of UUIDs, up to 100 | | | `allowAutoCombos` | boolean | `false` rejects requests for `auto/*` models with this key; keys that never set it are allowed | | `catalogScope` | `all` \| `combos` \| `models` | What `GET /v1/models` lists for this key (combos only, models only, or both); it does not change what the key may call | | `forcedModel` | string up to 300 chars, or `null` | Routes chat requests to this model/combo, over `X-Route-Model` and `body.model`; the key's limits still apply | | `noLog`, `autoResolve` | boolean | | | `throttleDelayMs` | integer, 0-300000 | | | `maxSessions` | integer, 0-10000 | | | `rateLimits` | array of `{ limit, window }` (positive integers, up to 50) or `null` | `null` clears the limits | | `accessSchedule` | schedule object or `null` | `null` clears the schedule | | `scopes` | array of strings, up to 32 | | | `allowedEndpoints` | array of strings, up to 20 | | | `streamDefaultMode` | `legacy` \| `json` | | | `cacheDefaultMode` | `legacy` \| `bypass` | See [Per-key cache bypass](#per-key-cache-bypass) | | `compressionEnabled` | boolean | | | `codexServiceMode` | one of the Codex service modes | | | `disableNonPublicModels` | boolean | | | `allowUsageCommand` | boolean | | | `usageLimitEnabled` | boolean | | | `dailyUsageLimitUsd`, `weeklyUsageLimitUsd` | number >= 0 or `null` | | | `chaosModeEnabled` | boolean | | ```bash curl -X PATCH "$OMNIROUTE_URL/api/keys/$KEY_ID" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "allowAutoCombos": false, "catalogScope": "combos" }' ``` ### Settings | Endpoint | Method | Description | | ------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/settings` | GET/PUT/PATCH | General settings. Security-policy keys (`requireLogin`, `newPassword`, `oidc*`, `corsOrigins`, `bruteForceProtection`) need `currentPassword` and an operator credential — API keys and `oma_` tokens get `403`; every mutation needs the `admin` access-token scope | | `/api/settings/proxy` | GET/PUT | Network proxy config | | `/api/settings/proxy/test` | POST | Test proxy connection | | `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist | | `/api/settings/thinking-budget` | GET/PUT | Thinking/reasoning **request** rewrite mode (passthrough / auto-strip / custom / adaptive). Independent of compression. See [THINKING_BUDGET.md](../guides/THINKING_BUDGET.md). | | `/api/settings/system-prompt` | GET/PUT | Global system prompt | | `/api/settings/compression` | GET/PUT | Global compression config | | `/api/settings/purge-request-history` | POST | Clear request log rows and local call-log artifacts | ### Context & Compression | Endpoint | Method | Description | | -------------------------------------- | -------------- | ------------------------------------------------------------------------ | | `/api/compression/preview` | POST | Preview off/lite/standard/aggressive/ultra/RTK/stacked compression | | `/api/compression/language-packs` | GET | List available Caveman language packs | | `/api/compression/rules` | GET | List Caveman rule metadata | | `/api/context/caveman/config` | GET/PUT | Caveman-specific settings alias | | `/api/context/rtk/config` | GET/PUT | RTK-specific settings, including custom filters and raw-output retention | | `/api/context/rtk/filters` | GET | RTK filter catalog and custom-filter diagnostics | | `/api/context/rtk/test` | POST | Run RTK preview/test against a text payload | | `/api/context/rtk/raw-output/[id]` | GET | Read retained redacted raw output by pointer id | | `/api/context/combos` | GET/POST | Compression combo list/create | | `/api/context/combos/[id]` | GET/PUT/DELETE | Compression combo detail/update/delete | | `/api/context/combos/[id]/assignments` | GET/PUT | Assign compression combos to routing combos | | `/api/context/analytics` | GET | Compression analytics alias | ### Monitoring | Endpoint | Method | Description | | ------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/sessions` | GET | Active session tracking | | `/api/rate-limits` | GET | Per-account rate limits | | `/api/monitoring/health` | GET | Health check + provider summary (`catalogCount`, `configuredCount`, `activeCount`, `monitoredCount`). Management view includes `credentialHealth`: probe-cache scalars, `failedConnections` when `failed>0`, and `staleDbNonOkCount` (SQLite sticky `test_status`, not the gauge). See [MONITORING_GUIDE.md](../ops/MONITORING_GUIDE.md#credentialhealth-probe-cache-vs-sqlite-test_status). | | `/api/cache/stats` | GET/DELETE | Cache stats / clear | | `/api/modality-bridge/stats` | GET | In-memory `attempts`, successes/`bridged`, failures, cache hits, `totalLatencyMs`, `latencySamples`, sample-denominated `averageLatencyMs`, and last-use time (reset on restart; management auth) | | `/api/modality-bridge/video/runtime` | GET | Strict trusted-loopback check before management auth/probe; sanitized FFmpeg/ffprobe availability and versions (no-store) | | `/api/modality-bridge/video/extract` | POST | Internal authenticated trusted-loopback byte broker; 50 MiB input, bounded queue/32 MiB output, `503` capacity, `499` disconnect, `504` deadline; not a public upload API | ### Backup & Export/Import | Endpoint | Method | Description | | --------------------------- | ------ | ----------------------------------------------------------------------- | | `/api/db-backups` | GET | List available backups | | `/api/db-backups` | PUT | Create a manual backup | | `/api/db-backups` | POST | Restore from a specific backup | | `/api/db-backups` | PATCH | Save backup retention settings (operator-only) | | `/api/db-backups` | DELETE | Prune stored backups to the retention settings (operator-only) | | `/api/db-backups/export` | GET | Download database as .sqlite file | | `/api/db-backups/import` | POST | Upload .sqlite file to replace database (`?dryRun=true` validates only) | | `/api/db-backups/exportAll` | GET | Download full backup as .tar.gz archive | `/api/db-backups/export`, `/api/db-backups/exportAll` and `/api/settings/export-json` hand out every stored credential, so they require the dashboard session or the local CLI (or login disabled). API keys of any scope and `oma_` access tokens get `403`. The restores hold the same boundary: `POST /api/db-backups/import` (`dryRun=true` included), `POST /api/db-backups` (restore by id) and `POST /api/settings/import-json` replace the live database or settings, so a machine credential gets `403` there too; `omniroute sync import` sends the local CLI credential. So do `DELETE /api/db-backups` (prune stored backups) and `PATCH /api/db-backups` (retention settings: the persisted values drive the automatic pruning after every backup). Listing (`GET`) and creating a manual backup (`PUT`) keep ordinary management auth. ### Cloud Sync | Endpoint | Method | Description | | ---------------------- | ------- | --------------------- | | `/api/sync/cloud` | Various | Cloud sync operations | | `/api/sync/initialize` | POST | Initialize sync | | `/api/cloud/*` | Various | Cloud management | ### Tunnels | Endpoint | Method | Description | | -------------------------- | ------ | ----------------------------------------------------------------------- | | `/api/tunnels/cloudflared` | GET | Read Cloudflare Quick Tunnel install/runtime status for the dashboard | | `/api/tunnels/cloudflared` | POST | Enable or disable the Cloudflare Quick Tunnel (`action=enable/disable`) | | `/api/tunnels/ngrok` | GET | Read ngrok Tunnel runtime status for the dashboard | | `/api/tunnels/ngrok` | POST | Enable or disable the ngrok Tunnel (`action=enable/disable`) | ### CLI Tools | Endpoint | Method | Description | | ---------------------------------- | ------ | ------------------- | | `/api/cli-tools/claude-settings` | GET | Claude CLI status | | `/api/cli-tools/codex-settings` | GET | Codex CLI status | | `/api/cli-tools/droid-settings` | GET | Droid CLI status | | `/api/cli-tools/openclaw-settings` | GET | OpenClaw CLI status | | `/api/cli-tools/runtime/[toolId]` | GET | Generic CLI runtime | CLI responses include: `installed`, `runnable`, `command`, `commandPath`, `runtimeMode`, `reason`. ### ACP Agents | Endpoint | Method | Description | | ----------------- | ------ | -------------------------------------------------------- | | `/api/acp/agents` | GET | List all detected agents (built-in + custom) with status | | `/api/acp/agents` | POST | Add custom agent or refresh detection cache | | `/api/acp/agents` | DELETE | Remove a custom agent by `id` query param | GET response includes `agents[]` (id, name, binary, version, installed, protocol, isCustom) and `summary` (total, installed, notFound, builtIn, custom). ### Resilience & Rate Limits | Endpoint | Method | Description | | --------------------------------- | --------- | ------------------------------------------------------------------------------------ | | `/api/resilience` | GET/PATCH | Get/update request queue, connection cooldown, provider breaker, and wait settings | | `/api/resilience/reset` | POST | Reset provider circuit breakers | | `/api/resilience/model-cooldowns` | GET | List active per-(provider, connection, model) lockouts, sorted by remaining time | | `/api/resilience/model-cooldowns` | DELETE | Clear a model lockout — body `{provider, model}` or `{all: true}` to wipe everything | | `/api/rate-limits` | GET | Per-account rate limit status | | `/api/rate-limit` | GET | Global rate limit configuration | > All four `/api/resilience/*` routes require **management auth** (`requireManagementAuth`). See [Resilience (extended)](#resilience-extended) for a full breakdown of provider breaker vs connection cooldown vs model lockout. ### Evals | Endpoint | Method | Description | | ------------ | -------- | --------------------------------- | | `/api/evals` | GET/POST | List eval suites / run evaluation | ### Policies | Endpoint | Method | Description | | --------------- | --------------- | ----------------------- | | `/api/policies` | GET/POST/DELETE | Manage routing policies | ### Compliance | Endpoint | Method | Description | | --------------------------- | ------ | ----------------------------- | | `/api/compliance/audit-log` | GET | Compliance audit log (last N) | ### v1beta (Gemini-Compatible) | Endpoint | Method | Description | | -------------------------- | ------ | --------------------------------- | | `/v1beta/models` | GET | List models in Gemini format | | `/v1beta/models/{...path}` | POST | Gemini `generateContent` endpoint | These endpoints mirror Gemini's API format for clients that expect native Gemini SDK compatibility. ### Internal / System APIs | Endpoint | Method | Description | | ------------------------ | ------ | ------------------------------------------------------ | | `/api/init` | GET | Application initialization check (used on first run) | | `/api/tags` | GET | Ollama-compatible model tags (for Ollama clients) | | `/api/restart` | POST | Trigger graceful server restart | | `/api/shutdown` | POST | Trigger graceful server shutdown | | `/api/system/env/repair` | POST | Repair OAuth provider environment variables | | `/api/system/version` | GET | Current/latest version, update status, release channel | | `/api/system/version` | POST | Start a deployment-aware update to the latest version | > **Note:** These endpoints are used internally by the system or for Ollama client compatibility. They are not typically called by end users. ### OAuth Environment Repair _(v3.6.1+)_ ```bash POST /api/system/env/repair Content-Type: application/json { "provider": "claude-code" } ``` Repairs missing or corrupted OAuth environment variables for a specific provider. Returns: ```json { "success": true, "repaired": ["CLAUDE_CODE_OAUTH_CLIENT_ID", "CLAUDE_CODE_OAUTH_CLIENT_SECRET"], "backupPath": "/home/user/.omniroute/backups/env-repair-2026-04-11.bak" } ``` ### Version and Release Channel ```bash GET /api/system/version ``` Loopback-only management route (admin auth). Returns the running version, the latest published version and the auto-update status. `releaseChannel` and `channels` are additive fields (rail 3.8.54); `channel` keeps its meaning — the deployment mode the dashboard updater uses (`npm`, `source` or `docker-compose`). ```json { "current": "3.8.52", "latest": "3.8.52", "updateAvailable": false, "channel": "npm", "autoUpdateSupported": true, "autoUpdateError": null, "news": null, "releaseChannel": "latest", "channels": { "latest": "3.8.52", "next": "3.8.53-rc.1" } } ``` - `releaseChannel` — npm channel of the running build: `nightly` for `-nightly.*` versions, `next` for other pre-releases (`-rc.*`, `-beta.*`, `-alpha.*`), `lts` for a stable version of an older major than `channels.latest`, otherwise `latest`. Same rules as `scripts/release/dist-tag.mjs`, which picks the npm dist-tag at publish time. - `channels` — published head of each dist-tag, from `npm view omniroute dist-tags` (registry HTTP fallback), cached with the same 10-minute TTL as `latest`. `latest` is always present (falls back to the `latest` field, then `"unavailable"`); `next`, `nightly` and `lts` appear only when that dist-tag exists. A `Cache-Control: no-cache` request refreshes both lookups. The channel model (`latest` = v3 until the 4.0 GA, `next` = rc, `nightly` = `develop` builds, `lts` = v3 patches after the 4.0 GA) is described in `docs/ops/RELEASE_STRATEGY.md`. --- ## Audio Transcription ```bash POST /v1/audio/transcriptions Authorization: Bearer your-api-key Content-Type: multipart/form-data ``` Transcribe audio files using any configured STT provider. The first path segment selects the native provider (`openai/…`, `deepgram/…`). Gateways that re-export another vendor's model use a qualified id (`openrouter/deepgram/nova-3`). **Request:** ```bash curl -X POST http://localhost:20128/v1/audio/transcriptions \ -H "Authorization: Bearer your-api-key" \ -F "file=@recording.mp3" \ -F "model=openai/whisper-1" ``` **Response:** ```json { "text": "Hello, this is the transcribed audio content.", "task": "transcribe", "language": "en", "duration": 12.5 } ``` **Example model ids:** `openai/whisper-1` (requires an OpenAI key), `openrouter/deepgram/nova-3` (requires an OpenRouter key), `deepgram/nova-3` (requires a native Deepgram key). A bare `deepgram/nova-3` request does **not** use OpenRouter. **Supported formats:** `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`. --- ## Ollama Compatibility For clients that use Ollama's API format: ```bash # Chat endpoint (Ollama format) POST /v1/api/chat # Model listing (Ollama format) GET /api/tags ``` Requests are automatically translated between Ollama and internal formats. ## Tokenized VS Code / Headerless Aliases Use these aliases when an integration cannot inject an `Authorization` header and needs the API key embedded in the base URL. ```bash # OpenAI-style catalog alias GET /api/v1/vscode/{token}/ GET /api/v1/vscode/{token}/models # OpenAI-style chat aliases POST /api/v1/vscode/{token}/chat/completions POST /api/v1/vscode/{token}/responses # Ollama-style aliases POST /api/v1/vscode/{token}/api/chat GET /api/v1/vscode/{token}/api/tags ``` Example: ```bash curl https://your-host.example/api/v1/vscode/YOUR_API_KEY/models curl -X POST https://your-host.example/api/v1/vscode/YOUR_API_KEY/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"auto","messages":[{"role":"user","content":"hello"}]}' ``` Notes: - The tokenized aliases reuse the same handlers as `/v1/*` and `/api/tags`; response shapes stay identical. - Prefer `Authorization: Bearer ...` whenever the client supports custom headers. - URL-based tokens may appear in reverse-proxy logs, browser history, and telemetry outside OmniRoute. Treat them as a compatibility option, not the default authentication mode. --- ## Telemetry ```bash # Get latency telemetry summary (p50/p95/p99 per provider) GET /api/telemetry/summary ``` **Response:** ```json { "providers": { "claudeCode": { "p50": 245, "p95": 890, "p99": 1200, "count": 150 }, "github": { "p50": 180, "p95": 620, "p99": 950, "count": 320 } } } ``` --- ## Budget ```bash # Get budget status for all API keys GET /api/usage/budget # Set or update a budget POST /api/usage/budget Content-Type: application/json { "apiKeyId": "key-123", "dailyLimitUsd": 5.00, "weeklyLimitUsd": 30.00, "monthlyLimitUsd": 100.00, "warningThreshold": 0.8, "resetInterval": "monthly" } ``` > **Schema notes** (`setBudgetSchema`): `apiKeyId` is required; at least one of `dailyLimitUsd`, `weeklyLimitUsd`, or `monthlyLimitUsd` must be greater than zero. Optional fields: `warningThreshold` (0–1), `resetInterval` (`daily` | `weekly` | `monthly`), `resetTime` (`HH:MM`). The legacy `{keyId, limit, period}` shape returns `400 Bad Request`. ## Token Limits Per-API-key **token** budgets (distinct from the USD-based Budget above). Enforced inline on the request path: when a key's current window usage reaches its limit, requests are rejected with `429 Too Many Requests`. Limits can be scoped to a specific `model`, a `provider`, or applied `global`ly across the key; when several limits match a request, the most restrictive one wins. ```bash # List a key's token limits (includes live window usage) GET /api/usage/token-limits?apiKeyId=key-123 # Create or update a token limit POST /api/usage/token-limits Content-Type: application/json { "apiKeyId": "key-123", "scopeType": "model", "scopeValue": "openai/gpt-4o", "tokenLimit": 1000000, "resetInterval": "monthly", "enabled": true } # Delete a token limit by id DELETE /api/usage/token-limits?id=tl-abc ``` > **Schema notes** (`setTokenLimitSchema`): `apiKeyId` and `scopeType` (`model` | `provider` | `global`) are required. `scopeValue` is required unless `scopeType` is `global` (e.g. a model id for `model` scope, a provider id for `provider` scope). `tokenLimit` must be a positive integer (coerced from string). Optional: `id` (omit to create, supply to update), `resetInterval` (`daily` | `weekly` | `monthly`, default `monthly`), `resetTime` (`HH:MM`), `enabled` (default `true`). `GET` responses enrich each limit with `tokensUsed`, `remaining`, `windowStart`, `periodStartAt`, and `nextResetAt`. This is a management-class endpoint (auth enforced centrally by the authz pipeline). ## Request Processing 1. Client sends request to `/v1/*` 2. Route handler calls `handleChat`, `handleEmbedding`, `handleAudioTranscription`, or `handleImageGeneration` 3. Model is resolved (direct provider/model or alias/combo) 4. Credentials selected from local DB with account availability filtering 5. For chat: `handleChatCore` checks semantic/signature cache and resolves combo compression settings 6. Proactive compression runs before provider translation when enabled (`lite`, Caveman, RTK, or stacked) 7. Provider executor sends upstream request 8. Response translated back to client format (chat) or returned as-is (embeddings/images/audio) 9. Usage, compression analytics, and request logs are recorded 10. Fallback applies on errors according to combo rules Full architecture reference: [`ARCHITECTURE.md`](../architecture/ARCHITECTURE.md) --- ## Combo Management Higher-level routing combos (already summarized under `/api/combos*`) can also be mapped 1:1 from a model id pattern, allowing transparent redirection of an OpenAI-style model id to a combo. | Method | Path | Description | | ------ | -------------------------------- | ------------------------------------------------------------------------------ | | GET | `/api/model-combo-mappings` | List all model→combo mappings | | POST | `/api/model-combo-mappings` | Create mapping — body: `{pattern, comboId, priority?, enabled?, description?}` | | GET | `/api/model-combo-mappings/[id]` | Retrieve a single mapping | | PUT | `/api/model-combo-mappings/[id]` | Update fields of an existing mapping | | DELETE | `/api/model-combo-mappings/[id]` | Remove a mapping | **Auth:** management session/API key (`requireManagementAuth`). --- ## Webhooks Outbound webhook subscriptions for OmniRoute events (request completion, quota exhaustion, key rotation, etc.). | Method | Path | Description | | ------ | ------------------------- | --------------------------------------------------------------------- | | GET | `/api/webhooks` | List webhooks (secrets are masked to `...`) | | POST | `/api/webhooks` | Create webhook — body: `{url, events?: ["*"], secret?, description?}` | | GET | `/api/webhooks/[id]` | Retrieve a webhook | | PUT | `/api/webhooks/[id]` | Update url/events/secret/description | | DELETE | `/api/webhooks/[id]` | Remove a webhook | | POST | `/api/webhooks/[id]/test` | Send a test payload to the webhook URL and return delivery status | **Auth:** management session/API key (`requireManagementAuth`). --- ## Registered Keys (Auto-Management) Used by the auto-key management subsystem to issue and rotate API keys against a backing provider/account, with daily/hourly quotas. | Method | Path | Description | | ------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/v1/registered-keys` | List registered keys (masked prefix only) | | POST | `/api/v1/registered-keys` | Issue a new registered key — body: `{name, provider?, accountId?, idempotencyKey?, expiresAt?, dailyBudget?, hourlyBudget?}`. Returns the raw key **once**. Returns `429` on quota refusal. | | GET | `/api/v1/registered-keys/[id]` | Retrieve a registered key's metadata (no raw material) | | DELETE | `/api/v1/registered-keys/[id]` | Revoke a registered key | | POST | `/api/v1/registered-keys/[id]/revoke` | Explicit revoke endpoint (same effect as DELETE) | **Auth:** management session/API key (`requireManagementAuth`) — the same guard as `/api/keys`: a dashboard session, the local CLI token, or an API key with `manage`/`admin` scope; inference-only API keys get `403`. See also `/v1/quotas/check` and `/v1/issues/report`. --- ## Agents Protocol Cloud agent tasks (Claude Code, Codex Cloud, OpenHands, etc.) executed remotely on behalf of OmniRoute users. | Method | Path | Description | | ------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/v1/agents/tasks` | List tasks — optional `?provider=`, `?status=`, `?limit=` (1–500, default 50) | | POST | `/api/v1/agents/tasks` | Create task — body validated by `CreateCloudAgentTaskSchema` (`providerId`, `prompt`, `source`, `options?`). Returns `201` with task envelope | | DELETE | `/api/v1/agents/tasks?id=...` | Delete a task | | GET | `/api/v1/agents/tasks/[id]` | Read task — synchronously refreshes status from the upstream cloud agent when an `external_id` is set | | POST | `/api/v1/agents/tasks/[id]` | Discriminated action: `{action: "approve"}`, `{action: "message", message}`, or `{action: "cancel"}` | | DELETE | `/api/v1/agents/tasks/[id]` | Delete a specific task by id | > **Auth:** management auth required on every method (`requireCloudAgentManagementAuth`). Prior to v3.8.0 these were unauthenticated — see commit `588a0333` for the breaking change. ```bash # Create a Claude Code cloud task curl -X POST http://localhost:20128/api/v1/agents/tasks \ -H "Authorization: Bearer your-management-key" \ -H "Content-Type: application/json" \ -d '{"providerId":"claude-code-cloud","prompt":"Fix the failing test","source":{"repo":"...","branch":"..."}}' ``` --- ## Management Proxies Outbound HTTP(S)/SOCKS proxies that can be assigned to providers, accounts, or globally. | Method | Path | Description | | ------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | GET | `/api/v1/management/proxies` | List proxies (with `?id=` returns one; with `?id=&where_used=1` returns the assignment graph) | | POST | `/api/v1/management/proxies` | Create proxy — body validated by `createProxyRegistrySchema` | | PATCH | `/api/v1/management/proxies` | Update proxy — body validated by `updateProxyRegistrySchema` (requires `id`) | | DELETE | `/api/v1/management/proxies?id=...&force=1` | Delete proxy (use `force=1` to detach assignments) | | GET | `/api/v1/management/proxies/assignments` | List assignments — filterable by `proxy_id`, `scope`, `scope_id`; pass `resolve_connection_id=` to resolve the active proxy for a connection | | PUT | `/api/v1/management/proxies/assignments` | Assign — body validated by `proxyAssignmentSchema` (`{scope, scopeId?, proxyId?}`). Clears dispatcher cache | | PUT | `/api/v1/management/proxies/bulk-assign` | Bulk-assign — body validated by `bulkProxyAssignmentSchema` (`{scope, scopeIds[], proxyId?}`) | | GET | `/api/v1/management/proxies/health?hours=24` | Aggregate proxy health (success/fail counts, latency) over a window | **Auth:** management session/API key on every route (`requireManagementAuth`). > The task description's `POST /api/v1/management/proxies/[id]/assignments` and `POST /api/v1/management/proxies/[id]/health` are served by the flat `/assignments` and `/health` routes shown above — there are no per-id subroutes in the codebase. --- ## Resilience (extended) OmniRoute exposes three independent temporary-failure mechanisms; the management endpoints below let operators read and override them: | Scope | State storage | Read | Reset / clear | | ------------------- | ------------------------------------------ | ----------------------------------------- | ------------------------------------------- | | Provider breaker | `domain_circuit_breakers` + in-memory | `/api/monitoring/health` | `POST /api/resilience/reset` | | Connection cooldown | `rateLimitedUntil` on provider connections | `/api/rate-limits`, `/api/providers/[id]` | (re-enables lazily; clear via provider PUT) | | Model lockout | In-memory model-availability registry | `GET /api/resilience/model-cooldowns` | `DELETE /api/resilience/model-cooldowns` | `PATCH /api/resilience` accepts provider breaker overrides under `providerBreaker.oauth` and `providerBreaker.apikey`. Each profile supports `degradationThreshold`, `failureThreshold`, and `resetTimeoutMs`; the same fields are exposed in Dashboard → Settings → Resilience. ```bash # Clear a single model lockout curl -X DELETE http://localhost:20128/api/resilience/model-cooldowns \ -H "Cookie: auth_token=..." \ -H "Content-Type: application/json" \ -d '{"provider":"openai","model":"gpt-4o-mini"}' # Wipe every lockout curl -X DELETE http://localhost:20128/api/resilience/model-cooldowns \ -H "Cookie: auth_token=..." \ -d '{"all":true}' ``` Full conceptual reference and breaker defaults: see [`CLAUDE.md`](../../CLAUDE.md) → "Resilience Runtime State". --- ## Skills Skill framework for extending OmniRoute with custom executable handlers, plus marketplace integrations. | Method | Path | Description | | ------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/skills` | List installed skills — filterable by `?q=`, `?mode=on\|off\|auto`, `?source=skillsmp\|skillssh\|local`, paginated | | GET | `/api/skills/[id]` | Retrieve one skill | | PUT | `/api/skills/[id]` | Update skill (name, description, mode, schema, handler, tags) | | DELETE | `/api/skills/[id]` | Uninstall a skill | | POST | `/api/skills/install` | Install a skill from a raw manifest — body: `{name, version, description, schema:{input, output}, handlerCode, apiKeyId?}` | | GET | `/api/skills/executions` | List recent skill executions (audit trail with inputs/outputs/duration) | | GET | `/api/skills/marketplace?q=...` | Search/popular list from the SkillsMP marketplace (requires `skillsmpApiKey` setting) | | POST | `/api/skills/marketplace/install` | Install a skill by id from SkillsMP | | GET | `/api/skills/skillssh?q=&limit=` | Search the skills.sh registry | | POST | `/api/skills/skillssh/install` | Install a skill by id from skills.sh | **Auth:** management session/API key. Marketplace search routes accept either management auth or a Bearer API key (`isAuthenticated`). --- ## Memory Persistent conversational/factual memory store, scoped per API key / session. | Method | Path | Description | | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------ | | GET | `/api/memory` | List memories — `?apiKeyId=`, `?type=`, `?sessionId=`, `?q=`, with `offset/limit` or `page/limit` pagination | | POST | `/api/memory` | Create memory — body validated by Zod: `{content, key, type?, sessionId?, apiKeyId?, metadata?, expiresAt?}` | | GET | `/api/memory/[id]` | Retrieve one memory | | DELETE | `/api/memory/[id]` | Delete a memory | | GET | `/api/memory/health` | Memory subsystem health (DB connectivity, embeddings backend, vector index status) | **Auth:** management session/API key (`requireManagementAuth`). `type` enum: `FACTUAL`, `EPISODIC`, `SEMANTIC`, `PROCEDURAL` (see `MemoryType` in `src/lib/memory/types.ts`). --- ## MCP Server OmniRoute ships an embedded Model Context Protocol server with 3 transports (stdio, SSE, streamable-http) and scoped tools. The dashboard endpoints below read status/audit data and proxy the HTTP transports. | Method | Path | Description | | ------ | ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------- | | GET | `/api/mcp/status` | Heartbeat, transport, online state, last call, top tools, 24h success rate | | GET | `/api/mcp/tools` | List of MCP tools with `name`, `description`, `scopes`, `phase`, `auditLevel`, `sourceEndpoints` | | GET | `/api/mcp/sse` | Open SSE stream for the SSE transport (returns `503` if MCP disabled or transport mismatch) | | POST | `/api/mcp/sse` | Send JSON-RPC frame on the SSE transport | | GET | `/api/mcp/stream` | Open SSE side of the Streamable HTTP transport (server-initiated messages) | | POST | `/api/mcp/stream` | Send JSON-RPC frame on the Streamable HTTP transport | | DELETE | `/api/mcp/stream` | End a Streamable HTTP session | | GET | `/api/mcp/audit` | Query audit log — `?limit=`, `?offset=`, `?tool=`, `?success=true | false`, `?apiKeyId=` | | GET | `/api/mcp/audit/stats` | Aggregate audit stats (totals, success rate, avg duration, top tools) | **Auth:** the `sse`/`stream` transports honor the MCP-specific auth surface (Bearer API key with `mcp` scope); the `status`/`tools`/`audit*` routes are readable from the dashboard (no extra auth required beyond reaching the dashboard host). > Both HTTP transports are gated by `settings.mcpEnabled` and `settings.mcpTransport` — a transport mismatch returns `400`, an MCP disabled state returns `503`. --- ## A2A Server OmniRoute exposes an A2A (Agent-to-Agent) JSON-RPC 2.0 endpoint plus a REST wrapper for inspection/dashboard use. ### JSON-RPC ```bash POST /a2a Authorization: Bearer your-api-key # optional unless OMNIROUTE_API_KEY is set Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "message/send", "params": { "skill": "smart-routing", "messages": [{"role": "user", "content": "Route this coding task"}] } } ``` Supported methods (all gated on `settings.a2aEnabled`): | Method | Description | | ---------------- | ------------------------------------------------------------------ | | `message/send` | Synchronous skill execution; returns `{task, artifacts, metadata}` | | `message/stream` | Streaming SSE execution of the same skill set | | `tasks/get` | Fetch a task by `taskId` | | `tasks/cancel` | Cancel a task by `taskId` | Built-in skills: `smart-routing`, `quota-management`, `provider-discovery`, `cost-analysis`, `health-report`. ### Agent Card ```bash GET /.well-known/agent.json ``` Returns the public A2A agent card (name, description, capabilities, skill catalog, auth scheme) — cached publicly for 1h. No auth required. ### REST helpers | Method | Path | Description | | ------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------- | | GET | `/api/a2a/status` | A2A enabled + task stats + cached agent card summary | | GET | `/api/a2a/tasks` | List tasks — `?state=submitted\|working\|completed\|failed\|cancelled`, `?skill=`, `?limit=` (≤200), `?offset=` | | POST | `/api/a2a/tasks` | (Not implemented as a REST helper — create via JSON-RPC `message/send`) | | GET | `/api/a2a/tasks/[id]` | Retrieve one task | | POST | `/api/a2a/tasks/[id]/cancel` | Cancel a task | **Auth:** the REST helpers run without management auth (dashboard-readable); the JSON-RPC `/a2a` route uses Bearer `OMNIROUTE_API_KEY` if configured. --- ## Cloud, Evals & Assess | Method | Path | Description | | ------ | ------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------- | ----------------------------------- | | POST | `/api/cloud/auth` | Verify a Bearer key and return masked provider connections + model aliases for cloud sync clients | | POST | `/api/cloud/credentials/update` | Update encrypted credentials for a cloud-synced provider | | POST | `/api/cloud/model/resolve` | Resolve a logical model id to a concrete provider/model using the local routing table | | GET | `/api/cloud/models/alias` | List model aliases as exposed to cloud sync | | GET | `/api/assess` | Read latest assessment categorizations (per-provider/model) | | POST | `/api/assess` | Run an assessment — body: `{scope: {type:"all"} | {type:"provider", providerId} | {type:"model", modelId}, trigger?}` | | GET | `/api/evals` | List built-in eval suites + most recent runs | | POST | `/api/evals` | Trigger an eval run | | POST | `/api/evals/suites` | Create a custom eval suite — body validated by `evalSuiteSaveSchema` | | GET | `/api/evals/suites/[id]` | Retrieve a custom eval suite | **Auth:** `/api/cloud/auth` validates a Bearer key directly and returns the masked key and `projectId` of each connection only for a key with the `manage` / `admin` scope; the other `/api/cloud/*`, `/api/evals/*`, and `/api/assess` routes require management session/API key. `/api/assess` POST uses `validateBody` with a discriminated-union scope schema. --- ## ACP (Agent Client Protocol) Management as child processes. These endpoints manage ACP agent detection and custom agent registration. | Method | Path | Description | | ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | GET | `/api/acp/agents` | List all known CLI agents (built-in + custom) with installation status, version, binary | | POST | `/api/acp/agents` | Register a custom ACP agent or refresh cache — body: `{id, name, binary, versionCommand, providerAlias, spawnArgs, protocol}` or `{action: "refresh"}` | | DELETE | `/api/acp/agents` | Remove a custom ACP agent — query param: `?id=` | **Response example** (`GET /api/acp/agents`): ```json { "agents": [ { "id": "claude", "name": "Claude Code CLI", "binary": "claude", "version": "1.0.45", "installed": true, "protocol": "stdio", "providerAlias": "claude", "isCustom": false }, { "id": "my-custom-cli", "name": "My Custom CLI", "installed": false, "protocol": "stdio", "providerAlias": "my-provider", "isCustom": true } ], "cacheTtlMs": 60000, "cacheAge": 1234 } ``` **Auth:** Requires management session (dashboard `auth_token` cookie) or a management-scoped API key. See [ACP Framework](../frameworks/ACP.md) for full details. --- ## Analytics & Observability Real-time analytics endpoints for monitoring routing, compression, and provider diversity. These power the `/dashboard/analytics/*` pages. ### Auto-routing analytics | Method | Path | Description | | ------ | ------------------------------------ | -------------------------------------------------------------------------------------------------- | | GET | `/api/analytics/auto-routing` | Aggregate auto-routing stats: total calls, strategy distribution, tier distribution, top providers | | GET | `/api/analytics/auto-routing?days=7` | Time-windowed stats (default 24h) | **Response example**: ```json { "window": "24h", "totalCalls": 1234, "strategyBreakdown": { "rules": 800, "cost": 200, "latency": 150, "sla-aware": 50, "lkgp": 34 }, "tierBreakdown": { "ultra": 100, "pro": 500, "standard": 400, "free": 234 }, "topProviders": [ { "provider": "openai", "calls": 500, "avgLatencyMs": 850 }, { "provider": "anthropic", "calls": 300, "avgLatencyMs": 1200 } ] } ``` ### Compression analytics | Method | Path | Description | | ------ | ---------------------------- | ------------------------------------------------------------------------------------- | | GET | `/api/analytics/compression` | Aggregate compression stats: tokens saved, savings %, mode distribution, engine usage | **Response example**: ```json { "window": "24h", "totalOriginalTokens": 5000000, "totalCompressedTokens": 3500000, "totalSavings": 1500000, "savingsPct": 30.0, "modeBreakdown": { "lite": 400, "standard": 600, "aggressive": 100, "ultra": 50, "rtk": 84 }, "engineBreakdown": { "caveman": 800, "rtk": 434 } } ``` ### Provider diversity tracking | Method | Path | Description | | ------ | -------------------------- | -------------------------------------------------------------------------------------------------------- | | GET | `/api/analytics/diversity` | Shannon entropy-based diversity tracking: prevents single points of failure by measuring provider spread | **Response example**: ```json { "window": "24h", "shannonEntropy": 2.45, "maxEntropy": 3.17, "diversityRatio": 0.77, "providerUsage": { "openai": 0.4, "anthropic": 0.25, "google": 0.2, "kiro": 0.15 }, "warnings": ["OpenAI accounts for 40% of traffic — consider diversifying"] } ``` **Auth:** Requires management session or management-scoped API key. --- ## Admin Operations Admin-only endpoints for operational management. | Method | Path | Description | | ------ | ------------------------ | ------------------------------------------------------------------------------------------- | | GET | `/api/admin/concurrency` | Read current concurrency limits (global + per-provider) | | POST | `/api/admin/concurrency` | Update concurrency limits — body: `{global?: number, perProvider?: Record}` | **Auth:** Requires management session with admin scope. --- ## CLI Tools Management Manage CLI tools that integrate with OmniRoute (antigravity, commandCode, devin-cli, etc.). See [Provider Reference](./PROVIDER_REFERENCE.md) for the full list. | Method | Path | Description | | ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/cli-tools/all-statuses` | Status of all CLI tools (installed, version, last seen) | | GET | `/api/cli-tools/status` | Status detail for one CLI tool (`?tool=` query) | | POST | `/api/cli-tools/apply` | Write a tool's generated config (`dryRun` previews; `422` + `containerEphemeralTarget` when containerized; `migration` notes a legacy Codex YAML) | | GET | `/api/cli-tools/backups` | List CLI tool configuration backups | | POST | `/api/cli-tools/backups` | Create a backup of all CLI tool configurations | | POST | `/api/cli-tools/backups` | Restore: same endpoint with `{tool, backupId}` in the body restores that backup | | GET | `/api/cli-tools/antigravity-mitm` | Antigravity MITM proxy status (the "antigravity-mitm" CLI tool) | | POST | `/api/cli-tools/antigravity-mitm/alias` | Configure antigravity-mitm aliases | **Auth:** Requires management session. --- ## Agent Skills Manage AI agent skills (similar to OpenAI's custom GPTs but for agents). | Method | Path | Description | | ------ | ---------------------------- | --------------------------------------------------------------------------------------- | | GET | `/api/agent-skills` | List all agent skills (built-in + custom) | | GET | `/api/agent-skills/[id]` | Get a specific agent skill | | POST | `/api/agent-skills` | Create a custom agent skill — body: `{name, description, prompt, model?, temperature?}` | | PUT | `/api/agent-skills/[id]` | Update a custom agent skill | | DELETE | `/api/agent-skills/[id]` | Delete a custom agent skill | | GET | `/api/agent-skills/[id]/raw` | Get raw prompt + metadata (no execution) | | POST | `/api/agent-skills/generate` | AI-generate a new skill from a natural language description | **Auth:** Requires management session or management-scoped API key. --- ## Cache Management Manage the semantic cache and reasoning cache. | Method | Path | Description | | ------ | ---------------------- | ------------------------------------------------------------------------------------------------------- | | GET | `/api/cache` | Cache overview: total entries, hit rate, size on disk | | GET | `/api/cache/entries` | List cached entries (with pagination) | | DELETE | `/api/cache/entries` | Delete cache entries (filter by query parameters) | | GET | `/api/cache/stats` | Detailed cache statistics (per-provider, per-model) | | GET | `/api/cache/reasoning` | Reasoning cache status (for reasoning replay) | | DELETE | `/api/cache/reasoning` | Clear reasoning cache — query params: `?toolCallId=` (single) or `?provider=

` or no params (all) | **Auth:** Requires management session. --- ## Memory System Manage persistent memory (FTS5 + vector embeddings). | Method | Path | Description | | ------ | ------------------ | ----------------------------------------------------------------------- | | GET | `/api/memory` | List memory entries (filter by scope, type, search query) | | POST | `/api/memory` | Create a new memory entry — body: `{scope, type, content, metadata?}` | | GET | `/api/memory/[id]` | Get a specific memory entry | | PUT | `/api/memory/[id]` | Update a memory entry | | DELETE | `/api/memory/[id]` | Delete a memory entry | | GET | `/api/memory?q=` | Search memory (FTS5 + vector) — stats are included in the same response | **Auth:** Requires management session or management-scoped API key. --- ## Webhooks Manage webhook subscriptions for events. | Method | Path | Description | | ------ | ------------------------------- | ------------------------------------------------------------------------- | | GET | `/api/webhooks` | List all webhook subscriptions | | POST | `/api/webhooks` | Create a webhook subscription — body: `{url, events[], secret?, active?}` | | GET | `/api/webhooks/[id]` | Get a specific webhook subscription | | PUT | `/api/webhooks/[id]` | Update a webhook subscription | | DELETE | `/api/webhooks/[id]` | Delete a webhook subscription | | GET | `/api/webhooks/[id]/deliveries` | List delivery history for a webhook (success/failure log) | | POST | `/api/webhooks/[id]/test` | Send a test event to a webhook | **Auth:** Requires management session. See [Webhooks Framework](../frameworks/WEBHOOKS.md) for full event types. --- ## Skills Framework Manage Skills (the agentic extensions framework). | Method | Path | Description | | ------ | ------------------------ | --------------------------------------------------------------------------------------- | | GET | `/api/skills` | List all installed skills (built-in + custom) | | POST | `/api/skills/install` | Install a skill from a local path or URL | | DELETE | `/api/skills/[id]` | Uninstall a skill | | PUT | `/api/skills/[id]` | Enable or disable a skill — body: `{enabled?: boolean, mode?: "on" \| "off" \| "auto"}` | | POST | `/api/skills/executions` | Execute a skill — body: `{skillName, apiKeyId, input?, sessionId?}` | | GET | `/api/skills/executions` | List execution history for all skills (filter by `?apiKeyId=`) | **Auth:** Requires management session or management-scoped API key. See [Skills Framework](../frameworks/SKILLS.md) for full details. --- ## Plugins Manage OmniRoute plugins (third-party extensions). | Method | Path | Description | | ------ | ---------------------------------- | ------------------------------------- | | GET | `/api/plugins` | List installed plugins | | POST | `/api/plugins/marketplace/install` | Install a plugin from the marketplace | | DELETE | `/api/plugins/[name]` | Uninstall a plugin | | POST | `/api/plugins/[name]/activate` | Activate a plugin | | POST | `/api/plugins/[name]/deactivate` | Deactivate a plugin | | GET | `/api/plugins/[name]/config` | Get plugin configuration | | PUT | `/api/plugins/[name]/config` | Update plugin configuration | **Auth:** Requires management session. See [Plugins Framework](../frameworks/PLUGIN_SDK.md) for full details. --- ## Shadow Routing Shadow / A-B comparison of providers is **not a standalone REST surface** — it is configured through combo routing (see [Auto-Combo](../routing/AUTO-COMBO.md)). Per-combo comparison metrics are served by `GET /api/combos/metrics`. --- ## Guardrails Inspect the runtime guardrails (PII detection, prompt injection detection, vision bridging). Guardrails run on every request; per-call opt-out is via the `x-omniroute-disabled-guardrails` request header — there is no persisted enable/disable surface. | Method | Path | Description | | ------ | ---------------------- | ---------------------------------------------------------------------------------------- | | GET | `/api/guardrails` | List the registered guardrails and their status (name / enabled / priority) | | POST | `/api/guardrails/test` | Dry-run the pre-call pipeline over a sample input — body: `{input, disabledGuardrails?}` | **Auth:** Requires management session. See [Security > Guardrails](../security/GUARDRAILS.md) for full details. --- --- ## Authentication See [Management Authentication](../guides/MANAGEMENT-AUTH.md) for the four credential families (dashboard session, local CLI token, `oma_live_…` Access Token, manage-scoped API key) and how they differ from inference keys. - Dashboard routes (`/dashboard/*`) use `auth_token` cookie - Login uses saved password hash; fallback to `INITIAL_PASSWORD` - `requireLogin` toggleable via `/api/settings/require-login` - `/v1/*` routes optionally require Bearer API key when `REQUIRE_API_KEY=true` - "management token" / "management-scoped API key" in this reference means one of the families in that guide — not an undefined extra secret type > **Breaking change (v3.8.0)** — `/api/v1/agents/tasks/*` and the cooldown management endpoints now require **management auth** (dashboard `auth_token` cookie or a management-scoped API key). Clients that previously called these routes unauthenticated will receive `401 Unauthorized`. See commit `588a0333` (`fix(auth): require management auth for agent and cooldown APIs`).