From 8f7d72c806ed99075e4d6954373f75d5aacf481c Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:56:39 -0300 Subject: [PATCH 1/2] fix(api): sync public OpenAPI deployment metadata (#18331) --- .../fixes/18331-public-openapi-metadata.md | 1 + package.json | 1 + public/openapi.yaml | 13160 ++++++++++++++-- scripts/build/assembleStandalone.mjs | 3 + scripts/build/build-next-isolated.mjs | 2 + scripts/build/pack-artifact-policy.ts | 1 + scripts/build/publicOpenApi.mjs | 36 + scripts/dev/run-next.mjs | 2 + .../public-openapi-artifact-18331.test.ts | 74 + 9 files changed, 11791 insertions(+), 1489 deletions(-) create mode 100644 changelog.d/fixes/18331-public-openapi-metadata.md create mode 100644 scripts/build/publicOpenApi.mjs create mode 100644 tests/unit/build/public-openapi-artifact-18331.test.ts diff --git a/changelog.d/fixes/18331-public-openapi-metadata.md b/changelog.d/fixes/18331-public-openapi-metadata.md new file mode 100644 index 000000000000..55df5a71d653 --- /dev/null +++ b/changelog.d/fixes/18331-public-openapi-metadata.md @@ -0,0 +1 @@ +- fix(api): regenerate the public OpenAPI spec with the build version and deployment-relative server URL (#18331). diff --git a/package.json b/package.json index 76aa86ac3c06..d685e47d6db0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "scripts/check/check-supported-node-runtime.ts", "scripts/dev/sync-env.mjs", "scripts/build/assembleStandalone.mjs", + "scripts/build/publicOpenApi.mjs", "scripts/build/backendOnlyPages.mjs", "scripts/build/build-tproxy-native.mjs", "scripts/build/native-binary-compat.mjs", diff --git a/public/openapi.yaml b/public/openapi.yaml index 7f0958a75a96..f04a9f2277d3 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.35 + version: 3.8.53 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, @@ -19,8 +19,8 @@ info: name: MIT servers: - - url: http://localhost:20128 - description: Local development + - url: ./ + description: This deployment tags: - name: Playground @@ -43,14 +43,23 @@ tags: description: Content moderation - name: Rerank description: Document reranking + - name: Search + description: Unified web, news, and X search - name: Models description: Available model listing - name: Providers description: Provider connection management - name: Provider Nodes description: Provider node configuration + - name: Proxy Subscriptions + description: >- + Operator-supplied proxy subscription links (Karing-style). Nodes are fetched, + parsed, and synced into the provider/account pool on create, refresh, or the + background scheduler tick. - name: API Keys description: API key management + - name: Session Leases + description: Client-neutral exclusive managed session connection leases - name: Combos description: Routing combo management - name: Settings @@ -97,7 +106,267 @@ tags: All routes are LOCAL_ONLY + SPAWN_CAPABLE (hard rules #15, #17). See docs/frameworks/TRAFFIC_INSPECTOR.md. + - name: A2a + description: A2a endpoints (generated route coverage) + - name: Acp + description: Acp endpoints (generated route coverage) + - name: Admin + description: Admin endpoints (generated route coverage) + - name: Analytics + description: Analytics endpoints (generated route coverage) + - name: Assess + description: Assess endpoints (generated route coverage) + - name: Auth + description: Auth endpoints (generated route coverage) + - name: Batches + description: Batches endpoints (generated route coverage) + - name: Cache + description: Cache endpoints (generated route coverage) + - name: Chaos + description: Chaos endpoints (generated route coverage) + - name: Codex + description: Codex endpoints (generated route coverage) + - name: Conductor + description: Conductor endpoints (generated route coverage) + - name: Context + description: Context endpoints (generated route coverage) + - name: Conversations + description: Conversations endpoints (generated route coverage) + - name: Copilot + description: Copilot endpoints (generated route coverage) + - name: Cursor cli + description: Cursor cli endpoints (generated route coverage) + - name: Dahl + description: Dahl endpoints (generated route coverage) + - name: Db + description: Db endpoints (generated route coverage) + - name: Db backups + description: Db backups endpoints (generated route coverage) + - name: Discovery + description: Discovery endpoints (generated route coverage) + - name: Docs + description: Docs endpoints (generated route coverage) + - name: Evals + description: Evals endpoints (generated route coverage) + - name: Files + description: Files endpoints (generated route coverage) + - name: Free models + description: Free models endpoints (generated route coverage) + - name: Free provider rankings + description: Free provider rankings endpoints (generated route coverage) + - name: Free tier + description: Free tier endpoints (generated route coverage) + - name: Gamification + description: Gamification endpoints (generated route coverage) + - name: Github skills + description: Github skills endpoints (generated route coverage) + - name: Guardrails + description: Guardrails endpoints (generated route coverage) + - name: Headroom + description: Headroom endpoints (generated route coverage) + - name: Health + description: Health endpoints (generated route coverage) + - name: Intelligence + description: Intelligence endpoints (generated route coverage) + - name: Internal + description: Internal endpoints (generated route coverage) + - name: Issue agent + description: Issue agent endpoints (generated route coverage) + - name: Keys + description: Keys endpoints (generated route coverage) + - name: Local + description: Local endpoints (generated route coverage) + - name: Logs + description: Logs endpoints (generated route coverage) + - name: Mcp + description: Mcp endpoints (generated route coverage) + - name: Middleware + description: Middleware endpoints (generated route coverage) + - name: Model capability overrides + description: Model capability overrides endpoints (generated route coverage) + - name: Model combo mappings + description: Model combo mappings endpoints (generated route coverage) + - name: Network + description: Network endpoints (generated route coverage) + - name: Omniroute + description: Omniroute endpoints (generated route coverage) + - name: OmnirouteApiCatchAll + description: OmnirouteApiCatchAll endpoints (generated route coverage) + - name: Plugins + description: Plugins endpoints (generated route coverage) + - name: Provider metrics + description: Provider metrics endpoints (generated route coverage) + - name: Provider stats + description: Provider stats endpoints (generated route coverage) + - name: Proxy fallback + description: Proxy fallback endpoints (generated route coverage) + - name: Quota + description: Quota endpoints (generated route coverage) + - name: Radar + description: Radar endpoints (generated route coverage) + - name: Relay + description: Relay endpoints (generated route coverage) + - name: Resilience + description: Resilience endpoints (generated route coverage) + - name: Routing + description: Routing endpoints (generated route coverage) + - name: Services + description: Services endpoints (generated route coverage) + - name: Session pools + description: Session pools endpoints (generated route coverage) + - name: Skills + description: Skills endpoints (generated route coverage) + - name: Sync + description: Sync endpoints (generated route coverage) + - name: Synced available models + description: Synced available models endpoints (generated route coverage) + - name: Tools + description: Tools endpoints (generated route coverage) + - name: Tunnels + description: Tunnels endpoints (generated route coverage) + - name: Upstream proxy + description: Upstream proxy endpoints (generated route coverage) + - name: V1 Accounts + description: V1 Accounts endpoints (generated route coverage) + - name: V1 Agents + description: V1 Agents endpoints (generated route coverage) + - name: V1 Antigravity + description: V1 Antigravity endpoints (generated route coverage) + - name: V1 Auto-combo + description: V1 Auto-combo endpoints (generated route coverage) + - name: V1 Batches + description: V1 Batches endpoints (generated route coverage) + - name: V1 Classify + description: V1 Classify endpoints (generated route coverage) + - name: V1 Combos + description: V1 Combos endpoints (generated route coverage) + - name: V1 Completions + description: V1 Completions endpoints (generated route coverage) + - name: V1 Files + description: V1 Files endpoints (generated route coverage) + - name: V1 Images + description: V1 Images endpoints (generated route coverage) + - name: V1 Issues + description: V1 Issues endpoints (generated route coverage) + - name: V1 Management + description: V1 Management endpoints (generated route coverage) + - name: V1 Me + description: V1 Me endpoints (generated route coverage) + - name: V1 Models + description: V1 Models endpoints (generated route coverage) + - name: V1 Muse-code + description: V1 Muse-code endpoints (generated route coverage) + - name: V1 Music + description: V1 Music endpoints (generated route coverage) + - name: V1 OmnirouteCatchAll + description: V1 OmnirouteCatchAll endpoints (generated route coverage) + - name: V1 Providers + description: V1 Providers endpoints (generated route coverage) + - name: V1 Quotas + description: V1 Quotas endpoints (generated route coverage) + - name: V1 Registered-keys + description: V1 Registered-keys endpoints (generated route coverage) + - name: V1 Relay + description: V1 Relay endpoints (generated route coverage) + - name: V1 Responses + description: V1 Responses endpoints (generated route coverage) + - name: V1 Search + description: V1 Search endpoints (generated route coverage) + - name: V1 Segment + description: V1 Segment endpoints (generated route coverage) + - name: V1 Video-bridge + description: V1 Video-bridge endpoints (generated route coverage) + - name: V1 Videos + description: V1 Videos endpoints (generated route coverage) + - name: V1 Vscode + description: V1 Vscode endpoints (generated route coverage) + - name: V1 Web + description: V1 Web endpoints (generated route coverage) + - name: Version manager + description: Version manager endpoints (generated route coverage) + - name: Vnc session + description: Vnc session endpoints (generated route coverage) + - name: Webhooks + description: Webhooks endpoints (generated route coverage) + paths: + /api/v1/session-leases: + post: + tags: + - Session Leases + summary: Acquire, inspect, renew, or release an exclusive managed connection lease + description: | + Requires an API key with `lease:exclusive` and an explicit non-empty + `allowedConnections` policy. The opaque owner is bound to the authenticated API key; + the lease owns an eligible connection, not a provider or model. Managed inference + requests present the owner and exact generation headers. Temporary foreign occupancy + returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. Acquire, renew, and release retain + their connection-free response shapes. The explicit status action is owner-, key-, and + generation-fenced and returns only privacy-safe display metadata for an active binding. + security: + - BearerAuth: [] + parameters: + - name: X-OmniRoute-Lease-Owner + in: header + required: true + schema: + type: string + pattern: ^vlo_[A-Za-z0-9_-]{43}$ + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - type: object + required: [action, model] + properties: + action: { type: string, const: acquire } + model: { type: string, minLength: 1, maxLength: 512 } + - type: object + required: [action, generation] + properties: + action: { type: string, const: status } + generation: { type: integer, minimum: 1 } + - type: object + required: [action, generation] + properties: + action: { type: string, const: renew } + generation: { type: integer, minimum: 1 } + - type: object + required: [action, generation] + properties: + action: { type: string, const: release } + generation: { type: integer, minimum: 1 } + reason: + type: string + enum: [OWNER_EXIT, CLIENT_CANCELLED] + responses: + "200": + description: Lease lifecycle state, with privacy-safe connection display metadata only for status + content: + application/json: + schema: + $ref: "#/components/schemas/ExclusiveConnectionLeaseLifecycle" + "400": + description: Missing or invalid lease context/action + "401": + description: Missing or invalid API key + "403": + description: Managed lease scope or key configuration required + "409": + description: Stale generation, missing binding, or connection fence rejection + "415": + description: Lifecycle mutations require application/json + "429": + description: Eligible managed connections are held by foreign active leases + headers: + Retry-After: + schema: { type: integer, minimum: 1, maximum: 3600 } + content: + application/json: + schema: + $ref: "#/components/schemas/ExclusiveConnectionLeaseCapacity" # --- Playground + Search Tools (plans 17+18) --- /api/playground/improve-prompt: post: @@ -703,9 +972,6 @@ paths: type: string enum: - FTS5 - - none - reason: - type: string embedding: type: object properties: @@ -1079,6 +1345,207 @@ paths: $ref: "#/components/responses/Unauthorized" # ─── Proxy Endpoints ────────────────────────────────────────── + /api/v1/search: + get: + tags: [Search] + summary: List search providers + description: Lists configured search providers and their supported search types. + responses: + "200": + description: Search provider catalog + content: + application/json: + schema: + type: object + required: [object, data] + properties: + object: + type: string + const: list + data: + type: array + items: + type: object + required: [id, object, created, name, search_types] + properties: + id: + type: string + object: + type: string + const: search_provider + created: + type: integer + name: + type: string + search_types: + type: array + items: + type: string + enum: [web, news, x] + post: + tags: [Search] + summary: Run a unified search + description: >- + Searches the web, news, or X through a configured provider. Set `provider` + to `xquik-search` to use Xquik for X search. The aliases `xquik` and + `xquik_search` resolve to the same provider. + AnySearch (`anysearch-search`, aliases `anysearch` / `anysearch_search`) + provides free fallback-only web search. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [query] + properties: + query: + type: string + minLength: 1 + maxLength: 500 + provider: + type: string + minLength: 1 + description: A search provider id or registered alias. + examples: [xquik-search] + max_results: + type: integer + minimum: 1 + maximum: 100 + default: 5 + search_type: + type: string + enum: [web, news, x, finance] + default: web + description: "`finance` is supported by tavily-search only." + offset: + type: integer + minimum: 0 + default: 0 + country: + type: string + maxLength: 2 + language: + type: string + minLength: 2 + maxLength: 5 + time_range: + type: string + enum: [any, hour, day, week, month, year] + content: + type: object + properties: + snippet: { type: boolean, default: true } + full_page: { type: boolean, default: false } + format: { type: string, enum: [text, markdown], default: text } + max_characters: { type: integer, minimum: 100, maximum: 100000 } + filters: + type: object + properties: + include_domains: + type: array + maxItems: 20 + items: { type: string, maxLength: 253 } + exclude_domains: + type: array + maxItems: 20 + items: { type: string, maxLength: 253 } + safe_search: { type: string, enum: [off, moderate, strict] } + provider_options: + type: object + additionalProperties: true + description: >- + Provider-specific passthrough. Exa and Tavily read an allow-listed, + type-checked bag under `exa` / `exa-search` and `tavily` / `tavily-search`; + unknown keys and wrongly typed values are dropped. See + docs/reference/API_REFERENCE.md (Search API). + strict_filters: + type: boolean + default: false + additionalProperties: true + responses: + "200": + description: Normalized search results + content: + application/json: + schema: + type: object + required: [id, provider, query, results, answer, usage, metrics, errors, cached] + properties: + id: + type: string + pattern: ^search- + provider: + type: string + query: + type: string + cached: + type: boolean + results: + type: array + items: + type: object + required: [title, url, snippet, position, citation] + properties: + title: { type: string } + url: { type: string, format: uri } + display_url: { type: string } + snippet: { type: string } + position: { type: integer, minimum: 1 } + score: + type: [number, "null"] + minimum: 0 + maximum: 1 + published_at: { type: [string, "null"] } + favicon_url: { type: [string, "null"], format: uri } + citation: + type: object + required: [provider, retrieved_at, rank] + properties: + provider: { type: string } + retrieved_at: { type: string, format: date-time } + rank: { type: integer, minimum: 1 } + answer: + type: [object, "null"] + usage: + type: object + required: [queries_used, search_cost_usd] + properties: + queries_used: { type: integer, minimum: 0 } + search_cost_usd: { type: number, minimum: 0 } + llm_tokens: { type: integer, minimum: 0 } + metrics: + type: object + required: [response_time_ms, upstream_latency_ms, total_results_available] + properties: + response_time_ms: { type: number, minimum: 0 } + upstream_latency_ms: { type: number, minimum: 0 } + gateway_latency_ms: { type: number, minimum: 0 } + total_results_available: { type: [integer, "null"], minimum: 0 } + errors: + type: array + items: + type: object + required: [provider, code, message] + properties: + provider: { type: string } + code: { type: string } + message: { type: string } + "400": + description: Invalid request, provider, credentials, or search type + "401": + $ref: "#/components/responses/Unauthorized" + "403": + description: Search provider blocked by API key or security policy + "429": + description: Every eligible provider credential is rate limited + "500": + $ref: "#/components/responses/InternalError" + "502": + description: Search provider failed + /api/v1/chat/completions: post: tags: [Chat] @@ -1129,6 +1596,13 @@ paths: schema: type: string description: Number of fallback attempts (only present when > 0). + X-OmniRoute-Decision: + schema: + type: string + description: >- + Routing decision trace: `strategy=; provider=; latency_ms=`. + `` is the combo strategy (e.g. `priority`, `weighted`, `fusion`) or + `single` for a non-combo request. Emitted on every completion response. X-OmniRoute-Request-Id: schema: type: string @@ -1311,13 +1785,49 @@ paths: required: [input, model] properties: input: + description: >- + Text, token IDs, or up to 32 canonical multimodal items. Structured media + input is accepted only when the resolved registry model advertises the + requested modalities. URL sources must be public HTTPS and are fetched + server-side into bounded inline media (redirect revalidation, timeout, + size limits, public DNS checks, and connection pinning) before provider + submission. Inline media is limited to 8 MiB decoded per item and 16 MiB + decoded per request. Canonical items are translated to provider-native + request shapes (Jina modality-keyed objects / Gemini embedContent parts) + rather than forwarded unchanged. oneOf: - type: string - type: array + minItems: 1 items: type: string + - type: array + minItems: 1 + items: + type: integer + minimum: 0 + - type: array + minItems: 1 + items: + type: array + minItems: 1 + items: + type: integer + minimum: 0 + - type: array + minItems: 1 + maxItems: 32 + items: + $ref: "#/components/schemas/EmbeddingMultimodalItem" model: type: string + dimensions: + type: integer + minimum: 1 + encoding_format: + type: string + enum: [float, base64] + additionalProperties: true responses: "200": description: >- @@ -1326,6 +1836,38 @@ paths: cost is computed per modality when pricing is available, otherwise `0` (fail-open). + /api/v1/multimodal-embeddings: + post: + tags: [Embeddings] + summary: Create embeddings (Jina multimodal-embeddings alias) + description: >- + Same handler as `POST /api/v1/embeddings`. Provided so Jina-compatible + clients that call `/v1/multimodal-embeddings` do not receive HTTP 404 + `unknown_route`. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [input, model] + additionalProperties: true + responses: + "200": + description: Embedding vectors (same contract as POST /api/v1/embeddings). + "401": + $ref: "#/components/responses/Unauthorized" + get: + tags: [Embeddings] + summary: List embedding models (Jina multimodal-embeddings alias) + security: + - BearerAuth: [] + responses: + "200": + description: Embedding model catalog (same as GET /api/v1/embeddings). + /api/v1/providers/{provider}/embeddings: post: tags: [Embeddings] @@ -1533,9 +2075,71 @@ paths: get: tags: [Models] summary: List available models - description: Returns all models available across configured providers. + description: >- + Returns all models available across configured providers. The discovery + filters (`capability`, `min_context`, `kind`, `q`) are opt-in: without them + the response is unchanged. They narrow the caller's own (key-filtered) + catalog and run before `limit`/`after` paging. security: - BearerAuth: [] + parameters: + - in: query + name: capability + required: false + description: >- + Keep models whose `capabilities.` is `true`. Repeatable or + comma-separated; every listed capability must match. + schema: + type: array + items: + type: string + enum: + [ + attachment, + reasoning, + structured_output, + temperature, + thinking, + tool_calling, + vision, + ] + style: form + explode: true + - in: query + name: min_context + required: false + description: Keep models whose `context_length` is at least this value. + schema: + type: integer + minimum: 0 + - in: query + name: kind + required: false + description: Catalog surface (`type`); models without a `type` are `chat`. + schema: + type: string + enum: [chat, audio, embedding, image, moderation, music, rerank, video] + - in: query + name: q + required: false + description: Case-insensitive substring match on the model `id` or `name`. + schema: + type: string + minLength: 1 + maxLength: 200 + - in: query + name: limit + required: false + description: OpenAI-style page size (applied after the discovery filters). + schema: + type: integer + minimum: 1 + - in: query + name: after + required: false + description: OpenAI-style cursor — return models after this id. + schema: + type: string responses: "200": description: Model list @@ -1551,24 +2155,79 @@ paths: type: array items: $ref: "#/components/schemas/Model" + "400": + description: Invalid discovery filter value + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" - /api/v1/providers/{provider}/models: - get: + /api/v1/models/query: + post: tags: [Models] - summary: List models for a specific provider - description: Returns only models for the selected provider with provider prefix removed from each model id. + summary: Query a model shortlist + description: >- + Filtered shortlist over the same catalog as `GET /v1/models` (same auth and + per-key visibility). Results are ordered by `owned_by`, then `id`, and capped + by `limit` (default 20, maximum 100). security: - BearerAuth: [] - parameters: - - in: path - name: provider - required: true - schema: - type: string - description: Provider id or alias (for example `openai`, `claude`, `cc`). + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + capabilities: + description: Array or comma-separated string; every capability must be `true`. + oneOf: + - type: array + items: + type: string + enum: + [ + attachment, + reasoning, + structured_output, + temperature, + thinking, + tool_calling, + vision, + ] + - type: string + min_context: + type: integer + minimum: 0 + max_output: + type: integer + minimum: 0 + description: Keep models whose `max_output_tokens` is at least this value. + kind: + type: string + enum: [chat, audio, embedding, image, moderation, music, rerank, video] + q: + type: string + minLength: 1 + maxLength: 200 + owned_by: + type: string + exclude: + description: Model ids to leave out (array or comma-separated string). + oneOf: + - type: array + items: + type: string + - type: string + limit: + type: integer + minimum: 1 + default: 20 + description: Values above 100 are capped at 100. responses: "200": - description: Provider-scoped model list + description: Model shortlist content: application/json: schema: @@ -1582,13 +2241,93 @@ paths: items: $ref: "#/components/schemas/Model" "400": - description: Unknown provider + description: Invalid JSON, unknown field or invalid filter value + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" - /api/models: + /api/v1/models/health: get: tags: [Models] - summary: List models (management) - responses: + summary: Per-model pause state for the caller's key + description: >- + Read-only liveness view over the in-memory model lockout store. `ok` + means no pause observed by this instance (not a health verdict); + connection-level pauses live outside the store read here. Additive: + leaves GET /v1/models and its cache untouched. `retry_after` carries + the shortest remaining lockout in seconds, rounded up, only when + `state` is `cooling`. One documented difference from the dispatch 429 + `model_cooldown` rule: a model with one lockout-covered connection and + one connection-level cooldown still reads `cooling` here, while the + 429 requires every connection to carry a model cooldown. + security: + - BearerAuth: [] + parameters: + - in: query + name: ids + required: false + description: >- + Optional comma-separated model ids to look up. Ids in this + list that resolve to no visible model are returned with + `state: unknown` instead of being omitted. + schema: + type: string + responses: + "200": + description: Model health list + content: + application/json: + schema: + type: object + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: "#/components/schemas/ModelHealth" + "401": + description: Authentication required + + /api/v1/providers/{provider}/models: + get: + tags: [Models] + summary: List models for a specific provider + description: Returns only models for the selected provider with provider prefix removed from each model id. + security: + - BearerAuth: [] + parameters: + - in: path + name: provider + required: true + schema: + type: string + description: Provider id or alias (for example `openai`, `claude`, `cc`). + responses: + "200": + description: Provider-scoped model list + content: + application/json: + schema: + type: object + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: "#/components/schemas/Model" + "400": + description: Unknown provider + + /api/models: + get: + tags: [Models] + summary: List models (management) + responses: "200": description: Internal model list with aliases @@ -1689,6 +2428,70 @@ paths: "200": description: Test result + /api/providers/{id}/test-message: + get: + tags: [Providers] + summary: Read an account's saved test model and the global test message + description: Requires management authentication. Returns modelId and prompt without credentials. + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Saved modelId and effective prompt + "404": + description: Connection not found + put: + tags: [Providers] + summary: Save the chat model used by this account's test-message button + description: Requires management authentication. Saving never sends an inference request. + parameters: + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [modelId] + additionalProperties: false + properties: + modelId: + type: string + minLength: 1 + maxLength: 256 + responses: + "200": + description: Saved modelId + "400": + description: Invalid model selection + "404": + description: Connection not found + post: + tags: [Providers] + summary: Send one real test message through the selected account + description: >- + Requires management authentication. Uses the account's saved model and the global + connectionTestPrompt setting (default: What is 1 + 1? Reply with only the number.). + The prompt is configurable through PATCH /api/settings and limited to 500 characters. + This manual request bypasses the response cache, uses only this connection and + never falls back to another account. It consumes quota; usage-window behavior + is determined by the provider. Saving settings does not schedule requests. + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: modelId, prompt, responseText and latencyMs + "400": + description: Missing model selection or invalid chat model + "403": + description: Paid model blocked by settings + "404": + description: Connection not found + "409": + description: Account inactive or a test message is already running + "504": + description: No completed response within the test deadline + /api/providers/{id}/models: get: tags: [Providers] @@ -1699,6 +2502,16 @@ paths: "200": description: Provider model list + /api/providers/cursor/agent-availability: + get: + tags: [Providers] + summary: Check cursor-agent availability + description: "Credential-free, informational check for whether cursor-agent is installed and authenticated on this host — backs the dashboard's dismissible install-nudge banner. Returns only cursorAgentAvailable (boolean); never tokens or machineId." + x-loopback-only: true + responses: + "200": + description: Availability result + /api/providers/test-batch: post: tags: [Providers] @@ -1711,6 +2524,7 @@ paths: post: tags: [Providers] summary: Validate provider credentials + description: "Validates provider credentials over HTTP. The devin CLI-spawn fallback is call-site gated on the trusted peer-locality header (loopback/LAN only, Hard Rules #15/#17, audit #15159 S-01, #15766 shape); the route itself is management-auth remote-reachable." responses: "200": description: Validation result @@ -1751,6 +2565,7 @@ paths: post: tags: [Providers] summary: Auto-detect and import the local Antigravity CLI (agy) login from disk + x-always-protected: true responses: "200": description: Created or updated provider connection @@ -1767,9 +2582,18 @@ paths: post: tags: [Provider Nodes] summary: Create provider node + description: >- + Optional `providerIdSuffix` (letters, digits, `-`, `_`, `.`; max 64; must start with a + letter or digit) replaces the random UUID in the generated node id + (`openai-compatible--`, `anthropic-compatible-`, + `anthropic-compatible-cc-`). Blank keeps the UUID. responses: "201": description: Created node + "400": + description: Validation failure (including an invalid providerIdSuffix) + "409": + description: A provider node with the resulting id already exists /api/provider-nodes/{id}: patch: @@ -1801,9 +2625,166 @@ paths: get: tags: [Provider Nodes] summary: List provider models + description: >- + With a provider query parameter, includes modelOutputOverrides for manual + output-token caps, including synced models without a custom-model row. responses: "200": description: Provider model list + put: + tags: [Provider Nodes] + summary: Update model metadata or per-model overrides + description: >- + Requires management authentication. maxOutputTokenOverride sets a manual + max_output_tokens capability without creating a custom-model row when it + is the only change. A null value clears that override. Existing metadata + fields remain available on this operation. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [provider, modelId] + properties: + provider: + type: string + modelId: + type: string + maxOutputTokenOverride: + type: integer + minimum: 1 + nullable: true + responses: + "200": + description: Model settings saved + "400": + description: Invalid model target or override value + + /api/provider-models/validate-and-add: + post: + tags: [Provider Nodes] + summary: Validate and add a custom chat model on one exact connection + description: >- + Always requires management authentication and explicit inference consent. + Runs bounded generation, streamed synthetic tool-call and continuation proofs + through the chat core; no real tool is executed. Configuration changes, + alternate connections, retries and proxy executor fallback fail closed. + Only after all proofs pass is a previously absent custom model inserted and + read back atomically. Every response is Cache-Control no-store. Requests can + incur upstream charges. The validation receipt is informational, not redeemable. + Initial strict support is limited to native Base/Default execution for OpenAI + and canonical OpenAI-compatible nodes using a single Bearer credential and an + exact JSON model field. Specialized executors and other providers are unsupported; + session pools, extra-key rotation, HTTP redirects and credential refresh are refused. + Active runtime plugins also fail closed; plugin policies are not silently skipped. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + required: [provider, modelId, connectionId, allowInference] + properties: + provider: + type: string + minLength: 1 + maxLength: 120 + modelId: + type: string + minLength: 1 + maxLength: 240 + connectionId: + type: string + minLength: 1 + maxLength: 160 + allowInference: + type: boolean + enum: [true] + modelName: + type: string + minLength: 1 + maxLength: 240 + max_input_tokens: + type: integer + minimum: 1 + maximum: 9007199254740991 + max_output_tokens: + type: integer + minimum: 1 + maximum: 9007199254740991 + apiFormat: + type: string + enum: [chat-completions, responses] + default: chat-completions + responses: + "201": + description: All three proofs passed and custom model persistence was read back + content: + application/json: + schema: + type: object + required: [schemaVersion, model, persistenceVerified, validation] + properties: + schemaVersion: + type: integer + enum: [1] + model: + type: object + persistenceVerified: + type: boolean + enum: [true] + validation: + type: object + required: [id, status, provider, modelId, connectionId, validatedAt, stages] + properties: + id: + type: string + format: uuid + status: + type: string + enum: [passed] + provider: + type: string + modelId: + type: string + connectionId: + type: string + validatedAt: + type: string + format: date-time + stages: + type: object + required: [generation, toolCall, continuation] + properties: + generation: + type: string + enum: [passed] + toolCall: + type: string + enum: [passed] + continuation: + type: string + enum: [passed] + "400": + description: Invalid input or missing explicit inference consent + "401": + description: Management authentication required + "403": + description: Insufficient management authorization + "408": + description: Validation cancelled or deadline exceeded + "409": + description: Existing model, busy or unavailable connection, or changed configuration + "413": + description: Request body exceeds 16 KiB + "422": + description: Required model proofs did not pass + "500": + description: Sanitized internal validation failure /api/keys: get: @@ -1889,6 +2870,122 @@ paths: "404": description: Key not found + /api/keys/{id}/access: + post: + tags: [API Keys] + summary: Add or remove allowed models and combos on an API key atomically + description: | + Adds or removes allowed models and combos on an API key without lost updates. + Serialized per key ID using an in-process async lock. + Keys allowing all models (modelAccessMode: all) or all combos (allowedCombos containing 'combo/*') + require switchToRestricted: true when adding new models or combos, otherwise returning a 409 Conflict. + When switching to restricted mode, the allowlist becomes exactly the net added items (at least one required). + Direct concurrent PATCH calls to /api/keys/{id} completely overwrite the policy outside this lock. + parameters: + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + add: + type: object + properties: + models: + type: array + minItems: 1 + maxItems: 1000 + items: + type: string + minLength: 1 + combos: + type: array + minItems: 1 + maxItems: 500 + items: + type: string + minLength: 1 + remove: + type: object + properties: + models: + type: array + minItems: 1 + maxItems: 1000 + items: + type: string + minLength: 1 + combos: + type: array + minItems: 1 + maxItems: 500 + items: + type: string + minLength: 1 + switchToRestricted: + type: boolean + responses: + "200": + description: API key access updated + content: + application/json: + schema: + type: object + required: + - id + - modelAccessMode + - allowedModels + - allowedCombos + - changed + properties: + id: + type: string + modelAccessMode: + type: string + enum: [all, restricted] + allowedModels: + type: array + items: + type: string + allowedCombos: + type: array + items: + type: string + changed: + type: boolean + "400": + description: Invalid request, caps exceeded, or empty restricted list + "401": + description: Authentication required + "403": + description: API key lacks 'manage' scope + "404": + description: Key not found + "409": + description: Key allows all models or all combos and switchToRestricted was not set + content: + application/json: + schema: + type: object + required: + - error + properties: + error: + type: object + required: + - code + - message + properties: + code: + type: string + enum: + - key_allows_all_models + - key_allows_all_combos + message: + type: string + /api/combos: get: tags: [Combos] @@ -1910,15 +3007,63 @@ paths: description: Created combo /api/combos/{id}: - patch: + get: tags: [Combos] - summary: Update combo + summary: Get combo by ID parameters: - $ref: "#/components/parameters/ResourceId" responses: "200": - description: Updated combo - delete: + description: Combo details + "404": + description: Combo not found + put: + tags: [Combos] + summary: Update combo + description: >- + Partial update: the body is merged onto the stored combo, so a field left out keeps + its current value. An array that IS sent replaces the stored one outright. + parameters: + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Updated combo + "400": + description: Invalid body, or the resulting combo fails validation + "404": + description: Combo not found + "409": + description: Name already taken, or the combo is quota-share managed + patch: + tags: [Combos] + summary: Update combo + description: >- + Partial update: the body is merged onto the stored combo, so a field left out keeps + its current value. An array that IS sent replaces the stored one outright. + parameters: + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Updated combo + "400": + description: Invalid body, or the resulting combo fails validation + "404": + description: Combo not found + "409": + description: Name already taken, or the combo is quota-share managed + delete: tags: [Combos] summary: Delete combo parameters: @@ -1968,6 +3113,13 @@ paths: patch: tags: [Settings] summary: Update settings + description: >- + Security-policy keys (`requireLogin`, `newPassword`, every `oidc*` key, `corsOrigins`, + `bruteForceProtection`, the LOCAL_ONLY bypass list) need `currentPassword` and an + operator credential (dashboard session, local CLI, or login disabled); API keys of any + scope and `oma_` access tokens get 403 on them. Ordinary settings stay writable for a + manage-scoped key or an `admin` access token. Every mutation needs the `admin` + access-token scope. requestBody: required: true content: @@ -1977,6 +3129,14 @@ paths: responses: "200": description: Updated settings + "400": + description: Validation failed, or `PASSWORD_REQUIRED` for a security-policy key + "401": + description: Unauthenticated, or `PASSWORD_MISMATCH` + "403": + description: >- + Machine credential (API key of any scope, `oma_` access token) on a security-policy + key, or an access token below the `admin` scope /api/settings/purge-request-history: post: @@ -2033,6 +3193,11 @@ paths: put: tags: [Compression] summary: Update global compression settings + description: >- + Partial update: only the sent top-level keys change. `engines` merges by engine id — + an entry overwrites only the fields it sends, engines left out are kept as-is, and + until an engines row is stored the merge base is the map derived from the legacy + per-engine settings. security: - ManagementSessionAuth: [] requestBody: @@ -2053,6 +3218,19 @@ paths: autoTriggerTokens: type: integer minimum: 0 + engines: + type: object + description: >- + Per-engine toggles, merged by engine id into the stored map. Each entry + needs `enabled`; `level` survives an entry that omits it. + additionalProperties: + type: object + properties: + enabled: + type: boolean + level: + type: string + required: [enabled] rtkConfig: type: object additionalProperties: true @@ -2219,6 +3397,36 @@ paths: "200": description: RTK filter catalog and diagnostics + /api/context/rtk/import: + post: + tags: [Compression] + summary: Validate or install an RTK TOML schema v1 filter file + security: + - ManagementSessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [action, content] + additionalProperties: false + properties: + action: + type: string + enum: [validate, install] + content: + type: string + maxLength: 1048576 + overwrite: + type: boolean + description: Replace an existing global file and create a backup + responses: + "200": + description: Filter metadata, inline-test outcomes, warnings, and installation status + "400": + description: Invalid TOML, schema, regular expression, inline test, or install request + /api/context/rtk/test: post: tags: [Compression] @@ -2360,10 +3568,39 @@ paths: /api/settings/require-login: post: tags: [Settings] - summary: Toggle login requirement + summary: Set the management password and/or toggle the login requirement + description: >- + Open without a credential only during the first-run bootstrap window (no password + configured yet). Afterwards the write is operator-only: a dashboard session, the local + CLI, or an instance with login disabled. API keys of any scope and `oma_` access tokens + are refused with 403. A dashboard session must send `currentPassword` to replace an + existing password; the local CLI may reset it without one (recovery path). + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + requireLogin: + type: boolean + password: + type: string + minLength: 4 + currentPassword: + type: string + description: Current password; required when a dashboard session changes `password`. responses: "200": description: Updated + "400": + description: Invalid body or no field to update + "401": + description: No credential outside the bootstrap window + "403": + description: >- + Machine credential (API key of any scope, `oma_` access token), or a dashboard + session changing the password without the correct `currentPassword` /api/settings/ip-filter: get: @@ -2421,6 +3658,92 @@ paths: "200": description: Updated IP filter configuration + /api/settings/pii: + get: + tags: [Settings] + summary: Get PII masking settings + description: | + Returns the opt-in flag state (`PII_REDACTION_ENABLED`, `PII_RESPONSE_SANITIZATION`), + the per-type selection (`enabledTypes`, `null` = all built-in types), the custom + patterns and in-memory detection counters (counts only, never matched values). + responses: + "200": + description: PII masking settings + put: + tags: [Settings] + summary: Update PII masking settings + description: | + Replaces the per-type selection and/or the custom pattern list. Custom regex patterns + that fail the safe-regex (catastrophic backtracking) check, do not compile, or match an + empty string are refused with 400. Does not turn masking on. + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + enabledTypes: + type: array + nullable: true + items: + type: string + customPatterns: + type: array + maxItems: 20 + items: + type: object + required: [id, name, type, pattern] + properties: + id: + type: string + name: + type: string + type: + type: string + enum: [regex, wildcard] + pattern: + type: string + maxLength: 500 + flags: + type: string + enabled: + type: boolean + responses: + "200": + description: Updated PII masking settings + "400": + description: Validation failed (including an unsafe pattern) + + /api/settings/pii/test: + post: + tags: [Settings] + summary: Test a custom PII pattern + description: Dry-runs one pattern against a sample of at most 8 KB. Nothing is stored. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [pattern, sampleText] + properties: + type: + type: string + enum: [regex, wildcard] + pattern: + type: string + flags: + type: string + sampleText: + type: string + responses: + "200": + description: Matches and a redacted preview + "400": + description: Invalid body, oversized sample or unsafe pattern + /api/settings/system-prompt: get: tags: [Settings] @@ -2508,66 +3831,373 @@ paths: "200": description: Ollama model tags - # ─── Usage & Analytics ───────────────────────────────────────── + # ─── Proxy Subscriptions ─────────────────────────────────────── - /api/usage/analytics: + /api/v1/management/proxy-subscriptions: get: - tags: [Usage] - summary: Get usage analytics - parameters: - - name: period - in: query - schema: - type: string - enum: [day, week, month] - default: day + tags: [Proxy Subscriptions] + summary: List proxy subscriptions + description: >- + Lists all operator-supplied proxy subscription links. Also starts the + background auto-refresh scheduler (idempotent) so enabled subscriptions + stay in sync. Credentials embedded in `url` are redacted in the response. + security: + - ManagementSessionAuth: [] responses: "200": - description: Usage analytics data - - /api/usage/call-logs: - get: - tags: [Usage] - summary: Get call logs - parameters: - - name: limit - in: query - schema: - type: integer - default: 50 - - name: offset - in: query - schema: - type: integer - default: 0 + description: Subscription list + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/ProxySubscription" + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + post: + tags: [Proxy Subscriptions] + summary: Create a proxy subscription + description: >- + Creates a subscription record. If `mode` is `rule`, at least one entry in + `ruleProviders` is required. `updateIntervalMinutes` defaults to 60 and + `enabled` defaults to `false` when omitted or not exactly `true`. + security: + - ManagementSessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, url] + properties: + name: + type: string + minLength: 1 + url: + type: string + minLength: 1 + mode: + type: string + enum: [global, rule] + default: global + ruleProviders: + type: array + items: + type: string + description: Required (non-empty) when mode is "rule". + localCoreEndpoint: + type: string + nullable: true + coreConfigPath: + type: string + nullable: true + description: >- + Absolute adopted config path: lowercase `.json` renders + sing-box, `.yaml`/`.yml` renders Mihomo. Empty means no + generation; each sync writes + `.generated` beside it, never into it. + coreBinaryPath: + type: string + nullable: true + description: >- + Absolute path of the core binary used to verify the + generated file before replacing the adopted file. + Empty means write beside it only. + updateIntervalMinutes: + type: integer + default: 60 + enabled: + type: boolean + default: false responses: - "200": - description: Paginated call logs + "201": + description: Created subscription (url redacted) + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySubscription" + "400": + description: Missing name/url, or ruleProviders empty while mode is "rule" + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" - /api/usage/call-logs/{id}: + /api/v1/management/proxy-subscriptions/{id}: get: - tags: [Usage] - summary: Get a specific call log + tags: [Proxy Subscriptions] + summary: Get a proxy subscription + security: + - ManagementSessionAuth: [] parameters: - $ref: "#/components/parameters/ResourceId" responses: "200": - description: Call log detail - - /api/usage/{connectionId}: - get: - tags: [Usage] - summary: Get usage for a specific connection + description: Subscription detail (url redacted) + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySubscription" + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "404": + description: Subscription not found + patch: + tags: [Proxy Subscriptions] + summary: Update a proxy subscription + description: >- + Partial update — only fields present in the body are changed + (name/url/mode/ruleProviders/localCoreEndpoint/coreConfigPath/coreBinaryPath/updateIntervalMinutes/enabled). + security: + - ManagementSessionAuth: [] parameters: - - name: connectionId - in: path - required: true - schema: - type: string - responses: - "200": - description: Connection usage data - + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + url: + type: string + mode: + type: string + enum: [global, rule] + ruleProviders: + type: array + items: + type: string + localCoreEndpoint: + type: string + nullable: true + coreConfigPath: + type: string + nullable: true + coreBinaryPath: + type: string + nullable: true + updateIntervalMinutes: + type: integer + enabled: + type: boolean + responses: + "200": + description: Updated subscription (url redacted) + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySubscription" + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "404": + description: Subscription not found + delete: + tags: [Proxy Subscriptions] + summary: Delete a proxy subscription + description: Removes the subscription record and unbinds/drops its synced proxy_registry rows. + security: + - ManagementSessionAuth: [] + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Deleted + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "404": + description: Subscription not found + + /api/v1/management/proxy-subscriptions/{id}/nodes: + get: + tags: [Proxy Subscriptions] + summary: Get a subscription's last-parsed node summary + description: >- + Returns the last-parsed node list without re-fetching the (possibly slow) + subscription URL. + security: + - ManagementSessionAuth: [] + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Last-parsed node summary + content: + application/json: + schema: + type: object + properties: + id: + type: string + name: + type: string + mode: + type: string + enum: [global, rule] + enabled: + type: boolean + status: + type: string + enum: [ok, error, empty] + error: + type: string + nullable: true + lastFetchedAt: + type: string + format: date-time + nullable: true + nodes: + type: array + items: {} + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "404": + description: Subscription not found + + /api/v1/management/proxy-subscriptions/{id}/refresh: + post: + tags: [Proxy Subscriptions] + summary: Refresh a proxy subscription + description: >- + Re-fetches and re-parses the subscription URL, syncs its nodes into + `proxy_registry`, and (re)binds the pool. + security: + - ManagementSessionAuth: [] + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Sync result + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + nodes: + type: integer + needsCore: + type: integer + boundProxies: + type: integer + status: + type: string + enum: [ok, error, empty] + error: + type: string + nullable: true + applied: + type: boolean + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "404": + description: Subscription not found + + # ─── Usage & Analytics ───────────────────────────────────────── + + /api/usage/analytics: + get: + tags: [Usage] + summary: Get usage analytics + parameters: + - name: period + in: query + schema: + type: string + enum: [day, week, month] + default: day + - name: view + in: query + description: >- + `apiKeys` returns only `byApiKey`: lifetime requests, cost and last use for each + configured API key, from one grouped query. The API Manager key list uses it. + schema: + type: string + enum: [apiKeys] + responses: + "200": + description: Usage analytics data + + /api/usage/call-logs: + get: + tags: [Usage] + summary: Get call logs + parameters: + - name: limit + in: query + schema: + type: integer + default: 50 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + "200": + description: Paginated call logs + + /api/usage/call-logs/filters: + get: + tags: [Usage] + summary: Get call log filter options + description: >- + Distinct providers, models, accounts and API keys seen in call logs, plus every + configured API key (id and name only). Feeds the Logs tab filter dropdowns. + responses: + "200": + description: Filter options (providers, models, accounts, apiKeys, configuredKeys) + + /api/usage/call-logs/{id}: + get: + tags: [Usage] + summary: Get a specific call log + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Call log detail + + /api/usage/{connectionId}: + get: + tags: [Usage] + summary: Get usage for a specific connection + parameters: + - name: connectionId + in: path + required: true + schema: + type: string + responses: + "200": + description: Connection usage data + /api/usage/history: get: tags: [Usage] @@ -2622,36 +4252,231 @@ paths: "200": description: Updated budget configuration - # ─── Pricing ─────────────────────────────────────────────────── - - /api/pricing: - get: - tags: [Pricing] - summary: Get model pricing - responses: - "200": - description: Current pricing configuration - post: - tags: [Pricing] - summary: Set model pricing - responses: - "200": - description: Updated pricing - - /api/pricing/defaults: + /api/usage/cache-health: get: - tags: [Pricing] - summary: Get default pricing + tags: [Usage] + summary: Get prompt-cache health summary + description: | + Summarizes the `write/read` cache ratio from `call_logs` for a time window: + distribution (p50/p90/p99/max of cache-write tokens), warm/cold/rewrite/uncached + call counts, the "heavy write" outlier share (10x the window median, floored at + 1024 tokens — Anthropic's cache-creation minimum), and a per-model breakdown. + Only successful (`status = 200`) calls with a non-null cache column are counted. + parameters: + - name: range + in: query + schema: + type: string + enum: [1h, 24h, 7d, 30d] + default: 24h + - name: model + in: query + description: Filter to one model or requested-model alias (1-200 chars). + schema: + type: string + minLength: 1 + maxLength: 200 responses: "200": - description: Default pricing data + description: Cache health summary for the requested window + content: + application/json: + schema: + type: object + properties: + totalCalls: + type: integer + cacheReadTotal: + type: integer + cacheWriteTotal: + type: integer + writeReadRatio: + type: number + warmCalls: + type: integer + coldCalls: + type: integer + rewriteCalls: + type: integer + uncachedCalls: + type: integer + writeP50: + type: integer + writeP90: + type: integer + writeP99: + type: integer + writeMax: + type: integer + heavyWriteCalls: + type: integer + heavyWriteCallShare: + type: number + heavyWriteTokenShare: + type: number + heavyWriteThreshold: + type: integer + verdict: + type: string + enum: [healthy, degraded, thrash, no-data] + byModel: + type: array + items: + type: object + properties: + model: + type: string + calls: + type: integer + cacheReadTotal: + type: integer + cacheWriteTotal: + type: integer + writeReadRatio: + type: number + heavyWriteCalls: + type: integer + timeRange: + type: string + since: + type: string + format: date-time + truncated: + type: boolean + description: True when the window held more than 5000 rows and only the newest were summarized. + "400": + description: Invalid query parameters - /api/pricing/models: + /api/usage/model-latency-stats: get: - tags: [Pricing] - summary: Get pricing per model - description: Returns pricing information organized by model. - responses: + tags: [Usage] + summary: Get per-model/provider latency statistics + description: >- + Aggregates `usage_history` rows into per-(provider, model) latency stats + (avg/p50/p95/p99, std-dev, TTFT, tokens/sec) over a rolling window. Falls + back from successful-only to all-sample rows when the successful count is + below `minSamples`. + security: + - ManagementSessionAuth: [] + parameters: + - name: windowHours + in: query + schema: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: 720 + default: 24 + - name: minSamples + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: maxRows + in: query + schema: + type: integer + minimum: 1 + maximum: 50000 + default: 10000 + - name: provider + in: query + schema: + type: string + minLength: 1 + maxLength: 64 + - name: model + in: query + schema: + type: string + minLength: 1 + maxLength: 256 + responses: + "200": + description: Latency stats keyed by provider/model, plus the window used + content: + application/json: + schema: + type: object + properties: + entries: + type: array + items: + type: object + properties: + provider: + type: string + model: + type: string + key: + type: string + totalRequests: + type: integer + successfulRequests: + type: integer + successRate: + type: number + avgLatencyMs: + type: number + p50LatencyMs: + type: number + p95LatencyMs: + type: number + p99LatencyMs: + type: number + latencyStdDev: + type: number + windowHours: + type: number + avgTtftMs: + type: number + avgE2ELatencyMs: + type: number + avgTokensPerSecond: + type: number + windowHours: + type: number + generatedAt: + type: string + format: date-time + "400": + description: Invalid query parameters + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + + # ─── Pricing ─────────────────────────────────────────────────── + + /api/pricing: + get: + tags: [Pricing] + summary: Get model pricing + responses: + "200": + description: Current pricing configuration + post: + tags: [Pricing] + summary: Set model pricing + responses: + "200": + description: Updated pricing + + /api/pricing/defaults: + get: + tags: [Pricing] + summary: Get default pricing + responses: + "200": + description: Default pricing data + + /api/pricing/models: + get: + tags: [Pricing] + summary: Get pricing per model + description: Returns pricing information organized by model. + responses: "200": description: Per-model pricing data @@ -2799,7 +4624,10 @@ paths: "200": description: Token created (token returned once) "403": - description: Insufficient scope + description: >- + Insufficient scope, or an `admin` token was requested by a machine credential + (API key or access token). Admin tokens require the dashboard session or the + local CLI; machine credentials can mint `write`/`read` only. /api/cli/tokens/{id}: delete: @@ -2914,12 +4742,14 @@ paths: get: tags: [CLI Tools] summary: Get Claude CLI settings + x-loopback-only: true responses: "200": description: Claude CLI configuration post: tags: [CLI Tools] summary: Apply Claude CLI settings + x-loopback-only: true requestBody: required: true content: @@ -2932,6 +4762,7 @@ paths: delete: tags: [CLI Tools] summary: Reset Claude CLI settings + x-loopback-only: true responses: "200": description: Claude CLI settings reset @@ -2940,12 +4771,14 @@ paths: get: tags: [CLI Tools] summary: Get Cline CLI settings + x-loopback-only: true responses: "200": description: Cline CLI configuration post: tags: [CLI Tools] summary: Apply Cline CLI settings + x-loopback-only: true requestBody: required: true content: @@ -2958,6 +4791,7 @@ paths: delete: tags: [CLI Tools] summary: Reset Cline CLI settings + x-loopback-only: true responses: "200": description: Cline CLI settings reset @@ -2966,12 +4800,14 @@ paths: get: tags: [CLI Tools] summary: Get Codex profiles + x-always-protected: true responses: "200": description: Codex profile list post: tags: [CLI Tools] summary: Create Codex profile + x-always-protected: true requestBody: required: true content: @@ -2984,6 +4820,7 @@ paths: put: tags: [CLI Tools] summary: Update Codex profile + x-always-protected: true requestBody: required: true content: @@ -2996,6 +4833,7 @@ paths: delete: tags: [CLI Tools] summary: Delete Codex profile + x-always-protected: true responses: "200": description: Profile deleted @@ -3004,12 +4842,14 @@ paths: get: tags: [CLI Tools] summary: Get Codex CLI settings + x-loopback-only: true responses: "200": description: Codex CLI configuration post: tags: [CLI Tools] summary: Apply Codex CLI settings + x-loopback-only: true requestBody: required: true content: @@ -3022,6 +4862,7 @@ paths: delete: tags: [CLI Tools] summary: Reset Codex CLI settings + x-loopback-only: true responses: "200": description: Codex CLI settings reset @@ -3030,12 +4871,14 @@ paths: get: tags: [CLI Tools] summary: Get Droid CLI settings + x-loopback-only: true responses: "200": description: Droid CLI configuration post: tags: [CLI Tools] summary: Apply Droid CLI settings + x-loopback-only: true requestBody: required: true content: @@ -3048,6 +4891,7 @@ paths: delete: tags: [CLI Tools] summary: Reset Droid CLI settings + x-loopback-only: true responses: "200": description: Droid CLI settings reset @@ -3056,12 +4900,14 @@ paths: get: tags: [CLI Tools] summary: Get Kilo CLI settings + x-loopback-only: true responses: "200": description: Kilo CLI configuration post: tags: [CLI Tools] summary: Apply Kilo CLI settings + x-loopback-only: true requestBody: required: true content: @@ -3074,6 +4920,7 @@ paths: delete: tags: [CLI Tools] summary: Reset Kilo CLI settings + x-loopback-only: true responses: "200": description: Kilo CLI settings reset @@ -3082,12 +4929,14 @@ paths: get: tags: [CLI Tools] summary: Get OpenClaw CLI settings + x-loopback-only: true responses: "200": description: OpenClaw CLI configuration post: tags: [CLI Tools] summary: Apply OpenClaw CLI settings + x-loopback-only: true requestBody: required: true content: @@ -3100,6 +4949,7 @@ paths: delete: tags: [CLI Tools] summary: Reset OpenClaw CLI settings + x-loopback-only: true responses: "200": description: OpenClaw CLI settings reset @@ -3303,6 +5153,33 @@ paths: "400": description: Invalid request body + /api/services/9router/auto-restart-adopted: + post: + tags: [Embedded Services] + summary: Toggle 9Router auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) 9Router + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [enabled] + properties: + enabled: + type: boolean + responses: + "204": + description: Flag updated + "400": + description: Invalid request body + "500": + description: Update failed + /api/services/cliproxy/install: post: tags: [Embedded Services] @@ -3464,1812 +5341,9775 @@ paths: "400": description: Invalid request body - /api/services/{name}/logs: - get: + /api/services/cliproxy/auto-restart-adopted: + post: tags: [Embedded Services] - summary: Stream service logs via SSE + summary: Toggle CLIProxyAPI auto-restart-when-adopted description: >- - Returns a Server-Sent Events stream from the service's in-memory ring buffer - (5 MB, circular). Sends a `snapshot` event with historical lines first, then - live `log` events, plus a `heartbeat` every 15 s. + When enabled, an externally-adopted (not OmniRoute-spawned) CLIProxyAPI + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. **LOCAL_ONLY** — loopback only. - parameters: - - name: name - in: path - required: true - schema: - type: string - enum: [9router, cliproxy] - - name: tail - in: query - schema: - type: integer - default: 200 - maximum: 1000 - description: Number of historical lines to include in the initial snapshot - - name: filter - in: query - schema: - type: string - maxLength: 200 - description: >- - Case-insensitive substring filter applied to log lines. - No regex — ReDoS-safe by design. - responses: - "200": - description: SSE log stream - content: - text/event-stream: - schema: - type: string - description: >- - Events: `snapshot` (LogLine[]), `log` (LogLine), `heartbeat` ({}) - "400": - description: filter parameter exceeds maximum length - "404": - description: Service not found - - # ─── OAuth ───────────────────────────────────────────────────── - - /api/oauth/{provider}/{action}: - get: - tags: [OAuth] - summary: OAuth flow handler - description: Handles OAuth authorization and callback for providers - parameters: - - name: provider - in: path - required: true - schema: - type: string - - name: action - in: path - required: true - schema: - type: string - enum: [authorize, callback, refresh, status] - responses: - "200": - description: OAuth flow response - "302": - description: Redirect to provider auth page - - /api/oauth/cursor/auto-import: - get: - tags: [OAuth] - summary: Auto-import Cursor OAuth credentials - description: Automatically detects and imports Cursor credentials from local config. - responses: - "200": - description: Import result - - /api/oauth/cursor/import: - get: - tags: [OAuth] - summary: Get Cursor import status - responses: - "200": - description: Current import status - post: - tags: [OAuth] - summary: Import Cursor OAuth credentials requestBody: required: true content: application/json: schema: type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: Credentials imported - - /api/oauth/kiro/auto-import: - get: - tags: [OAuth] - summary: Auto-import Kiro OAuth credentials - description: Automatically detects and imports Kiro credentials from local config. - responses: - "200": - description: Import result + "204": + description: Flag updated + "400": + description: Invalid request body + "500": + description: Update failed - /api/oauth/kiro/import: - get: - tags: [OAuth] - summary: Get Kiro import status - responses: - "200": - description: Current import status + /api/services/mux/install: post: - tags: [OAuth] - summary: Import Kiro OAuth credentials + tags: [Embedded Services] + summary: Install Mux from npm + description: >- + Installs the `mux` npm package (coder/mux — local agent-orchestration + daemon) under DATA_DIR/services/mux/. **LOCAL_ONLY** — loopback only. requestBody: - required: true + required: false content: application/json: schema: type: object + properties: + version: + type: string + default: latest responses: "200": - description: Credentials imported - - /api/oauth/kiro/social-authorize: - get: - tags: [OAuth] - summary: Initiate Kiro social OAuth authorization - description: Starts the social OAuth flow for Kiro. - responses: - "302": - description: Redirect to OAuth provider - - /api/oauth/kiro/social-exchange: - post: - tags: [OAuth] - summary: Exchange Kiro social OAuth token - description: Exchanges the authorization code for access tokens. - requestBody: - required: true - content: - application/json: - schema: - type: object - responses: - "200": - description: Token exchange result - - # ─── Cloud ───────────────────────────────────────────────────── + description: Install succeeded + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + "400": + description: Invalid request body + "500": + description: npm install failed - /api/cloud/auth: + /api/services/mux/start: post: - tags: [Cloud] - summary: Authenticate with cloud worker - description: Authenticates with the OmniRoute cloud worker for remote access. - requestBody: - required: true - content: - application/json: - schema: - type: object + tags: [Embedded Services] + summary: Start Mux + description: >- + Spawns `mux server --host 127.0.0.1 --port `. Idempotent if + already running. **LOCAL_ONLY** — loopback only. responses: "200": - description: Authentication result + description: Service started + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" + "409": + description: Mux is not installed + "503": + description: Start failed - /api/cloud/credentials/update: - put: - tags: [Cloud] - summary: Update cloud worker credentials - requestBody: - required: true - content: - application/json: - schema: - type: object + /api/services/mux/stop: + post: + tags: [Embedded Services] + summary: Stop Mux + description: >- + Gracefully stops Mux. Idempotent. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Credentials updated + description: Service stopped + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" - /api/cloud/model/resolve: + /api/services/mux/restart: post: - tags: [Cloud] - summary: Resolve model via cloud - description: Resolves a model request through the cloud worker. - requestBody: - required: true - content: - application/json: - schema: - type: object + tags: [Embedded Services] + summary: Restart Mux + description: >- + stop() then start() under the operation lock. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Resolved model info + description: Service restarted + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" - /api/cloud/models/alias: - get: - tags: [Cloud] - summary: Get cloud model aliases - responses: - "200": - description: Cloud model alias list - put: - tags: [Cloud] - summary: Update cloud model alias + /api/services/mux/update: + post: + tags: [Embedded Services] + summary: Update Mux to a newer npm version + description: >- + Stops, installs newer version, restarts. + **LOCAL_ONLY** — loopback only. requestBody: - required: true + required: false content: application/json: schema: type: object + properties: + version: + type: string + default: latest responses: "200": - description: Alias updated - - # ─── Fallback ────────────────────────────────────────────────── + description: Update succeeded + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + "500": + description: Update failed - /api/fallback/chains: + /api/services/mux/status: get: - tags: [Fallback] - summary: List fallback chains - description: Returns all registered fallback chains for model routing. + tags: [Embedded Services] + summary: Get Mux status + description: >- + Returns live supervisor state and DB metadata. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Fallback chain list + description: Status response + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" + + /api/services/mux/auto-start: post: - tags: [Fallback] - summary: Create fallback chain - description: Registers a fallback routing chain for a model. + tags: [Embedded Services] + summary: Toggle Mux auto-start + description: >- + When enabled, Mux starts automatically on the next OmniRoute boot. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: type: object - required: [model, chain] + required: [enabled] properties: - model: - type: string - chain: - type: array - items: - type: object - properties: - provider: - type: string - priority: - type: integer - enabled: - type: boolean + enabled: + type: boolean responses: "200": - description: Fallback chain created - delete: - tags: [Fallback] - summary: Delete fallback chain + description: Auto-start flag updated + content: + application/json: + schema: + type: object + properties: + autoStart: + type: boolean + "400": + description: Invalid request body + + /api/services/mux/auto-restart-adopted: + post: + tags: [Embedded Services] + summary: Toggle Mux auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) Mux + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: type: object - required: [model] + required: [enabled] properties: - model: - type: string + enabled: + type: boolean responses: - "200": - description: Fallback chain deleted - - # ─── System ──────────────────────────────────────────────────── + "204": + description: Flag updated + "400": + description: Invalid request body + "500": + description: Update failed - /api/auth/login: + /api/services/openwa/install: post: - tags: [System] - summary: Authenticate user + tags: [Embedded Services] + summary: Install open-wa from npm + description: >- + Installs the `@open-wa/wa-automate` npm package (WhatsApp Web + automation via headless Chromium/Puppeteer) under + DATA_DIR/services/openwa/. Bundles a Puppeteer Chromium download, so + this install is much slower than the other embedded services. + **LOCAL_ONLY** — loopback only. requestBody: - required: true + required: false content: application/json: schema: type: object - required: [password] properties: - password: + version: type: string - minLength: 1 + default: latest responses: "200": - description: JWT token returned + description: Install succeeded + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string "400": - description: Invalid login request - "401": - description: Invalid password - "403": - description: Password setup required - "429": - description: Too many failed attempts + description: Invalid request body + "500": + description: npm install failed - /api/auth/logout: + /api/services/openwa/start: post: - tags: [System] - summary: Log out + tags: [Embedded Services] + summary: Start open-wa + description: >- + Spawns `wa-automate --port --host 127.0.0.1 + --session-data-path data`. The HTTP API does not start listening + until the WhatsApp client handshake resolves, which blocks on a + human scanning the pairing QR code (shown in the logs panel) on + first pairing — state stays `starting` well past the other + services' typical cold-start window. Idempotent if already running. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Session cleared + description: Service started + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" + "409": + description: open-wa is not installed + "503": + description: Start failed - /api/init: - get: - tags: [System] - summary: Initialize application + /api/services/openwa/stop: + post: + tags: [Embedded Services] + summary: Stop open-wa + description: >- + Gracefully stops open-wa. Idempotent. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Init status - - /api/restart: - post: - tags: [System] - summary: Restart the application - responses: - "200": - description: Restart initiated + description: Service stopped + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" - /api/shutdown: + /api/services/openwa/restart: post: - tags: [System] - summary: Shutdown the application - x-always-protected: true + tags: [Embedded Services] + summary: Restart open-wa + description: >- + stop() then start() under the operation lock. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Shutdown initiated + description: Service restarted + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" - /api/db-backups: - get: - tags: [System] - summary: List database backups - responses: - "200": - description: Backup list + /api/services/openwa/update: post: - tags: [System] - summary: Create database backup - responses: - "200": - description: Backup created - patch: - tags: [System] - summary: Save database backup retention settings + tags: [Embedded Services] + summary: Update open-wa to a newer npm version + description: >- + Stops, installs newer version, restarts. + **LOCAL_ONLY** — loopback only. requestBody: - required: true + required: false content: application/json: schema: type: object properties: - keepLatest: - type: integer - minimum: 1 - maximum: 200 - retentionDays: - type: integer - minimum: 0 - maximum: 3650 + version: + type: string + default: latest responses: "200": - description: Backup retention settings saved + description: Update succeeded + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + "500": + description: Update failed - /api/storage/health: + /api/services/openwa/status: get: - tags: [System] - summary: Check storage health + tags: [Embedded Services] + summary: Get open-wa status + description: >- + Returns live supervisor state and DB metadata. `health` only reflects + whether `/api-docs/` answered — it does not indicate whether a + WhatsApp session is paired. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Storage health status + description: Status response + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" - /api/sync/cloud: + /api/services/openwa/auto-start: post: - tags: [System] - summary: Sync with cloud + tags: [Embedded Services] + summary: Toggle open-wa auto-start + description: >- + When enabled, open-wa starts automatically on the next OmniRoute boot. + **LOCAL_ONLY** — loopback only. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [enabled] + properties: + enabled: + type: boolean responses: "200": - description: Sync result + description: Auto-start flag updated + content: + application/json: + schema: + type: object + properties: + autoStart: + type: boolean + "400": + description: Invalid request body - /api/sync/initialize: + /api/services/openwa/auto-restart-adopted: post: - tags: [System] - summary: Initialize cloud sync - responses: - "200": - description: Sync initialized - - # ─── Resilience & Monitoring ──────────────────────────────────── - - /api/resilience: - get: - tags: [System] - summary: Get resilience configuration - responses: - "200": - description: Request queue, connection cooldown, provider breaker, and wait settings - patch: - tags: [System] - summary: Update resilience configuration + tags: [Embedded Services] + summary: Toggle open-wa auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) open-wa + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: Updated resilience configuration + "204": + description: Flag updated + "400": + description: Invalid request body + "500": + description: Update failed - /api/resilience/reset: + /api/services/bifrost/install: post: - tags: [System] - summary: Reset circuit breakers - responses: - "200": - description: Circuit breakers reset - - /api/monitoring/health: - get: - tags: [System] - summary: System health check - description: Returns system health including uptime, memory, circuit breakers, rate limits - responses: - "200": - description: Health status - - /api/rate-limits: - get: - tags: [System] - summary: Get per-account rate limit status + tags: [Embedded Services] + summary: Install Bifrost + description: >- + Installs the `@maximhq/bifrost` npm package under DATA_DIR/services/bifrost/. + The package downloads the Go binary on first run. Accepts an optional `version` + field (semver or `latest`). **LOCAL_ONLY** — loopback only. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + version: + type: string + default: latest responses: "200": - description: Rate limit status by account + description: Installation result + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + installPath: + type: string + durationMs: + type: number - /api/sessions: - get: - tags: [System] - summary: Get active sessions + /api/services/bifrost/start: + post: + tags: [Embedded Services] + summary: Start Bifrost + description: Starts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. responses: "200": - description: Active session list + description: Service status after start + "409": + description: Bifrost is not installed - /api/cache: - get: - tags: [System] - summary: Get cache statistics - responses: - "200": - description: Semantic cache and idempotency stats - delete: - tags: [System] - summary: Clear all caches + /api/services/bifrost/stop: + post: + tags: [Embedded Services] + summary: Stop Bifrost + description: Stops the supervised Bifrost process. **LOCAL_ONLY** — loopback only. responses: "200": - description: Caches cleared + description: Service status after stop - /api/cache/stats: - get: - tags: [System] - summary: Get detailed cache statistics - description: Returns detailed statistics for all cache layers. - responses: - "200": - description: Detailed cache stats - delete: - tags: [System] - summary: Clear cache statistics + /api/services/bifrost/restart: + post: + tags: [Embedded Services] + summary: Restart Bifrost + description: Restarts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. responses: "200": - description: Cache stats cleared - - # ─── Telemetry & Token Health ─────────────────────────────────── + description: Service status after restart + "409": + description: Bifrost is not installed - /api/telemetry/summary: - get: - tags: [Telemetry] - summary: Get telemetry summary - description: Returns aggregated telemetry data including request metrics and performance stats. + /api/services/bifrost/update: + post: + tags: [Embedded Services] + summary: Update Bifrost + description: >- + Updates Bifrost to the latest npm version. Stops the running process, + installs the new version, and restarts if it was previously running. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Telemetry summary data + description: Update result - /api/token-health: + /api/services/bifrost/status: get: - tags: [Telemetry] - summary: Get token health status - description: Returns health status of OAuth tokens across all providers. + tags: [Embedded Services] + summary: Get Bifrost status + description: Returns live and DB status for the supervised Bifrost service. **LOCAL_ONLY** — loopback only. responses: "200": - description: Token health status - - # ─── Evals & Policies ────────────────────────────────────────── + description: Bifrost service status - /api/evals: - get: - tags: [System] - summary: List eval suites - responses: - "200": - description: Eval suite list + /api/services/bifrost/auto-start: post: - tags: [System] - summary: Run evaluation + tags: [Embedded Services] + summary: Toggle Bifrost auto-start + description: >- + When enabled, Bifrost starts automatically on the next OmniRoute boot. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: Eval results - - /api/evals/{suiteId}: - get: - tags: [System] - summary: Get eval suite details - parameters: - - name: suiteId - in: path - required: true - schema: - type: string - responses: - "200": - description: Eval suite details + "204": + description: Auto-start flag updated + "400": + description: Invalid request body - /api/policies: - get: - tags: [System] - summary: List routing policies - responses: - "200": - description: Policy list + /api/services/bifrost/auto-restart-adopted: post: - tags: [System] - summary: Create routing policy + tags: [Embedded Services] + summary: Toggle Bifrost auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) Bifrost + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "201": - description: Created policy - delete: - tags: [System] - summary: Delete routing policy - responses: - "200": - description: Policy deleted + "204": + description: Flag updated + "400": + description: Invalid request body + "500": + description: Update failed - /api/compliance/audit-log: - get: - tags: [System] - summary: Get compliance audit log - description: > - Returns paginated audit log entries. Use `level=high` to filter to - high-level actions only (powers the Activity feed). Use `level=all` - (default) for full compliance table. - security: - - bearerAuth: [] - parameters: - - name: level - in: query - schema: - type: string - enum: [high, all] - default: all - description: "high = Activity feed events only; all = all audit events" - - name: action - in: query - schema: - type: string - description: Filter by exact action string (e.g. "provider.added") - - name: actor - in: query - schema: - type: string - description: Filter by actor identifier - - name: limit - in: query - schema: - type: integer - default: 50 - maximum: 500 - - name: offset - in: query - schema: - type: integer - default: 0 + /api/services/dario/install: + post: + tags: [Embedded Services] + summary: Install Dario from npm + description: >- + Installs the `@askalf/dario` npm package (Claude-account-pool proxy) under + DATA_DIR/services/dario/. Uses execFile (no shell interpolation — hard rule + #13). **LOCAL_ONLY** — loopback only. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + version: + type: string + default: latest + description: npm version tag or semver to install responses: "200": - description: Audit log entries - "401": - description: Unauthorized + description: Install succeeded + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + path: + type: string + "400": + description: Invalid request body "500": - description: Internal server error + description: npm install failed - # ─── Quota Sharing (Group B, plan 22) ──────────────────────────── + /api/services/dario/start: + post: + tags: [Embedded Services] + summary: Start Dario + description: >- + Spawns the Dario process. Idempotent if already running. + **LOCAL_ONLY** — loopback only. + responses: + "200": + description: Service started (or already running) + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceStatus" + "409": + description: Dario is not installed + "503": + description: Start failed - /api/quota/pools: - get: - tags: [Quota] - summary: List quota pools - security: - - bearerAuth: [] + /api/services/dario/stop: + post: + tags: [Embedded Services] + summary: Stop Dario + description: >- + Gracefully stops Dario. Idempotent — returns a stopped status even if no + supervisor is currently tracking the process. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Array of QuotaPool objects + description: Service stopped content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/QuotaPool" - "401": - description: Unauthorized + $ref: "#/components/schemas/ServiceStatus" "500": - description: Internal server error + description: Stop failed + + /api/services/dario/restart: post: - tags: [Quota] - summary: Create quota pool - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/PoolCreate" + tags: [Embedded Services] + summary: Restart Dario + description: >- + Equivalent to stop() then start() under the operation lock. + **LOCAL_ONLY** — loopback only. responses: - "201": - description: Pool created + "200": + description: Service restarted content: application/json: schema: - $ref: "#/components/schemas/QuotaPool" - "400": - description: Validation error (Zod) - "401": - description: Unauthorized + $ref: "#/components/schemas/ServiceStatus" + "409": + description: Dario is not installed + "503": + description: Restart failed + + /api/services/dario/update: + post: + tags: [Embedded Services] + summary: Update Dario to a newer npm version + description: >- + Stops the service (if running), installs the newer npm version, then + restarts it if it was running before the update. **LOCAL_ONLY** — loopback + only. + responses: + "200": + description: Update result (no-op if already on the latest version) + content: + application/json: + schema: + type: object + properties: + updated: + type: boolean + installedVersion: + type: string + latestVersion: + type: string + oldVersion: + type: string + nullable: true + newVersion: + type: string "500": - description: Internal server error + description: Update failed - /api/quota/pools/{id}: + /api/services/dario/status: get: - tags: [Quota] - summary: Get quota pool by ID - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string + tags: [Embedded Services] + summary: Get Dario status + description: >- + Returns combined live supervisor state and DB metadata, including the + auto-start / auto-restart-adopted flags and whether an update is available. + **LOCAL_ONLY** — loopback only. responses: "200": - description: QuotaPool object + description: Status response content: application/json: schema: - $ref: "#/components/schemas/QuotaPool" - "401": - description: Unauthorized - "404": - description: Pool not found + $ref: "#/components/schemas/ServiceStatusExtended" "500": - description: Internal server error - patch: - tags: [Quota] - summary: Update quota pool (name or allocations) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string + description: Status read failed + + /api/services/dario/auto-start: + post: + tags: [Embedded Services] + summary: Toggle Dario auto-start + description: >- + When enabled, Dario starts automatically on the next OmniRoute boot. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/PoolUpdate" + type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: Updated pool + "204": + description: Auto-start flag updated "400": - description: Validation error - "401": - description: Unauthorized - "404": - description: Pool not found + description: Invalid request body "500": - description: Internal server error - delete: - tags: [Quota] - summary: Delete quota pool - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string + description: Update failed + + /api/services/dario/auto-restart-adopted: + post: + tags: [Embedded Services] + summary: Toggle Dario auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) Dario process + is restarted under OmniRoute's own supervisor on the next health-check + cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [enabled] + properties: + enabled: + type: boolean responses: "204": - description: Deleted - "401": - description: Unauthorized - "404": - description: Pool not found + description: Flag updated + "400": + description: Invalid request body "500": - description: Internal server error + description: Update failed - /api/quota/pools/{id}/usage: - get: - tags: [Quota] - summary: Get pool usage snapshot (per-key consumption + burn rate) - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string + /api/services/dario/admin/login-start: + post: + tags: [Embedded Services] + summary: Start a Dario account-pool login (device-code style) + description: >- + Forwards to the running Dario instance's `POST /admin/login/start` using + the stored admin token. The operator opens the returned `authorize_url`, + approves in their own Claude account, then posts the displayed code to + `/admin/login-complete`. **LOCAL_ONLY** — loopback only. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + alias: + type: string + description: Optional account alias; Dario assigns one if omitted. responses: "200": - description: PoolUsageSnapshot + description: Login challenge created content: application/json: schema: - $ref: "#/components/schemas/PoolUsageSnapshot" + type: object + properties: + alias: + type: string + authorize_url: + type: string + expires_at: + type: string + instructions: + type: string + "400": + description: Invalid request body "401": - description: Unauthorized - "404": - description: Pool not found - "500": - description: Internal server error + description: Missing or invalid admin auth + "502": + description: Dario did not respond or Dario is not running - /api/quota/plans: - get: - tags: [Quota] - summary: List resolved provider plans (catalog + manual overrides) - security: - - bearerAuth: [] + /api/services/dario/admin/login-complete: + post: + tags: [Embedded Services] + summary: Complete a Dario account-pool login + description: >- + Forwards to the running Dario instance's `POST /admin/login/complete`. + On success the account becomes routable immediately (Dario hot-reloads + its pool). **LOCAL_ONLY** — loopback only. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [alias, code] + properties: + alias: + type: string + code: + type: string responses: "200": - description: Array of ProviderPlan + description: Account added + content: + application/json: + schema: + type: object + properties: + alias: + type: string + status: + type: string + expires_at: + type: string + "400": + description: Invalid request body "401": - description: Unauthorized - "500": - description: Internal server error + description: Missing or invalid admin auth + "502": + description: Dario did not respond or Dario is not running - /api/quota/plans/{connectionId}: + /api/services/dario/admin/accounts: get: - tags: [Quota] - summary: Get resolved plan for a connection - security: - - bearerAuth: [] - parameters: - - name: connectionId - in: path - required: true - schema: - type: string + tags: [Embedded Services] + summary: List Dario account-pool accounts + description: >- + Forwards to the running Dario instance's `GET /admin/accounts`. + **LOCAL_ONLY** — loopback only. responses: "200": - description: ProviderPlan (source = auto | manual) + description: Account list + content: + application/json: + schema: + type: object + properties: + accounts: + type: array + items: + type: object + count: + type: integer "401": - description: Unauthorized - "404": - description: Connection not found - "500": - description: Internal server error - put: - tags: [Quota] - summary: Upsert manual plan override for a connection - security: - - bearerAuth: [] + description: Missing or invalid admin auth + "502": + description: Dario did not respond or Dario is not running + delete: + tags: [Embedded Services] + summary: Remove a Dario account-pool account + description: >- + Forwards to the running Dario instance's `DELETE /admin/accounts/`. + The alias is taken from a `?alias=` query param or a `{ alias }` JSON body. + **LOCAL_ONLY** — loopback only. parameters: - - name: connectionId - in: path - required: true + - name: alias + in: query + required: false schema: type: string requestBody: - required: true + required: false content: application/json: schema: - $ref: "#/components/schemas/PlanUpsert" + type: object + properties: + alias: + type: string responses: "200": - description: Updated plan + description: Account removed + content: + application/json: + schema: + type: object + properties: + alias: + type: string + removed: + type: boolean "400": - description: Validation error (Zod) - "401": - description: Unauthorized - "500": - description: Internal server error - delete: - tags: [Quota] - summary: Delete manual plan override (reverts to catalog/auto) - security: - - bearerAuth: [] - parameters: - - name: connectionId - in: path - required: true - schema: - type: string - responses: - "204": - description: Override deleted + description: Missing alias "401": - description: Unauthorized - "404": - description: Override not found - "500": - description: Internal server error + description: Missing or invalid admin auth + "502": + description: Dario did not respond or Dario is not running - /api/quota/preview: + /api/services/dario/admin/import-from-omniroute: get: - tags: [Quota] - summary: Dry-run quota enforcement check (preview only, no consumption recorded) - security: - - bearerAuth: [] - parameters: - - name: apiKeyId - in: query - required: true - schema: - type: string - - name: poolId - in: query - required: true - schema: - type: string - - name: estimatedTokens - in: query - schema: - type: number - - name: estimatedUsd - in: query - schema: - type: number - - name: estimatedRequests - in: query - schema: - type: integer + tags: [Embedded Services] + summary: List OmniRoute claude connections eligible for Dario import + description: >- + Returns eligible OmniRoute `claude` OAuth provider connections (metadata + only — id/name/email/org tier, never tokens) so the UI can offer a picker + when more than one exists. **LOCAL_ONLY** — loopback only. responses: "200": - description: EnforceDecision (allow/block + reason) - "400": - description: Validation error (Zod) + description: Eligible connections + content: + application/json: + schema: + type: object + properties: + connections: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + email: + type: string + nullable: true + organizationType: + type: string + nullable: true + organizationRateLimitTier: + type: string + nullable: true "401": - description: Unauthorized - "500": - description: Internal server error - - /api/settings/quota-store: - get: - tags: [Settings] - summary: Get current quota store driver settings - description: Redis URL is masked in the response (shows only scheme+host). - security: - - bearerAuth: [] - responses: - "200": - description: QuotaStoreSettings (driver + masked redisUrl) - "401": - description: Unauthorized - "500": - description: Internal server error - put: - tags: [Settings] - summary: Update quota store driver settings - security: - - bearerAuth: [] + description: Missing or invalid admin auth + post: + tags: [Embedded Services] + summary: Import an OmniRoute claude connection's OAuth tokens into Dario + description: >- + Writes the source connection's access/refresh token pair directly into + Dario's own account-file store (`~/.dario/accounts/.json`), reusing + the shared Claude Code OAuth client_id, then restarts the Dario supervisor + so it picks up the new account. **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/QuotaStoreSettings" + type: object + required: [connectionId] + properties: + connectionId: + type: string + alias: + type: string + description: Optional custom alias; derived from the source email if omitted. responses: "200": - description: Settings updated + description: Account imported + content: + application/json: + schema: + type: object + properties: + alias: + type: string + imported: + type: boolean + sourceConnectionId: + type: string + sourceEmail: + type: string + nullable: true "400": - description: Validation error (Zod) — e.g. driver=redis without valid URL + description: Invalid request body, unsupported connection, or missing tokens "401": - description: Unauthorized + description: Missing or invalid admin auth + "404": + description: Connection not found "500": - description: Internal server error - - # ─── v1beta (Gemini-Compatible) ───────────────────────────────── - - /api/v1beta/models: - get: - tags: [Models] - summary: List models (Gemini format) - description: Returns models in Gemini v1beta format for native SDK compatibility - security: - - BearerAuth: [] - responses: - "200": - description: Model list in Gemini format + description: Import failed - /api/v1beta/models/{path}: + /api/services/llmlingua/install: post: - tags: [Models] - summary: Gemini generateContent - description: Gemini-compatible generateContent endpoint - security: - - BearerAuth: [] - parameters: - - name: path - in: path - required: true - schema: - type: string + tags: [Embedded Services] + summary: Install LLMLingua + description: >- + Installs `@atjsh/llmlingua-2` (real LLMLingua-2 ONNX prompt-compression + package) plus its `@huggingface/transformers`/`js-tiktoken` peers under + DATA_DIR/services/llmlingua/, and writes the sidecar server script. + Accepts an optional `version` field (semver or `latest`). + **LOCAL_ONLY** — loopback only. requestBody: - required: true + required: false content: application/json: schema: type: object + properties: + version: + type: string + default: latest responses: "200": - description: Generated content + description: Installation result + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + installedVersion: + type: string + installPath: + type: string + durationMs: + type: number - # ─── AgentBridge ────────────────────────────────────────────── + /api/services/llmlingua/start: + post: + tags: [Embedded Services] + summary: Start LLMLingua + description: Starts the supervised LLMLingua sidecar process. **LOCAL_ONLY** — loopback only. + responses: + "200": + description: Service status after start + "409": + description: LLMLingua is not installed - /api/tools/agent-bridge/agents: - get: - tags: [AgentBridge] - summary: List all 9 IDE agents with current state + /api/services/llmlingua/stop: + post: + tags: [Embedded Services] + summary: Stop LLMLingua + description: Stops the supervised LLMLingua sidecar process. **LOCAL_ONLY** — loopback only. + responses: + "200": + description: Service status after stop + + /api/services/llmlingua/restart: + post: + tags: [Embedded Services] + summary: Restart LLMLingua + description: Restarts the supervised LLMLingua sidecar process. **LOCAL_ONLY** — loopback only. + responses: + "200": + description: Service status after restart + "409": + description: LLMLingua is not installed + + /api/services/llmlingua/update: + post: + tags: [Embedded Services] + summary: Update LLMLingua description: >- - Returns the state (dns_enabled, cert_trusted, setup_completed, last_started_at, - last_error) for all 9 configured IDE agents. LOCAL_ONLY. + Updates LLMLingua to the latest npm version. Stops the running process, + installs the new version, and restarts if it was previously running. + **LOCAL_ONLY** — loopback only. responses: "200": - description: Array of agent state rows - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/AgentBridgeAgentState" - "403": - description: Loopback-only — request came from a non-loopback address + description: Update result - /api/tools/agent-bridge/state: + /api/services/llmlingua/status: get: - tags: [AgentBridge] - summary: Get global AgentBridge server state - description: Returns running status, port, cert info, and intercepted request count. + tags: [Embedded Services] + summary: Get LLMLingua status + description: Returns live and DB status for the supervised LLMLingua service. **LOCAL_ONLY** — loopback only. responses: "200": - description: Server state - content: - application/json: - schema: - $ref: "#/components/schemas/AgentBridgeServerState" + description: LLMLingua service status - /api/tools/agent-bridge/server: + /api/services/llmlingua/auto-start: post: - tags: [AgentBridge] - summary: Control AgentBridge MITM server - description: Start, stop, restart, trust-cert, or regenerate-cert. SPAWN_CAPABLE. + tags: [Embedded Services] + summary: Toggle LLMLingua auto-start + description: >- + When enabled, LLMLingua starts automatically on the next OmniRoute boot. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/AgentBridgeServerAction" + type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: Action executed + "204": + description: Auto-start flag updated "400": - description: Invalid action - "409": - description: Port 443 conflict + description: Invalid request body - /api/tools/agent-bridge/agents/{agentId}/dns: + /api/services/llmlingua/auto-restart-adopted: post: - tags: [AgentBridge] - summary: Enable or disable DNS for one agent - description: Adds or removes /etc/hosts entries for the agent's host list. SPAWN_CAPABLE. - parameters: - - name: agentId - in: path - required: true - schema: - $ref: "#/components/schemas/AgentId" + tags: [Embedded Services] + summary: Toggle LLMLingua auto-restart-when-adopted + description: >- + When enabled, an externally-adopted (not OmniRoute-spawned) LLMLingua + process is restarted under OmniRoute's own supervisor on the next + health-check cycle instead of being left as adopted-only. + **LOCAL_ONLY** — loopback only. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/AgentBridgeDnsAction" + type: object + required: [enabled] + properties: + enabled: + type: boolean responses: - "200": - description: DNS updated + "204": + description: Flag updated "400": - description: Validation error + description: Invalid request body + "500": + description: Update failed - /api/tools/agent-bridge/agents/{agentId}/mappings: + /api/services/{name}/logs: get: - tags: [AgentBridge] - summary: Get model mappings for one agent + tags: [Embedded Services] + summary: Stream service logs via SSE + description: >- + Returns a Server-Sent Events stream from the service's in-memory ring buffer + (5 MB, circular). Sends a `snapshot` event with historical lines first, then + live `log` events, plus a `heartbeat` every 15 s. + **LOCAL_ONLY** — loopback only. parameters: - - name: agentId + - name: name in: path required: true schema: - $ref: "#/components/schemas/AgentId" + type: string + enum: [9router, cliproxy, mux, bifrost, dario, openwa, llmlingua] + - name: tail + in: query + schema: + type: integer + default: 200 + maximum: 1000 + description: Number of historical lines to include in the initial snapshot + - name: filter + in: query + schema: + type: string + maxLength: 200 + description: >- + Case-insensitive substring filter applied to log lines. + No regex — ReDoS-safe by design. responses: "200": - description: Array of source→target model mappings + description: SSE log stream content: - application/json: + text/event-stream: schema: - type: array - items: - $ref: "#/components/schemas/AgentBridgeMappingRow" - put: - tags: [AgentBridge] - summary: Update model mappings for one agent + type: string + description: >- + Events: `snapshot` (LogLine[]), `log` (LogLine), `heartbeat` ({}) + "400": + description: filter parameter exceeds maximum length + "404": + description: Service not found + + # ─── OAuth ───────────────────────────────────────────────────── + + /api/oauth/{provider}/{action}: + get: + tags: [OAuth] + summary: OAuth flow handler + description: Handles OAuth authorization and callback for providers parameters: - - name: agentId + - name: provider in: path required: true schema: - $ref: "#/components/schemas/AgentId" - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/AgentBridgeMappingPut" + type: string + - name: action + in: path + required: true + schema: + type: string + enum: [authorize, callback, refresh, status] responses: "200": - description: Mappings updated + description: OAuth flow response + "302": + description: Redirect to provider auth page - /api/tools/agent-bridge/bypass: + /api/oauth/cursor/auto-import: get: - tags: [AgentBridge] - summary: List bypass patterns (hosts never decrypted) + tags: [OAuth] + summary: Auto-import Cursor OAuth credentials + description: Automatically detects and imports Cursor credentials from local config. responses: "200": - description: Bypass patterns - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/AgentBridgeBypassRow" - put: - tags: [AgentBridge] - summary: Update user bypass patterns + description: Import result + + /api/oauth/cursor/import: + get: + tags: [OAuth] + summary: Get Cursor import status + responses: + "200": + description: Current import status + post: + tags: [OAuth] + summary: Import Cursor OAuth credentials requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/AgentBridgeBypassUpsert" + type: object responses: "200": - description: Patterns updated + description: Credentials imported - /api/tools/agent-bridge/cert: + /api/oauth/kiro/auto-import: + get: + tags: [OAuth] + summary: Auto-import Kiro OAuth credentials + description: Automatically detects and imports Kiro credentials from local config. + responses: + "200": + description: Import result + + /api/oauth/kiro/import: + get: + tags: [OAuth] + summary: Get Kiro import status + responses: + "200": + description: Current import status post: - tags: [AgentBridge] - summary: Download or regenerate the AgentBridge CA certificate + tags: [OAuth] + summary: Import Kiro OAuth credentials requestBody: required: true content: application/json: schema: type: object - required: [action] - properties: - action: - type: string - enum: [download, regenerate] responses: "200": - description: CA certificate PEM (download) or regeneration confirmation + description: Credentials imported - /api/tools/agent-bridge/upstream-ca: + /api/oauth/kiro/social-authorize: get: - tags: [AgentBridge] - summary: Get configured upstream CA cert path + tags: [OAuth] + summary: Initiate Kiro social OAuth authorization + description: Starts the social OAuth flow for Kiro. responses: - "200": - description: Upstream CA configuration - content: - application/json: - schema: - type: object - properties: - path: - type: string - nullable: true + "302": + description: Redirect to OAuth provider + + /api/oauth/kiro/social-exchange: post: - tags: [AgentBridge] - summary: Set upstream CA cert path for corporate TLS environments + tags: [OAuth] + summary: Exchange Kiro social OAuth token + description: Exchanges the authorization code for access tokens. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/AgentBridgeUpstreamCaPost" + type: object responses: "200": - description: Upstream CA configured - "400": - description: Path does not exist or is not readable + description: Token exchange result - # ─── Traffic Inspector ───────────────────────────────────────── + # ─── Cloud ───────────────────────────────────────────────────── - /api/tools/traffic-inspector/requests: - get: - tags: [Traffic Inspector] - summary: List intercepted requests (filterable) - parameters: - - name: profile - in: query - schema: - type: string - enum: [llm, custom, all] - - name: host - in: query - schema: - type: string - - name: agent - in: query - schema: - $ref: "#/components/schemas/AgentId" - - name: status - in: query - schema: - type: string - enum: ["2xx", "3xx", "4xx", "5xx", error] - - name: source - in: query - schema: - $ref: "#/components/schemas/CaptureSource" - - name: sessionId - in: query - schema: - type: string - format: uuid + /api/cloud/auth: + post: + tags: [Cloud] + summary: Authenticate with cloud worker + description: Authenticates with the OmniRoute cloud worker for remote access. + requestBody: + required: true + content: + application/json: + schema: + type: object responses: "200": - description: Array of intercepted requests - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/InterceptedRequest" - delete: - tags: [Traffic Inspector] - summary: Clear the in-memory traffic buffer - responses: - "204": - description: Buffer cleared + description: Authentication result - /api/tools/traffic-inspector/requests/{id}: - get: - tags: [Traffic Inspector] - summary: Get a single intercepted request by ID - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + /api/cloud/credentials/update: + put: + tags: [Cloud] + summary: Update cloud worker credentials + requestBody: + required: true + content: + application/json: + schema: + type: object responses: "200": - description: Intercepted request details - content: - application/json: - schema: - $ref: "#/components/schemas/InterceptedRequest" - "404": - description: Request not found in buffer + description: Credentials updated - /api/tools/traffic-inspector/requests/{id}/replay: + /api/cloud/model/resolve: post: - tags: [Traffic Inspector] - summary: Replay a captured request through OmniRoute router - description: Re-executes the original request body against /v1/chat/completions. Consumes quota. - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + tags: [Cloud] + summary: Resolve model via cloud + description: Resolves a model request through the cloud worker. + requestBody: + required: true + content: + application/json: + schema: + type: object responses: "200": - description: Replay response (streaming or JSON) - "404": - description: Request not found + description: Resolved model info - /api/tools/traffic-inspector/requests/{id}/annotation: + /api/cloud/models/alias: + get: + tags: [Cloud] + summary: Get cloud model aliases + responses: + "200": + description: Cloud model alias list put: - tags: [Traffic Inspector] - summary: Save or update annotation on a request - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + tags: [Cloud] + summary: Update cloud model alias requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/InspectorAnnotationPut" + type: object responses: "200": - description: Annotation saved + description: Alias updated - /api/tools/traffic-inspector/ws: - get: - tags: [Traffic Inspector] - summary: Live WebSocket stream of intercepted requests - description: >- - Upgrade to WebSocket. On connect, server sends `{type:"snapshot", data:[...]}`. - Subsequent events: `{type:"new", data:{...}}`, `{type:"update", data:{...}}`, - `{type:"clear"}`. LOCAL_ONLY. - responses: - "101": - description: WebSocket upgrade successful - "403": - description: Non-loopback origin rejected + # ─── Fallback ────────────────────────────────────────────────── - /api/tools/traffic-inspector/export.har: + /api/fallback/chains: get: - tags: [Traffic Inspector] - summary: Export current filtered request list as HAR 1.2 - parameters: - - name: profile - in: query - schema: - type: string - enum: [llm, custom, all] - - name: sessionId - in: query - schema: - type: string - format: uuid - responses: - "200": - description: HAR file (JSON) - content: - application/json: - schema: - type: object - description: HAR 1.2 format - - /api/tools/traffic-inspector/hosts: - get: - tags: [Traffic Inspector] - summary: List custom capture hosts + tags: [Fallback] + summary: List fallback chains + description: Returns all registered fallback chains for model routing. responses: "200": - description: Custom hosts list - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/InspectorCustomHost" + description: Fallback chain list post: - tags: [Traffic Inspector] - summary: Add a custom capture host (edits /etc/hosts) + tags: [Fallback] + summary: Create fallback chain + description: Registers a fallback routing chain for a model. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/InspectorCustomHostCreate" + type: object + required: [model, chain] + properties: + model: + type: string + chain: + type: array + items: + type: object + properties: + provider: + type: string + priority: + type: integer + enabled: + type: boolean responses: - "201": - description: Host added - "409": - description: Host already exists - - /api/tools/traffic-inspector/hosts/{host}: + "200": + description: Fallback chain created delete: - tags: [Traffic Inspector] - summary: Remove a custom capture host - parameters: - - name: host - in: path - required: true - schema: - type: string - responses: - "204": - description: Host removed - patch: - tags: [Traffic Inspector] - summary: Toggle enabled state of a custom host - parameters: - - name: host - in: path - required: true - schema: - type: string + tags: [Fallback] + summary: Delete fallback chain requestBody: required: true content: application/json: schema: type: object - required: [enabled] + required: [model] properties: - enabled: - type: boolean + model: + type: string responses: "200": - description: Host updated + description: Fallback chain deleted - /api/tools/traffic-inspector/capture-modes: - get: - tags: [Traffic Inspector] - summary: Get state of all 4 capture modes - responses: - "200": - description: Capture modes state - content: - application/json: - schema: - $ref: "#/components/schemas/InspectorCaptureModesState" + # ─── System ──────────────────────────────────────────────────── - /api/tools/traffic-inspector/capture-modes/http-proxy: + /api/auth/login: post: - tags: [Traffic Inspector] - summary: Start or stop the HTTP_PROXY listener (port 8080) - description: SPAWN_CAPABLE — spawns a net.Server listener. + tags: [System] + summary: Authenticate user requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/InspectorCaptureModeAction" + type: object + required: [password] + properties: + password: + type: string + minLength: 1 responses: "200": - description: Action executed - "409": - description: Port conflict (EADDRINUSE) when starting + description: JWT token returned + "400": + description: Invalid login request + "401": + description: Invalid password + "403": + description: Password setup required + "429": + description: Too many failed attempts - /api/tools/traffic-inspector/capture-modes/system-proxy: + /api/auth/logout: post: - tags: [Traffic Inspector] - summary: Apply or revert system-wide proxy settings - description: SPAWN_CAPABLE — executes networksetup/gsettings/netsh. Requires admin. - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/InspectorSystemProxyAction" + tags: [System] + summary: Log out responses: "200": - description: System proxy updated - "500": - description: OS command failed (permission error) + description: Session cleared - /api/tools/traffic-inspector/sessions: + /api/auth/oidc/login: get: - tags: [Traffic Inspector] - summary: List all saved recording sessions - responses: - "200": - description: Sessions list - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/InspectorSession" - post: - tags: [Traffic Inspector] - summary: Start a new recording session - requestBody: - required: false - content: - application/json: - schema: - $ref: "#/components/schemas/InspectorSessionStart" + tags: [System] + summary: Start OIDC login for the dashboard admin gate + description: | + Builds an authorization URL from the configured OIDC issuer/client (discovered + via `{issuer}/.well-known/openid-configuration`, falling back to `{issuer}/authorize`), + sets a short-lived `oidc_state` cookie, and redirects the browser. Password login + remains available as a fallback while OIDC is enabled. responses: - "201": - description: Session started - content: - application/json: - schema: - $ref: "#/components/schemas/InspectorSession" + "302": + description: Redirect to the identity provider's authorization endpoint + "400": + description: OIDC is not configured (missing issuer, client id/secret, or disabled) - /api/tools/traffic-inspector/sessions/{id}: + /api/auth/oidc/callback: get: - tags: [Traffic Inspector] - summary: Get session snapshot (all captured requests) - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - responses: - "200": - description: Session with embedded requests - "404": - description: Session not found - patch: - tags: [Traffic Inspector] - summary: Stop or rename a recording session + tags: [System] + summary: Complete OIDC login for the dashboard admin gate + description: | + Validates the `state` cookie, exchanges the authorization `code` for tokens, + verifies the ID token against the issuer's JWKS (audience = client id), and + requires at least one dashboard admission policy: `oidcAllowedSubjects` + (matching `sub` or verified `email`) or `oidcAdmissionClaim` (matching a + configured array claim in the verified ID token). When both are present, + both must match. Without either policy the callback returns `not_configured`. + On success it mints the same 30-day `auth_token` dashboard-session JWT + used by password login and redirects to `/dashboard`. parameters: - - name: id - in: path + - name: code + in: query required: true schema: type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/InspectorSessionPatch" - responses: - "200": - description: Session updated - delete: - tags: [Traffic Inspector] - summary: Delete a recording session - parameters: - - name: id - in: path + - name: state + in: query required: true schema: type: string - format: uuid responses: - "204": - description: Session deleted + "302": + description: >- + Redirect to `/dashboard` on success, or to + `/login?oidc_error={missing_code|invalid_state|not_configured|token_exchange|token_response|no_id_token|subject_not_allowed|role_not_allowed|id_token_invalid|server_misconfigured}` + on failure. - /api/tools/traffic-inspector/sessions/{id}/export.har: + /api/init: get: - tags: [Traffic Inspector] - summary: Export a recorded session as HAR 1.2 - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + tags: [System] + summary: Initialize application responses: "200": - description: HAR file for this session - content: - application/json: - schema: - type: object - description: HAR 1.2 format - "404": - description: Session not found + description: Init status - /api/tools/traffic-inspector/internal/ingest: + /api/restart: post: - tags: [Traffic Inspector] - summary: Internal ingest endpoint for server.cjs passthrough path - description: >- - Accepts a serialized InterceptedRequest from the CJS MITM server for requests - that do not go through TypeScript handlers (e.g., passthrough hosts). Requires - INSPECTOR_INTERNAL_INGEST_TOKEN header. LOCAL_ONLY. - security: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/InterceptedRequest" + tags: [System] + summary: Restart the application responses: - "204": - description: Ingested - "401": - description: Invalid or missing ingest token + "200": + description: Restart initiated - # ─── OpenAPI Spec ────────────────────────────────────────────── + /api/shutdown: + post: + tags: [System] + summary: Shutdown the application + x-always-protected: true + responses: + "200": + description: Shutdown initiated - /api/openapi/spec: + /api/db-backups: get: tags: [System] - summary: Get OpenAPI specification catalog + summary: List database backups + responses: + "200": + description: Backup list + post: + tags: [System] + summary: Restore a database backup by id description: >- - Returns a structured JSON catalog parsed from this `openapi.yaml`, - including info, servers, tags, schemas, and a flat list of endpoints - (method, path, tags, summary, security, parameters, responses). - Used by the in-app API explorer. + Replaces the live database with the stored backup named in `backupId`. Operator-only: + dashboard session, local CLI, or login disabled. responses: "200": - description: Parsed OpenAPI catalog - content: - application/json: - schema: - type: object - properties: - info: - type: object - servers: - type: array - items: - type: object - tags: - type: array - items: - type: object - endpoints: - type: array - items: - type: object - properties: - method: - type: string - path: - type: string - tags: - type: array - items: - type: string - summary: - type: string - description: - type: string - security: - type: boolean - parameters: - type: array - items: - type: object - requestBody: - type: boolean - responses: - type: array - items: - type: string - schemas: - type: array - items: - type: string - "404": - description: openapi.yaml file not found on disk - "500": - description: Failed to parse OpenAPI spec + description: Backup restored + "403": + description: >- + Machine credential refused. Restoring replaces the live database, so it requires + the dashboard session or the local CLI (or login disabled); API keys of any scope + and `oma_` access tokens get 403. + patch: + tags: [System] + summary: Save database backup retention settings + description: >- + Persists `keepLatest` / `retentionDays`. Operator-only: dashboard session, local CLI, or + login disabled — the persisted values drive the automatic pruning after every backup. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + keepLatest: + type: integer + minimum: 1 + maximum: 200 + retentionDays: + type: integer + minimum: 0 + maximum: 3650 + responses: + "200": + description: Backup retention settings saved + "403": + description: >- + Machine credential refused. The saved retention is what automatic pruning applies + after every backup, so it requires the dashboard session or the local CLI (or login + disabled); API keys of any scope and `oma_` access tokens get 403. - # ─── Agent Skills Catalog ──────────────────────────────────────────────────── + /api/storage/health: + get: + tags: [System] + summary: Check storage health + responses: + "200": + description: Storage health status - /api/agent-skills: + /api/sync/cloud: + post: + tags: [System] + summary: Sync with cloud + responses: + "200": + description: Sync result + + /api/sync/initialize: + post: + tags: [System] + summary: Initialize cloud sync + responses: + "200": + description: Sync initialized + + # ─── Background Jobs (local-only administration) ─────────────── + + /api/jobs: get: - tags: [Agent Skills] - summary: List agent skills catalog - description: | - Returns the full 42-entry Agent Skills catalog with optional filtering. - Skills describe how to use OmniRoute's REST API and CLI — they are structured - SKILL.md documentation files discoverable by external agents, MCP clients, and - A2A orchestrators. No authentication required. + tags: [System] + summary: List registered background jobs + description: Local-only runtime administration. Returns each registered job and its latest run. + x-internal: true + responses: + "200": + description: Registered jobs + "500": + description: Failed to list jobs + + /api/jobs/{id}/enable: + post: + tags: [System] + summary: Enable a background job + description: Local-only runtime administration. Enables the job and restarts its timer. + x-internal: true parameters: - - name: category - in: query - required: false + - name: id + in: path + required: true schema: type: string - enum: [api, cli] - description: Filter by category (api = REST API skills, cli = CLI skills) - - name: area - in: query - required: false + responses: + "200": + description: Job enabled + "404": + description: Job not found + "500": + description: Failed to enable job + + /api/jobs/{id}/disable: + post: + tags: [System] + summary: Disable a background job + description: Local-only runtime administration. Disables the job and stops its timer. + x-internal: true + parameters: + - name: id + in: path + required: true schema: type: string - description: Filter by area slug (e.g. "providers", "models", "cli-serve") responses: "200": - description: Catalog list - content: - application/json: - schema: - type: object - required: [skills, count, coverage] - properties: - skills: - type: array - items: - $ref: "#/components/schemas/AgentSkill" - count: - type: integer - coverage: - $ref: "#/components/schemas/SkillCoverage" - "400": - $ref: "#/components/responses/BadRequest" + description: Job disabled + "404": + description: Job not found "500": - $ref: "#/components/responses/InternalError" + description: Failed to disable job - /api/agent-skills/{id}: - get: - tags: [Agent Skills] - summary: Get a single agent skill - description: | - Returns metadata for a single agent skill by its canonical ID - (e.g. `omni-providers`, `cli-serve`). No authentication required. + /api/jobs/{id}/run-now: + post: + tags: [System] + summary: Trigger a background job + description: >- + Local-only runtime administration. Starts the job, or waits for an in-flight + run before queueing the next one, subject to OMNIROUTE_RUNNOW_TIMEOUT_MS. + x-internal: true parameters: - name: id in: path required: true schema: type: string - pattern: "^[a-z][a-z0-9-]*$" - description: Canonical skill ID - example: omni-providers responses: "200": - description: Agent skill metadata - content: - application/json: - schema: - $ref: "#/components/schemas/AgentSkill" - "400": - $ref: "#/components/responses/BadRequest" + description: Job trigger accepted "404": - $ref: "#/components/responses/NotFound" + description: Job not found "500": - $ref: "#/components/responses/InternalError" + description: Failed to trigger job - /api/agent-skills/{id}/raw: + /api/jobs/{id}/runs: get: - tags: [Agent Skills] - summary: Get raw SKILL.md content - description: | - Returns the SKILL.md content for a skill as `text/markdown`. - Resolution order: local filesystem `skills/{id}/SKILL.md` → GitHub raw URL (1-hour cache). - No authentication required. + tags: [System] + summary: Read background-job run history + description: Local-only runtime administration. Returns newest-first run history for one job. + x-internal: true parameters: - name: id in: path required: true schema: type: string - pattern: "^[a-z][a-z0-9-]*$" - description: Canonical skill ID - example: omni-providers responses: "200": - description: SKILL.md content as Markdown - headers: - X-Skill-Source: - schema: - type: string - enum: [filesystem, github, generated] - description: Where the content was loaded from - X-Skill-Fetched-At: - schema: - type: string - format: date-time - description: ISO timestamp of when the content was fetched - Cache-Control: - schema: - type: string - description: "public, max-age=3600" - content: - text/markdown: - schema: - type: string - "400": - $ref: "#/components/responses/BadRequest" + description: Job run history "404": - $ref: "#/components/responses/NotFound" - "502": - description: Upstream GitHub fetch failed - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" + description: Job not found "500": - $ref: "#/components/responses/InternalError" + description: Failed to load job runs - /api/agent-skills/coverage: + # ─── Log export ───────────────────────────────────────────────── + + /api/log-export/types: get: - tags: [Agent Skills] - summary: Get SKILL.md coverage stats - description: | - Returns how many of the 22 API skills and 20 CLI skills have SKILL.md - files on the local filesystem vs the catalog totals. No authentication required. + tags: [System] + summary: List log-export destination types + description: >- + Returns every registered destination type with the config field descriptors the + dashboard renders its form from. Adding a destination type needs no UI change. + responses: + "200": + description: Destination types and their config fields + "500": + description: Failed to list destination types + + /api/log-export/destinations: + get: + tags: [System] + summary: List log-export destinations + description: Secrets are redacted; a stored secret is returned as the literal `__stored__`. + responses: + "200": + description: Configured destinations with cursor, backlog, and last-run state + "500": + description: Failed to list destinations + post: + tags: [System] + summary: Create a log-export destination + description: >- + The config object is validated against the destination type's own schema. Secret + keys declared by the type are encrypted at rest and never echoed back. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, type, config] + properties: + name: + type: string + type: + type: string + enabled: + type: boolean + config: + type: object + additionalProperties: true + batchSize: + type: integer + maxRowsPerRun: + type: integer + responses: + "201": + description: Destination created + "400": + description: Unknown destination type or invalid configuration + "500": + description: Failed to create destination + + /api/log-export/destinations/{id}: + get: + tags: [System] + summary: Read one log-export destination + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Destination with secrets redacted + "404": + description: Destination not found + "500": + description: Failed to read destination + put: + tags: [System] + summary: Update a log-export destination + description: >- + Sending `__stored__` for a secret keeps the stored credential. The destination type + is immutable after creation. + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + enabled: + type: boolean + config: + type: object + additionalProperties: true + batchSize: + type: integer + maxRowsPerRun: + type: integer + responses: + "200": + description: Destination updated + "400": + description: Invalid configuration + "404": + description: Destination not found + "500": + description: Failed to update destination + delete: + tags: [System] + summary: Delete a log-export destination + description: Removes the destination and its export cursor. + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Destination deleted + "404": + description: Destination not found + "500": + description: Failed to delete destination + + /api/log-export/destinations/{id}/test: + post: + tags: [System] + summary: Test a log-export destination + description: >- + Probes credentials and reachability without writing rows. A failed probe is reported + as 200 with `ok: false` and an operator-facing reason. + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Probe result + "404": + description: Destination not found + + /api/log-export/destinations/{id}/run: + post: + tags: [System] + summary: Run a log-export destination now + description: >- + Drains pending call logs into this destination using the same cursor semantics as the + hourly job. + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Run result with exported count, batches, and remaining backlog + "404": + description: Destination not found + "500": + description: Failed to run the export + + /api/log-export/status: + get: + tags: [System] + summary: Log-export scheduler status + description: Cron expression, recent job runs, and the per-destination backlog. + responses: + "200": + description: Scheduler state and destination backlog + "500": + description: Failed to read status + + # ─── Resilience & Monitoring ──────────────────────────────────── + + /api/resilience: + get: + tags: [System] + summary: Get resilience configuration + responses: + "200": + description: Request queue, connection cooldown, provider breaker, and wait settings + patch: + tags: [System] + summary: Update resilience configuration + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Updated resilience configuration + + /api/resilience/connections: + get: + tags: [System] + summary: Inspect connection resilience state + description: >- + Local-only operational view of per-connection cooldowns, provider circuit + breakers, model lockouts, and recent breaker transitions. Credential columns + are excluded by an explicit database whitelist. + x-internal: true + parameters: + - name: windowMs + in: query + schema: + type: integer + minimum: 0 + maximum: 86400000 + default: 3600000 + - name: provider + in: query + schema: + type: string + minLength: 1 + maxLength: 64 + responses: + "200": + description: Connection, breaker, lockout, window, and degradation metadata + "400": + description: Invalid query parameters + "500": + description: Failed to collect resilience state + + /api/telegram/update: + post: + tags: [System] + summary: Receive Telegram updates or Mini App messages + description: >- + Public Telegram integration endpoint. Bot updates are acknowledged after + reply dispatch is queued. Mini App requests must include Telegram-signed + initData, which is verified with TELEGRAM_BOT_TOKEN before chat proxying. + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + properties: + initData: + type: string + message: + type: string + update_id: + type: integer + responses: + "200": + description: Update acknowledged or Mini App reply returned + "400": + description: Invalid JSON, request shape, or missing Mini App message + "401": + description: Invalid Mini App initData signature + "503": + description: Telegram integration is not configured + + /api/resilience/reset: + post: + tags: [System] + summary: Reset circuit breakers + responses: + "200": + description: Circuit breakers reset + + /api/health: + get: + tags: [System] + summary: Liveness probe + description: >- + Unauthenticated liveness probe. Returns `{ status, timestamp }` and nothing + else — version, uptime and memory stay behind the authenticated + `GET /api/monitoring/health`. Sent with + `Cache-Control: no-store, no-cache, must-revalidate`. + security: [] + responses: + "200": + description: Service is alive. + content: + application/json: + schema: + type: object + required: [status, timestamp] + properties: + status: + type: string + enum: [ok] + timestamp: + type: string + format: date-time + + /api/monitoring/health: + get: + tags: [System] + summary: System health check + description: Returns system health including uptime, memory, circuit breakers, rate limits + responses: + "200": + description: Health status + + /api/monitoring/compression: + get: + tags: [System] + summary: Get compression result-memo statistics + description: >- + In-process compression result-memo observability snapshot — size, capacity, + lifetime hits/misses/hitRate plus 1m/5m/15m/1h windowed rates. Lightweight + (no DB, no provider reads) companion to `GET /api/monitoring/health` intended + for frequent polling. Sent with `Cache-Control: no-store, no-cache, + must-revalidate`. Counters reset on process restart. + responses: + "200": + description: Compression memo stats (`compression.memo` + `timestamp`) + "503": + description: Compression stats unavailable + + /api/rate-limits: + get: + tags: [System] + summary: Get per-account rate limit status + responses: + "200": + description: Rate limit status by account + post: + tags: [System] + summary: Toggle rate limit protection for a connection + description: Supported write path to toggle rateLimitProtection for a connection (PUT /api/providers/[id] deliberately does not modify this field). Setting enabled to false disables explicit protection, but does not opt an active API-key connection out of the global auto-enable safety net. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [connectionId, enabled] + properties: + connectionId: + type: string + description: Provider connection ID + enabled: + type: boolean + description: Whether explicit rate limit protection is enabled + responses: + "200": + description: Rate limit protection updated + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + connectionId: + type: string + enabled: + type: boolean + "400": + description: Invalid request body + "500": + description: Failed to toggle rate limit + + /api/sessions: + get: + tags: [System] + summary: Get active sessions + responses: + "200": + description: Active session list + + /api/cache: + get: + tags: [System] + summary: Get cache statistics + responses: + "200": + description: Semantic cache and idempotency stats + delete: + tags: [System] + summary: Clear all caches + responses: + "200": + description: Caches cleared + + /api/modality-bridge/stats: + get: + tags: [System] + summary: Get Modality Bridge telemetry + description: In-memory per-modality bridge counters (attempts, successes, bridged, cacheHits, failures, totalLatencyMs, latencySamples, averageLatencyMs, lastUsedAt). The bridged field is the backward-compatible success count. Latency averages include sampled operations only; an unsampled Vision or Audio operation does not fabricate a zero-millisecond sample. Counters reset on process restart. + security: + - ManagementSessionAuth: [] + responses: + "200": + description: Per-modality bridge stats (vision, audio, video) + "401": + description: Unauthorized + + /api/modality-bridge/video/runtime: + get: + x-loopback-only: true + tags: [System] + summary: Get Video Bridge runtime status + description: Requires trusted loopback locality before authentication or probing, then management authentication. Returns sanitized FFmpeg and ffprobe availability and versions. The response never contains commands, paths, or stderr. + security: + - ManagementSessionAuth: [] + responses: + "200": + description: Video Bridge runtime availability + "401": + description: Unauthorized + "403": + description: Localhost access required + + /api/modality-bridge/video/extract: + post: + x-loopback-only: true + tags: [System] + summary: Extract bounded Video Bridge frames through the internal broker + description: Internal per-process-authenticated trusted-loopback broker. Accepts at most 50 MiB of video bytes; URLs, paths, executable names, and command arguments are not part of the contract. The body pipeline and streamed handler reader both enforce the input cap. The broker applies fixed FFmpeg/ffprobe confinement, a single extraction slot with four pending jobs/100 MiB queued input, a 4 MiB per-frame cap, and a 32 MiB total response cap. Optional focus bounds and scene-aware sampling are deterministic and bounded. Transcript provenance is a metadata contract on the parent video part, not an instruction to run speech-to-text. This is not a public upload API. + security: [] + parameters: + - in: query + name: frames + required: true + schema: + type: integer + minimum: 1 + maximum: 16 + - in: query + name: samplingPolicy + required: false + description: Optional deterministic sampling policy. Scene-aware detection falls back to uniform sampling on detector failure. + schema: + type: string + enum: [uniform, scene_aware, segment_aware] + default: uniform + - in: query + name: start + required: false + description: Optional focus-window start in seconds. The broker clamps it to the media duration. + schema: + type: number + minimum: 0 + - in: query + name: end + required: false + description: Optional focus-window end in seconds. It must be greater than the normalized start. + schema: + type: number + minimum: 0 + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + maxLength: 52428800 + responses: + "200": + description: Sanitized duration and bounded JPEG data-URI frames + "400": + description: Invalid fixed broker contract + "403": + description: Authenticated trusted-loopback broker identity required + "413": + description: Input exceeds the 50 MiB byte limit + "422": + description: Media rejected or extraction failed + "499": + description: Client request aborted + "503": + description: Queue capacity is exhausted, or FFmpeg/ffprobe is unavailable on PATH + headers: + Retry-After: + description: Present with value 1 when queue capacity is exhausted + schema: + type: integer + minimum: 1 + "504": + description: Fixed 120-second broker extraction deadline exceeded + + /api/modality-bridge/video/drilldown: + get: + x-loopback-only: true + tags: [System] + summary: Read a bounded Video Bridge drill-down slice + description: Internal loopback/token-authenticated lookup into a short-lived cache isolated by an opaque principal, session, and media reference. It never downloads media or starts a subprocess; start/end and frame count only select already materialized, canonicalized JPEG frames whose dimensions were derived from their bytes. This cache substrate is not yet wired to the transparent Video Bridge request path and does not yet expose multi-resolution selection. + security: [] + parameters: + - in: header + name: x-omniroute-video-bridge-principal + required: true + description: Canonical visible-ASCII, opaque non-secret principal ID; production tenant derivation is required before enabling a caller + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: "^[!-~]{1,256}$" + - in: query + name: sessionId + required: true + description: Canonical opaque ID without surrounding whitespace + schema: { type: string, minLength: 1, maxLength: 128 } + - in: query + name: videoRef + required: true + description: Canonical opaque reference without surrounding whitespace + schema: { type: string, minLength: 1, maxLength: 4096 } + - in: query + name: start + required: false + schema: { type: number, minimum: 0 } + - in: query + name: end + required: false + schema: { type: number, minimum: 0 } + - in: query + name: frames + required: false + schema: { type: integer, minimum: 1, maximum: 16 } + responses: + "200": { description: Bounded cached frame slice with derivation audit metadata } + "403": { description: Trusted loopback/token identity and principal required } + "404": { description: Drill-down session or media key was not found } + post: + x-loopback-only: true + tags: [System] + summary: Store a bounded Video Bridge drill-down result + description: Internal lifecycle operation for explicitly authorized callers. The short-lived cache is isolated by principal, session, and media reference; enforces independent per-principal and global retained-byte quotas; accepts canonical Base64 only after a warning-sensitive bounded full JPEG decode/re-encode; strips trailing polyglot bytes; retains and charges only the canonical JPEG output; derives resolution from decoded bytes; and does not alter the primary request cost. The JSON wire budget includes Base64 overhead for the 32 MiB decoded-input ceiling. + security: [] + parameters: + - in: header + name: x-omniroute-video-bridge-principal + required: true + description: Canonical visible-ASCII, opaque non-secret principal ID; production tenant derivation is required before enabling a caller + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: "^[!-~]{1,256}$" + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + required: [sessionId, videoRef, derivation, durationSeconds, frames] + properties: + sessionId: + type: string + minLength: 1 + maxLength: 128 + description: Canonical opaque ID without surrounding whitespace + videoRef: + type: string + minLength: 1 + maxLength: 4096 + description: Canonical opaque reference without surrounding whitespace + derivation: + type: object + additionalProperties: false + required: [parentContentHash, policy, version] + properties: + parentContentHash: + type: string + pattern: "^sha256:[a-f0-9]{64}$" + policy: + type: string + pattern: "^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}$" + version: + type: string + pattern: "^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}$" + durationSeconds: { type: number, exclusiveMinimum: 0, maximum: 600 } + frames: + type: array + minItems: 1 + maxItems: 16 + items: + type: object + additionalProperties: false + required: [timestampSeconds, dataUri] + properties: + timestampSeconds: { type: number, minimum: 0 } + dataUri: + type: string + minLength: 27 + maxLength: 5592431 + description: Canonical Base64 data URI whose decoded bytes pass a warning-sensitive bounded full JPEG decode/re-encode; trailing bytes are discarded and width and height are derived server-side + responses: + "201": { description: Drill-down result stored } + "403": { description: Trusted loopback/token identity and principal required } + "413": { description: Payload exceeds the bounded session budget } + "499": { description: Caller cancelled before the derivation was committed } + delete: + x-loopback-only: true + tags: [System] + summary: Delete a Video Bridge drill-down session + security: [] + parameters: + - in: header + name: x-omniroute-video-bridge-principal + required: true + description: Canonical visible-ASCII, opaque non-secret principal ID; production tenant derivation is required before enabling a caller + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: "^[!-~]{1,256}$" + - in: query + name: sessionId + required: true + description: Canonical opaque ID without surrounding whitespace + schema: { type: string, minLength: 1, maxLength: 128 } + responses: + "200": { description: Session entries removed } + "403": { description: Trusted loopback/token identity and principal required } + + /api/cache/stats: + get: + tags: [System] + summary: Get detailed cache statistics + description: Returns detailed statistics for all cache layers. + responses: + "200": + description: Detailed cache stats + delete: + tags: [System] + summary: Clear cache statistics + responses: + "200": + description: Cache stats cleared + + # ─── Telemetry & Token Health ─────────────────────────────────── + + /api/telemetry/summary: + get: + tags: [Telemetry] + summary: Get telemetry summary + description: Returns aggregated telemetry data including request metrics and performance stats. + responses: + "200": + description: Telemetry summary data + + /api/token-health: + get: + tags: [Telemetry] + summary: Get token health status + description: Returns health status of OAuth tokens across all providers. + responses: + "200": + description: Token health status + + # ─── Evals & Policies ────────────────────────────────────────── + + /api/evals: + get: + tags: [System] + summary: List eval suites + responses: + "200": + description: Eval suite list + post: + tags: [System] + summary: Run evaluation + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Eval results + + /api/evals/{suiteId}: + get: + tags: [System] + summary: Get eval suite details + parameters: + - name: suiteId + in: path + required: true + schema: + type: string + responses: + "200": + description: Eval suite details + + /api/policies: + get: + tags: [System] + summary: List routing policies + responses: + "200": + description: Policy list + post: + tags: [System] + summary: Create routing policy + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "201": + description: Created policy + delete: + tags: [System] + summary: Delete routing policy + responses: + "200": + description: Policy deleted + + /api/compliance/audit-log: + get: + tags: [System] + summary: Get compliance audit log + description: > + Returns paginated audit log entries. Use `level=high` to filter to + high-level actions only (powers the Activity feed). Use `level=all` + (default) for full compliance table. + security: + - bearerAuth: [] + parameters: + - name: level + in: query + schema: + type: string + enum: [high, all] + default: all + description: "high = Activity feed events only; all = all audit events" + - name: action + in: query + schema: + type: string + description: Filter by exact action string (e.g. "provider.added") + - name: actor + in: query + schema: + type: string + description: Filter by actor identifier + - name: limit + in: query + schema: + type: integer + default: 50 + maximum: 500 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + "200": + description: Audit log entries + "401": + description: Unauthorized + "500": + description: Internal server error + + # ─── Quota Sharing (Group B, plan 22) ──────────────────────────── + + /api/quota/pools: + get: + tags: [Quota] + summary: List quota pools + security: + - bearerAuth: [] + responses: + "200": + description: Array of QuotaPool objects + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QuotaPool" + "401": + description: Unauthorized + "500": + description: Internal server error + post: + tags: [Quota] + summary: Create quota pool + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PoolCreate" + responses: + "201": + description: Pool created + content: + application/json: + schema: + $ref: "#/components/schemas/QuotaPool" + "400": + description: Validation error (Zod) + "401": + description: Unauthorized + "500": + description: Internal server error + + /api/quota/pools/{id}: + get: + tags: [Quota] + summary: Get quota pool by ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: QuotaPool object + content: + application/json: + schema: + $ref: "#/components/schemas/QuotaPool" + "401": + description: Unauthorized + "404": + description: Pool not found + "500": + description: Internal server error + patch: + tags: [Quota] + summary: Update quota pool (name or allocations) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PoolUpdate" + responses: + "200": + description: Updated pool + "400": + description: Validation error + "401": + description: Unauthorized + "404": + description: Pool not found + "500": + description: Internal server error + delete: + tags: [Quota] + summary: Delete quota pool + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Deleted + "401": + description: Unauthorized + "404": + description: Pool not found + "500": + description: Internal server error + + /api/quota/pools/{id}/usage: + get: + tags: [Quota] + summary: Get pool usage snapshot (per-key consumption + burn rate) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: PoolUsageSnapshot + content: + application/json: + schema: + $ref: "#/components/schemas/PoolUsageSnapshot" + "401": + description: Unauthorized + "404": + description: Pool not found + "500": + description: Internal server error + + /api/quota/plans: + get: + tags: [Quota] + summary: List resolved provider plans (catalog + manual overrides) + security: + - bearerAuth: [] + responses: + "200": + description: Array of ProviderPlan + "401": + description: Unauthorized + "500": + description: Internal server error + + /api/quota/plans/{connectionId}: + get: + tags: [Quota] + summary: Get resolved plan for a connection + security: + - bearerAuth: [] + parameters: + - name: connectionId + in: path + required: true + schema: + type: string + responses: + "200": + description: ProviderPlan (source = auto | manual) + "401": + description: Unauthorized + "404": + description: Connection not found + "500": + description: Internal server error + put: + tags: [Quota] + summary: Upsert manual plan override for a connection + security: + - bearerAuth: [] + parameters: + - name: connectionId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PlanUpsert" + responses: + "200": + description: Updated plan + "400": + description: Validation error (Zod) + "401": + description: Unauthorized + "500": + description: Internal server error + delete: + tags: [Quota] + summary: Delete manual plan override (reverts to catalog/auto) + security: + - bearerAuth: [] + parameters: + - name: connectionId + in: path + required: true + schema: + type: string + responses: + "204": + description: Override deleted + "401": + description: Unauthorized + "404": + description: Override not found + "500": + description: Internal server error + + /api/quota/preview: + get: + tags: [Quota] + summary: Dry-run quota enforcement check (preview only, no consumption recorded) + security: + - bearerAuth: [] + parameters: + - name: apiKeyId + in: query + required: true + schema: + type: string + - name: poolId + in: query + required: true + schema: + type: string + - name: estimatedTokens + in: query + schema: + type: number + - name: estimatedUsd + in: query + schema: + type: number + - name: estimatedRequests + in: query + schema: + type: integer + responses: + "200": + description: EnforceDecision (allow/block + reason) + "400": + description: Validation error (Zod) + "401": + description: Unauthorized + "500": + description: Internal server error + + /api/settings/quota-store: + get: + tags: [Settings] + summary: Get current quota store driver settings + description: Redis URL is masked in the response (shows only scheme+host). + security: + - bearerAuth: [] + responses: + "200": + description: QuotaStoreSettings (driver + masked redisUrl) + "401": + description: Unauthorized + "500": + description: Internal server error + put: + tags: [Settings] + summary: Update quota store driver settings + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QuotaStoreSettings" + responses: + "200": + description: Settings updated + "400": + description: Validation error (Zod) — e.g. driver=redis without valid URL + "401": + description: Unauthorized + "500": + description: Internal server error + + # ─── v1beta (Gemini-Compatible) ───────────────────────────────── + + /api/v1beta/models: + get: + tags: [Models] + summary: List models (Gemini format) + description: Returns models in Gemini v1beta format for native SDK compatibility + security: + - BearerAuth: [] + responses: + "200": + description: Model list in Gemini format + + /api/v1beta/models/{path}: + post: + tags: [Models] + summary: Gemini generateContent + description: Gemini-compatible generateContent endpoint + security: + - BearerAuth: [] + parameters: + - name: path + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Generated content + + # ─── AgentBridge ────────────────────────────────────────────── + + /api/tools/agent-bridge/agents: + get: + tags: [AgentBridge] + summary: List all 9 IDE agents with current state + description: >- + Returns the state (dns_enabled, cert_trusted, setup_completed, last_started_at, + last_error) for all 9 configured IDE agents. LOCAL_ONLY. + responses: + "200": + description: Array of agent state rows + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/AgentBridgeAgentState" + "403": + description: Loopback-only — request came from a non-loopback address + + /api/tools/agent-bridge/state: + get: + tags: [AgentBridge] + summary: Get global AgentBridge server state + description: Returns running status, port, cert info, and intercepted request count. + responses: + "200": + description: Server state + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeServerState" + + /api/tools/agent-bridge/server: + post: + tags: [AgentBridge] + summary: Control AgentBridge MITM server + description: Start, stop, restart, trust-cert, or regenerate-cert. SPAWN_CAPABLE. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeServerAction" + responses: + "200": + description: Action executed + "400": + description: Invalid action + "409": + description: Port 443 conflict + + /api/tools/agent-bridge/agents/{agentId}/dns: + post: + tags: [AgentBridge] + summary: Enable or disable DNS for one agent + description: Adds or removes /etc/hosts entries for the agent's host list. SPAWN_CAPABLE. + parameters: + - name: agentId + in: path + required: true + schema: + $ref: "#/components/schemas/AgentId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeDnsAction" + responses: + "200": + description: DNS updated + "400": + description: Validation error + + /api/tools/agent-bridge/agents/{agentId}/mappings: + get: + tags: [AgentBridge] + summary: Get model mappings for one agent + parameters: + - name: agentId + in: path + required: true + schema: + $ref: "#/components/schemas/AgentId" + responses: + "200": + description: Array of source→target model mappings + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/AgentBridgeMappingRow" + put: + tags: [AgentBridge] + summary: Update model mappings for one agent + parameters: + - name: agentId + in: path + required: true + schema: + $ref: "#/components/schemas/AgentId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeMappingPut" + responses: + "200": + description: Mappings updated + + /api/tools/agent-bridge/bypass: + get: + tags: [AgentBridge] + summary: List bypass patterns (hosts never decrypted) + responses: + "200": + description: Bypass patterns + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/AgentBridgeBypassRow" + put: + tags: [AgentBridge] + summary: Update user bypass patterns + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeBypassUpsert" + responses: + "200": + description: Patterns updated + + /api/tools/agent-bridge/cert: + post: + tags: [AgentBridge] + summary: Download or regenerate the AgentBridge CA certificate + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [action] + properties: + action: + type: string + enum: [download, regenerate] + responses: + "200": + description: CA certificate PEM (download) or regeneration confirmation + + /api/tools/agent-bridge/upstream-ca: + get: + tags: [AgentBridge] + summary: Get configured upstream CA cert path + responses: + "200": + description: Upstream CA configuration + content: + application/json: + schema: + type: object + properties: + path: + type: string + nullable: true + post: + tags: [AgentBridge] + summary: Set upstream CA cert path for corporate TLS environments + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AgentBridgeUpstreamCaPost" + responses: + "200": + description: Upstream CA configured + "400": + description: Path does not exist or is not readable + + # ─── Traffic Inspector ───────────────────────────────────────── + + /api/tools/traffic-inspector/requests: + get: + tags: [Traffic Inspector] + summary: List intercepted requests (filterable) + parameters: + - name: profile + in: query + schema: + type: string + enum: [llm, custom, all] + - name: host + in: query + schema: + type: string + - name: agent + in: query + schema: + $ref: "#/components/schemas/AgentId" + - name: status + in: query + schema: + type: string + enum: ["2xx", "3xx", "4xx", "5xx", error] + - name: source + in: query + schema: + $ref: "#/components/schemas/CaptureSource" + - name: sessionId + in: query + schema: + type: string + format: uuid + responses: + "200": + description: Array of intercepted requests + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InterceptedRequest" + delete: + tags: [Traffic Inspector] + summary: Clear the in-memory traffic buffer + responses: + "204": + description: Buffer cleared + + /api/tools/traffic-inspector/requests/{id}: + get: + tags: [Traffic Inspector] + summary: Get a single intercepted request by ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Intercepted request details + content: + application/json: + schema: + $ref: "#/components/schemas/InterceptedRequest" + "404": + description: Request not found in buffer + + /api/tools/traffic-inspector/requests/{id}/replay: + post: + tags: [Traffic Inspector] + summary: Replay a captured request through OmniRoute router + description: Re-executes the original request body against /v1/chat/completions. Consumes quota. + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Replay response (streaming or JSON) + "404": + description: Request not found + + /api/tools/traffic-inspector/requests/{id}/annotation: + put: + tags: [Traffic Inspector] + summary: Save or update annotation on a request + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorAnnotationPut" + responses: + "200": + description: Annotation saved + + /api/tools/traffic-inspector/ws: + get: + tags: [Traffic Inspector] + summary: Live WebSocket stream of intercepted requests + description: >- + Upgrade to WebSocket. On connect, server sends `{type:"snapshot", data:[...]}`. + Subsequent events: `{type:"new", data:{...}}`, `{type:"update", data:{...}}`, + `{type:"clear"}`. LOCAL_ONLY. + responses: + "101": + description: WebSocket upgrade successful + "403": + description: Non-loopback origin rejected + + /api/tools/traffic-inspector/export.har: + get: + tags: [Traffic Inspector] + summary: Export current filtered request list as HAR 1.2 + parameters: + - name: profile + in: query + schema: + type: string + enum: [llm, custom, all] + - name: sessionId + in: query + schema: + type: string + format: uuid + responses: + "200": + description: HAR file (JSON) + content: + application/json: + schema: + type: object + description: HAR 1.2 format + + /api/tools/traffic-inspector/hosts: + get: + tags: [Traffic Inspector] + summary: List custom capture hosts + responses: + "200": + description: Custom hosts list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectorCustomHost" + post: + tags: [Traffic Inspector] + summary: Add a custom capture host (edits /etc/hosts) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorCustomHostCreate" + responses: + "201": + description: Host added + "409": + description: Host already exists + + /api/tools/traffic-inspector/hosts/{host}: + delete: + tags: [Traffic Inspector] + summary: Remove a custom capture host + parameters: + - name: host + in: path + required: true + schema: + type: string + responses: + "204": + description: Host removed + patch: + tags: [Traffic Inspector] + summary: Toggle enabled state of a custom host + parameters: + - name: host + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [enabled] + properties: + enabled: + type: boolean + responses: + "200": + description: Host updated + + /api/tools/traffic-inspector/capture-modes: + get: + tags: [Traffic Inspector] + summary: Get state of all 4 capture modes + responses: + "200": + description: Capture modes state + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorCaptureModesState" + + /api/tools/traffic-inspector/capture-modes/http-proxy: + post: + tags: [Traffic Inspector] + summary: Start or stop the HTTP_PROXY listener (port 8080) + description: SPAWN_CAPABLE — spawns a net.Server listener. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorCaptureModeAction" + responses: + "200": + description: Action executed + "409": + description: Port conflict (EADDRINUSE) when starting + + /api/tools/traffic-inspector/capture-modes/system-proxy: + post: + tags: [Traffic Inspector] + summary: Apply or revert system-wide proxy settings + description: SPAWN_CAPABLE — executes networksetup/gsettings/netsh. Requires admin. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorSystemProxyAction" + responses: + "200": + description: System proxy updated + "500": + description: OS command failed (permission error) + + /api/tools/traffic-inspector/sessions: + get: + tags: [Traffic Inspector] + summary: List all saved recording sessions + responses: + "200": + description: Sessions list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InspectorSession" + post: + tags: [Traffic Inspector] + summary: Start a new recording session + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorSessionStart" + responses: + "201": + description: Session started + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorSession" + + /api/tools/traffic-inspector/sessions/{id}: + get: + tags: [Traffic Inspector] + summary: Get session snapshot (all captured requests) + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Session with embedded requests + "404": + description: Session not found + patch: + tags: [Traffic Inspector] + summary: Stop or rename a recording session + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InspectorSessionPatch" + responses: + "200": + description: Session updated + delete: + tags: [Traffic Inspector] + summary: Delete a recording session + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "204": + description: Session deleted + + /api/tools/traffic-inspector/sessions/{id}/export.har: + get: + tags: [Traffic Inspector] + summary: Export a recorded session as HAR 1.2 + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: HAR file for this session + content: + application/json: + schema: + type: object + description: HAR 1.2 format + "404": + description: Session not found + + /api/tools/traffic-inspector/internal/ingest: + post: + tags: [Traffic Inspector] + summary: Internal ingest endpoint for server.cjs passthrough path + description: >- + Accepts a serialized InterceptedRequest from the CJS MITM server for requests + that do not go through TypeScript handlers (e.g., passthrough hosts). Requires + INSPECTOR_INTERNAL_INGEST_TOKEN header. LOCAL_ONLY. + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InterceptedRequest" + responses: + "204": + description: Ingested + "401": + description: Invalid or missing ingest token + + # ─── OpenAPI Spec ────────────────────────────────────────────── + + /api/openapi/spec: + get: + tags: [System] + summary: Get OpenAPI specification catalog + description: >- + Returns a structured JSON catalog parsed from this `openapi.yaml`, + including info, servers, tags, schemas, and a flat list of endpoints + (method, path, tags, summary, security, parameters, responses). + Used by the in-app API explorer. When `requireLogin` is enabled, this + management endpoint requires an authenticated dashboard session; + otherwise it is available without authentication. + security: + - ManagementSessionAuth: [] + responses: + "200": + description: Parsed OpenAPI catalog + content: + application/json: + schema: + type: object + properties: + info: + type: object + servers: + type: array + items: + type: object + tags: + type: array + items: + type: object + endpoints: + type: array + items: + type: object + properties: + method: + type: string + path: + type: string + tags: + type: array + items: + type: string + summary: + type: string + description: + type: string + security: + type: boolean + parameters: + type: array + items: + type: object + requestBody: + type: boolean + responses: + type: array + items: + type: string + schemas: + type: array + items: + type: string + "404": + description: openapi.yaml file not found on disk + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "500": + description: Failed to parse OpenAPI spec + + /api/openapi/try: + post: + tags: [System] + summary: Proxy an API Explorer request to an OmniRoute endpoint + description: >- + Executes an API Explorer request through a server-side, same-origin proxy. The target + must start with `/api/`, `/v1/`, `/v1beta/`, `/a2a`, or + `/.well-known/agent.json`; protocol-relative and cross-origin targets are rejected. + Hop-by-hop, proxy, host, cookie, and forwarding headers supplied in `headers` are + stripped, while any dashboard cookie on the original request is forwarded separately. + When `requireLogin` is disabled, the management-auth bypass mirrors the runtime setting; + otherwise a management Bearer credential or dashboard session is required. Failures + caught after authentication, including request JSON parsing, fetch, and response-body + parsing failures, are returned in the normal HTTP 200 result envelope so the Explorer + can display them; `status: 0` identifies that caught-failure path. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [path] + properties: + method: + type: string + enum: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS] + default: GET + path: + type: string + minLength: 1 + pattern: "^/(?:api/|v1/|v1beta/|a2a|\\.well-known/agent\\.json)" + description: Same-origin OmniRoute API path, optionally including a query string. + headers: + type: object + default: {} + additionalProperties: + type: string + description: >- + Headers to forward after removing connection, content-length, cookie, host, + keep-alive, proxy-authenticate, proxy-authorization, te, trailer, + transfer-encoding, upgrade, x-forwarded-for, x-forwarded-host, and + x-forwarded-proto headers. + body: + description: >- + Optional JSON value. A truthy value is serialized unless it is already a + string, and is not forwarded when `method` is `GET`. + responses: + "200": + description: Upstream response or displayable caught-failure envelope + content: + application/json: + schema: + type: object + additionalProperties: false + required: [status, statusText, headers, body, latencyMs, contentType] + properties: + status: + type: integer + minimum: 0 + description: Upstream HTTP status, or 0 when request processing throws. + statusText: + type: string + headers: + type: object + additionalProperties: + type: string + body: + description: >- + Parsed JSON, response text truncated after 10,000 characters, or a sanitized + caught-error object. + latencyMs: + type: integer + minimum: 0 + contentType: + type: string + "400": + description: Invalid request body or non-same-origin path + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ValidationErrorResponse" + - type: object + required: [error] + properties: + error: + type: string + example: Path must be same-origin + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + "503": + $ref: "#/components/responses/InternalError" + + # ─── Agent Skills Catalog ──────────────────────────────────────────────────── + + /api/agent-skills: + get: + tags: [Agent Skills] + summary: List agent skills catalog + description: | + Returns the full 42-entry Agent Skills catalog with optional filtering. + Skills describe how to use OmniRoute's REST API and CLI — they are structured + SKILL.md documentation files discoverable by external agents, MCP clients, and + A2A orchestrators. No authentication required. + parameters: + - name: category + in: query + required: false + schema: + type: string + enum: [api, cli] + description: Filter by category (api = REST API skills, cli = CLI skills) + - name: area + in: query + required: false + schema: + type: string + description: Filter by area slug (e.g. "providers", "models", "cli-serve") + responses: + "200": + description: Catalog list + content: + application/json: + schema: + type: object + required: [skills, count, coverage] + properties: + skills: + type: array + items: + $ref: "#/components/schemas/AgentSkill" + count: + type: integer + coverage: + $ref: "#/components/schemas/SkillCoverage" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalError" + + /api/agent-skills/{id}: + get: + tags: [Agent Skills] + summary: Get a single agent skill + description: | + Returns metadata for a single agent skill by its canonical ID + (e.g. `omni-providers`, `cli-serve`). No authentication required. + parameters: + - name: id + in: path + required: true + schema: + type: string + pattern: "^[a-z][a-z0-9-]*$" + description: Canonical skill ID + example: omni-providers + responses: + "200": + description: Agent skill metadata + content: + application/json: + schema: + $ref: "#/components/schemas/AgentSkill" + "400": + $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalError" + + /api/agent-skills/{id}/raw: + get: + tags: [Agent Skills] + summary: Get raw SKILL.md content + description: | + Returns the SKILL.md content for a skill as `text/markdown`. + Resolution order: local filesystem `skills/{id}/SKILL.md` → GitHub raw URL (1-hour cache). + No authentication required. + parameters: + - name: id + in: path + required: true + schema: + type: string + pattern: "^[a-z][a-z0-9-]*$" + description: Canonical skill ID + example: omni-providers + responses: + "200": + description: SKILL.md content as Markdown + headers: + X-Skill-Source: + schema: + type: string + enum: [filesystem, github, generated] + description: Where the content was loaded from + X-Skill-Fetched-At: + schema: + type: string + format: date-time + description: ISO timestamp of when the content was fetched + Cache-Control: + schema: + type: string + description: "public, max-age=3600" + content: + text/markdown: + schema: + type: string + "400": + $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NotFound" + "502": + description: Upstream GitHub fetch failed + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + $ref: "#/components/responses/InternalError" + + /api/agent-skills/coverage: + get: + tags: [Agent Skills] + summary: Get SKILL.md coverage stats + description: | + Returns how many of the 22 API skills and 20 CLI skills have SKILL.md + files on the local filesystem vs the catalog totals. No authentication required. + responses: + "200": + description: Coverage stats + content: + application/json: + schema: + $ref: "#/components/schemas/SkillCoverage" + "500": + $ref: "#/components/responses/InternalError" + + /api/agent-skills/generate: + post: + tags: [Agent Skills] + summary: Trigger SKILL.md generator + description: | + Runs the Agent Skills generator which writes `skills/{id}/SKILL.md` for + all 42 catalog entries (or a subset via `onlyIds`). Preserves + ` ... ` blocks. + **Requires management authentication.** + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + dryRun: + type: boolean + default: true + description: "If true, reports what would be generated without writing files" + prune: + type: boolean + default: false + description: "If true, deletes skill directories not in the catalog" + onlyIds: + type: array + items: + type: string + description: "If provided, only regenerate these skill IDs" + responses: + "200": + description: Generator report + content: + application/json: + schema: + type: object + required: [generated, unchanged, pruned, orphansDetected, errors] + properties: + generated: + type: array + items: + type: string + description: IDs that got new/updated SKILL.md + unchanged: + type: array + items: + type: string + description: IDs whose content was already up to date + pruned: + type: array + items: + type: string + description: IDs whose directories were deleted (prune mode) + orphansDetected: + type: array + items: + type: string + description: Directories found in skills/ not in the catalog + errors: + type: array + items: + type: object + required: [id, error] + properties: + id: + type: string + error: + type: string + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + "503": + description: Generator module not available + /api/v1/ocr: + post: + tags: + - Images + summary: Document OCR + description: >- + Multi-provider document OCR endpoint (Mistral OCR–compatible request + and response shape). Accepts a JSON body referencing a document/image + and returns extracted text. `model` selects the provider via a + `provider/model` prefix (e.g. `mistral/mistral-ocr-latest`, + `azure-document-intelligence/prebuilt-read`, + `vertex-deepseek-ocr/deepseek-ocr-maas`); a bare model id (e.g. + `mistral-ocr-latest`) resolves to its registered provider, and an + omitted `model` defaults to Mistral. Azure Document Intelligence is + asynchronous upstream — the handler polls the returned operation + until it succeeds or fails before responding, so this endpoint can + take longer to return for that provider. Success responses carry the + `X-OmniRoute-*` cost-telemetry headers. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + model: + type: string + description: >- + `provider/model` id or bare model id. Registered ids: + `mistral/mistral-ocr-latest`, + `azure-document-intelligence/prebuilt-read`, + `vertex-deepseek-ocr/deepseek-ocr-maas`. Defaults to + `mistral-ocr-latest` when omitted. + document: + type: object + responses: + "200": + description: OCR result with extracted text. + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/audio/translations: + post: + tags: + - Audio + summary: Translate audio to English + description: >- + OpenAI Whisper–compatible audio translation (multipart/form-data). + Unlike `/api/v1/audio/transcriptions`, output is always English + regardless of the source language. Success responses carry the + `X-OmniRoute-*` cost-telemetry headers. + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + model: + type: string + responses: + "200": + description: English translation of the audio. + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/voices: + get: + tags: [Audio] + summary: List ElevenLabs voices + description: >- + Proxies `GET https://api.elevenlabs.io/v1/voices` using the stored + `elevenlabs` provider credentials (the caller never sends `xi-api-key`). + The incoming query string is forwarded unchanged. + security: + - BearerAuth: [] + responses: + "200": + description: ElevenLabs voice list, relayed verbatim. + "401": + description: No usable `elevenlabs` credential is configured. + "429": + description: Every configured `elevenlabs` credential is rate limited. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/speech-to-text: + post: + tags: [Audio] + summary: ElevenLabs speech-to-text + description: >- + Streams the request body to `POST https://api.elevenlabs.io/v1/speech-to-text` + using the stored `elevenlabs` provider credentials. `content-type` and + `accept` are forwarded; the upstream body is relayed unchanged. + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "200": + description: Transcription payload, relayed verbatim from ElevenLabs. + "401": + description: No usable `elevenlabs` credential is configured. + "429": + description: Every configured `elevenlabs` credential is rate limited. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/text-to-speech/{voiceId}: + post: + tags: [Audio] + summary: ElevenLabs text-to-speech + description: >- + Streams the request body to + `POST https://api.elevenlabs.io/v1/text-to-speech/{voiceId}` using the stored + `elevenlabs` provider credentials. `voiceId` must match `^[A-Za-z0-9_-]+$` + or the request is rejected with 400 before any upstream call. + security: + - BearerAuth: [] + parameters: + - name: voiceId + in: path + required: true + schema: + type: string + pattern: "^[A-Za-z0-9_-]+$" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + text: + type: string + responses: + "200": + description: Audio stream, relayed verbatim from ElevenLabs. + "400": + description: Invalid ElevenLabs voice ID. + "401": + description: No usable `elevenlabs` credential is configured. + "429": + description: Every configured `elevenlabs` credential is rate limited. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/sound-generation: + post: + tags: [Audio] + summary: Create an ElevenLabs sound effect + description: >- + Proxies POST /v1/sound-generation using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + parameters: + - name: output_format + in: query + description: Native ElevenLabs audio encoding; availability depends on the upstream plan. + schema: + type: string + - name: enable_logging + in: query + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsSoundGeneration" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + audio/mpeg: + schema: + type: string + format: binary + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/music: + post: + tags: [Audio] + summary: Compose ElevenLabs music + description: >- + Proxies POST /v1/music using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + parameters: + - name: output_format + in: query + description: Native ElevenLabs audio encoding; availability depends on the upstream plan. + schema: + type: string + - name: enable_logging + in: query + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsMusicGeneration" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + audio/mpeg: + schema: + type: string + format: binary + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/music/stream: + post: + tags: [Audio] + summary: Stream ElevenLabs music + description: >- + Proxies POST /v1/music/stream using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + parameters: + - name: output_format + in: query + description: Native ElevenLabs audio encoding; availability depends on the upstream plan. + schema: + type: string + - name: enable_logging + in: query + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsMusicGeneration" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + audio/mpeg: + schema: + type: string + format: binary + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/music/detailed: + post: + tags: [Audio] + summary: Compose ElevenLabs music with its composition plan + description: >- + Proxies POST /v1/music/detailed using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + parameters: + - name: output_format + in: query + description: Native ElevenLabs audio encoding; availability depends on the upstream plan. + schema: + type: string + - name: enable_logging + in: query + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsMusicGeneration" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + multipart/mixed: + schema: + type: string + format: binary + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/music/detailed/stream: + post: + tags: [Audio] + summary: Stream ElevenLabs music and metadata as SSE + description: >- + Proxies POST /v1/music/detailed/stream using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + parameters: + - name: output_format + in: query + description: Native ElevenLabs audio encoding; availability depends on the upstream plan. + schema: + type: string + - name: enable_logging + in: query + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsMusicGeneration" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + text/event-stream: + schema: + type: string + format: binary + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/music/plan: + post: + tags: [Audio] + summary: Create an ElevenLabs music composition plan + description: >- + Proxies POST /v1/music/plan using the stored ElevenLabs connection selected + within the caller key's scope. Native response bytes and Content-Type are + preserved without buffering, including multipart boundaries. Query parameters + are forwarded unchanged. Endpoint, model, budget and connection restrictions apply. + Upstream plan restrictions (402) are preserved; a credit balance does not override them. + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ElevenLabsMusicPlan" + responses: + "200": + description: Native ElevenLabs response, relayed unchanged. + headers: + character-cost: + description: Credits consumed, when reported by ElevenLabs. + schema: + type: string + song-id: + description: Generated song identifier, when reported by ElevenLabs music. + schema: + type: string + request-id: + schema: + type: string + content: + application/json: + schema: + type: object + additionalProperties: true + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "402": + description: The ElevenLabs plan does not permit this operation. + "403": + description: The caller key does not permit this endpoint, model or connection. + "422": + description: Native ElevenLabs request validation failed. + "429": + description: OmniRoute policy, provider cooldown or upstream quota exceeded. + "502": + description: Could not reach ElevenLabs. + /api/v1/explain/routing: + get: + tags: [Telemetry] + summary: Routing explainability snapshot + description: >- + Returns the most recent routing events (bounded in-memory ring buffer) plus + the per-provider/model quality snapshot from `open-sse/services/routing`. + Routing metadata only — never prompts, bodies, headers or credentials. + Auth mirrors `/api/v1/combos`: a valid Bearer API key or a dashboard + session; with `REQUIRE_API_KEY=false` anonymous reads are allowed. + security: + - BearerAuth: [] + parameters: + - name: limit + in: query + required: false + description: Events/quality rows to return; clamped to 1–500 (default 50). + schema: + type: integer + minimum: 1 + maximum: 500 + default: 50 + responses: + "200": + description: Routing explain payload. + content: + application/json: + schema: + type: object + properties: + object: + type: string + enum: [routing_explain] + sinks: + type: array + items: + type: string + otelEnabled: + type: boolean + events: + type: array + items: + type: object + quality: + type: array + items: + type: object + otel: + type: object + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/providers/suggested-models: + get: + tags: + - Providers + summary: Suggested media models + description: >- + Read-only server-side proxy to the public HuggingFace Hub models search + API, used by the dashboard to suggest models for a media provider kind + without exposing an HF token client-side. Never accepts or returns + credentials. + parameters: + - name: type + in: query + schema: + type: string + description: Media kind to search for (e.g. `image`, `audio`, `video`). + responses: + "200": + description: List of suggested HuggingFace Hub models. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/provider-plugin-manifest: + get: + tags: + - Providers + summary: Provider plugin manifest + description: Returns the manifest describing installed provider plugins. + responses: + "200": + description: Provider plugin manifest. + "500": + $ref: "#/components/responses/InternalError" + /api/keys/{id}/devices: + get: + tags: + - API Keys + summary: List devices for an API key + description: >- + Lists the distinct devices (masked IP + User-Agent fingerprints) + tracked for an API key by the in-memory device tracker. IPs are masked + before storage; the route never sees the raw client IP. + x-internal: true + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Distinct devices seen for the API key. + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "404": + $ref: "#/components/responses/NotFound" + /api/settings/purge-usage-history: + post: + tags: + - Settings + summary: Purge usage history + description: Dashboard-only. Purges stored usage-history records. + x-internal: true + responses: + "200": + description: Usage history purged. + "401": + $ref: "#/components/responses/Unauthorized" + /api/oauth/codex/import-token: + post: + tags: + - OAuth + summary: Import a Codex connection from a bare access token + description: >- + Dashboard-only. Creates a Codex (ChatGPT/OpenAI) connection from a raw + access token with no refresh token (authType `access_token`). + x-internal: true + responses: + "200": + description: Connection imported. + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + /api/cli-tools/crush-settings: + get: + tags: + - CLI Tools + summary: Read Crush CLI OmniRoute config + x-loopback-only: true + description: Local-only. Reads the OmniRoute provider block in Crush's config. + x-internal: true + responses: + "200": + description: Current Crush config state. + post: + tags: + - CLI Tools + summary: Write Crush CLI OmniRoute config + x-loopback-only: true + description: Local-only. Registers OmniRoute as an `openai-compat` provider in Crush's config. + x-internal: true + responses: + "200": + description: Crush config updated. + delete: + tags: + - CLI Tools + summary: Remove OmniRoute from Crush CLI config + x-loopback-only: true + description: Local-only. Removes the OmniRoute provider block from Crush's config. + x-internal: true + responses: + "200": + description: Crush config entry removed. + /api/cli-tools/codewhale-settings: + get: + tags: + - CLI Tools + summary: Read CodeWhale CLI OmniRoute config + x-loopback-only: true + description: >- + Local-only. Reads the OmniRoute config block from + `~/.codewhale/config.toml` (with `~/.deepseek/config.toml` legacy + fallback). + x-internal: true + responses: + "200": + description: Current CodeWhale config state. + post: + tags: + - CLI Tools + summary: Write CodeWhale CLI OmniRoute config + x-loopback-only: true + description: Local-only. Writes the OmniRoute config block in CodeWhale TOML format. + x-internal: true + responses: + "200": + description: CodeWhale config updated. + delete: + tags: + - CLI Tools + summary: Remove OmniRoute from CodeWhale CLI config + x-loopback-only: true + description: Local-only. Removes the OmniRoute config block from CodeWhale's config. + x-internal: true + responses: + "200": + description: CodeWhale config entry removed. + + # --- Generated route coverage (docs audit 2026-08-31) ----------------------- + # Minimal entries for every implemented route not documented above. Methods + # are parsed from each route.ts's exports; summaries are path-derived. + # Regenerate with: node scripts/ad-hoc/gen-openapi-missing-paths.mjs --apply + /api/{omnirouteApiCatchAll}: + delete: + tags: + - OmnirouteApiCatchAll + summary: "DELETE " + responses: + "200": + description: OK + get: + tags: + - OmnirouteApiCatchAll + summary: "GET " + responses: + "200": + description: OK + patch: + tags: + - OmnirouteApiCatchAll + summary: "PATCH " + responses: + "200": + description: OK + post: + tags: + - OmnirouteApiCatchAll + summary: "POST " + responses: + "200": + description: OK + put: + tags: + - OmnirouteApiCatchAll + summary: "PUT " + responses: + "200": + description: OK + /api/a2a/status: + get: + tags: + - A2a + summary: "GET a2a › status" + responses: + "200": + description: OK + /api/a2a/tasks: + get: + tags: + - A2a + summary: "GET a2a › tasks" + responses: + "200": + description: OK + post: + tags: + - A2a + summary: "POST a2a › tasks" + responses: + "200": + description: OK + /api/a2a/tasks/{id}: + get: + tags: + - A2a + summary: "GET a2a › tasks › " + responses: + "200": + description: OK + /api/a2a/tasks/{id}/cancel: + post: + tags: + - A2a + summary: "POST a2a › tasks › › cancel" + responses: + "200": + description: OK + /api/acp/agents: + delete: + tags: + - Acp + summary: "DELETE acp › agents" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Acp + summary: "GET acp › agents" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Acp + summary: "POST acp › agents" + x-loopback-only: true + responses: + "200": + description: OK + /api/admin/concurrency: + get: + tags: + - Admin + summary: "GET admin › concurrency" + responses: + "200": + description: OK + post: + tags: + - Admin + summary: "POST admin › concurrency" + responses: + "200": + description: OK + /api/analytics/auto-routing: + get: + tags: + - Analytics + summary: "GET analytics › auto routing" + responses: + "200": + description: OK + /api/analytics/compression: + get: + tags: + - Analytics + summary: "GET analytics › compression" + responses: + "200": + description: OK + /api/analytics/diversity: + get: + tags: + - Analytics + summary: "GET analytics › diversity" + responses: + "200": + description: OK + /api/assess: + get: + tags: + - Assess + summary: "GET assess" + responses: + "200": + description: OK + post: + tags: + - Assess + summary: "POST assess" + responses: + "200": + description: OK + /api/auth/csrf: + get: + tags: + - Auth + summary: "GET auth › csrf" + responses: + "200": + description: OK + /api/auth/status: + get: + tags: + - Auth + summary: "GET auth › status" + responses: + "200": + description: OK + /api/batches: + get: + tags: + - Batches + summary: "GET batches" + responses: + "200": + description: OK + /api/batches/{id}: + get: + tags: + - Batches + summary: "GET batches › " + responses: + "200": + description: OK + /api/cache/entries: + delete: + tags: + - Cache + summary: "DELETE cache › entries" + responses: + "200": + description: OK + get: + tags: + - Cache + summary: "GET cache › entries" + responses: + "200": + description: OK + /api/cache/reasoning: + delete: + tags: + - Cache + summary: "DELETE cache › reasoning" + responses: + "200": + description: OK + get: + tags: + - Cache + summary: "GET cache › reasoning" + responses: + "200": + description: OK + /api/chaos/config: + delete: + tags: + - Chaos + summary: "DELETE chaos › config" + responses: + "200": + description: OK + get: + tags: + - Chaos + summary: "GET chaos › config" + responses: + "200": + description: OK + put: + tags: + - Chaos + summary: "PUT chaos › config" + responses: + "200": + description: OK + /api/chaos/run: + post: + tags: + - Chaos + summary: "POST chaos › run" + responses: + "200": + description: OK + /api/cli-tools/all-statuses: + get: + tags: + - Cli tools + summary: "GET cli tools › all statuses" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/apply: + post: + tags: + - Cli tools + summary: "POST cli tools › apply" + description: >- + Submit the original toolId, apiKey, optional baseUrl/model and optional + dryRun in the JSON body. Returned content is a redacted, non-cacheable + preview, not an importable configuration. A non-dry-run request writes + the original generated configuration; the container write guard remains active. + responses: + "200": + description: OK + /api/cli-tools/config: + get: + tags: + - Cli tools + summary: "GET cli tools › config" + description: >- + Returns redacted, non-cacheable previews. Send the configuration API key + in x-omniroute-config-api-key, separate from management authentication. + API keys in query strings are rejected. Preview content must not be + copied into a credential-bearing configuration or submitted as an apply payload. + parameters: + - in: header + name: x-omniroute-config-api-key + required: true + schema: + type: string + minLength: 1 + - in: query + name: baseUrl + required: false + schema: + type: string + format: uri + description: Absolute HTTP(S) URL without embedded username/password credentials. + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › config" + description: >- + Submit toolId, apiKey and optional baseUrl/model as JSON. Unknown fields + are rejected. Returned content is a redacted, non-cacheable preview, + not an importable configuration. To apply, submit the original inputs + to POST /api/cli-tools/apply instead of replaying the preview content. + responses: + "200": + description: OK + /api/cli-tools/deepseek-tui-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › deepseek tui settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › deepseek tui settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › deepseek tui settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/detect: + get: + tags: + - Cli tools + summary: "GET cli tools › detect" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/forge-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › forge settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › forge settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › forge settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/grok-build-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › grok build settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › grok build settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › grok build settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/hermes-agent-settings: + get: + tags: + - Cli tools + summary: "GET cli tools › hermes agent settings" + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › hermes agent settings" + responses: + "200": + description: OK + /api/cli-tools/jcode-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › jcode settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › jcode settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › jcode settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/keys: + get: + tags: + - Cli tools + summary: "GET cli tools › keys" + responses: + "200": + description: OK + /api/cli-tools/letta-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › letta settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › letta settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › letta settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/logs: + get: + tags: + - Cli tools + summary: "GET cli tools › logs" + responses: + "200": + description: OK + /api/cli-tools/omp-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › omp settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › omp settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › omp settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/openclaw/auto-order: + get: + tags: + - Cli tools + summary: "GET cli tools › openclaw › auto order" + responses: + "200": + description: OK + /api/cli-tools/pi-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › pi settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › pi settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › pi settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/qwen-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › qwen settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › qwen settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › qwen settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/smelt-settings: + delete: + tags: + - Cli tools + summary: "DELETE cli tools › smelt settings" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Cli tools + summary: "GET cli tools › smelt settings" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Cli tools + summary: "POST cli tools › smelt settings" + x-loopback-only: true + responses: + "200": + description: OK + /api/cli-tools/status: + get: + tags: + - Cli tools + summary: "GET cli tools › status" + x-loopback-only: true + responses: + "200": + description: OK + /api/codex/connect/{token}: + get: + tags: + - Codex + summary: "GET codex › connect › " + responses: + "200": + description: OK + post: + tags: + - Codex + summary: "POST codex › connect › " + responses: + "200": + description: OK + /api/combos/auto: + get: + tags: + - Combos + summary: "GET combos › auto" + responses: + "200": + description: OK + /api/combos/builder/options: + get: + tags: + - Combos + summary: "GET combos › builder › options" + responses: + "200": + description: OK + /api/combos/duplicate: + post: + tags: + - Combos + summary: "POST combos › duplicate" + responses: + "200": + description: OK + /api/combos/reorder: + post: + tags: + - Combos + summary: "POST combos › reorder" + responses: + "200": + description: OK + /api/compression/compare: + post: + tags: + - Compression + summary: "POST compression › compare" + responses: + "200": + description: OK + /api/compression/compare/verify: + post: + tags: + - Compression + summary: "POST compression › compare › verify" + responses: + "200": + description: OK + /api/compression/engines: + get: + tags: + - Compression + summary: "GET compression › engines" + responses: + "200": + description: OK + /api/compression/retrieve: + post: + tags: + - Compression + summary: "POST compression › retrieve" + responses: + "200": + description: OK + /api/conductor/ask: + post: + tags: + - Conductor + summary: "POST conductor › ask" + responses: + "200": + description: OK + /api/conductor/fleet: + get: + tags: + - Conductor + summary: "GET conductor › fleet" + responses: + "200": + description: OK + /api/conductor/tasks: + post: + tags: + - Conductor + summary: "POST conductor › tasks" + responses: + "201": + description: Created + /api/conductor/tasks/{id}: + get: + tags: + - Conductor + summary: "GET conductor › tasks › " + responses: + "200": + description: OK + /api/conductor/tasks/{id}/cancel: + post: + tags: + - Conductor + summary: "POST conductor › tasks › › cancel" + responses: + "200": + description: OK + /api/context/analytics: + get: + tags: + - Context + summary: "GET context › analytics" + responses: + "200": + description: OK + /api/context/analytics/engine: + get: + tags: + - Context + summary: "GET context › analytics › engine" + responses: + "200": + description: OK + /api/context/caveman/config: + get: + tags: + - Context + summary: "GET context › caveman › config" + responses: + "200": + description: OK + put: + tags: + - Context + summary: "PUT context › caveman › config" + responses: + "200": + description: OK + /api/context/combos: + get: + tags: + - Context + summary: "GET context › combos" + responses: + "200": + description: OK + post: + tags: + - Context + summary: "POST context › combos" + responses: + "200": + description: OK + /api/context/combos/{id}: + delete: + tags: + - Context + summary: "DELETE context › combos › " + responses: + "200": + description: OK + get: + tags: + - Context + summary: "GET context › combos › " + responses: + "200": + description: OK + put: + tags: + - Context + summary: "PUT context › combos › " + responses: + "200": + description: OK + /api/context/combos/{id}/assignments: + get: + tags: + - Context + summary: "GET context › combos › › assignments" + responses: + "200": + description: OK + put: + tags: + - Context + summary: "PUT context › combos › › assignments" + responses: + "200": + description: OK + /api/context/combos/default: + get: + tags: + - Context + summary: "GET context › combos › default" + responses: + "200": + description: OK + post: + tags: + - Context + summary: "POST context › combos › default" + responses: + "200": + description: OK + put: + tags: + - Context + summary: "PUT context › combos › default" + responses: + "200": + description: OK + /api/context/rtk/discover: + get: + tags: + - Context + summary: "GET context › rtk › discover" + responses: + "200": + description: OK + /api/context/rtk/learn: + get: + tags: + - Context + summary: "GET context › rtk › learn" + responses: + "200": + description: OK + /api/conversations: + get: + tags: + - Conversations + summary: "GET conversations" + responses: + "200": + description: OK + /api/conversations/{id}/tree: + get: + tags: + - Conversations + summary: "GET conversations › › tree" + responses: + "200": + description: OK + /api/copilot/chat: + post: + tags: + - Copilot + summary: "POST copilot › chat" + x-loopback-only: true + responses: + "200": + description: OK + /api/cursor-cli/{path}: + get: + tags: + - Cursor cli + summary: "GET cursor cli › " + responses: + "200": + description: OK + post: + tags: + - Cursor cli + summary: "POST cursor cli › " + responses: + "200": + description: OK + /api/dahl/tokens: + post: + tags: + - Dahl + summary: "POST dahl › tokens" + responses: + "200": + description: OK + /api/db-backups/export: + get: + tags: + - Db backups + summary: "GET db backups › export" + responses: + "200": + description: OK + "403": + description: >- + Machine credential refused. Exports carry 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. + /api/db-backups/exportAll: + get: + tags: + - Db backups + summary: "GET db backups › exportAll" + x-loopback-only: true + responses: + "200": + description: OK + "403": + description: >- + Machine credential refused. Exports carry 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. + /api/db-backups/import: + post: + tags: + - Db backups + summary: "POST db backups › import" + description: >- + Replaces the live database with an uploaded `.sqlite` backup (multipart `file` field, + or the raw bytes with `?filename=`). The upload is checked for SQLite integrity and the + required OmniRoute tables first. Operator-only: accepts the dashboard session, the + local CLI credential, or login disabled; API keys of any scope and `oma_` access tokens + are refused (403), for the dry run as well. + parameters: + - name: dryRun + in: query + description: >- + `true` validates the upload and returns what would be imported (row counts, schema + version) without the pre-import backup and without touching the live database. A + multipart `dryRun` form field is honoured too. + required: false + schema: + type: string + enum: ["true", "false", "1", "0"] + - name: filename + in: query + description: Name of the uploaded file for raw-byte uploads; must end in `.sqlite`. + required: false + schema: + type: string + responses: + "200": + description: >- + Imported (`imported: true`), or, for a dry run, validated (`dryRun: true, + valid: true, imported: false`) with the counts the upload would load. + "400": + description: Not a valid OmniRoute SQLite backup (type, size, integrity or tables). + "401": + description: No accepted credential. + "403": + description: >- + Machine credential refused. Importing replaces the live database, so it requires + the dashboard session or the local CLI (or login disabled); API keys of any scope + and `oma_` access tokens get 403. + /api/db/health: + get: + tags: + - Db + summary: "GET db › health" + responses: + "200": + description: OK + post: + tags: + - Db + summary: "POST db › health" + responses: + "200": + description: OK + /api/discovery/results: + get: + tags: + - Discovery + summary: "GET discovery › results" + x-loopback-only: true + responses: + "200": + description: OK + /api/discovery/results/{id}: + delete: + tags: + - Discovery + summary: "DELETE discovery › results › " + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Discovery + summary: "GET discovery › results › " + x-loopback-only: true + responses: + "200": + description: OK + /api/discovery/scan: + post: + tags: + - Discovery + summary: "POST discovery › scan" + x-loopback-only: true + responses: + "200": + description: OK + /api/discovery/verify/{id}: + post: + tags: + - Discovery + summary: "POST discovery › verify › " + x-loopback-only: true + responses: + "200": + description: OK + /api/docs: + get: + tags: + - Docs + summary: "GET docs" + responses: + "200": + description: OK + /api/docs/codex-cli: + get: + tags: + - Docs + summary: "GET docs › codex cli" + responses: + "200": + description: OK + /api/evals/suites: + post: + tags: + - Evals + summary: "POST evals › suites" + responses: + "200": + description: OK + /api/evals/suites/{suiteId}: + delete: + tags: + - Evals + summary: "DELETE evals › suites › " + responses: + "200": + description: OK + get: + tags: + - Evals + summary: "GET evals › suites › " + responses: + "200": + description: OK + put: + tags: + - Evals + summary: "PUT evals › suites › " + responses: + "200": + description: OK + /api/files: + get: + tags: + - Files + summary: "GET files" + responses: + "200": + description: OK + /api/files/{id}/content: + get: + tags: + - Files + summary: "GET files › › content" + responses: + "200": + description: OK + /api/free-models: + get: + tags: + - Free models + summary: "GET free models" + responses: + "200": + description: OK + /api/free-provider-rankings: + get: + tags: + - Free provider rankings + summary: "GET free provider rankings" + responses: + "200": + description: OK + /api/free-tier/summary: + get: + tags: + - Free tier + summary: "GET free tier › summary" + responses: + "200": + description: OK + /api/gamification/anomalies: + get: + tags: + - Gamification + summary: "GET gamification › anomalies" + responses: + "200": + description: OK + /api/gamification/badges: + get: + tags: + - Gamification + summary: "GET gamification › badges" + responses: + "200": + description: OK + /api/gamification/badges/earned: + get: + tags: + - Gamification + summary: "GET gamification › badges › earned" + responses: + "200": + description: OK + /api/gamification/federation/leaderboard: + get: + tags: + - Gamification + summary: "GET gamification › federation › leaderboard" + responses: + "200": + description: OK + /api/gamification/federation/score: + post: + tags: + - Gamification + summary: "POST gamification › federation › score" + responses: + "200": + description: OK + /api/gamification/invite: + delete: + tags: + - Gamification + summary: "DELETE gamification › invite" + responses: + "200": + description: OK + get: + tags: + - Gamification + summary: "GET gamification › invite" + responses: + "200": + description: OK + post: + tags: + - Gamification + summary: "POST gamification › invite" + responses: + "200": + description: OK + /api/gamification/invite/redeem: + post: + tags: + - Gamification + summary: "POST gamification › invite › redeem" + responses: + "200": + description: OK + /api/gamification/leaderboard: + get: + tags: + - Gamification + summary: "GET gamification › leaderboard" + responses: + "200": + description: OK + /api/gamification/level: + get: + tags: + - Gamification + summary: "GET gamification › level" + responses: + "200": + description: OK + /api/gamification/notifications: + get: + tags: + - Gamification + summary: "GET gamification › notifications" + responses: + "200": + description: OK + /api/gamification/rotate: + post: + tags: + - Gamification + summary: "POST gamification › rotate" + responses: + "200": + description: OK + /api/gamification/servers: + delete: + tags: + - Gamification + summary: "DELETE gamification › servers" + responses: + "200": + description: OK + get: + tags: + - Gamification + summary: "GET gamification › servers" + responses: + "200": + description: OK + post: + tags: + - Gamification + summary: "POST gamification › servers" + responses: + "200": + description: OK + /api/gamification/stream: + get: + tags: + - Gamification + summary: "GET gamification › stream" + responses: + "200": + description: OK + /api/gamification/transfer: + get: + tags: + - Gamification + summary: "GET gamification › transfer" + responses: + "200": + description: OK + post: + tags: + - Gamification + summary: "POST gamification › transfer" + responses: + "200": + description: OK + /api/github-skills: + get: + tags: + - Github skills + summary: "GET github skills" + responses: + "200": + description: OK + post: + tags: + - Github skills + summary: "POST github skills" + responses: + "200": + description: OK + /api/guardrails: + get: + tags: + - Guardrails + summary: "GET guardrails" + responses: + "200": + description: OK + /api/guardrails/test: + post: + tags: + - Guardrails + summary: "POST guardrails › test" + responses: + "200": + description: OK + /api/headroom/start: + post: + tags: + - Headroom + summary: "POST headroom › start" + x-loopback-only: true + responses: + "200": + description: OK + /api/headroom/status: + get: + tags: + - Headroom + summary: "GET headroom › status" + responses: + "200": + description: OK + /api/headroom/stop: + post: + tags: + - Headroom + summary: "POST headroom › stop" + x-loopback-only: true + responses: + "200": + description: OK + /api/health/degradation: + get: + tags: + - Health + summary: "GET health › degradation" + responses: + "200": + description: OK + /api/health/ping: + get: + tags: + - Health + summary: "GET health › ping" + responses: + "200": + description: OK + /api/intelligence/sync: + delete: + tags: + - Intelligence + summary: "DELETE intelligence › sync" + responses: + "200": + description: OK + get: + tags: + - Intelligence + summary: "GET intelligence › sync" + responses: + "200": + description: OK + post: + tags: + - Intelligence + summary: "POST intelligence › sync" + responses: + "200": + description: OK + /api/internal/codex-responses-ws: + post: + tags: + - Internal + summary: "POST internal › codex responses ws" + responses: + "200": + description: OK + /api/issue-agent/runs: + get: + tags: + - Issue agent + summary: "GET issue agent › runs" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Issue agent + summary: "POST issue agent › runs" + x-loopback-only: true + responses: + "200": + description: OK + /api/keys/{id}/regenerate: + post: + tags: + - Keys + summary: "POST keys › › regenerate" + responses: + "200": + description: OK + /api/keys/{id}/reveal: + get: + tags: + - Keys + summary: "GET keys › › reveal" + responses: + "200": + description: OK + /api/keys/{id}/usage-limits: + get: + tags: + - Keys + summary: "GET keys › › usage limits" + responses: + "200": + description: OK + /api/keys/groups: + get: + tags: + - Keys + summary: "GET keys › groups" + responses: + "200": + description: OK + post: + tags: + - Keys + summary: "POST keys › groups" + responses: + "200": + description: OK + /api/keys/groups/{id}: + delete: + tags: + - Keys + summary: "DELETE keys › groups › " + responses: + "200": + description: OK + get: + tags: + - Keys + summary: "GET keys › groups › " + responses: + "200": + description: OK + put: + tags: + - Keys + summary: "PUT keys › groups › " + description: >- + Updates the group. Setting isActive to false pauses its allow rules while + preserving membership restrictions and deny rules. Other active groups may + still grant access unless a deny matches. Keys belonging only to inactive + groups have no group grants. Keys without groups remain unrestricted by + group policy; per-key restrictions still apply. + responses: + "200": + description: OK + /api/keys/groups/{id}/keys: + delete: + tags: + - Keys + summary: "DELETE keys › groups › › keys" + responses: + "200": + description: OK + get: + tags: + - Keys + summary: "GET keys › groups › › keys" + responses: + "200": + description: OK + post: + tags: + - Keys + summary: "POST keys › groups › › keys" + responses: + "200": + description: OK + /api/keys/groups/{id}/permissions: + delete: + tags: + - Keys + summary: "DELETE keys › groups › › permissions" + responses: + "200": + description: OK + get: + tags: + - Keys + summary: "GET keys › groups › › permissions" + responses: + "200": + description: OK + post: + tags: + - Keys + summary: "POST keys › groups › › permissions" + responses: + "200": + description: OK + /api/local/redis/start: + post: + tags: + - Local + summary: "POST local › redis › start" + x-loopback-only: true + responses: + "200": + description: OK + /api/local/redis/status: + get: + tags: + - Local + summary: "GET local › redis › status" + x-loopback-only: true + responses: + "200": + description: OK + /api/local/redis/stop: + post: + tags: + - Local + summary: "POST local › redis › stop" + x-loopback-only: true + responses: + "200": + description: OK + /api/logs/{id}: + get: + tags: + - Logs + summary: "GET logs › " + responses: + "200": + description: OK + /api/logs/console: + get: + tags: + - Logs + summary: "GET logs › console" + responses: + "200": + description: OK + /api/logs/detail: + get: + tags: + - Logs + summary: "GET logs › detail" + responses: + "200": + description: OK + post: + tags: + - Logs + summary: "POST logs › detail" + responses: + "200": + description: OK + /api/logs/export: + get: + tags: + - Logs + summary: "GET logs › export" + description: >- + Streams a JSON log export. The initial estimatedCount and the + X-OmniRoute-Export-Count header are preflight estimates; + X-OmniRoute-Export-Count-Kind is estimate. The final count is the number + of rows actually emitted in logs, including on mid-stream failure. + Failures also append emitted and a sanitized error. Cap metadata is + available before row iteration. This is a live, best-effort export: + concurrent changes may remove call-log IDs before hydration, and proxy-log + LIMIT/OFFSET pages may repeat or omit rows when inserts/deletes shift pages. + Neither the export nor totalAvailable represents a transactional snapshot. + x-always-protected: true + responses: + "200": + description: OK + /api/mcp/audit: + get: + tags: + - Mcp + summary: "GET mcp › audit" + x-loopback-only: true + responses: + "200": + description: OK + /api/mcp/audit/stats: + get: + tags: + - Mcp + summary: "GET mcp › audit › stats" + x-loopback-only: true + responses: + "200": + description: OK + /api/mcp/sse: + get: + tags: + - Mcp + summary: "GET mcp › sse" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Mcp + summary: "POST mcp › sse" + x-loopback-only: true + responses: + "200": + description: OK + /api/mcp/status: + get: + tags: + - Mcp + summary: "GET mcp › status" + x-loopback-only: true + responses: + "200": + description: OK + /api/mcp/stream: + delete: + tags: + - Mcp + summary: "DELETE mcp › stream" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Mcp + summary: "GET mcp › stream" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Mcp + summary: "POST mcp › stream" + x-loopback-only: true + responses: + "200": + description: OK + /api/mcp/tools: + get: + tags: + - Mcp + summary: "GET mcp › tools" + x-loopback-only: true + responses: + "200": + description: OK + /api/middleware/hooks: + get: + tags: + - Middleware + summary: "GET middleware › hooks" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Middleware + summary: "POST middleware › hooks" + x-loopback-only: true + responses: + "200": + description: OK + /api/middleware/hooks/{name}: + delete: + tags: + - Middleware + summary: "DELETE middleware › hooks › " + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Middleware + summary: "GET middleware › hooks › " + x-loopback-only: true + responses: + "200": + description: OK + put: + tags: + - Middleware + summary: "PUT middleware › hooks › " + x-loopback-only: true + responses: + "200": + description: OK + /api/model-capability-overrides: + delete: + tags: + - Model capability overrides + summary: "DELETE model capability overrides" + responses: + "200": + description: OK + get: + tags: + - Model capability overrides + summary: "GET model capability overrides" + responses: + "200": + description: OK + patch: + tags: + - Model capability overrides + summary: "PATCH model capability overrides" + responses: + "200": + description: OK + /api/model-combo-mappings: + get: + tags: + - Model combo mappings + summary: "GET model combo mappings" + responses: + "200": + description: OK + post: + tags: + - Model combo mappings + summary: "POST model combo mappings" + responses: + "200": + description: OK + /api/model-combo-mappings/{id}: + delete: + tags: + - Model combo mappings + summary: "DELETE model combo mappings › " + responses: + "200": + description: OK + get: + tags: + - Model combo mappings + summary: "GET model combo mappings › " + responses: + "200": + description: OK + put: + tags: + - Model combo mappings + summary: "PUT model combo mappings › " + responses: + "200": + description: OK + /api/models/openrouter-catalog: + get: + tags: + - Models + summary: "GET models › openrouter catalog" + responses: + "200": + description: OK + /api/models/test: + post: + tags: + - Models + summary: "POST models › test" + responses: + "200": + description: OK + /api/models/test-all: + post: + tags: + - Models + summary: "POST models › test all" + responses: + "200": + description: OK + /api/network/info: + get: + tags: + - Network + summary: "GET network › info" + responses: + "200": + description: OK + /api/oauth/{provider}/paste-credentials: + post: + tags: + - Oauth + summary: "POST oauth › › paste credentials" + responses: + "200": + description: OK + /api/oauth/cliproxy-import: + get: + tags: + - Oauth + summary: "GET oauth › cliproxy import" + responses: + "200": + description: OK + post: + tags: + - Oauth + summary: "POST oauth › cliproxy import" + responses: + "200": + description: OK + /api/oauth/codex/import: + post: + tags: + - Oauth + summary: "POST oauth › codex › import" + responses: + "200": + description: OK + /api/oauth/cursor/login/cancel: + post: + tags: + - Oauth + summary: "POST oauth › cursor › login › cancel" + responses: + "200": + description: OK + /api/oauth/cursor/login/poll: + post: + tags: + - Oauth + summary: "POST oauth › cursor › login › poll" + responses: + "200": + description: OK + /api/oauth/cursor/login/start: + post: + tags: + - Oauth + summary: "POST oauth › cursor › login › start" + responses: + "200": + description: OK + /api/oauth/kiro/api-key: + post: + tags: + - Oauth + summary: "POST oauth › kiro › api key" + responses: + "200": + description: OK + /api/oauth/trae/authorize-state: + post: + tags: + - Oauth + summary: "POST oauth › trae › authorize state" + responses: + "200": + description: OK + /api/oauth/trae/import: + get: + tags: + - Oauth + summary: "GET oauth › trae › import" + responses: + "200": + description: OK + post: + tags: + - Oauth + summary: "POST oauth › trae › import" + responses: + "200": + description: OK + /api/omniroute/route/preview: + post: + tags: + - Omniroute + summary: "POST omniroute › route › preview" + responses: + "200": + description: OK + /api/omniroute/status: + get: + tags: + - Omniroute + summary: "GET omniroute › status" + responses: + "200": + description: OK + /api/playground/simulate-route: + post: + tags: + - Playground + summary: "POST playground › simulate route" + responses: + "200": + description: OK + /api/plugins: + get: + tags: + - Plugins + summary: "GET plugins" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Plugins + summary: "POST plugins" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/{name}: + delete: + tags: + - Plugins + summary: "DELETE plugins › " + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Plugins + summary: "GET plugins › " + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/{name}/activate: + post: + tags: + - Plugins + summary: "POST plugins › › activate" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/{name}/config: + get: + tags: + - Plugins + summary: "GET plugins › › config" + x-loopback-only: true + responses: + "200": + description: OK + put: + tags: + - Plugins + summary: "PUT plugins › › config" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/{name}/deactivate: + post: + tags: + - Plugins + summary: "POST plugins › › deactivate" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/marketplace: + get: + tags: + - Plugins + summary: "GET plugins › marketplace" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/marketplace/install: + post: + tags: + - Plugins + summary: "POST plugins › marketplace › install" + x-loopback-only: true + responses: + "200": + description: OK + /api/plugins/scan: + post: + tags: + - Plugins + summary: "POST plugins › scan" + x-loopback-only: true + responses: + "200": + description: OK + /api/pricing/sync: + delete: + tags: + - Pricing + summary: "DELETE pricing › sync" + responses: + "200": + description: OK + get: + tags: + - Pricing + summary: "GET pricing › sync" + responses: + "200": + description: OK + post: + tags: + - Pricing + summary: "POST pricing › sync" + responses: + "200": + description: OK + /api/provider-metrics: + get: + tags: + - Provider metrics + summary: "GET provider metrics" + responses: + "200": + description: OK + /api/provider-stats: + get: + tags: + - Provider stats + summary: "GET provider stats" + responses: + "200": + description: OK + /api/providers/{id}/cc-alias: + get: + tags: + - Providers + summary: "GET providers › › cc alias" + responses: + "200": + description: OK + put: + tags: + - Providers + summary: "PUT providers › › cc alias" + responses: + "200": + description: OK + /api/providers/{id}/chatgpt-web-codex-doctor: + get: + tags: + - Providers + summary: "GET providers › › chatgpt web codex doctor" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/{id}/claude-auth/apply-local: + post: + tags: + - Providers + summary: "POST providers › › claude auth › apply local" + x-always-protected: true + responses: + "200": + description: OK + /api/providers/{id}/claude-auth/export: + post: + tags: + - Providers + summary: "POST providers › › claude auth › export" + x-always-protected: true + responses: + "200": + description: OK + /api/providers/{id}/codex-auth/apply-local: + post: + tags: + - Providers + summary: "POST providers › › codex auth › apply local" + x-always-protected: true + responses: + "200": + description: OK + /api/providers/{id}/codex-auth/export: + post: + tags: + - Providers + summary: "POST providers › › codex auth › export" + x-always-protected: true + responses: + "200": + description: OK + /api/providers/{id}/interception-rules: + delete: + tags: + - Providers + summary: "DELETE providers › › interception rules" + responses: + "200": + description: OK + get: + tags: + - Providers + summary: "GET providers › › interception rules" + responses: + "200": + description: OK + put: + tags: + - Providers + summary: "PUT providers › › interception rules" + responses: + "200": + description: OK + /api/providers/{id}/login: + post: + tags: + - Providers + summary: "POST providers › › login" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/{id}/param-filters: + delete: + tags: + - Providers + summary: "DELETE providers › › param filters" + responses: + "200": + description: OK + get: + tags: + - Providers + summary: "GET providers › › param filters" + responses: + "200": + description: OK + put: + tags: + - Providers + summary: "PUT providers › › param filters" + responses: + "200": + description: OK + /api/providers/{id}/refresh: + post: + tags: + - Providers + summary: "POST providers › › refresh" + responses: + "200": + description: OK + /api/providers/{id}/refresh-cursor: + post: + tags: + - Providers + summary: "POST providers › › refresh cursor" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/{id}/refresh-token: + post: + tags: + - Providers + summary: "POST providers › › refresh token" + responses: + "200": + description: OK + /api/providers/{id}/sync-models: + post: + tags: + - Providers + summary: "POST providers › › sync models" + responses: + "200": + description: OK + /api/providers/bulk: + post: + tags: + - Providers + summary: "POST providers › bulk" + description: "The devin CLI-spawn fallback (webProvidersB.ts) is call-site gated on the trusted peer-locality header — loopback/LAN only (Hard Rules #15/#17, audit #15159 S-01, #15766 shape); the route is management-auth remote-reachable." + responses: + "200": + description: OK + /api/providers/bulk-web-session: + post: + tags: + - Providers + summary: "POST providers › bulk web session" + responses: + "200": + description: OK + /api/providers/claude-auth/import: + post: + tags: + - Providers + summary: "POST providers › claude auth › import" + responses: + "200": + description: OK + /api/providers/claude-auth/import-bulk: + post: + tags: + - Providers + summary: "POST providers › claude auth › import bulk" + responses: + "200": + description: OK + /api/providers/claude-auth/zip-extract: + post: + tags: + - Providers + summary: "POST providers › claude auth › zip extract" + responses: + "200": + description: OK + /api/providers/codex-auth/import: + post: + tags: + - Providers + summary: "POST providers › codex auth › import" + responses: + "200": + description: OK + /api/providers/codex-auth/import-bulk: + post: + tags: + - Providers + summary: "POST providers › codex auth › import bulk" + responses: + "200": + description: OK + /api/providers/codex-auth/zip-extract: + post: + tags: + - Providers + summary: "POST providers › codex auth › zip extract" + responses: + "200": + description: OK + /api/providers/command-code/auth/apply: + post: + tags: + - Providers + summary: "POST providers › command code › auth › apply" + responses: + "200": + description: OK + /api/providers/command-code/auth/callback: + post: + tags: + - Providers + summary: "POST providers › command code › auth › callback" + responses: + "200": + description: OK + /api/providers/command-code/auth/start: + post: + tags: + - Providers + summary: "POST providers › command code › auth › start" + responses: + "200": + description: OK + /api/providers/command-code/auth/status: + get: + tags: + - Providers + summary: "GET providers › command code › auth › status" + responses: + "200": + description: OK + post: + tags: + - Providers + summary: "POST providers › command code › auth › status" + responses: + "200": + description: OK + /api/providers/expiration: + get: + tags: + - Providers + summary: "GET providers › expiration" + responses: + "200": + description: OK + /api/providers/free-onboarding: + get: + tags: + - Providers + summary: "GET providers › free onboarding" + responses: + "200": + description: OK + post: + tags: + - Providers + summary: "POST providers › free onboarding" + responses: + "200": + description: OK + /api/providers/health-autopilot: + get: + tags: + - Providers + summary: "GET providers › health autopilot" + responses: + "200": + description: OK + /api/providers/health-autopilot/actions: + post: + tags: + - Providers + summary: "POST providers › health autopilot › actions" + x-always-protected: true + responses: + "200": + description: OK + /api/providers/health-matrix: + get: + tags: + - Providers + summary: "GET providers › health matrix" + responses: + "200": + description: OK + /api/providers/import: + post: + tags: + - Providers + summary: "POST providers › import" + description: "The devin CLI-spawn fallback (webProvidersB.ts) is call-site gated on the trusted peer-locality header — loopback/LAN only (Hard Rules #15/#17, audit #15159 S-01, #15766 shape); the route is management-auth remote-reachable." + responses: + "200": + description: OK + /api/providers/openrouter-stats: + get: + tags: + - Providers + summary: "GET providers › openrouter stats" + responses: + "200": + description: OK + /api/providers/quota-windows: + get: + tags: + - Providers + summary: "GET providers › quota windows" + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect: + post: + tags: + - Providers + summary: "POST providers › volcengine plan › connect" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect/{sessionId}/cancel: + post: + tags: + - Providers + summary: "POST providers › volcengine plan › connect › › cancel" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect/{sessionId}/code: + post: + tags: + - Providers + summary: "POST providers › volcengine plan › connect › › code" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect/{sessionId}/identity: + post: + tags: + - Providers + summary: "POST providers › volcengine plan › connect › › identity" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect/{sessionId}/resend: + post: + tags: + - Providers + summary: "POST providers › volcengine plan › connect › › resend" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/volcengine-plan/connect/{sessionId}/status: + get: + tags: + - Providers + summary: "GET providers › volcengine plan › connect › › status" + x-loopback-only: true + responses: + "200": + description: OK + /api/providers/web-session-contract: + get: + tags: + - Providers + summary: "GET providers › web session contract" + responses: + "200": + description: OK + /api/providers/zed/discover: + post: + tags: + - Providers + summary: "POST providers › zed › discover" + responses: + "200": + description: OK + /api/providers/zed/import: + post: + tags: + - Providers + summary: "POST providers › zed › import" + responses: + "200": + description: OK + /api/providers/zed/manual-import: + post: + tags: + - Providers + summary: "POST providers › zed › manual import" + responses: + "200": + description: OK + /api/proxy-fallback/test: + post: + tags: + - Proxy fallback + summary: "POST proxy fallback › test" + responses: + "200": + description: OK + /api/quota/groups: + get: + tags: + - Quota + summary: "GET quota › groups" + responses: + "200": + description: OK + post: + tags: + - Quota + summary: "POST quota › groups" + responses: + "200": + description: OK + /api/quota/groups/{id}: + delete: + tags: + - Quota + summary: "DELETE quota › groups › " + responses: + "200": + description: OK + patch: + tags: + - Quota + summary: "PATCH quota › groups › " + responses: + "200": + description: OK + /api/quota/keys/{id}/models: + get: + tags: + - Quota + summary: "GET quota › keys › › models" + responses: + "200": + description: OK + /api/quota/pools/{id}/log: + get: + tags: + - Quota + summary: "GET quota › pools › › log" + responses: + "200": + description: OK + /api/radar/catalog: + get: + tags: + - Radar + summary: "GET radar › catalog" + responses: + "200": + description: OK + /api/radar/intel: + get: + tags: + - Radar + summary: "GET radar › intel" + responses: + "200": + description: OK + /api/radar/intel/sync: + post: + tags: + - Radar + summary: "POST radar › intel › sync" + responses: + "200": + description: OK + /api/radar/local-model-state: + delete: + tags: + - Radar + summary: "DELETE radar › local model state" + responses: + "200": + description: OK + get: + tags: + - Radar + summary: "GET radar › local model state" + responses: + "200": + description: OK + patch: + tags: + - Radar + summary: "PATCH radar › local model state" + responses: + "200": + description: OK + put: + tags: + - Radar + summary: "PUT radar › local model state" + responses: + "200": + description: OK + /api/radar/offers: + get: + tags: + - Radar + summary: "GET radar › offers" + responses: + "200": + description: OK + /api/radar/offers/sync: + post: + tags: + - Radar + summary: "POST radar › offers › sync" + responses: + "200": + description: OK + /api/radar/referrals: + get: + tags: + - Radar + summary: "GET radar › referrals" + responses: + "200": + description: OK + /api/radar/settings: + get: + tags: + - Radar + summary: "GET radar › settings" + responses: + "200": + description: OK + post: + tags: + - Radar + summary: "POST radar › settings" + responses: + "200": + description: OK + /api/radar/status: + get: + tags: + - Radar + summary: "GET radar › status" + responses: + "200": + description: OK + /api/radar/sync: + post: + tags: + - Radar + summary: "POST radar › sync" + responses: + "200": + description: OK + /api/radar/sync-all: + post: + tags: + - Radar + summary: "POST radar › sync all" + responses: + "200": + description: OK + /api/relay/tokens: + get: + tags: + - Relay + summary: "GET relay › tokens" + x-always-protected: true + responses: + "200": + description: OK + post: + tags: + - Relay + summary: "POST relay › tokens" + x-always-protected: true + responses: + "200": + description: OK + /api/relay/tokens/{id}: + delete: + tags: + - Relay + summary: "DELETE relay › tokens › " + x-always-protected: true + responses: + "200": + description: OK + get: + tags: + - Relay + summary: "GET relay › tokens › " + x-always-protected: true + responses: + "200": + description: OK + patch: + tags: + - Relay + summary: "PATCH relay › tokens › " + x-always-protected: true + responses: + "200": + description: OK + /api/resilience/model-cooldowns: + delete: + tags: + - Resilience + summary: "DELETE resilience › model cooldowns" + responses: + "200": + description: OK + get: + tags: + - Resilience + summary: "GET resilience › model cooldowns" + responses: + "200": + description: OK + /api/resilience/cooldowns: + get: + tags: + - Resilience + summary: "GET resilience › cooldowns" + description: >- + Every connection with its cooldown, model lockouts and terminal state, for the + Cooldown Manager page. Never includes credentials. Optional `provider` query filter. + responses: + "200": + description: OK + post: + tags: + - Resilience + summary: "POST resilience › cooldowns" + description: >- + Clears connection cooldowns and model lockouts for `connectionIds`, or for every + connection when `all` is true (optionally within `provider`). Terminal states + (banned, expired, credits_exhausted) are skipped. + responses: + "200": + description: "`{ ok, cleared, unchanged, skippedTerminal, lockoutsCleared }`" + "400": + description: Neither connectionIds nor all was provided + /api/routing/decisions/{requestId}: + get: + tags: + - Routing + summary: "GET routing › decisions › " + responses: + "200": + description: OK + /api/search/providers: + get: + tags: + - Search + summary: "GET search › providers" + responses: + "200": + description: OK + /api/search/stats: + get: + tags: + - Search + summary: "GET search › stats" + responses: + "200": + description: OK + /api/services/9router/models: + get: + tags: + - Services + summary: "GET services › 9router › models" + x-loopback-only: true + responses: + "200": + description: OK + /api/services/9router/provider-expose: + post: + tags: + - Services + summary: "POST services › 9router › provider expose" + x-loopback-only: true + responses: + "200": + description: OK + /api/services/cliproxy/accounts: + get: + tags: + - Services + summary: "GET services › cliproxy › accounts" + x-loopback-only: true + responses: + "200": + description: OK + /api/services/cliproxy/provider-expose: + post: + tags: + - Services + summary: "POST services › cliproxy › provider expose" + x-loopback-only: true + responses: + "200": + description: OK + /api/session-pools: + get: + tags: + - Session pools + summary: "GET session pools" + responses: + "200": + description: OK + /api/session-pools/{provider}: + get: + tags: + - Session pools + summary: "GET session pools › " + responses: + "200": + description: OK + /api/settings/authz-inventory: + get: + tags: + - Settings + summary: "GET settings › authz inventory" + responses: + "200": + description: OK + /api/settings/auto-disable-accounts: + get: + tags: + - Settings + summary: "GET settings › auto disable accounts" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › auto disable accounts" + responses: + "200": + description: OK + /api/settings/background-degradation: + get: + tags: + - Settings + summary: "GET settings › background degradation" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › background degradation" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › background degradation" + responses: + "200": + description: OK + /api/settings/cache-config: + get: + tags: + - Settings + summary: "GET settings › cache config" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › cache config" + responses: + "200": + description: OK + /api/settings/cache-metrics: + delete: + tags: + - Settings + summary: "DELETE settings › cache metrics" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › cache metrics" + responses: + "200": + description: OK + /api/settings/cc-discovery-metrics: + get: + tags: + - Settings + summary: "GET settings › cc discovery metrics" + responses: + "200": + description: OK + /api/settings/compression/rules: + get: + tags: + - Settings + summary: "GET settings › compression › rules" + responses: + "200": + description: OK + /api/settings/compression/run-telemetry: + get: + tags: + - Settings + summary: "GET settings › compression › run telemetry" + responses: + "200": + description: OK + /api/settings/database: + get: + tags: + - Settings + summary: "GET settings › database" + x-always-protected: true + responses: + "200": + description: OK + patch: + tags: + - Settings + summary: "PATCH settings › database" + x-always-protected: true + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › database" + x-always-protected: true + responses: + "200": + description: OK + /api/settings/database/integrity-check: + post: + tags: + - Settings + summary: "POST settings › database › integrity check" + description: >- + Runs SQLite `PRAGMA quick_check` on demand and stores the result. Reading the storage + settings (GET /api/settings/database) only returns the last stored result and never + runs the scan. + x-always-protected: true + responses: + "200": + description: Scan finished; returns the stored result. + content: + application/json: + schema: + type: object + properties: + integrityCheck: + type: string + enum: [ok, error] + lastIntegrityCheckAt: + type: string + format: date-time + "401": + description: Unauthorized + "500": + description: Failed to run integrity check + /api/settings/database/refresh-stats: + post: + tags: + - Settings + summary: "POST settings › database › refresh stats" + responses: + "200": + description: OK + /api/settings/database/vacuum: + get: + tags: + - Settings + summary: "GET settings › database › vacuum" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › database › vacuum" + responses: + "200": + description: OK + /api/settings/export-json: + get: + tags: + - Settings + summary: "GET settings › export json" + x-always-protected: true + responses: + "200": + description: OK + "403": + description: >- + Machine credential refused. Exports carry 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. + /api/settings/favicon: + get: + tags: + - Settings + summary: "GET settings › favicon" + responses: + "200": + description: OK + /api/settings/feature-flags: + delete: + tags: + - Settings + summary: "DELETE settings › feature flags" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › feature flags" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › feature flags" + responses: + "200": + description: OK + /api/settings/free-proxies: + delete: + tags: + - Settings + summary: "DELETE settings › free proxies" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › free proxies" + responses: + "200": + description: OK + /api/settings/free-proxies/{id}/add-to-pool: + post: + tags: + - Settings + summary: "POST settings › free proxies › › add to pool" + responses: + "200": + description: OK + /api/settings/free-proxies/bulk-add-to-pool: + post: + tags: + - Settings + summary: "POST settings › free proxies › bulk add to pool" + responses: + "200": + description: OK + /api/settings/free-proxies/stats: + get: + tags: + - Settings + summary: "GET settings › free proxies › stats" + responses: + "200": + description: OK + /api/settings/free-proxies/sync: + post: + tags: + - Settings + summary: "POST settings › free proxies › sync" + responses: + "200": + description: OK + /api/settings/import-json: + post: + tags: + - Settings + summary: "POST settings › import json" + x-always-protected: true + responses: + "200": + description: OK + "403": + description: >- + Machine credential refused. The import replaces settings, connections and API keys, + so it requires the dashboard session or the local CLI (or login disabled); API keys + of any scope and `oma_` access tokens get 403. + /api/settings/lkgp-cache: + delete: + tags: + - Settings + summary: "DELETE settings › lkgp cache" + responses: + "200": + description: OK + /api/settings/local-corpus: + delete: + tags: + - Settings + summary: "DELETE settings › local corpus" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › local corpus" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › local corpus" + responses: + "200": + description: OK + /api/settings/mitm: + get: + tags: + - Settings + summary: "GET settings › mitm" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › mitm" + x-loopback-only: true + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › mitm" + x-loopback-only: true + responses: + "200": + description: OK + /api/settings/model-aliases: + delete: + tags: + - Settings + summary: "DELETE settings › model aliases" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › model aliases" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › model aliases" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › model aliases" + responses: + "200": + description: OK + /api/settings/models-dev: + get: + tags: + - Settings + summary: "GET settings › models dev" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › models dev" + responses: + "200": + description: OK + /api/settings/notion: + delete: + tags: + - Settings + summary: "DELETE settings › notion" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › notion" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › notion" + responses: + "200": + description: OK + /api/settings/obsidian: + delete: + tags: + - Settings + summary: "DELETE settings › obsidian" + x-always-protected: true + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › obsidian" + x-always-protected: true + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › obsidian" + x-always-protected: true + responses: + "200": + description: OK + /api/settings/obsidian/webdav: + delete: + tags: + - Settings + summary: "DELETE settings › obsidian › webdav" + x-always-protected: true + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › obsidian › webdav" + x-always-protected: true + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › obsidian › webdav" + x-always-protected: true + responses: + "200": + description: OK + /api/settings/oneproxy: + delete: + tags: + - Settings + summary: "DELETE settings › oneproxy" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › oneproxy" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › oneproxy" + responses: + "200": + description: OK + /api/settings/oneproxy/rotate: + post: + tags: + - Settings + summary: "POST settings › oneproxy › rotate" + responses: + "200": + description: OK + /api/settings/proxies: + delete: + tags: + - Settings + summary: "DELETE settings › proxies" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › proxies" + responses: + "200": + description: OK + patch: + tags: + - Settings + summary: "PATCH settings › proxies" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › proxies" + responses: + "200": + description: OK + /api/settings/proxies/{id}/repair-relay: + post: + tags: + - Settings + summary: "POST settings › proxies › › repair relay" + responses: + "200": + description: OK + /api/settings/proxies/assignments: + get: + tags: + - Settings + summary: "GET settings › proxies › assignments" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › proxies › assignments" + responses: + "200": + description: OK + /api/settings/proxies/auto-test: + post: + tags: + - Settings + summary: "POST settings › proxies › auto test" + responses: + "200": + description: OK + /api/settings/proxies/batch-activate: + post: + tags: + - Settings + summary: "POST settings › proxies › batch activate" + responses: + "200": + description: OK + /api/settings/proxies/batch-delete: + post: + tags: + - Settings + summary: "POST settings › proxies › batch delete" + responses: + "200": + description: OK + /api/settings/proxies/bulk-assign: + put: + tags: + - Settings + summary: "PUT settings › proxies › bulk assign" + responses: + "200": + description: OK + /api/settings/proxies/bulk-import: + post: + tags: + - Settings + summary: "POST settings › proxies › bulk import" + responses: + "200": + description: OK + /api/settings/proxies/egress: + get: + tags: + - Settings + summary: "GET settings › proxies › egress" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › proxies › egress" + responses: + "200": + description: OK + /api/settings/proxies/health: + get: + tags: + - Settings + summary: "GET settings › proxies › health" + responses: + "200": + description: OK + /api/settings/proxies/migrate: + post: + tags: + - Settings + summary: "POST settings › proxies › migrate" + responses: + "200": + description: OK + /api/settings/proxies/pool: + delete: + tags: + - Settings + summary: "DELETE settings › proxies › pool" + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › proxies › pool" + responses: + "200": + description: OK + patch: + tags: + - Settings + summary: "PATCH settings › proxies › pool" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › proxies › pool" + responses: + "200": + description: OK + /api/settings/proxies/pool/egress-observation: + get: + tags: + - Settings + summary: "GET settings › proxies › pool › egress observation" + description: >- + Read-only observation of how many distinct egress IPs actually served the members of a + proxy pool over the last 24 h, read from the proxy log (numbers only, never used for + routing). Opt-in through the PROXY_POOL_EGRESS_OBSERVATION feature flag: while it is + off, or when the read fails, the body is JSON null. Results are cached for 30 seconds + per normalized scope (key is read as account, global as the stored global pool). + security: + - ManagementSessionAuth: [] + parameters: + - name: scope + in: query + required: true + schema: + type: string + enum: + - global + - provider + - account + - combo + - key + - name: scopeId + in: query + required: false + description: Required for every scope except global; ignored for global. + schema: + type: string + maxLength: 256 + responses: + "200": + description: The observation, or null when the feature flag is off or the read failed. + content: + application/json: + schema: + type: object + nullable: true + required: + - connections + - distinctExits + - maxConnectionsOnOneExit + - windowHours + properties: + connections: + type: integer + minimum: 0 + description: Distinct OmniRoute connections logged through the pool members. + distinctExits: + type: integer + minimum: 0 + description: Distinct egress IPs observed for those members. + maxConnectionsOnOneExit: + type: integer + minimum: 0 + description: Most connections seen behind one egress IP over the window. + windowHours: + type: integer + example: 24 + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "500": + $ref: "#/components/responses/InternalError" + /api/settings/proxy/cloudflare-deploy: + post: + tags: + - Settings + summary: "POST settings › proxy › cloudflare deploy" + responses: + "200": + description: OK + /api/settings/proxy/deno-deploy: + post: + tags: + - Settings + summary: "POST settings › proxy › deno deploy" + responses: + "200": + description: OK + /api/settings/proxy/vercel-deploy: + post: + tags: + - Settings + summary: "POST settings › proxy › vercel deploy" + responses: + "200": + description: OK + /api/settings/purge-call-logs: + post: + tags: + - Settings + summary: "POST settings › purge call logs" + responses: + "200": + description: OK + /api/settings/purge-detailed-logs: + post: + tags: + - Settings + summary: "POST settings › purge detailed logs" + responses: + "200": + description: OK + /api/settings/purge-logs: + post: + tags: + - Settings + summary: "POST settings › purge logs" + responses: + "200": + description: OK + /api/settings/purge-quota-snapshots: + post: + tags: + - Settings + summary: "POST settings › purge quota snapshots" + responses: + "200": + description: OK + /api/settings/quota/state: + get: + tags: + - Settings + summary: "GET settings › quota › state" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › quota › state" + responses: + "200": + description: OK + /api/settings/reasoning-routing-rules: + get: + tags: + - Settings + summary: "GET settings › reasoning routing rules" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › reasoning routing rules" + responses: + "200": + description: OK + /api/settings/reasoning-routing-rules/{id}: + delete: + tags: + - Settings + summary: "DELETE settings › reasoning routing rules › " + responses: + "200": + description: OK + get: + tags: + - Settings + summary: "GET settings › reasoning routing rules › " + responses: + "200": + description: OK + patch: + tags: + - Settings + summary: "PATCH settings › reasoning routing rules › " + responses: + "200": + description: OK + /api/settings/reasoning-routing-rules/simulate: + post: + tags: + - Settings + summary: "POST settings › reasoning routing rules › simulate" + responses: + "200": + description: OK + /api/settings/task-routing: + get: + tags: + - Settings + summary: "GET settings › task routing" + responses: + "200": + description: OK + post: + tags: + - Settings + summary: "POST settings › task routing" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › task routing" + responses: + "200": + description: OK + /api/settings/tier-config: + get: + tags: + - Settings + summary: "GET settings › tier config" + responses: + "200": + description: OK + put: + tags: + - Settings + summary: "PUT settings › tier config" + responses: + "200": + description: OK + /api/settings/transfer/catalog: + get: + tags: + - Settings + summary: "GET settings › transfer › catalog" + description: >- + Provider accounts selectable for a selective transfer (id, provider, auth type, name, + email, group). Never returns a credential. + x-always-protected: true + responses: + "200": + description: OK + "403": + description: >- + Machine credential refused. Selective transfer requires the dashboard session or + the local CLI (or login disabled); API keys of any scope and `oma_` access tokens + get 403. + /api/settings/transfer/export: + post: + tags: + - Settings + summary: "POST settings › transfer › export" + description: >- + Seals the selected provider accounts, plus the custom provider nodes they use, under + the caller's passphrase (scrypt + AES-256-GCM) and returns the file as an attachment. + Credentials are never written in plaintext; the cleartext header carries counts only. + x-always-protected: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [connectionIds, passphrase, acknowledgeCredentialExport] + properties: + connectionIds: + type: array + minItems: 1 + maxItems: 1000 + items: + type: string + passphrase: + type: string + minLength: 12 + acknowledgeCredentialExport: + type: boolean + enum: [true] + responses: + "200": + description: Transfer file (`format` omniroute-provider-transfer, `version` 1). + "400": + description: Invalid body, missing acknowledgment, or an account that no longer exists. + "403": + description: >- + Machine credential refused. Selective transfer requires the dashboard session or + the local CLI (or login disabled); API keys of any scope and `oma_` access tokens + get 403. + /api/settings/transfer/preview: + post: + tags: + - Settings + summary: "POST settings › transfer › preview" + description: >- + Opens a transfer file with its passphrase and reports, per account and provider node, + whether it is new, identical, in conflict or unsupported on this install, with the + allowed actions (add, skip, replace). Writes nothing and never returns a credential. + x-always-protected: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [bundle, passphrase] + properties: + bundle: + type: object + passphrase: + type: string + responses: + "200": + description: OK + "400": + description: Invalid file, or a wrong passphrase / modified file. + "403": + description: >- + Machine credential refused. Selective transfer requires the dashboard session or + the local CLI (or login disabled); API keys of any scope and `oma_` access tokens + get 403. + /api/settings/transfer/import: + post: + tags: + - Settings + summary: "POST settings › transfer › import" + description: >- + Applies a transfer file with per-item decisions keyed `connection:` / `node:` + (add, skip, replace; omitted keys use the recommended action). Every decision is + validated before the first write, a pre-import database backup is taken, nothing is + deleted, and destination priority, active flag and proxy settings are kept. + x-always-protected: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [bundle, passphrase] + properties: + bundle: + type: object + passphrase: + type: string + decisions: + type: object + additionalProperties: + type: string + enum: [add, skip, replace] + responses: + "200": + description: OK + "400": + description: Invalid file, wrong passphrase, or a decision not allowed for an item. + "403": + description: >- + Machine credential refused. Selective transfer requires the dashboard session or + the local CLI (or login disabled); API keys of any scope and `oma_` access tokens + get 403. + /api/skills: + get: + tags: + - Skills + summary: "GET skills" + responses: + "200": + description: OK + /api/skills/{id}: + delete: + tags: + - Skills + summary: "DELETE skills › " + responses: + "200": + description: OK + put: + tags: + - Skills + summary: "PUT skills › " + responses: + "200": + description: OK + /api/skills/collect/chaos: + post: + tags: + - Skills + summary: "POST skills › collect › chaos" + x-loopback-only: true + responses: + "200": + description: OK + /api/skills/collect/detect: + get: + tags: + - Skills + summary: "GET skills › collect › detect" + x-loopback-only: true + responses: + "200": + description: OK + /api/skills/collect/install: + post: + tags: + - Skills + summary: "POST skills › collect › install" + x-loopback-only: true + responses: + "200": + description: OK + /api/skills/executions: + get: + tags: + - Skills + summary: "GET skills › executions" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Skills + summary: "POST skills › executions" + x-loopback-only: true + responses: + "200": + description: OK + /api/skills/install: + post: + tags: + - Skills + summary: "POST skills › install" + x-loopback-only: true + responses: + "200": + description: OK + /api/skills/marketplace: + get: + tags: + - Skills + summary: "GET skills › marketplace" + responses: + "200": + description: OK + /api/skills/marketplace/install: + post: + tags: + - Skills + summary: "POST skills › marketplace › install" + responses: + "200": + description: OK + /api/skills/skillssh: + get: + tags: + - Skills + summary: "GET skills › skillssh" + responses: + "200": + description: OK + /api/skills/skillssh/install: + post: + tags: + - Skills + summary: "POST skills › skillssh › install" + responses: + "200": + description: OK + /api/sync/bundle: + get: + tags: + - Sync + summary: "GET sync › bundle" + responses: + "200": + description: OK + /api/sync/tokens: + get: + tags: + - Sync + summary: "GET sync › tokens" + x-always-protected: true + responses: + "200": + description: OK + post: + tags: + - Sync + summary: "POST sync › tokens" + x-always-protected: true + responses: + "200": + description: OK + /api/sync/tokens/{id}: + delete: + tags: + - Sync + summary: "DELETE sync › tokens › " + x-always-protected: true + responses: + "200": + description: OK + /api/synced-available-models: + get: + tags: + - Synced available models + summary: "GET synced available models" + responses: + "200": + description: OK + /api/system/env/repair: + get: + tags: + - System + summary: "GET system › env › repair" + responses: + "200": + description: OK + post: + tags: + - System + summary: "POST system › env › repair" + responses: + "200": + description: OK + /api/system/version: + get: + tags: + - System + summary: "GET system › version" + description: >- + Running version, latest published version, auto-update status and npm release channel. + `releaseChannel` and `channels` are additive (rail 3.8.54); `channel` keeps meaning the + deployment mode used by the dashboard updater. + x-loopback-only: true + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + required: [current, latest, updateAvailable, channel, releaseChannel, channels] + properties: + current: { type: string, description: Running OmniRoute version. } + latest: + type: string + description: Latest published version, or "unavailable" when every lookup failed. + updateAvailable: { type: boolean } + channel: + type: string + enum: [npm, source, docker-compose] + description: Deployment mode the auto-updater uses (not the npm release channel). + autoUpdateSupported: { type: boolean } + autoUpdateError: { type: [string, "null"] } + news: { description: Active release-news payload, or null. } + releaseChannel: + type: string + enum: [latest, next, nightly, lts] + description: >- + npm channel of the running build — `nightly` for `-nightly.*`, `next` for other + pre-releases, `lts` for a stable of an older major than `channels.latest`, + otherwise `latest` (same rules as scripts/release/dist-tag.mjs). + channels: + type: object + description: >- + Published head of each npm dist-tag (`npm view omniroute dist-tags`, cached + 10 minutes). `latest` is always present; the others only when the dist-tag + exists. + required: [latest] + properties: + latest: { type: string } + next: { type: string } + nightly: { type: string } + lts: { type: string } + "304": + description: Not modified (If-None-Match matched the ETag). + "401": + description: Unauthorized + post: + tags: + - System + summary: "POST system › version" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/agents/{id}: + get: + tags: + - Tools + summary: "GET tools › agent bridge › agents › " + x-loopback-only: true + responses: + "200": + description: OK + patch: + tags: + - Tools + summary: "PATCH tools › agent bridge › agents › " + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/agents/{id}/detect: + get: + tags: + - Tools + summary: "GET tools › agent bridge › agents › › detect" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/agents/{id}/detected-models: + get: + tags: + - Tools + summary: "GET tools › agent bridge › agents › › detected models" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/cert/download: + get: + tags: + - Tools + summary: "GET tools › agent bridge › cert › download" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/cert/regenerate: + post: + tags: + - Tools + summary: "POST tools › agent bridge › cert › regenerate" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/config: + get: + tags: + - Tools + summary: "GET tools › agent bridge › config" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Tools + summary: "POST tools › agent bridge › config" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/diagnose: + get: + tags: + - Tools + summary: "GET tools › agent bridge › diagnose" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/repair: + post: + tags: + - Tools + summary: "POST tools › agent bridge › repair" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/tproxy: + delete: + tags: + - Tools + summary: "DELETE tools › agent bridge › tproxy" + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Tools + summary: "GET tools › agent bridge › tproxy" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Tools + summary: "POST tools › agent bridge › tproxy" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/agent-bridge/upstream-ca/test: + post: + tags: + - Tools + summary: "POST tools › agent bridge › upstream ca › test" + x-loopback-only: true + responses: + "200": + description: OK + /api/tools/traffic-inspector/sessions/{id}/requests: + post: + tags: + - Tools + summary: "POST tools › traffic inspector › sessions › › requests" + x-loopback-only: true + responses: + "200": + description: OK + /api/translator/transform-stream: + post: + tags: + - Translator + summary: "POST translator › transform stream" + responses: + "200": + description: OK + /api/tunnels/cloudflared: + get: + tags: + - Tunnels + summary: "GET tunnels › cloudflared" + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Tunnels + summary: "POST tunnels › cloudflared" + x-loopback-only: true + responses: + "200": + description: OK + /api/tunnels/ngrok: + get: + tags: + - Tunnels + summary: "GET tunnels › ngrok" + responses: + "200": + description: OK + post: + tags: + - Tunnels + summary: "POST tunnels › ngrok" + responses: + "200": + description: OK + /api/tunnels/tailscale: + get: + tags: + - Tunnels + summary: "GET tunnels › tailscale" + responses: + "200": + description: OK + /api/tunnels/tailscale/check: + get: + tags: + - Tunnels + summary: "GET tunnels › tailscale › check" + responses: + "200": + description: OK + /api/tunnels/tailscale/disable: + post: + tags: + - Tunnels + summary: "POST tunnels › tailscale › disable" + x-loopback-only: true + responses: + "200": + description: OK + /api/tunnels/tailscale/enable: + post: + tags: + - Tunnels + summary: "POST tunnels › tailscale › enable" + x-loopback-only: true + responses: + "200": + description: OK + /api/tunnels/tailscale/install: + post: + tags: + - Tunnels + summary: "POST tunnels › tailscale › install" + x-loopback-only: true + responses: + "200": + description: OK + /api/tunnels/tailscale/login: + post: + tags: + - Tunnels + summary: "POST tunnels › tailscale › login" + x-loopback-only: true + responses: + "200": + description: OK + /api/tunnels/tailscale/start-daemon: + post: + tags: + - Tunnels + summary: "POST tunnels › tailscale › start daemon" + x-loopback-only: true + responses: + "200": + description: OK + /api/upstream-proxy/{providerId}: + delete: + tags: + - Upstream proxy + summary: "DELETE upstream proxy › " + responses: + "200": + description: OK + get: + tags: + - Upstream proxy + summary: "GET upstream proxy › " + responses: + "200": + description: OK + put: + tags: + - Upstream proxy + summary: "PUT upstream proxy › " + responses: + "200": + description: OK + /api/usage/budget/bulk: + get: + tags: + - Usage + summary: "GET usage › budget › bulk" + responses: + "200": + description: OK + /api/usage/codex-reset-credit: + get: + tags: + - Usage + summary: "GET usage › codex reset credit" + responses: + "200": + description: OK + post: + tags: + - Usage + summary: "POST usage › codex reset credit" + responses: + "200": + description: OK + /api/usage/combo-forecast: + get: + tags: + - Usage + summary: "GET usage › combo forecast" + responses: + "200": + description: OK + /api/usage/combo-health: + get: + tags: + - Usage + summary: "GET usage › combo health" + responses: + "200": + description: OK + /api/usage/combo-health-autopilot: + get: + tags: + - Usage + summary: "GET usage › combo health autopilot" + responses: + "200": + description: OK + /api/usage/combo-health-dashboard: + get: + tags: + - Usage + summary: "GET usage › combo health dashboard" + responses: + "200": + description: OK + /api/usage/combo-scoring-inspector: + get: + tags: + - Usage + summary: "GET usage › combo scoring inspector" + responses: + "200": + description: OK + /api/usage/combo-trace/{id}: + get: + tags: + - Usage + summary: "GET usage › combo trace › " + responses: + "200": + description: OK + /api/usage/om-usage: + get: + tags: + - Usage + summary: "GET usage › om usage" + responses: + "200": + description: OK + /api/usage/provider-limits: + get: + tags: + - Usage + summary: "GET usage › provider limits" + responses: + "200": + description: OK + post: + tags: + - Usage + summary: "POST usage › provider limits" + responses: + "200": + description: OK + /api/usage/provider-window-costs: + get: + tags: + - Usage + summary: "GET usage › provider window costs" + responses: + "200": + description: OK + /api/usage/quota: + get: + tags: + - Usage + summary: "GET usage › quota" + responses: + "200": + description: OK + /api/usage/requests-by-provider-date: + get: + tags: + - Usage + summary: "GET usage › requests by provider date" + responses: + "200": + description: OK + /api/usage/route-explain/{id}: + get: + tags: + - Usage + summary: "GET usage › route explain › " + responses: + "200": + description: OK + /api/usage/token-limits: + delete: + tags: + - Usage + summary: "DELETE usage › token limits" + responses: + "200": + description: OK + get: + tags: + - Usage + summary: "GET usage › token limits" + responses: + "200": + description: OK + post: + tags: + - Usage + summary: "POST usage › token limits" + description: >- + Save a per-key token limit. A key holds one limit per scope and reset interval, so daily, + weekly and monthly limits for the same scope coexist. Without `id`, a new limit is added (or + the key's limit for the same scope and interval is updated); with `id`, that limit is updated. + responses: + "200": + description: OK + "404": + description: No token limit with the given `id`. + "409": + description: The update would take another limit's scope and reset interval. + /api/usage/utilization: + get: + tags: + - Usage + summary: "GET usage › utilization" + responses: + "200": + description: OK + /api/v1/{omnirouteCatchAll}: + delete: + tags: + - V1 OmnirouteCatchAll + summary: "DELETE " + responses: + "200": + description: OK + get: + tags: + - V1 OmnirouteCatchAll + summary: "GET " + responses: + "200": + description: OK + patch: + tags: + - V1 OmnirouteCatchAll + summary: "PATCH " + responses: + "200": + description: OK + post: + tags: + - V1 OmnirouteCatchAll + summary: "POST " + responses: + "200": + description: OK + put: + tags: + - V1 OmnirouteCatchAll + summary: "PUT " + responses: + "200": + description: OK + /api/v1/accounts/{id}/limits: + get: + tags: + - V1 Accounts + summary: "GET accounts › › limits" + responses: + "200": + description: OK + put: + tags: + - V1 Accounts + summary: "PUT accounts › › limits" + responses: + "200": + description: OK + /api/v1/agents/credentials: + get: + tags: + - V1 Agents + summary: "GET agents › credentials" + responses: + "200": + description: OK + post: + tags: + - V1 Agents + summary: "POST agents › credentials" + responses: + "200": + description: OK + /api/v1/agents/health: + get: + tags: + - V1 Agents + summary: "GET agents › health" + responses: + "200": + description: OK + /api/v1/agents/tasks: + delete: + tags: + - V1 Agents + summary: "DELETE agents › tasks" + responses: + "200": + description: OK + get: + tags: + - V1 Agents + summary: "GET agents › tasks" + responses: + "200": + description: OK + post: + tags: + - V1 Agents + summary: "POST agents › tasks" + responses: + "200": + description: OK + /api/v1/agents/tasks/{id}: + delete: + tags: + - V1 Agents + summary: "DELETE agents › tasks › " + responses: + "200": + description: OK + get: + tags: + - V1 Agents + summary: "GET agents › tasks › " + responses: + "200": + description: OK + post: + tags: + - V1 Agents + summary: "POST agents › tasks › " + responses: + "200": + description: OK + /api/v1/antigravity: + post: + tags: + - V1 Antigravity + summary: "POST antigravity" + responses: + "200": + description: OK + /api/v1/auto-combo/{channel}/candidates: + get: + tags: + - V1 Auto-combo + summary: "GET auto combo › › candidates" + responses: + "200": + description: OK + /api/v1/batches: + get: + tags: + - V1 Batches + summary: "GET batches" + responses: + "200": + description: OK + post: + tags: + - V1 Batches + summary: "POST batches" + responses: + "200": + description: OK + /api/v1/batches/{id}: + delete: + tags: + - V1 Batches + summary: "DELETE batches › " + responses: + "200": + description: OK + get: + tags: + - V1 Batches + summary: "GET batches › " + responses: + "200": + description: OK + /api/v1/batches/{id}/cancel: + post: + tags: + - V1 Batches + summary: "POST batches › › cancel" + responses: + "200": + description: OK + /api/v1/batches/delete-completed: + delete: + tags: + - V1 Batches + summary: "DELETE batches › delete completed" + responses: + "200": + description: OK + /api/v1/classify: + post: + tags: + - V1 Classify + summary: "POST classify" + responses: + "200": + description: OK + /api/v1/combos: + get: + tags: + - V1 Combos + summary: "GET combos" + responses: + "200": + description: >- + OK. When capabilities are included, each `model` step in `models[]` carries + `supportsVision` (`true`, `false`, or `null` when no capability source knows), + and `capabilities.multimodal` is true exactly when every member reports `true`. + /api/v1/completions: + post: + tags: + - V1 Completions + summary: "POST completions" + responses: + "200": + description: OK + /api/v1/files: + get: + tags: + - V1 Files + summary: "GET files" + responses: + "200": + description: OK + post: + tags: + - V1 Files + summary: "POST files" + responses: + "200": + description: OK + /api/v1/files/{id}: + delete: + tags: + - V1 Files + summary: "DELETE files › " + responses: + "200": + description: OK + get: + tags: + - V1 Files + summary: "GET files › " + responses: + "200": + description: OK + /api/v1/files/{id}/content: + get: + tags: + - V1 Files + summary: "GET files › › content" + responses: + "200": + description: OK + /api/v1/images/edits: + post: + tags: + - V1 Images + summary: "POST images › edits" + responses: + "200": + description: OK + /api/v1/images/upscale: + get: + tags: + - V1 Images + summary: "GET images › upscale" + responses: + "200": + description: OK + post: + tags: + - V1 Images + summary: "POST images › upscale" + responses: + "200": + description: OK + /api/v1/media/{id}: + get: + tags: + - V1 Media + summary: "GET media › " + description: >- + Serves media that a video job stored on this gateway because the provider's own result URL + is auth-gated (OpenRouter `unsigned_urls`). The id is returned in the `url` of a + `/api/v1/videos/generations` response. Readable by the API key that generated it, a + manage-scope key or the dashboard session (same ownership rule as `/api/v1/files/{id}/content`); + anything else, an unknown id or an expired file (call-log retention window) answers 404. + parameters: + - name: id + in: path + required: true + schema: + type: string + pattern: "^([a-f0-9]{16}|none)__[a-f0-9]{32}\\.[a-z0-9]{2,5}$" + responses: + "200": + description: The stored media bytes (e.g. `video/mp4`), served inline. + "401": + description: Invalid API key. + "404": + description: Media not found, expired or not owned by the caller. + /api/v1/issues/report: + post: + tags: + - V1 Issues + summary: "POST issues › report" + responses: + "200": + description: OK + /api/v1/management/proxies: + delete: + tags: + - V1 Management + summary: "DELETE management › proxies" + responses: + "200": + description: OK + get: + tags: + - V1 Management + summary: "GET management › proxies" + responses: + "200": + description: OK + patch: + tags: + - V1 Management + summary: "PATCH management › proxies" + responses: + "200": + description: OK + post: + tags: + - V1 Management + summary: "POST management › proxies" + responses: + "200": + description: OK + /api/v1/management/proxies/assignments: + get: + tags: + - V1 Management + summary: "GET management › proxies › assignments" + responses: + "200": + description: OK + put: + tags: + - V1 Management + summary: "PUT management › proxies › assignments" + responses: + "200": + description: OK + /api/v1/management/proxies/bulk-assign: + put: + tags: + - V1 Management + summary: "PUT management › proxies › bulk assign" + responses: + "200": + description: OK + /api/v1/management/proxies/health: + get: + tags: + - V1 Management + summary: "GET management › proxies › health" + responses: + "200": + description: OK + /api/v1/me/status: + get: + tags: + - V1 Me + summary: "GET me › status" + responses: + "200": + description: OK + /api/v1/models/{model}: + get: + tags: + - V1 Models + summary: "GET models › " + responses: + "200": + description: OK + /api/v1/muse-code/models: + get: + tags: + - V1 Muse-code + summary: "GET muse code › models" + responses: + "200": + description: OK + /api/v1/music/generations: + get: + tags: + - V1 Music + summary: "GET music › generations" + responses: + "200": + description: OK + post: + tags: + - V1 Music + summary: "POST music › generations" + responses: + "200": + description: OK + /api/v1/providers/{provider}/limits: + get: + tags: + - V1 Providers + summary: "GET providers › › limits" + responses: + "200": + description: OK + put: + tags: + - V1 Providers + summary: "PUT providers › › limits" + responses: + "200": + description: OK + /api/v1/quotas/check: + get: + tags: + - V1 Quotas + summary: "GET quotas › check" + responses: + "200": + description: OK + /api/v1/registered-keys: + get: + tags: + - V1 Registered-keys + summary: "GET registered keys" + description: > + Management-only, like `/api/keys`: a dashboard session, the local CLI token, or an + API key with `manage`/`admin` scope. Inference-only API keys get 403. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + responses: + "200": + description: OK + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + post: + tags: + - V1 Registered-keys + summary: "POST registered keys" + description: > + Management-only, like `/api/keys`: a dashboard session, the local CLI token, or an + API key with `manage`/`admin` scope. Inference-only API keys get 403. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + responses: + "200": + description: OK + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + /api/v1/registered-keys/{id}: + delete: + tags: + - V1 Registered-keys + summary: "DELETE registered keys › " + description: > + Management-only, like `/api/keys`: a dashboard session, the local CLI token, or an + API key with `manage`/`admin` scope. Inference-only API keys get 403. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + responses: + "200": + description: OK + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + get: + tags: + - V1 Registered-keys + summary: "GET registered keys › " + description: > + Management-only, like `/api/keys`: a dashboard session, the local CLI token, or an + API key with `manage`/`admin` scope. Inference-only API keys get 403. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + responses: + "200": + description: OK + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + /api/v1/registered-keys/{id}/revoke: + post: + tags: + - V1 Registered-keys + summary: "POST registered keys › › revoke" + description: > + Management-only, like `/api/keys`: a dashboard session, the local CLI token, or an + API key with `manage`/`admin` scope. Inference-only API keys get 403. + security: + - BearerAuth: [] + - ManagementSessionAuth: [] + responses: + "200": + description: OK + "401": + $ref: "#/components/responses/ManagementAuthenticationRequired" + "403": + $ref: "#/components/responses/ManagementInvalidToken" + /api/v1/relay/chat/completions: + post: + tags: + - V1 Relay + summary: "POST relay › chat › completions" + responses: + "200": + description: OK + /api/v1/relay/chat/completions/bifrost: + post: + tags: + - V1 Relay + summary: "POST relay › chat › completions › bifrost" + responses: + "200": + description: OK + /api/v1/responses/{path}: + post: + tags: + - V1 Responses + summary: "POST responses › " + responses: + "200": + description: OK + /api/v1/search/analytics: + get: + tags: + - V1 Search + summary: "GET search › analytics" + responses: + "200": + description: OK + /api/v1/segment: + post: + tags: + - V1 Segment + summary: "POST segment" + responses: + "200": + description: OK + /api/v1/video-bridge/drilldown: + delete: + tags: + - V1 Video-bridge + summary: "DELETE video bridge › drilldown" + responses: + "200": + description: OK + get: + tags: + - V1 Video-bridge + summary: "GET video bridge › drilldown" + responses: + "200": + description: OK + /api/v1/videos/generations: + get: + tags: + - V1 Videos + summary: "GET videos › generations" + responses: + "200": + description: OK + post: + tags: + - V1 Videos + summary: "POST videos › generations" + responses: + "200": + description: OK + /api/v1/vscode/{token}: + get: + tags: + - V1 Vscode + summary: "GET vscode › " + responses: + "200": + description: OK + /api/v1/vscode/{token}/api/chat: + post: + tags: + - V1 Vscode + summary: "POST vscode › › api › chat" + responses: + "200": + description: OK + /api/v1/vscode/{token}/api/show: + post: + tags: + - V1 Vscode + summary: "POST vscode › › api › show" + responses: + "200": + description: OK + /api/v1/vscode/{token}/api/tags: + get: + tags: + - V1 Vscode + summary: "GET vscode › › api › tags" + responses: + "200": + description: OK + /api/v1/vscode/{token}/api/version: + get: + tags: + - V1 Vscode + summary: "GET vscode › › api › version" + responses: + "200": + description: OK + /api/v1/vscode/{token}/chat/completions: + post: + tags: + - V1 Vscode + summary: "POST vscode › › chat › completions" + responses: + "200": + description: OK + /api/v1/vscode/{token}/combos: + get: + tags: + - V1 Vscode + summary: "GET vscode › › combos" + responses: + "200": + description: OK + /api/v1/vscode/{token}/models: + get: + tags: + - V1 Vscode + summary: "GET vscode › › models" + responses: + "200": + description: OK + /api/v1/vscode/{token}/responses: + post: + tags: + - V1 Vscode + summary: "POST vscode › › responses" + responses: + "200": + description: OK + /api/v1/vscode/{token}/v1/chat/completions: + post: + tags: + - V1 Vscode + summary: "POST vscode › › v1 › chat › completions" + responses: + "200": + description: OK + /api/v1/vscode/{token}/v1/models: + get: + tags: + - V1 Vscode + summary: "GET vscode › › v1 › models" + responses: + "200": + description: OK + /api/v1/vscode/combos/{token}/{{slug}}: + get: + tags: + - V1 Vscode + summary: "GET vscode › combos › › <{slug>}" + responses: + "200": + description: OK + post: + tags: + - V1 Vscode + summary: "POST vscode › combos › › <{slug>}" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › " + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/api/chat: + post: + tags: + - V1 Vscode + summary: "POST vscode › raw › › api › chat" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/api/show: + post: + tags: + - V1 Vscode + summary: "POST vscode › raw › › api › show" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/api/tags: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › › api › tags" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/api/version: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › › api › version" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/chat/completions: + post: + tags: + - V1 Vscode + summary: "POST vscode › raw › › chat › completions" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/combos: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › › combos" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/models: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › › models" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/responses: + post: + tags: + - V1 Vscode + summary: "POST vscode › raw › › responses" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/v1/chat/completions: + post: + tags: + - V1 Vscode + summary: "POST vscode › raw › › v1 › chat › completions" + responses: + "200": + description: OK + /api/v1/vscode/raw/{token}/v1/models: + get: + tags: + - V1 Vscode + summary: "GET vscode › raw › › v1 › models" + responses: + "200": + description: OK + /api/v1/web/fetch: + post: + tags: + - V1 Web + summary: "POST web › fetch" + responses: + "200": + description: OK + /api/version-manager/check-update: + get: + tags: + - Version manager + summary: "GET version manager › check update" + responses: + "200": + description: OK + /api/version-manager/install: + post: + tags: + - Version manager + summary: "POST version manager › install" + x-loopback-only: true + responses: + "200": + description: OK + /api/version-manager/restart: + post: + tags: + - Version manager + summary: "POST version manager › restart" + x-loopback-only: true + responses: + "200": + description: OK + /api/version-manager/start: + post: + tags: + - Version manager + summary: "POST version manager › start" + x-loopback-only: true + responses: + "200": + description: OK + /api/version-manager/status: + get: + tags: + - Version manager + summary: "GET version manager › status" + responses: + "200": + description: OK + /api/version-manager/stop: + post: + tags: + - Version manager + summary: "POST version manager › stop" + x-loopback-only: true + responses: + "200": + description: OK + /api/vnc-session: + get: + tags: + - Vnc session + summary: "GET vnc session" + x-loopback-only: true + responses: + "200": + description: OK + /api/vnc-session/{params}: + delete: + tags: + - Vnc session + summary: "DELETE vnc session › " + x-loopback-only: true + responses: + "200": + description: OK + get: + tags: + - Vnc session + summary: "GET vnc session › " + x-loopback-only: true + responses: + "200": + description: OK + post: + tags: + - Vnc session + summary: "POST vnc session › " + x-loopback-only: true + responses: + "200": + description: OK + /api/webhooks: + get: + tags: + - Webhooks + summary: "GET webhooks" + responses: + "200": + description: OK + post: + tags: + - Webhooks + summary: "POST webhooks" + responses: + "200": + description: OK + /api/webhooks/{id}: + delete: + tags: + - Webhooks + summary: "DELETE webhooks › " + responses: + "200": + description: OK + get: + tags: + - Webhooks + summary: "GET webhooks › " + responses: + "200": + description: OK + put: + tags: + - Webhooks + summary: "PUT webhooks › " + responses: + "200": + description: OK + /api/webhooks/{id}/deliveries: + get: + tags: + - Webhooks + summary: "GET webhooks › › deliveries" + responses: + "200": + description: OK + /api/webhooks/{id}/test: + post: + tags: + - Webhooks + summary: "POST webhooks › › test" responses: "200": - description: Coverage stats - content: - application/json: - schema: - $ref: "#/components/schemas/SkillCoverage" - "500": - $ref: "#/components/responses/InternalError" - - /api/agent-skills/generate: + description: OK + /api/webhooks/validate-url: post: - tags: [Agent Skills] - summary: Trigger SKILL.md generator - description: | - Runs the Agent Skills generator which writes `skills/{id}/SKILL.md` for - all 42 catalog entries (or a subset via `onlyIds`). Preserves - ` ... ` blocks. - **Requires management authentication.** - security: - - BearerAuth: [] - - ManagementSessionAuth: [] - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - dryRun: - type: boolean - default: true - description: "If true, reports what would be generated without writing files" - prune: - type: boolean - default: false - description: "If true, deletes skill directories not in the catalog" - onlyIds: - type: array - items: - type: string - description: "If provided, only regenerate these skill IDs" + tags: + - Webhooks + summary: "POST webhooks › validate url" responses: "200": - description: Generator report - content: - application/json: - schema: - type: object - required: [generated, unchanged, pruned, orphansDetected, errors] - properties: - generated: - type: array - items: - type: string - description: IDs that got new/updated SKILL.md - unchanged: - type: array - items: - type: string - description: IDs whose content was already up to date - pruned: - type: array - items: - type: string - description: IDs whose directories were deleted (prune mode) - orphansDetected: - type: array - items: - type: string - description: Directories found in skills/ not in the catalog - errors: - type: array - items: - type: object - required: [id, error] - properties: - id: - type: string - error: - type: string - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" - "503": - description: Generator module not available + description: OK components: securitySchemes: @@ -5371,6 +15211,186 @@ components: requestId: 0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d schemas: + ElevenLabsSoundGeneration: + type: object + additionalProperties: true + required: [text] + properties: + text: + type: string + minLength: 1 + model_id: + type: string + nullable: true + default: eleven_text_to_sound_v2 + duration_seconds: + type: number + nullable: true + minimum: 0.5 + maximum: 30 + prompt_influence: + type: number + nullable: true + minimum: 0 + maximum: 1 + loop: + type: boolean + ElevenLabsMusicGeneration: + type: object + additionalProperties: true + description: Provide exactly one of prompt or composition_plan. Plan internals are validated by ElevenLabs. + oneOf: + - required: [prompt] + properties: + prompt: + type: string + composition_plan: + nullable: true + enum: [null] + - required: [composition_plan] + properties: + composition_plan: + type: object + prompt: + nullable: true + enum: [null] + properties: + prompt: + type: string + nullable: true + minLength: 1 + maxLength: 4100 + composition_plan: + type: object + nullable: true + additionalProperties: true + model_id: + type: string + nullable: true + default: music_v1 + music_length_ms: + type: integer + nullable: true + minimum: 3000 + maximum: 600000 + force_instrumental: + type: boolean + respect_sections_durations: + type: boolean + seed: + type: integer + nullable: true + minimum: 0 + maximum: 2147483647 + ElevenLabsMusicPlan: + type: object + additionalProperties: true + required: [prompt] + properties: + prompt: + type: string + minLength: 1 + maxLength: 4100 + model_id: + type: string + nullable: true + default: music_v1 + music_length_ms: + type: integer + nullable: true + minimum: 3000 + maximum: 600000 + source_composition_plan: + type: object + nullable: true + additionalProperties: true + ExclusiveConnectionLeaseLifecycle: + type: object + description: >- + Shared lease lifecycle response. The optional connection object is present only for an + explicit, successful owner-authenticated status action; acquire, renew, and release never + include connection metadata. + required: [state, generation, acquiredAt, renewedAt, expiresAt] + properties: + state: { type: string, enum: [ACTIVE, RELEASED] } + generation: { type: integer, minimum: 1 } + acquiredAt: { type: string, format: date-time } + renewedAt: { type: string, format: date-time } + expiresAt: { type: string, format: date-time } + connection: + type: object + description: >- + Privacy-safe metadata for the exact active binding. Wrong-key, wrong-owner, + stale-generation, missing, expired, released, or invalidated leases return a generic + 409 without this object. No credential, internal id, owner identity, or email fallback + is serialized. + required: [displayName, provider] + properties: + displayName: + type: [string, "null"] + description: Trimmed operator-configured name, or null when no privacy-safe name exists. + provider: + type: string + minLength: 1 + description: Non-sensitive provider display label, never a generated compatible-provider id. + ExclusiveConnectionLeaseCapacity: + type: object + required: [state, error, reason, retryAfter, eligibleCount, freeCount] + properties: + state: { type: string, const: WAITING_FOR_CAPACITY } + error: + type: object + required: [type, code, message] + properties: + type: { type: string, const: lease_error } + code: { type: string, const: LEASE_CAPACITY_UNAVAILABLE } + message: { type: string } + reason: { type: string, const: NO_FREE_ELIGIBLE_CONNECTION } + retryAfter: { type: integer, minimum: 1, maximum: 3600 } + eligibleCount: { type: integer, minimum: 0 } + freeCount: { type: integer, minimum: 0 } + EmbeddingMultimodalItem: + oneOf: + - type: object + required: [type, text] + properties: + type: + type: string + const: text + text: + type: string + minLength: 1 + - type: object + required: [type, source] + properties: + type: + type: string + enum: [image, audio, video, document] + source: + oneOf: + - type: object + required: [type, url] + properties: + type: + type: string + const: url + url: + type: string + format: uri + description: Public HTTPS URL. + - type: object + required: [type, data, media_type] + properties: + type: + type: string + const: base64 + data: + type: string + format: byte + media_type: + type: string + description: Provider-neutral structured embedding input item. + PlaygroundPreset: type: object required: @@ -5613,12 +15633,12 @@ components: type: string format: uri description: GitHub raw URL of the SKILL.md file - example: "https://raw.githubusercontent.com/cheaper-inference/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md" + example: "https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md" githubUrl: type: string format: uri description: GitHub blob URL for viewing the SKILL.md in the browser - example: "https://github.com/cheaper-inference/OmniRoute/blob/main/skills/omni-providers/SKILL.md" + example: "https://github.com/diegosouzapw/OmniRoute/blob/main/skills/omni-providers/SKILL.md" SkillCoverage: type: object @@ -6582,6 +16602,25 @@ components: system: type: string + ModelHealth: + type: object + description: >- + Per-model pause state. `ok` means no pause observed by this instance; + `unknown` marks ids that resolve to no routable provider model. + properties: + id: + type: string + state: + type: string + enum: [ok, cooling, unknown] + retry_after: + type: integer + description: Shortest remaining lockout in seconds, rounded up; only when cooling. + nullable: true + observed_at: + type: string + format: date-time + Model: type: object properties: @@ -6592,6 +16631,132 @@ components: example: model owned_by: type: string + pricing: + $ref: "#/components/schemas/ModelPricing" + + ModelPricing: + type: object + description: >- + Model price from the same pricing table OmniRoute uses for cost accounting. + Omitted when the price is unknown. A combo carries the price of its first + visible target. A model with no pricing row that the catalog marks free is + `"0"`. The string fields are OpenRouter-compatible (USD per token, plain + decimals, no exponent). The numeric fields are USD per 1M tokens. + Credit-metered providers (billed per request) carry only the numeric fields, + so `prompt` and `completion` are absent for them. + required: [input, output] + properties: + prompt: + type: string + description: USD per input token. + example: "0.000005" + completion: + type: string + description: USD per output token. + example: "0.000025" + input_cache_read: + type: string + description: USD per cached input token read. Present when there is a cache price. + example: "0.0000005" + input_cache_write: + type: string + description: USD per input token written to the cache. Present when there is a cache price. + example: "0.00000625" + input: + type: number + description: USD per 1M input tokens. + example: 5 + output: + type: number + description: USD per 1M output tokens. + example: 25 + cached: + type: number + description: USD per 1M cached input tokens read. + cache_creation: + type: number + description: USD per 1M input tokens written to the cache. + reasoning: + type: number + description: USD per 1M reasoning tokens, when the pricing row sets one. + long_context: + type: object + additionalProperties: true + description: Long-context rate tier from the pricing row, when it has one. + + ProxySubscription: + type: object + description: >- + An operator-supplied proxy subscription link (Karing-style). `url` is + redacted (credentials stripped) whenever the record is returned by the API. + properties: + id: + type: string + name: + type: string + url: + type: string + description: >- + Redacted subscription URL. May be a local/loopback address + (e.g. `http://127.0.0.1:8080/list`) — local-first fetch targets + are allowed by default (`OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS`); + cloud-metadata / link-local endpoints (169.254.0.0/16) are always + blocked. + enabled: + type: boolean + mode: + type: string + enum: [global, rule] + ruleProviders: + type: array + items: + type: string + nullable: true + localCoreEndpoint: + type: string + nullable: true + coreConfigPath: + type: string + nullable: true + description: >- + Absolute adopted config path: lowercase `.json` renders + sing-box, `.yaml`/`.yml` renders Mihomo. Empty means no + generation. + coreBinaryPath: + type: string + nullable: true + description: >- + Absolute path of the core binary used to verify the generated + file before replacing the adopted file. Null means write + beside it only. + updateIntervalMinutes: + type: integer + lastFetchedAt: + type: string + format: date-time + nullable: true + status: + type: string + enum: [ok, error, empty] + error: + type: string + nullable: true + lastNodes: + type: array + items: {} + nullable: true + lastErrorAt: + type: string + format: date-time + nullable: true + consecutiveFailures: + type: integer + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time ProviderConnection: type: object @@ -6658,12 +16823,20 @@ components: ComboCreate: type: object - required: [name, model] + required: [name, models] properties: name: type: string - model: - type: string + models: + type: array + minItems: 1 + items: + oneOf: + - type: string + description: "provider/model reference" + - type: object + description: "structured combo step (provider, model, weight, ...)" + additionalProperties: true strategy: type: string enum: @@ -6677,19 +16850,28 @@ components: - least-used - cost-optimized - reset-aware + - reset-window + - headroom - strict-random - auto - lkgp - context-optimized + - fusion default: priority - nodes: - type: array - items: - type: object - properties: - connectionId: - type: string - weight: - type: integer - priority: - type: integer + universal_handoff: + type: object + nullable: true + description: >- + Per-combo universal handoff config (the runtime also accepts the camelCase + `universalHandoff` alias). Omit to fall back to the global/default config; + `enabled: false` stops session-model history writes for this combo. + additionalProperties: false + properties: + enabled: { type: boolean } + trigger: { type: string, enum: [on-switch, always, on-error] } + providerAllowlist: { type: array, items: { type: string } } + maxMessagesForSummary: { type: integer, minimum: 5, maximum: 100 } + handoffModel: { type: string } + ttlMinutes: { type: integer, minimum: 1, maximum: 10080 } + preserveSystemPrompt: { type: boolean } + relayMode: { type: string, enum: [schema-locked, standard] } diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index beef46b864a1..b6ab9d586e1b 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -49,6 +49,7 @@ import fs from "node:fs/promises"; import fsSync from "node:fs"; import path from "node:path"; +import { writePublicOpenApi } from "./publicOpenApi.mjs"; import { colocateLlmlinguaOptionals, SEED_PACKAGES } from "./colocateOptionals.mjs"; import { WREQ_JS_NATIVE_BINDINGS } from "./wreqJsNative.mjs"; @@ -718,6 +719,7 @@ function copyStaticAndPublic({ distDir, relDistDir, projectRoot, resolvedOutDir const publicSrc = path.join(projectRoot, "public"); if (fsSync.existsSync(publicSrc)) { fsSync.cpSync(publicSrc, path.join(resolvedOutDir, "public"), { recursive: true, force: true }); + writePublicOpenApi(projectRoot, resolvedOutDir); stampServiceWorkerBuildId(resolvedOutDir); } } @@ -1215,6 +1217,7 @@ export function assembleStandalone({ // overwrites the build-id stamp copyStaticAndPublic just wrote. Stamp // again after that copy, or sw.js ships with the generic cache name and // a browser keeps the previous deploy's worker. + writePublicOpenApi(projectRoot, resolvedOutDir); stampServiceWorkerBuildId(resolvedOutDir); // Repair hollow externalized package dirs in BOTH locations Turbopack's standalone // tracer can populate: the top-level bundle node_modules, and — for projects with a diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs index b19d105f6a66..6003bf039f61 100644 --- a/scripts/build/build-next-isolated.mjs +++ b/scripts/build/build-next-isolated.mjs @@ -6,6 +6,7 @@ import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { pathToFileURL } from "node:url"; +import { writePublicOpenApi } from "./publicOpenApi.mjs"; import { generateGlobalErrorMessages } from "../i18n/generate-global-error-messages.mjs"; import { assembleStandalone, @@ -303,6 +304,7 @@ export async function main() { try { generateGlobalErrorMessages(projectRoot); + writePublicOpenApi(projectRoot); for (const entry of transientBuildPaths) { if (!(await exists(entry.sourcePath))) continue; await movePath(entry.sourcePath, entry.backupPath); diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 3f8fbb22f391..f33e739e6857 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -162,6 +162,7 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [ "open-sse/utils/setupPolyfill.ts", "package.json", "scripts/build/assembleStandalone.mjs", + "scripts/build/publicOpenApi.mjs", "scripts/build/backendOnlyPages.mjs", "scripts/build/build-tproxy-native.mjs", "scripts/build/build-next-isolated.mjs", diff --git a/scripts/build/publicOpenApi.mjs b/scripts/build/publicOpenApi.mjs new file mode 100644 index 000000000000..0caf402a3e54 --- /dev/null +++ b/scripts/build/publicOpenApi.mjs @@ -0,0 +1,36 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import * as yaml from "js-yaml"; + +/** Refresh the deployed spec from its canonical source without trusting request headers. */ +export function writePublicOpenApi(projectRoot, outputRoot = projectRoot) { + const source = join(projectRoot, "docs", "openapi.yaml"); + // Synthetic packaging fixtures and installed standalone bundles have no source docs. + if (!existsSync(source)) return false; + const original = readFileSync(source, "utf8"); + const spec = yaml.load(original); + const { version } = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")); + if (!spec?.info || !spec.paths || typeof version !== "string" || !version) { + throw new Error("Invalid OpenAPI source or package version"); + } + spec.info.version = version; + // OAS 3.1 Server Object: relative to the URL of /[basePath/]openapi.yaml. + spec.servers = [{ url: "./", description: "This deployment" }]; + // Preserve canonical formatting and comments; only deployment metadata differs. + const content = original + .replace(/^info:\r?\n[\s\S]*?(?=^\S)/m, (info) => + info.replace(/^ version:.*$/m, ` version: ${yaml.dump(version).trim()}`) + ) + .replace( + /^servers:\r?\n(?:[ \t].*(?:\r?\n|$)|\r?\n)*/m, + yaml.dump({ servers: spec.servers }) + "\n" + ); + if (JSON.stringify(yaml.load(content)) !== JSON.stringify(spec)) { + throw new Error("Unable to update OpenAPI deployment metadata without changing its contract"); + } + const target = join(outputRoot, "public", "openapi.yaml"); + if (existsSync(target) && readFileSync(target, "utf8") === content) return false; + mkdirSync(join(outputRoot, "public"), { recursive: true }); + writeFileSync(target, content); + return true; +} diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index f288528fdce5..149860e59739 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -4,6 +4,7 @@ import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import next from "next"; +import { writePublicOpenApi } from "../build/publicOpenApi.mjs"; import { bootstrapEnv } from "../build/bootstrap-env.mjs"; import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mjs"; import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs"; @@ -53,6 +54,7 @@ const dev = mode === "dev"; // Self-heal a stale better-sqlite3 native binary after a Node version switch // (nvm 22 <-> 24) before bootstrap touches the DB. No-op when the ABI matches. if (dev) { + writePublicOpenApi(process.cwd()); ensureNativeSqlite(); } diff --git a/tests/unit/build/public-openapi-artifact-18331.test.ts b/tests/unit/build/public-openapi-artifact-18331.test.ts new file mode 100644 index 000000000000..761be119a4d4 --- /dev/null +++ b/tests/unit/build/public-openapi-artifact-18331.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import * as yaml from "js-yaml"; +import { assembleStandalone } from "../../../scripts/build/assembleStandalone.mjs"; +import { writePublicOpenApi } from "../../../scripts/build/publicOpenApi.mjs"; + +interface Spec { + info: { version: string }; + servers: { url: string }[]; + paths: Record; +} +const root = new URL("../../../", import.meta.url); +const spec = yaml.load(readFileSync(new URL("public/openapi.yaml", root), "utf8")) as Spec; +const pkg = JSON.parse(readFileSync(new URL("package.json", root), "utf8")) as { version: string }; + +test("public OpenAPI describes the running build version", () => { + assert.equal(spec.info.version, pkg.version); +}); + +for (const documentUrl of [ + "https://api.example.test/openapi.yaml", + "https://api.example.test/omniroute/openapi.yaml", + "http://localhost:21234/openapi.yaml", +]) { + test(`OpenAPI requests stay on the deployment serving ${documentUrl}`, () => { + const server = new URL(spec.servers[0].url, documentUrl); + assert.equal( + server.href.replace(/\/$/, "") + "/api/v1/models", + documentUrl.replace(/\/openapi\.yaml$/, "/api/v1/models") + ); + }); +} + +test("public spec preserves canonical operations, schemas and security", () => { + const canonical = yaml.load(readFileSync(new URL("docs/openapi.yaml", root), "utf8")) as Spec; + assert.deepEqual( + { ...spec, info: undefined, servers: undefined }, + { ...canonical, info: undefined, servers: undefined } + ); + assert.deepEqual({ ...spec.info, version: undefined }, { ...canonical.info, version: undefined }); +}); + +test("packaging refreshes stale metadata and repeated generation does not rewrite the artifact", () => { + const projectRoot = mkdtempSync(join(tmpdir(), "openapi-18331-")); + try { + const distDir = join(projectRoot, ".build", "next"); + const outDir = join(projectRoot, "out"); + mkdirSync(join(distDir, "standalone"), { recursive: true }); + mkdirSync(join(projectRoot, "docs")); + mkdirSync(join(projectRoot, "public")); + writeFileSync(join(projectRoot, "package.json"), JSON.stringify({ version: "9.8.7" })); + writeFileSync( + join(projectRoot, "docs", "openapi.yaml"), + "openapi: 3.1.0\ninfo:\n title: Fixture\n version: old\nservers:\n - url: http://localhost:20128\npaths: {}\n" + ); + writeFileSync(join(projectRoot, "public", "openapi.yaml"), "stale public artifact"); + assembleStandalone({ projectRoot, distDir, outDir, copyNatives: false }); + const target = join(outDir, "public", "openapi.yaml"); + const packaged = yaml.load(readFileSync(target, "utf8")) as Spec; + assert.equal(packaged.info.version, "9.8.7"); + assert.equal( + new URL(packaged.servers[0].url, "https://proxy.test/nested/base/openapi.yaml").href, + "https://proxy.test/nested/base/" + ); + const before = statSync(target).mtimeMs; + assert.equal(writePublicOpenApi(projectRoot, outDir), false); + assert.equal(statSync(target).mtimeMs, before); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } +}); From 2b94ed3d20e9dfc87b493305fc63e341bc85e02b Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:42:50 -0300 Subject: [PATCH 2/2] fix(api): stop the public OpenAPI copy from reddening other PRs The artifact test compared the committed copy with docs/openapi.yaml and the package version, so any PR documenting a route, and the next version bump, went red. Test the generator's output instead, which is what a build ships, and pin only the committed copy's deployment-relative servers. Dev and the isolated build wrote that tracked file, dirtying the tree on any docs change. Only the assembly step generates it now, into its output directory. Add gen:public-openapi to refresh the copy, and public/openapi.yaml to the cycle bump set. --- .../fixes/18331-public-openapi-metadata.md | 2 +- docs/reference/API_REFERENCE.md | 2 + package.json | 1 + scripts/build/build-next-isolated.mjs | 2 - scripts/build/publicOpenApi.mjs | 10 +- scripts/dev/run-next.mjs | 2 - scripts/release/dry-run-lts-cut.mjs | 2 + .../public-openapi-artifact-18331.test.ts | 115 +++++++++++++++++- tests/unit/dry-run-lts-cut.test.ts | 1 + 9 files changed, 126 insertions(+), 11 deletions(-) diff --git a/changelog.d/fixes/18331-public-openapi-metadata.md b/changelog.d/fixes/18331-public-openapi-metadata.md index 55df5a71d653..5fefcce92fba 100644 --- a/changelog.d/fixes/18331-public-openapi-metadata.md +++ b/changelog.d/fixes/18331-public-openapi-metadata.md @@ -1 +1 @@ -- fix(api): regenerate the public OpenAPI spec with the build version and deployment-relative server URL (#18331). +- fix(api): regenerate the public OpenAPI spec with the build version and deployment-relative server URL. Builds generate it, and `npm run gen:public-openapi` refreshes the committed copy (#18331). diff --git a/docs/reference/API_REFERENCE.md b/docs/reference/API_REFERENCE.md index 600c2bde8a7b..098953eae307 100644 --- a/docs/reference/API_REFERENCE.md +++ b/docs/reference/API_REFERENCE.md @@ -13,6 +13,8 @@ Core reference for the OmniRoute API. It covers the public `/v1` surface and the For the focused OpenAI-compatible protocol and provider capability matrix, see [`OPENAI_COMPATIBILITY.md`](./OPENAI_COMPATIBILITY.md). +Each deployment serves its own copy of the spec at `/openapi.yaml`, generated from `docs/openapi.yaml` during the build. It carries the build's version and a relative server URL (`./`), so requests stay on the host that served the document. A tool that imports the downloaded file has no such host, and needs the base URL set by hand. + --- ## Table of Contents diff --git a/package.json b/package.json index ed9b44cf64d9..903af840dc12 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "dev:candidate": "node scripts/dev/candidate.mjs", "prebuild:docs": "node scripts/docs/gen-openapi-module.mjs", "gen:provider-reference": "bun scripts/docs/gen-provider-reference.ts", + "gen:public-openapi": "node scripts/build/publicOpenApi.mjs", "gen:i18n-routes": "node scripts/i18n/generate-route-namespaces.mjs", "bench:compression": "bun scripts/compression/benchmark.ts", "bench:heap-body": "node --expose-gc --import tsx/esm scripts/perf/request-body-heap.ts", diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs index d1a3ed017fda..2e9bf02b420f 100644 --- a/scripts/build/build-next-isolated.mjs +++ b/scripts/build/build-next-isolated.mjs @@ -6,7 +6,6 @@ import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { pathToFileURL } from "node:url"; -import { writePublicOpenApi } from "./publicOpenApi.mjs"; import { generateGlobalErrorMessages } from "../i18n/generate-global-error-messages.mjs"; import { assembleStandalone, @@ -351,7 +350,6 @@ export async function main() { try { generateGlobalErrorMessages(projectRoot); - writePublicOpenApi(projectRoot); for (const entry of transientBuildPaths) { if (!(await exists(entry.sourcePath))) continue; await movePath(entry.sourcePath, entry.backupPath); diff --git a/scripts/build/publicOpenApi.mjs b/scripts/build/publicOpenApi.mjs index 0caf402a3e54..b818988a0990 100644 --- a/scripts/build/publicOpenApi.mjs +++ b/scripts/build/publicOpenApi.mjs @@ -1,5 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import * as yaml from "js-yaml"; /** Refresh the deployed spec from its canonical source without trusting request headers. */ @@ -11,7 +12,7 @@ export function writePublicOpenApi(projectRoot, outputRoot = projectRoot) { const spec = yaml.load(original); const { version } = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")); if (!spec?.info || !spec.paths || typeof version !== "string" || !version) { - throw new Error("Invalid OpenAPI source or package version"); + throw new Error(`Invalid OpenAPI source (${source}) or package version`); } spec.info.version = version; // OAS 3.1 Server Object: relative to the URL of /[basePath/]openapi.yaml. @@ -34,3 +35,10 @@ export function writePublicOpenApi(projectRoot, outputRoot = projectRoot) { writeFileSync(target, content); return true; } + +// `npm run gen:public-openapi` refreshes the committed copy. Dev and build never write to it: +// builds generate their own output (assembleStandalone), so the source tree stays clean. +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const written = writePublicOpenApi(process.cwd()); + console.log(written ? "public/openapi.yaml updated." : "public/openapi.yaml is up to date."); +} diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 149860e59739..f288528fdce5 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -4,7 +4,6 @@ import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import next from "next"; -import { writePublicOpenApi } from "../build/publicOpenApi.mjs"; import { bootstrapEnv } from "../build/bootstrap-env.mjs"; import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mjs"; import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs"; @@ -54,7 +53,6 @@ const dev = mode === "dev"; // Self-heal a stale better-sqlite3 native binary after a Node version switch // (nvm 22 <-> 24) before bootstrap touches the DB. No-op when the ABI matches. if (dev) { - writePublicOpenApi(process.cwd()); ensureNativeSqlite(); } diff --git a/scripts/release/dry-run-lts-cut.mjs b/scripts/release/dry-run-lts-cut.mjs index 2d37c645c933..8693b25a61cd 100644 --- a/scripts/release/dry-run-lts-cut.mjs +++ b/scripts/release/dry-run-lts-cut.mjs @@ -75,6 +75,7 @@ const BUMP_FILES = Object.freeze([ "electron/package.json", "package-lock.json", "docs/openapi.yaml", + "public/openapi.yaml", ]); /** Workflows that stay dormant until the cut (docs/ops/RELEASE_STRATEGY.md → branches/channels). */ @@ -398,6 +399,7 @@ const BUMPERS = Object.freeze({ "electron/package.json": bumpManifest, "package-lock.json": bumpLockfile, "docs/openapi.yaml": bumpOpenApi, + "public/openapi.yaml": bumpOpenApi, }); // ── plan ──────────────────────────────────────────────────────────────────── diff --git a/tests/unit/build/public-openapi-artifact-18331.test.ts b/tests/unit/build/public-openapi-artifact-18331.test.ts index 761be119a4d4..51215148e0b4 100644 --- a/tests/unit/build/public-openapi-artifact-18331.test.ts +++ b/tests/unit/build/public-openapi-artifact-18331.test.ts @@ -1,22 +1,36 @@ import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import test from "node:test"; +import test, { after } from "node:test"; +import { fileURLToPath } from "node:url"; import * as yaml from "js-yaml"; import { assembleStandalone } from "../../../scripts/build/assembleStandalone.mjs"; import { writePublicOpenApi } from "../../../scripts/build/publicOpenApi.mjs"; +import { bumpOpenApi } from "../../../scripts/release/dry-run-lts-cut.mjs"; interface Spec { info: { version: string }; servers: { url: string }[]; paths: Record; } +interface Manifest { + scripts: Record; +} const root = new URL("../../../", import.meta.url); -const spec = yaml.load(readFileSync(new URL("public/openapi.yaml", root), "utf8")) as Spec; const pkg = JSON.parse(readFileSync(new URL("package.json", root), "utf8")) as { version: string }; -test("public OpenAPI describes the running build version", () => { +// Generated, never read from the committed copy: that copy may lag docs/openapi.yaml, and pinning +// it here would turn every PR that documents a route red. +const generationRoot = mkdtempSync(join(tmpdir(), "openapi-18331-generated-")); +after(() => rmSync(generationRoot, { recursive: true, force: true })); +writePublicOpenApi(fileURLToPath(root), generationRoot); +const spec = yaml.load( + readFileSync(join(generationRoot, "public", "openapi.yaml"), "utf8") +) as Spec; + +test("the generated spec describes the running build version", () => { assert.equal(spec.info.version, pkg.version); }); @@ -25,7 +39,7 @@ for (const documentUrl of [ "https://api.example.test/omniroute/openapi.yaml", "http://localhost:21234/openapi.yaml", ]) { - test(`OpenAPI requests stay on the deployment serving ${documentUrl}`, () => { + test(`generated requests stay on the deployment serving ${documentUrl}`, () => { const server = new URL(spec.servers[0].url, documentUrl); assert.equal( server.href.replace(/\/$/, "") + "/api/v1/models", @@ -34,7 +48,7 @@ for (const documentUrl of [ }); } -test("public spec preserves canonical operations, schemas and security", () => { +test("the generated spec preserves canonical operations, schemas and security", () => { const canonical = yaml.load(readFileSync(new URL("docs/openapi.yaml", root), "utf8")) as Spec; assert.deepEqual( { ...spec, info: undefined, servers: undefined }, @@ -43,6 +57,97 @@ test("public spec preserves canonical operations, schemas and security", () => { assert.deepEqual({ ...spec.info, version: undefined }, { ...canonical.info, version: undefined }); }); +test("the committed copy is a generated spec with deployment-relative servers", () => { + // Not its version or paths: those may lag until the next `npm run gen:public-openapi`. + const committed = yaml.load(readFileSync(new URL("public/openapi.yaml", root), "utf8")) as Spec; + assert.deepEqual(committed.servers, spec.servers); + assert.ok(Object.keys(committed.paths).length > 0); +}); + +/** A minimal project: the generator needs only package.json and docs/openapi.yaml. */ +function createFixtureProject(version: string, specVersion: string) { + const projectRoot = mkdtempSync(join(tmpdir(), "openapi-18331-fixture-")); + mkdirSync(join(projectRoot, "docs")); + writeFileSync(join(projectRoot, "package.json"), JSON.stringify({ version })); + writeFileSync( + join(projectRoot, "docs", "openapi.yaml"), + `openapi: 3.1.0\ninfo:\n title: Fixture\n version: ${specVersion}\nservers:\n - url: http://localhost:20128\npaths: {}\n` + ); + return projectRoot; +} + +test("a route documented after the last regeneration reaches the next generated spec", () => { + const projectRoot = createFixtureProject("9.8.7", "9.8.7"); + try { + assert.equal(writePublicOpenApi(projectRoot), true); + const source = join(projectRoot, "docs", "openapi.yaml"); + writeFileSync( + source, + readFileSync(source, "utf8").replace("paths: {}", "paths:\n /api/new:\n get: {}") + ); + + assert.equal(writePublicOpenApi(projectRoot), true); + const refreshed = yaml.load( + readFileSync(join(projectRoot, "public", "openapi.yaml"), "utf8") + ) as Spec; + assert.deepEqual(Object.keys(refreshed.paths), ["/api/new"]); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } +}); + +test("gen:public-openapi regenerates the committed path from the current directory", () => { + const projectRoot = createFixtureProject("9.8.7", "old"); + try { + const script = fileURLToPath(new URL("scripts/build/publicOpenApi.mjs", root)); + const run = () => spawnSync(process.execPath, [script], { cwd: projectRoot, encoding: "utf8" }); + + const first = run(); + assert.equal(first.status, 0, first.stderr); + assert.match(first.stdout, /updated/); + const target = join(projectRoot, "public", "openapi.yaml"); + assert.equal((yaml.load(readFileSync(target, "utf8")) as Spec).info.version, "9.8.7"); + assert.match(run().stdout, /up to date/); + assert.match( + (JSON.parse(readFileSync(new URL("package.json", root), "utf8")) as Manifest).scripts[ + "gen:public-openapi" + ], + /^node scripts\/build\/publicOpenApi\.mjs$/ + ); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } +}); + +test("a cycle bump keeps generating the new version and moves the committed copy too", () => { + const projectRoot = createFixtureProject("3.8.54", "3.8.54"); + try { + writePublicOpenApi(projectRoot); + const target = join(projectRoot, "public", "openapi.yaml"); + assert.equal((yaml.load(readFileSync(target, "utf8")) as Spec).info.version, "3.8.54"); + + // The committed copy keeps its body through the bump, like docs/openapi.yaml. + const committed = readFileSync(new URL("public/openapi.yaml", root), "utf8"); + const bumped = yaml.load(bumpOpenApi(committed, "9.9.9")) as Spec; + assert.equal(bumped.info.version, "9.9.9"); + assert.deepEqual(bumped.servers, spec.servers); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } +}); + +test("dev and the isolated build leave the tracked public spec alone", () => { + // Source pin: running either one here would mean booting Next or a full build. Only the + // assembly step, which writes to its output directory, may generate the spec. + for (const script of ["scripts/dev/run-next.mjs", "scripts/build/build-next-isolated.mjs"]) { + assert.doesNotMatch(readFileSync(new URL(script, root), "utf8"), /writePublicOpenApi/, script); + } + assert.match( + readFileSync(new URL("scripts/build/assembleStandalone.mjs", root), "utf8"), + /writePublicOpenApi\(projectRoot, resolvedOutDir\)/ + ); +}); + test("packaging refreshes stale metadata and repeated generation does not rewrite the artifact", () => { const projectRoot = mkdtempSync(join(tmpdir(), "openapi-18331-")); try { diff --git a/tests/unit/dry-run-lts-cut.test.ts b/tests/unit/dry-run-lts-cut.test.ts index 702e7bf9f95e..a346fd8072c7 100644 --- a/tests/unit/dry-run-lts-cut.test.ts +++ b/tests/unit/dry-run-lts-cut.test.ts @@ -290,6 +290,7 @@ test("the plan orders the cut and only ever pushes to the selected remote", () = "electron/package.json", "package-lock.json", "docs/openapi.yaml", + "public/openapi.yaml", ]); });