Compare commits

..

13 Commits

Author SHA1 Message Date
diegosouzapw
c32be09e6a feat(sse): wire the PROVIDER_PROFILES window gate into the global provider cooldown
providerFailureThreshold / providerFailureWindowMs / providerCooldownMs shipped
in PROVIDER_PROFILES with no runtime consumer (2026-08-31 docs audit, P0.1).
Provider-level entries in providerCooldownTracker now honor them: the whole
provider only counts as cooling after providerFailureThreshold failures inside
providerFailureWindowMs, then cools for providerCooldownMs. Connection-level
entries keep the pre-existing exponential backoff, and the layer stays opt-in
(PROVIDER_COOLDOWN_ENABLED, default off) — default behavior is unchanged.

TDD: tests/unit/provider-cooldown-window-gate.test.ts written first (4 red on
the old behavior), then the wiring; legacy tracker suite aligned to the new
contract (23/23 green). Docs: AGENTS.md breaker section + RESILIENCE_GUIDE
opt-in layer subsection; executors soft-drift refresh (104 -> 106).
2026-09-01 00:19:09 -03:00
Diego Rodrigues de Sa e Souza
63e4afa321 feat(dashboard): orchestration canvas — unified model + snapshot hook (part 1/2) (#12156)
Modelo puro do Orchestration Canvas (tipos, 3 mappers, mergeSnapshot com dedupe/staleness/cap, projeções flow+overview) + hook de polling com gatilho WS. Ciclo completo: 9 tasks TDD com review por task, review final whole-branch + fixes verificados 6/6, refactor de complexity re-validado (comportamento preservado). CI: 18 pass. Testes: 31 node:test + 2 vitest. Parte 2/2 (UI /dashboard/orchestration) na sequência.
2026-08-31 14:42:20 -03:00
Diego Rodrigues de Sa e Souza
7ca5e1c671 chore(lint): batch 6 of #12146 — memory, radar, audit, analytics, cache, usage, activity, home and RequestLoggerV2 react-hooks violations resolved (#12208)
45 violations across 27 files fixed at the source (no eslint-disable, no new
suppressions; the 45 matching react-hooks/* entries are removed from
config/quality/eslint-suppressions.json):

- set-state-in-effect (fetch-on-mount effects): async continuation wrapper.
- Prop/state sync effects (EditMemoryModal, radar/setup, EvalsTab): adjust
  during render with prev tracking.
- purity/refs (ActivityFeedClient, ProviderQuotaWidget, ReasoningCacheTab):
  Date.now() snapshots moved to state set from the fetch path; rendered refs
  converted to state.
- immutability (useCodexResetCreditRedemption): ref-store writes extracted to
  module-level helpers.
- exhaustive-deps (RequestLoggerV2, HomePageClient): COLUMN_SORT_MAP hoisted to
  module scope; openDetail/closeDetail wrapped in useCallback and added to the
  dependent hooks; versionInfo destructured to locals; baseUrl now reads
  location.origin via useSyncExternalStore (hydration-safe, no effect).

Refs #12146
2026-08-31 14:37:26 -03:00
Rahil Mavani
73db936f98 fix(api): keep registry width and type on embedding models (#11761)
* fix(api): keep registry width and type on embedding models

* docs: changelog fragment for embedding registry fix

* test(api): cover embedding width and type merge

Exercises /v1/models rather than the registry in isolation: a synced
model colliding with an embeddingRegistry entry must keep the width the
registry states, and a synced model the registry names must be typed as
an embedding model.

Fails on catalog.ts before e7fbb62 (2 failures), passes after.

Refs #11759
2026-08-31 14:16:41 -03:00
backryun
4b5266d3f8 fix(dev): isolate batch dispatch from instrumentation (#12081)
Co-authored-by: backryun <backryun@daonlab.local>
2026-08-31 14:14:02 -03:00
backryun
f8b01c966e fix(dev): make logging resources HMR-singleton (#12079)
Co-authored-by: backryun <backryun@daonlab.local>
2026-08-31 14:13:53 -03:00
backryun
e12fb110f9 [URGENT] fix(dev): reduce instrumentation executor fan-out (phase 3) (#12078)
* fix(dev): reduce instrumentation executor fan-out

* fix(ci): reduce credential refresh complexity

---------

Co-authored-by: backryun <backryun@daonlab.local>
2026-08-31 14:13:46 -03:00
backryun
2fbd0f5c25 fix(dev): isolate root layout settings reads (#12076)
Co-authored-by: backryun <backryun@daonlab.local>
2026-08-31 14:13:37 -03:00
Jacob Stoner
18c71b91dc feat(auto-combo): add weighted score router strategy (#12155)
Add a direct low-level mode for users who require explicit control over provider selection. score selects the highest configured weighted score directly while reusing the existing exploration rate.

Exact ties preserve configured candidate order. rules and all other strategies remain unchanged.
2026-08-31 14:10:50 -03:00
MSiva
9392bd55c2 fix(translator): preserve falsy primitive values in Gemini and Antigravity function response results (#12191) 2026-08-31 14:10:44 -03:00
opensource-elearning
90366903c4 fix: prevent Claude Code session kills via liveness-aware readiness + auto model echo (#12189)
- streamReadiness: reset deadline on each received chunk (keepalive = alive)
  with a hard maxTimeoutMs ceiling so truly-dead connections still fail fast.
  Preserves operator's 20s/100s intent for dead pulls while allowing slow-but-alive
  upstreams (reasoning warm-ups) to survive.

- chatCore + codexIdentity: auto-detect Claude Code CLI via user-agent/originator
  headers and enable model echo for it. The response  field now echoes
  the originally-requested alias/combo (e.g. ) instead of the
  resolved upstream id (e.g. ), so  restores
  cleanly without 'could not be restored' errors.

Refs: opensource-elearning/omniroute-fixes#1, diegosouzapw/OmniRoute#12185
2026-08-31 14:10:39 -03:00
Alvin T. Veroy
668beed5b8 fix(sse): absorb AbortError/request_signal_aborted in the client-abort crash guard (#12165)
OmniRoute's SSE teardown aborts in-flight legs with
`Error [AbortError]: request_signal_aborted` on client disconnects
(open-sse/utils/streamHandler.ts getClientAbortReason), and fetch/DOM
cancellation surfaces as AbortError with an abort-flavoured message.
isClientAbortError() only matched message 'aborted'/'Aborted' plus errno
codes, so these shapes fell through shouldSwallowUncaught() and were
re-thrown from the process-level uncaughtException/unhandledRejection
handlers — killing the whole server on a routine client disconnect
(observed as repeated exit-code-7 crashes with
'uncaughtException: Error [AbortError]: request_signal_aborted').

Match AbortError by name when the message is abort-flavoured; genuine
errors that merely mention 'abort' (e.g. TypeError) still crash loudly.

Tests: new unit cases for the SSE/DOM AbortError shapes, a child-process
regression proving the process survives both benign emissions with the
production no-logger install shape, and a child-process test proving
genuine errors keep crash semantics.
2026-08-31 14:10:33 -03:00
Bob.Hou
298ad0fd64 fix(translator): strip plaintext reasoning content for opaque responses backends (#12128) (#12171)
Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-31 14:10:26 -03:00
126 changed files with 3894 additions and 10432 deletions

View File

@@ -131,10 +131,14 @@ breaker runs on `circuitBreakerThreshold` / `circuitBreakerReset`:
| API key | `7` | `12` | `30s` |
| Local | (derived) | `2` | `15s` |
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2) and `providerCooldownMs`
(5min/10min/1min); those fields are loaded into the profile but have **no runtime consumer
today** — do not tune or document them as the live breaker. Every default is overridable
through the `OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2),
`providerFailureWindowMs` (15/30/5 min) and `providerCooldownMs` (5/10/1 min): these power the
**window gate of the opt-in global Provider Cooldown** (`PROVIDER_COOLDOWN_ENABLED`, default
off) — a provider-level entry in `open-sse/services/providerCooldownTracker.ts` only counts as
cooling after `providerFailureThreshold` failures inside `providerFailureWindowMs`, and then
cools for `providerCooldownMs`. They are NOT the live breaker's thresholds — do not tune them
expecting breaker behavior. Every default is overridable through the
`OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
runtime-accurate reference table lives in `docs/architecture/RESILIENCE_GUIDE.md`.
Only provider-level failure statuses should trip the provider breaker:

View File

@@ -0,0 +1 @@
- **feat(routing):** add a `score` Auto router strategy that selects the highest configured weighted score and reuses `explorationRate`.

View File

@@ -0,0 +1,5 @@
- Keep the embedding registry's vector width and `embedding` type on models when a synced model exists
for the same id, so `/v1/models` no longer reports registry-described embedding models widthless or
untyped (#11761)
- Correct `google/gemini-embedding-001` on the OpenRouter route to 3072 dimensions, the width it
returns when `dimensions` is not sent (#11761)

View File

@@ -0,0 +1 @@
- Absorb `Error [AbortError]: request_signal_aborted` and DOMException AbortError shapes in the process-level client-abort crash guard so routine client disconnects no longer kill the server (exit code 7).

View File

@@ -829,12 +829,6 @@
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 2
},
"react-hooks/exhaustive-deps": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/a2a/page.tsx": {
@@ -850,62 +844,16 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/activity/ActivityFeedClient.tsx": {
"react-hooks/purity": {
"count": 1
},
"react-hooks/refs": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/CacheHealthTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/RouteExplainabilityTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/audit/ComplianceTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
@@ -926,24 +874,6 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/cache/components/CacheEntriesTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/cache/components/ReasoningCacheTab.tsx": {
"react-hooks/purity": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/cache/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/cli-agents/CliAgentsPageClient.tsx": {
"no-restricted-syntax": {
"count": 4
@@ -1060,31 +990,6 @@
"count": 2
}
},
"src/app/(dashboard)/dashboard/memory/components/EditMemoryModal.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/memory/hooks/useEngineStatus.ts": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/memory/hooks/useMemorySettings.ts": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/onboarding/page.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1185,29 +1090,6 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/radar/RadarCatalogTable.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/radar/intel/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/radar/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/radar/setup/page.tsx": {
"react-hooks/preserve-manual-memoization": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
@@ -1376,11 +1258,6 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 3
@@ -1391,11 +1268,6 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useCodexResetCreditRedemption.ts": {
"react-hooks/immutability": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1404,17 +1276,11 @@
"src/app/(dashboard)/dashboard/usage/components/RateLimitStatus.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/webhooks/WebhooksPageClient.tsx": {
@@ -1437,17 +1303,6 @@
"count": 1
}
},
"src/app/(dashboard)/home/ProviderQuotaWidget.tsx": {
"react-hooks/purity": {
"count": 1
},
"react-hooks/refs": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/api/assess/route.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -2298,9 +2153,6 @@
"src/shared/components/RequestLoggerV2.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 3
},
"react-hooks/exhaustive-deps": {
"count": 6
}
},
"src/shared/components/RequestTimeline.tsx": {

View File

@@ -1,6 +1,5 @@
{
"_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).",
"_rebaseline_2026_08_31_12212_openapi_generated": "PR #12212 (docs audit follow-up nº 3): src/app/docs/lib/openapi.generated.ts 171->1347 — the module is emitted by scripts/docs/gen-openapi-module.mjs from docs/openapi.yaml, and the spec now documents all 692 implemented routes (was 276), so the generated output grew with the spec. Frozen at the generator output size; shrink by slimming the spec, never by hand-editing the generated module. Covered by tests/unit/openapi-security-tiers.test.ts (6/6) and the check:api-docs-refs gate (692/692 paths with a real route).",
"_rebaseline_2026_08_21_10987_logfare_provider": "PR #10987 (jonlwheat2-gif, feat/10644-logfare-provider, closes #10644) own growth: src/shared/constants/providers/apikey/gateways.ts 1298->1321 (+23, the logfare APIKEY_PROVIDERS_GATEWAYS catalog entry with Free badge/freeNote/apiHint documenting the request-logging policy, additive data at the existing registry chokepoint, same god-file no-split rationale as the prior gateways.ts rebaselines: #10531 freebuff, merge-storm 2026-08-11). Covered by tests/unit/logfare-registry.test.ts (1/1 passing).",
"_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.",
"_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).",
@@ -496,8 +495,7 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"open-sse/executors/commandCode.ts": 1271,
"src/app/docs/lib/openapi.generated.ts": 1347
"open-sse/executors/commandCode.ts": 1271
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",

View File

@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
Core capabilities:
- OpenAI-compatible API surface for CLI/tools (352 providers, 104 executors)
- OpenAI-compatible API surface for CLI/tools (352 providers, 106 executors)
- Request/response translation across provider formats
- Model combo fallback (multi-model sequence)
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`

View File

@@ -450,7 +450,7 @@ open-sse/
├── types.d.ts
├── config/ Provider registries, header profiles, identity, …
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
├── executors/ 104 provider-specific HTTP executors
├── executors/ 106 provider-specific HTTP executors
├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro)
├── transformer/ Responses API ↔ Chat Completions stream transformer
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)
@@ -480,7 +480,7 @@ open-sse/
### 4.2 `open-sse/executors/`
104 provider executors, each extending `BaseExecutor` (`base.ts`):
106 provider executors, each extending `BaseExecutor` (`base.ts`):
`antigravity`, `azure-openai`, `blackbox-web`, `cliproxyapi`,
`chatgpt-web-codex`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,

View File

@@ -50,6 +50,25 @@ OmniRoute has three distinct but related resilience mechanisms. Each has a diffe
---
### Opt-in global Provider Cooldown (window gate)
A fourth, **opt-in** layer (`PROVIDER_COOLDOWN_ENABLED`, default **off**) keeps a
cross-request memory of failing providers in
`open-sse/services/providerCooldownTracker.ts`, consulted by combo target
resolution so consecutive combo requests stop re-walking a provider that just
failed. Provider-level entries honor the `PROVIDER_PROFILES` window gate:
| Profile | trips after (`providerFailureThreshold`) | inside (`providerFailureWindowMs`) | cools for (`providerCooldownMs`) |
| ------- | ---------------------------------------: | ---------------------------------: | -------------------------------: |
| OAuth | `10` | `15min` | `5min` |
| API key | `15` | `30min` | `10min` |
Below the threshold the provider is **not** considered cooling; a success clears
the window. Connection-level entries (`provider:connectionId`) keep the
exponential `minRetryCooldownMs → maxRetryCooldownMs` backoff instead. Overrides:
`OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}`.
Regression guard: `tests/unit/provider-cooldown-window-gate.test.ts`.
## 2. Connection Cooldown
**Scope:** single provider connection/account/key.

File diff suppressed because it is too large Load Diff

View File

@@ -414,6 +414,8 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
`config.auto.routerStrategy`) to one of:
- `rules` — default weighted scoring
- `score` — selects the highest configured weighted score. Exact ties preserve configured
candidate order; the existing `explorationRate` samples from the full ranked pool.
- `cost` / `eco` — cheapest healthy provider
- `latency` / `fast` — lowest p95 latency with reliability penalty
- `sla-aware` / `sla` — prefer candidates that satisfy p95 latency, error-rate, and optional
@@ -422,7 +424,7 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
### Router strategies in detail
The auto-combo engine exposes 5 pluggable **RouterStrategy** implementations that
The auto-combo engine exposes 6 pluggable **RouterStrategy** implementations that
you can swap via `config.routerStrategy` (or the legacy `config.auto.routerStrategy`).
Each strategy picks one provider from the candidate pool, given a `RoutingContext`
(task type, tool/vision hints, token estimate, optional SLA policy, optional

View File

@@ -570,3 +570,37 @@ export function isVerifiedNativeCodexRequest(
): boolean {
return isCodexOriginatedHeaders(headers) && hasNativeCodexTurnBinding(body);
}
/**
* Detect the Claude Code CLI as the request *client* from request headers.
* Used to auto-enable model echo so session restores work when the resolved
* upstream model (e.g. `oc/nemotron-3-ultra-free`) is not recognized by the
* Claude Code client on `--resume`.
*/
export function isClaudeCodeOriginatedHeaders(
headers: Headers | Record<string, unknown> | null | undefined
): boolean {
const getHeader = (name: string): string => {
if (headers instanceof Headers) {
return headers.get(name)?.toLowerCase() ?? "";
}
if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
if (key.toLowerCase() === name && typeof value === "string") {
return value.toLowerCase();
}
}
}
return "";
};
// Claude Code identifies itself via the user-agent header
const userAgent = getHeader("user-agent");
if (userAgent.includes("claude-code") || userAgent.includes("anthropic-ai/claude-code")) {
return true;
}
// Also check originator if present
const originator = getHeader("originator");
if (originator.startsWith("claude-code")) return true;
return false;
}

View File

@@ -239,7 +239,7 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
{
id: "google/gemini-embedding-001",
name: "Gemini Embedding 001 (OpenRouter)",
dimensions: 768,
dimensions: 3072,
},
{
id: "google/gemini-embedding-2",

View File

@@ -0,0 +1,54 @@
import { assertCommonChatGptWebProviderAvailable } from "@/shared/constants/chatgptWebRetirement";
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
import { assertRuntimeProviderAvailable } from "@/shared/constants/providerRetirement";
import type { BaseExecutor } from "./base.ts";
import { getDefaultExecutor } from "./defaultResolver.ts";
type CredentialExecutorLoader = () => Promise<BaseExecutor>;
const specializedCredentialExecutors: Record<string, CredentialExecutorLoader> = {
antigravity: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()),
agy: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()),
github: () => import("./github.ts").then((m) => new m.GithubExecutor()),
"ghe-copilot": () => import("./ghe-copilot.ts").then((m) => new m.GheCopilotExecutor()),
kiro: () => import("./kiro.ts").then((m) => new m.KiroExecutor()),
"amazon-q": () => import("./kiro.ts").then((m) => new m.KiroExecutor("amazon-q")),
codex: () => import("./codex.ts").then((m) => new m.CodexExecutor()),
cursor: () => import("./cursor.ts").then((m) => new m.CursorExecutor()),
cu: () => import("./cursor.ts").then((m) => new m.CursorExecutor()),
"cursor-api": () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")),
cua: () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")),
trae: () => import("./trae.ts").then((m) => new m.TraeExecutor()),
gitlab: () => import("./gitlab.ts").then((m) => new m.GitlabExecutor()),
"gitlab-duo": () => import("./gitlab.ts").then((m) => new m.GitlabExecutor("gitlab-duo")),
"zed-hosted": () => import("./zed-hosted.ts").then((m) => new m.ZedHostedExecutor()),
"grok-cli": () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()),
gc: () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()),
auggie: () => import("./auggie.ts").then((m) => new m.AuggieExecutor()),
xai: () => import("./xai.ts").then((m) => new m.XaiExecutor()),
"xai-oauth": () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
xao: () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
};
const credentialExecutorCache = new Map<string, Promise<BaseExecutor>>();
/** Resolve only executors with credential-refresh behavior, without loading the chat registry. */
export async function getCredentialRefreshExecutor(provider: string): Promise<BaseExecutor> {
assertMicrosoftDesignerWebProviderAvailable(provider);
assertRuntimeProviderAvailable(provider);
assertCommonChatGptWebProviderAvailable(provider);
let executor = credentialExecutorCache.get(provider);
if (!executor) {
const specializedLoader = specializedCredentialExecutors[provider];
executor = specializedLoader
? specializedLoader()
: Promise.resolve(getDefaultExecutor(provider));
executor = executor.catch((error) => {
credentialExecutorCache.delete(provider);
throw error;
});
credentialExecutorCache.set(provider, executor);
}
return executor;
}

View File

@@ -0,0 +1,13 @@
import { DefaultExecutor } from "./default.ts";
const defaultExecutorCache = new Map<string, DefaultExecutor>();
/** Resolve the shared fallback executor without initializing the specialized executor registry. */
export function getDefaultExecutor(provider: string): DefaultExecutor {
let executor = defaultExecutorCache.get(provider);
if (!executor) {
executor = new DefaultExecutor(provider);
defaultExecutorCache.set(provider, executor);
}
return executor;
}

View File

@@ -9,7 +9,7 @@ import {
} from "./registry.ts";
// Type-only: pulls no runtime code, keeps DefaultExecutor the only eager class.
import type { BaseExecutor } from "./base.ts";
import { DefaultExecutor } from "./default.ts";
import { getDefaultExecutor } from "./defaultResolver.ts";
// R0.3 — declarative built-in table, made LAZY by #11220.
//
@@ -207,8 +207,6 @@ for (const [alias, load] of Object.entries(lazyExecutors)) {
registerLazyExecutor(alias, load);
}
const defaultCache = new Map();
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
// (CLOUD_AGENT_PROVIDERS / staticModels "Available Models" catalog) and have no
// chat-completions REGISTRY entry anywhere in open-sse/. Without this guard,
@@ -251,8 +249,7 @@ export async function getExecutor(provider: string): Promise<BaseExecutor> {
(err as Error & { status?: number }).status = 400;
throw err;
}
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
return defaultCache.get(provider)!;
return getDefaultExecutor(provider);
}
export function hasSpecializedExecutor(provider: string): boolean {

View File

@@ -77,7 +77,7 @@ import {
isStripReasoningRequested,
} from "./chatCore/headers.ts";
import { markCodexScopeRateLimited } from "./chatCore/codexFailover.ts";
import { getCodexClientSessionId, isCodexOriginatedHeaders } from "../config/codexIdentity.ts";
import { getCodexClientSessionId, isCodexOriginatedHeaders, isClaudeCodeOriginatedHeaders } from "../config/codexIdentity.ts";
import {
noteCodexTurnStateProvenance,
readCodexTurnStateHeader,
@@ -981,8 +981,14 @@ export async function handleChatCore({
const isCodexResponsesEcho =
(isResponsesEndpoint || sourceFormat === FORMATS.OPENAI_RESPONSES) &&
isCodexOriginatedHeaders(clientRawRequest?.headers);
// Detect Claude Code CLI so we can auto-enable model echo — this prevents
// session restore failures when the resolved upstream model (e.g.
// `oc/nemotron-3-ultra-free`) is not recognized by the client on `--resume`.
const isClaudeCodeClient = isClaudeCodeOriginatedHeaders(clientRawRequest?.headers);
let echoModel =
(settings.echoRequestedModelName === true || isCodexResponsesEcho) &&
(settings.echoRequestedModelName === true || isCodexResponsesEcho || isClaudeCodeClient) &&
typeof requestedModel === "string" &&
requestedModel
? requestedModel
@@ -5465,6 +5471,7 @@ export async function handleChatCore({
const streamReadiness = await ensureStreamReadiness(providerResponse, {
timeoutMs: streamReadinessPolicy.timeoutMs,
maxTimeoutMs: streamReadinessPolicy.maxTimeoutMs,
provider,
model,
log,

View File

@@ -4,13 +4,14 @@
* Inspired by ClawRouter commit 14c83c258 "refactor: extract routing into pluggable RouterStrategy system".
* Provides a RouterStrategy interface and built-in implementations:
* - RulesStrategy (default): wraps the existing 15-factor scoring engine
* - ScoreStrategy: highest configured weighted score, with explicit exploration
* - CostStrategy: always picks cheapest available model
* - LatencyStrategy: prioritizes low p95 latency with reliability weighting
* - SLAStrategy: prefers candidates that satisfy latency/error/cost SLOs
* - LKGPStrategy: tries last known good provider first
*/
import type { ProviderCandidate, ScoredProvider } from "./scoring.ts";
import type { ProviderCandidate, ScoredProvider, ScoringWeights } from "./scoring.ts";
import { scorePool } from "./scoring.ts";
import { getTaskFitness } from "./taskFitness.ts";
import { clamp01 } from "../../utils/number.ts";
@@ -32,6 +33,8 @@ export interface RoutingContext {
lastKnownGoodProvider?: string;
lkgpEnabled?: boolean;
sla?: SlaRoutingPolicy;
weights?: ScoringWeights;
explorationRate?: number;
}
export interface RoutingDecision {
@@ -108,6 +111,38 @@ class RulesStrategyImpl implements RouterStrategy {
}
}
// ── ScoreStrategy: configured score wins, with explicit exploration ──────────
class ScoreStrategyImpl implements RouterStrategy {
readonly name = "score";
readonly description = "Selects the highest configured weighted score, with explicit exploration";
select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision {
const eligible = pool.filter((candidate) => candidate.circuitBreakerState !== "OPEN");
const ranked = scorePool(
eligible.length > 0 ? eligible : pool,
context.taskType,
context.weights,
getTaskFitness
);
if (ranked.length === 0) throw new Error("[ScoreStrategy] No candidates to score");
const explorationRate = Math.min(1, Math.max(0, context.explorationRate ?? 0));
const isExploration = Math.random() < explorationRate && ranked.length > 1;
const selected = isExploration ? ranked[Math.floor(Math.random() * ranked.length)] : ranked[0];
return {
provider: selected.provider,
model: selected.model,
strategy: this.name,
reason: `ScoreStrategy: score=${selected.score.toFixed(3)}${isExploration ? " (exploration)" : ""}`,
candidatesConsidered: ranked.length,
finalScore: selected.score,
connectionId: selected.connectionId,
};
}
}
// ── CostStrategy: always picks cheapest healthy provider ─────────────────────
class CostStrategyImpl implements RouterStrategy {
@@ -337,12 +372,14 @@ class LKGPStrategyImpl implements RouterStrategy {
const strategyRegistry = new Map<string, RouterStrategy>();
const rulesStrategy = new RulesStrategyImpl();
const scoreStrategy = new ScoreStrategyImpl();
const costStrategy = new CostStrategyImpl();
const latencyStrategy = new LatencyStrategyImpl();
const slaStrategy = new SLAStrategyImpl();
const lkgpStrategy = new LKGPStrategyImpl();
strategyRegistry.set("rules", rulesStrategy);
strategyRegistry.set("score", scoreStrategy);
strategyRegistry.set("cost", costStrategy);
strategyRegistry.set("eco", costStrategy); // alias
strategyRegistry.set("latency", latencyStrategy);

View File

@@ -377,6 +377,8 @@ export async function resolveAutoStrategyOrder(
boolean | undefined,
estimatedInputTokens,
sla: slaPolicy,
weights,
explorationRate,
},
routingStrategy
);

View File

@@ -11,6 +11,8 @@ import {
DEFAULT_RESILIENCE_SETTINGS,
type ResilienceSettings,
} from "../../src/lib/resilience/settings";
import { PROVIDER_PROFILES } from "../config/constants.ts";
import { getProviderCategory } from "../config/providerRegistry.ts";
interface CooldownEntry {
/** Timestamp of last recorded failure (ms since epoch) */
@@ -19,6 +21,47 @@ interface CooldownEntry {
failureCount: number;
/** How long this entry must be retained for cleanup purposes */
retentionMs: number;
/**
* Provider-level entries only: timestamps of recent failures, pruned to the
* profile's `providerFailureWindowMs`. Powers the PROVIDER_PROFILES window
* gate (`providerFailureThreshold` failures inside the window trip a
* `providerCooldownMs` cooldown for the whole provider).
*/
failureTimestamps?: number[];
}
// ── PROVIDER_PROFILES window gate (whole-provider scope) ─────────────────────
// `providerFailureThreshold` / `providerFailureWindowMs` / `providerCooldownMs`
// shipped in PROVIDER_PROFILES with no runtime consumer (2026-08-31 docs
// audit, P0.1). Provider-level entries (no connectionId) now honor them: the
// provider only counts as cooling after `providerFailureThreshold` failures
// inside `providerFailureWindowMs`, and then cools for `providerCooldownMs`.
// Connection-level entries keep the pre-existing exponential backoff.
function providerWindowProfile(provider: string) {
const category = getProviderCategory(provider);
const profile = PROVIDER_PROFILES[category] ?? PROVIDER_PROFILES.apikey;
return {
failureThreshold: profile.providerFailureThreshold,
failureWindowMs: profile.providerFailureWindowMs,
cooldownMs: profile.providerCooldownMs,
};
}
function pruneWindow(timestamps: number[], windowMs: number, now: number): number[] {
const cutoff = now - windowMs;
const pruned = timestamps.filter((t) => t >= cutoff);
// Memory bound: the gate only ever needs `failureThreshold` recent samples;
// keep a small multiple so bursts cannot grow the array unbounded.
return pruned.length > 200 ? pruned.slice(-200) : pruned;
}
function providerWindowCooldownMs(provider: string, entry: CooldownEntry, now: number): number {
const { failureThreshold, failureWindowMs, cooldownMs } = providerWindowProfile(provider);
const inWindow = pruneWindow(entry.failureTimestamps ?? [], failureWindowMs, now);
if (inWindow.length < failureThreshold) return 0;
const elapsed = now - entry.lastFailureAt;
const remaining = cooldownMs - elapsed;
return remaining > 0 ? remaining : 0;
}
// Global cooldown state: keyed by "provider:connectionId" or "provider"
@@ -90,8 +133,21 @@ export function recordProviderCooldown(
existing.lastFailureAt = now;
existing.failureCount++;
existing.retentionMs = Math.max(existing.retentionMs, retentionMs);
if (!connectionId) {
const { failureWindowMs } = providerWindowProfile(provider);
existing.failureTimestamps = pruneWindow(
[...(existing.failureTimestamps ?? []), now],
failureWindowMs,
now
);
}
} else {
cooldownMap.set(key, { lastFailureAt: now, failureCount: 1, retentionMs });
cooldownMap.set(key, {
lastFailureAt: now,
failureCount: 1,
retentionMs,
...(connectionId ? {} : { failureTimestamps: [now] }),
});
}
startCleanupIfNeeded();
@@ -119,6 +175,11 @@ export function isProviderInCooldown(
if (entry.failureCount === 0) return false;
const now = Date.now();
if (!connectionId) {
return providerWindowCooldownMs(provider, entry, now) > 0;
}
const elapsed = now - entry.lastFailureAt;
const minCooldownMs =
@@ -151,6 +212,12 @@ export function getRemainingCooldownMs(
if (!entry) return 0;
const now = Date.now();
if (!connectionId) {
if (entry.failureCount === 0) return 0;
return providerWindowCooldownMs(provider, entry, now);
}
const elapsed = now - entry.lastFailureAt;
const minCooldownMs =
@@ -183,8 +250,9 @@ export function recordProviderSuccess(provider: string, connectionId: string | u
const key = cooldownKey(provider, connectionId);
const entry = cooldownMap.get(key);
if (entry) {
// Reset failure count but keep the entry
// Reset failure count and the provider-level failure window, keep the entry
entry.failureCount = 0;
entry.failureTimestamps = [];
}
}

View File

@@ -43,7 +43,19 @@ export function resolveReasoningTransport(
): ReasoningTransport {
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
const transport = REASONING_TRANSPORTS.get(normalized);
return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext");
if (transport) return transport;
// #12128: Generic Responses-protocol endpoints (e.g. openai-compatible-responses-*,
// custom-openai-responses, proxy backends) implement the OpenAI/Codex Responses API
// where reasoning input items cannot accept plaintext content (maxItems: 0).
if (
normalized.startsWith("openai-compatible-responses") ||
normalized.startsWith("custom-openai-responses") ||
normalized.includes("codex") ||
normalized.includes("responses")
) {
return "opaque";
}
return preserveEncryptedReasoning ? "opaque" : "plaintext";
}
function asRecord(value: unknown): JsonRecord | null {

View File

@@ -40,6 +40,7 @@ import {
normalizeResponsesReasoningEffort,
RESPONSES_STORE_MARKER,
} from "./request/openai-responses/helpers.ts";
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
bootstrapTranslatorRegistry();
export { register } from "./registry.ts";
@@ -575,6 +576,14 @@ export function translateRequest(
// Normalize openai-responses input shape for providers that require list input.
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
result = normalizeOpenAIResponsesRequest(result);
// #12128: Sanitize reasoning input items for Responses targets (strip plaintext content for opaque backends)
applyReasoningInputPolicy(result as Record<string, unknown>, "responses", {
provider,
preserveEncryptedReasoning:
(credentials as { providerSpecificData?: { preserveEncryptedReasoning?: boolean } } | null)
?.providerSpecificData?.preserveEncryptedReasoning === true,
onIncompatibleReasoning: "drop",
});
}
// Second role normalization: only for OPENAI_RESPONSES. Here messages are built from input

View File

@@ -220,12 +220,15 @@ function preserveRequired(obj: unknown): void {
return;
}
const record = obj as JsonRecord;
if (Array.isArray(record.required) && record.properties && typeof record.properties === "object") {
if (
Array.isArray(record.required) &&
record.properties &&
typeof record.properties === "object"
) {
const properties = record.properties as JsonRecord;
const valid = (record.required as unknown[]).filter(
(field) =>
typeof field === "string" &&
Object.prototype.hasOwnProperty.call(properties, field)
typeof field === "string" && Object.prototype.hasOwnProperty.call(properties, field)
);
if (valid.length === 0) {
delete record.required;
@@ -298,12 +301,13 @@ function convertContent(content) {
// Function response → collect all, each becomes a separate tool message
if (part.functionResponse) {
const resp = part.functionResponse.response;
const resultPayload =
resp && typeof resp === "object" && "result" in resp ? resp.result : (resp ?? {});
toolResults.push({
role: "tool",
tool_call_id: part.functionResponse.id || part.functionResponse.name,
content: JSON.stringify(
part.functionResponse.response?.result || part.functionResponse.response || {}
),
content: JSON.stringify(resultPayload),
});
}
}
@@ -316,9 +320,7 @@ function convertContent(content) {
const assistantMsg: JsonRecord = { role: "assistant" };
if (textParts.length > 0) {
assistantMsg.content =
textParts.length === 1 && textParts[0].type === "text"
? textParts[0].text
: textParts;
textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
}
if (reasoningContent) {
assistantMsg.reasoning_content = reasoningContent;

View File

@@ -147,12 +147,13 @@ function convertGeminiContent(content) {
}
if (part.functionResponse) {
const resp = part.functionResponse.response;
const resultPayload =
resp && typeof resp === "object" && "result" in resp ? resp.result : (resp ?? {});
return {
role: "tool",
tool_call_id: part.functionResponse.id || part.functionResponse.name,
content: JSON.stringify(
part.functionResponse.response?.result || part.functionResponse.response || {}
),
content: JSON.stringify(resultPayload),
};
}
}

View File

@@ -471,6 +471,9 @@ export async function ensureStreamReadiness(
response: Response,
options: {
timeoutMs: number;
/** Hard ceiling for liveness-extended deadlines. When omitted, no hard ceiling
* is applied beyond `timeoutMs`. */
maxTimeoutMs?: number;
provider?: string | null;
model?: string | null;
log?: StreamReadinessLogger | null;
@@ -489,7 +492,14 @@ export async function ensureStreamReadiness(
};
const startedAt = Date.now();
const effectiveTimeoutMs = Math.max(0, Math.floor(options.timeoutMs));
const deadline = startedAt + effectiveTimeoutMs;
// Hard ceiling: the deadline may extend on liveness signals (bytes arriving),
// but never past this absolute maximum. When maxTimeoutMs is omitted the
// initial timeoutMs itself acts as the ceiling (no extension).
const maxDeadline =
options.maxTimeoutMs != null
? startedAt + Math.max(effectiveTimeoutMs, Math.floor(options.maxTimeoutMs))
: startedAt + effectiveTimeoutMs;
let deadline = startedAt + effectiveTimeoutMs;
let handedOffReader = false;
const buildReadyResponse = () =>
@@ -500,7 +510,7 @@ export async function ensureStreamReadiness(
});
const timeoutReason = () =>
`Stream produced no non-ping SSE event within ${effectiveTimeoutMs}ms`;
`Stream produced no non-ping SSE event within ${deadline - startedAt}ms (max=${maxDeadline - startedAt}ms)`;
try {
while (true) {
@@ -593,6 +603,22 @@ export async function ensureStreamReadiness(
chunks.push(readResult.value);
const decodedChunk = decoder.decode(readResult.value, { stream: true });
// Liveness extension: bytes arrived → connection is alive, not dead.
// Reset the deadline so slow-but-alive upstreams (reasoning warm-ups,
// keepalive-only phases) are not aborted. The hard ceiling (maxDeadline)
// prevents unbounded waits and preserves the operator's fast-fail intent
// for truly dead connections.
const now = Date.now();
if (deadline < maxDeadline) {
deadline = Math.min(now + effectiveTimeoutMs, maxDeadline);
if (now - startedAt > effectiveTimeoutMs) {
options.log?.debug?.(
"STREAM",
`readiness deadline extended to ${deadline - startedAt}ms (liveness signal) (${options.provider || "provider"}/${options.model || "unknown"})`
);
}
}
if (appendStreamReadinessSignal(readinessState, decodedChunk)) {
options.log?.debug?.(
"STREAM",

View File

@@ -14,6 +14,7 @@ export type StreamReadinessPolicyInput = {
export type StreamReadinessPolicyResult = {
timeoutMs: number;
baseTimeoutMs: number;
maxTimeoutMs: number;
reasons: string[];
};
@@ -121,7 +122,7 @@ export function resolveStreamReadinessTimeout(
): StreamReadinessPolicyResult {
const baseTimeoutMs = Math.max(0, Math.floor(input.baseTimeoutMs || 0));
if (baseTimeoutMs <= 0) {
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, reasons: ["disabled"] };
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, maxTimeoutMs: baseTimeoutMs, reasons: ["disabled"] };
}
const maxTimeoutMs = Math.max(baseTimeoutMs, input.maxTimeoutMs ?? DEFAULT_MAX_TIMEOUT_MS);
@@ -197,5 +198,5 @@ export function resolveStreamReadinessTimeout(
timeoutMs = Math.min(timeoutMs, maxTimeoutMs);
if (timeoutMs === baseTimeoutMs) reasons.push("base");
return { timeoutMs, baseTimeoutMs, reasons };
return { timeoutMs, baseTimeoutMs, maxTimeoutMs, reasons };
}

View File

@@ -1,129 +0,0 @@
#!/usr/bin/env node
// One-shot generator (2026-08-31 docs audit follow-up nº 3): append a minimal,
// honest OpenAPI entry for every real route that docs/openapi.yaml does not
// document yet. Enumerates routes with the SAME lib the check:api-docs-refs
// gate uses, so the generated set can never diverge from the gate's universe.
// Minimal by design: real methods (parsed from each route.ts's exports), a
// group tag, a neutral path-derived summary and a generic 200 — no invented
// semantics. Rich schemas stay hand-curated in the existing entries.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { collectApiRouteFiles, toApiUrlPath, apiRoot } from "../check/lib/apiRoutes.mjs";
import { isLocalOnlyPath, ALWAYS_PROTECTED_API_PATHS } from "../../src/server/authz/routeGuard.ts";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const SPEC = path.join(ROOT, "docs", "openapi.yaml");
const APPLY = process.argv.includes("--apply");
const normalizeParams = (p) => p.replace(/\{[^}]+\}/g, "{}");
// --- real routes + their exported HTTP methods --------------------------------
const METHOD_RE =
/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|export\s+const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|export\s*\{[^}]*\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b[^}]*\}/g;
function routeMethods(absFile) {
const src = fs.readFileSync(absFile, "utf8");
const methods = new Set();
for (const m of src.matchAll(METHOD_RE)) {
const name = m[1] || m[2];
if (name) methods.add(name);
if (m[3]) {
// re-export list: capture every method inside the braces
for (const inner of m[0].matchAll(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g))
methods.add(inner[1]);
}
}
methods.delete("OPTIONS"); // CORS preflight — not a documented operation
methods.delete("HEAD");
return [...methods];
}
const routeFiles = collectApiRouteFiles(ROOT);
const API_ROOT = apiRoot(ROOT);
const routes = new Map(); // urlPath -> methods
for (const rel of routeFiles) {
const abs = path.join(ROOT, rel);
const url = toApiUrlPath(path.dirname(abs), API_ROOT);
if (url) routes.set(url, routeMethods(abs));
}
// --- paths already in the spec -------------------------------------------------
const spec = fs.readFileSync(SPEC, "utf8");
const specPaths = new Set();
for (const m of spec.matchAll(/^ {2}(\/[^\s:]+):\s*$/gm)) specPaths.add(normalizeParams(m[1]));
const missing = [...routes.entries()]
.filter(([url]) => !specPaths.has(normalizeParams(url)))
.filter(([, methods]) => methods.length > 0)
.sort(([a], [b]) => a.localeCompare(b));
// --- tag + summary derivation --------------------------------------------------
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
function groupTag(url) {
const seg = url.replace(/^\/api\//, "").split("/");
if (seg[0] === "v1") return seg[1] ? `V1 ${cap(seg[1].replace(/\{|\}/g, ""))}` : "V1";
return cap(seg[0].replace(/\{|\}/g, "").replace(/-/g, " "));
}
function summaryFor(url, method) {
const tail = url
.replace(/^\/api\/(v1\/)?/, "")
.replace(/\{([^}]+)\}/g, "<$1>")
.replace(/[/]/g, " ")
.replace(/-/g, " ");
return `${method} ${tail}`;
}
// --- emit YAML -----------------------------------------------------------------
const existingTags = new Set(
[...spec.matchAll(/^ {2}- name: (.+)$/gm)].map((m) => m[1].trim().toLowerCase())
);
const newTags = new Map();
const lines = [];
lines.push("");
lines.push(" # --- Generated route coverage (docs audit 2026-08-31) -----------------------");
lines.push(" # Minimal entries for every implemented route not documented above. Methods");
lines.push(" # are parsed from each route.ts's exports; summaries are path-derived.");
lines.push(
" # Regenerate with: node --import tsx/esm scripts/ad-hoc/gen-openapi-missing-paths.mjs --apply"
);
for (const [url, methods] of missing) {
const tag = groupTag(url);
if (!existingTags.has(tag.toLowerCase()) && !newTags.has(tag))
newTags.set(tag, `${tag} endpoints (generated route coverage)`);
lines.push(` ${url}:`);
const loopbackOnly = isLocalOnlyPath(url);
const alwaysProtected = ALWAYS_PROTECTED_API_PATHS.includes(url);
for (const method of methods.sort()) {
lines.push(` ${method.toLowerCase()}:`);
lines.push(` tags:`);
lines.push(` - ${tag}`);
lines.push(` summary: "${summaryFor(url, method)}"`);
if (loopbackOnly || isLocalOnlyPath(url, method)) lines.push(` x-loopback-only: true`);
if (alwaysProtected) lines.push(` x-always-protected: true`);
lines.push(` responses:`);
lines.push(` "200":`);
lines.push(` description: OK`);
}
}
const tagLines = [...newTags.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, description]) => ` - name: ${name}\n description: ${description}`)
.join("\n");
console.log(
`real routes: ${routes.size} · already in spec: ${specPaths.size} · missing with methods: ${missing.length} · new tags: ${newTags.size}`
);
if (!APPLY) {
console.log("(dry-run) pass --apply to write docs/openapi.yaml");
process.exit(0);
}
let out = spec;
// append new tags right after the last existing tag entry (before `paths:`)
if (tagLines) out = out.replace(/\npaths:\n/, `\n${tagLines}\n\npaths:\n`);
// insert generated paths right before the components section
out = out.replace(/\ncomponents:\n/, `\n${lines.join("\n")}\n\ncomponents:\n`);
fs.writeFileSync(SPEC, out);
console.log(`wrote ${missing.length} paths + ${newTags.size} tags to docs/openapi.yaml`);

View File

@@ -137,6 +137,11 @@ function createNextApp() {
});
}
// The custom HTTP server owns process exit. Application instrumentation still
// registers its cleanup function, but must not install a competing signal
// listener that can race this runner's async server/Next teardown.
globalThis.__omnirouteCustomServerOwnsShutdown = true;
let nextApp = createNextApp();
// Best-effort self-heal for a corrupted Turbopack persistent dev cache (#6289):
@@ -231,6 +236,7 @@ async function start() {
systemdNotifier.stopping();
try {
await new Promise((resolve) => server.close(resolve));
await globalThis.__omnirouteRequestShutdown?.(signal);
await nextApp.close();
} catch (error) {
console.error("[SHUTDOWN] Failed during signal:", signal, error);

View File

@@ -74,142 +74,6 @@ curl https://localhost:20128/api/keys/{id}/devices \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/keys/{id}/regenerate
POST keys <id> regenerate
```bash
curl -X POST https://localhost:20128/api/keys/{id}/regenerate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/keys/{id}/reveal
GET keys <id> reveal
```bash
curl https://localhost:20128/api/keys/{id}/reveal \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/keys/{id}/usage-limits
GET keys <id> usage limits
```bash
curl https://localhost:20128/api/keys/{id}/usage-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/keys/groups
GET keys groups
```bash
curl https://localhost:20128/api/keys/groups \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/keys/groups
POST keys groups
```bash
curl -X POST https://localhost:20128/api/keys/groups \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/keys/groups/{id}
GET keys groups <id>
```bash
curl https://localhost:20128/api/keys/groups/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/keys/groups/{id}
PUT keys groups <id>
```bash
curl -X PUT https://localhost:20128/api/keys/groups/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/keys/groups/{id}
DELETE keys groups <id>
```bash
curl -X DELETE https://localhost:20128/api/keys/groups/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/keys/groups/{id}/keys
GET keys groups <id> keys
```bash
curl https://localhost:20128/api/keys/groups/{id}/keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/keys/groups/{id}/keys
POST keys groups <id> keys
```bash
curl -X POST https://localhost:20128/api/keys/groups/{id}/keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/keys/groups/{id}/keys
DELETE keys groups <id> keys
```bash
curl -X DELETE https://localhost:20128/api/keys/groups/{id}/keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/keys/groups/{id}/permissions
GET keys groups <id> permissions
```bash
curl https://localhost:20128/api/keys/groups/{id}/permissions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/keys/groups/{id}/permissions
POST keys groups <id> permissions
```bash
curl -X POST https://localhost:20128/api/keys/groups/{id}/permissions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/keys/groups/{id}/permissions
DELETE keys groups <id> permissions
```bash
curl -X DELETE https://localhost:20128/api/keys/groups/{id}/permissions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -69,24 +69,6 @@ curl https://localhost:20128/api/auth/oidc/callback \
-b cookie.jar
```
### GET /api/auth/csrf
GET auth csrf
```bash
curl https://localhost:20128/api/auth/csrf \
-b cookie.jar
```
### GET /api/auth/status
GET auth status
```bash
curl https://localhost:20128/api/auth/status \
-b cookie.jar
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -52,42 +52,6 @@ curl -X DELETE https://localhost:20128/api/cache/stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cache/entries
GET cache entries
```bash
curl https://localhost:20128/api/cache/entries \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/cache/entries
DELETE cache entries
```bash
curl -X DELETE https://localhost:20128/api/cache/entries \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cache/reasoning
GET cache reasoning
```bash
curl https://localhost:20128/api/cache/reasoning \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/cache/reasoning
DELETE cache reasoning
```bash
curl -X DELETE https://localhost:20128/api/cache/reasoning \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -385,372 +385,6 @@ curl -X DELETE https://localhost:20128/api/cli-tools/codewhale-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/all-statuses
GET cli tools all statuses
```bash
curl https://localhost:20128/api/cli-tools/all-statuses \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/apply
POST cli tools apply
```bash
curl -X POST https://localhost:20128/api/cli-tools/apply \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/cli-tools/config
GET cli tools config
```bash
curl https://localhost:20128/api/cli-tools/config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/config
POST cli tools config
```bash
curl -X POST https://localhost:20128/api/cli-tools/config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/cli-tools/deepseek-tui-settings
GET cli tools deepseek tui settings
```bash
curl https://localhost:20128/api/cli-tools/deepseek-tui-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/deepseek-tui-settings
POST cli tools deepseek tui settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/deepseek-tui-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/deepseek-tui-settings
DELETE cli tools deepseek tui settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/deepseek-tui-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/detect
GET cli tools detect
```bash
curl https://localhost:20128/api/cli-tools/detect \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/forge-settings
GET cli tools forge settings
```bash
curl https://localhost:20128/api/cli-tools/forge-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/forge-settings
POST cli tools forge settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/forge-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/forge-settings
DELETE cli tools forge settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/forge-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/grok-build-settings
GET cli tools grok build settings
```bash
curl https://localhost:20128/api/cli-tools/grok-build-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/grok-build-settings
POST cli tools grok build settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/grok-build-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/grok-build-settings
DELETE cli tools grok build settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/grok-build-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/hermes-agent-settings
GET cli tools hermes agent settings
```bash
curl https://localhost:20128/api/cli-tools/hermes-agent-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/hermes-agent-settings
POST cli tools hermes agent settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/hermes-agent-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/cli-tools/jcode-settings
GET cli tools jcode settings
```bash
curl https://localhost:20128/api/cli-tools/jcode-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/jcode-settings
POST cli tools jcode settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/jcode-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/jcode-settings
DELETE cli tools jcode settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/jcode-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/keys
GET cli tools keys
```bash
curl https://localhost:20128/api/cli-tools/keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/letta-settings
GET cli tools letta settings
```bash
curl https://localhost:20128/api/cli-tools/letta-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/letta-settings
POST cli tools letta settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/letta-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/letta-settings
DELETE cli tools letta settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/letta-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/logs
GET cli tools logs
```bash
curl https://localhost:20128/api/cli-tools/logs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/omp-settings
GET cli tools omp settings
```bash
curl https://localhost:20128/api/cli-tools/omp-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/omp-settings
POST cli tools omp settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/omp-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/omp-settings
DELETE cli tools omp settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/omp-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/openclaw/auto-order
GET cli tools openclaw auto order
```bash
curl https://localhost:20128/api/cli-tools/openclaw/auto-order \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/pi-settings
GET cli tools pi settings
```bash
curl https://localhost:20128/api/cli-tools/pi-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/pi-settings
POST cli tools pi settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/pi-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/pi-settings
DELETE cli tools pi settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/pi-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/qwen-settings
GET cli tools qwen settings
```bash
curl https://localhost:20128/api/cli-tools/qwen-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/qwen-settings
POST cli tools qwen settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/qwen-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/qwen-settings
DELETE cli tools qwen settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/qwen-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/smelt-settings
GET cli tools smelt settings
```bash
curl https://localhost:20128/api/cli-tools/smelt-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/cli-tools/smelt-settings
POST cli tools smelt settings
```bash
curl -X POST https://localhost:20128/api/cli-tools/smelt-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/cli-tools/smelt-settings
DELETE cli tools smelt settings
```bash
curl -X DELETE https://localhost:20128/api/cli-tools/smelt-settings \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/cli-tools/status
GET cli tools status
```bash
curl https://localhost:20128/api/cli-tools/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -131,46 +131,6 @@ curl -X DELETE https://localhost:20128/api/fallback/chains \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/combos/auto
GET combos auto
```bash
curl https://localhost:20128/api/combos/auto \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/combos/builder/options
GET combos builder options
```bash
curl https://localhost:20128/api/combos/builder/options \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/combos/duplicate
POST combos duplicate
```bash
curl -X POST https://localhost:20128/api/combos/duplicate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/combos/reorder
POST combos reorder
```bash
curl -X POST https://localhost:20128/api/combos/reorder \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -43,48 +43,6 @@ curl https://localhost:20128/api/compression/rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/compression/compare
POST compression compare
```bash
curl -X POST https://localhost:20128/api/compression/compare \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/compression/compare/verify
POST compression compare verify
```bash
curl -X POST https://localhost:20128/api/compression/compare/verify \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/compression/engines
GET compression engines
```bash
curl https://localhost:20128/api/compression/engines \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/compression/retrieve
POST compression retrieve
```bash
curl -X POST https://localhost:20128/api/compression/retrieve \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -74,24 +74,6 @@ curl https://localhost:20128/api/context/rtk/raw-output/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/context/rtk/discover
GET context rtk discover
```bash
curl https://localhost:20128/api/context/rtk/discover \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/context/rtk/learn
GET context rtk learn
```bash
curl https://localhost:20128/api/context/rtk/learn \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -14,46 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
### GET /api/system/env/repair
GET system env repair
```bash
curl https://localhost:20128/api/system/env/repair \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/system/env/repair
POST system env repair
```bash
curl -X POST https://localhost:20128/api/system/env/repair \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/system/version
GET system version
```bash
curl https://localhost:20128/api/system/version \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/system/version
POST system version
```bash
curl -X POST https://localhost:20128/api/system/version \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
_No endpoints mapped for this area yet._
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -462,898 +462,6 @@ curl https://localhost:20128/api/v1/provider-plugin-manifest \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/{omnirouteCatchAll}
GET <omnirouteCatchAll>
```bash
curl https://localhost:20128/api/v1/{omnirouteCatchAll} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/{omnirouteCatchAll}
POST <omnirouteCatchAll>
```bash
curl -X POST https://localhost:20128/api/v1/{omnirouteCatchAll} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/v1/{omnirouteCatchAll}
PUT <omnirouteCatchAll>
```bash
curl -X PUT https://localhost:20128/api/v1/{omnirouteCatchAll} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PATCH /api/v1/{omnirouteCatchAll}
PATCH <omnirouteCatchAll>
```bash
curl -X PATCH https://localhost:20128/api/v1/{omnirouteCatchAll} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/v1/{omnirouteCatchAll}
DELETE <omnirouteCatchAll>
```bash
curl -X DELETE https://localhost:20128/api/v1/{omnirouteCatchAll} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/accounts/{id}/limits
GET accounts <id> limits
```bash
curl https://localhost:20128/api/v1/accounts/{id}/limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/v1/accounts/{id}/limits
PUT accounts <id> limits
```bash
curl -X PUT https://localhost:20128/api/v1/accounts/{id}/limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/agents/credentials
GET agents credentials
```bash
curl https://localhost:20128/api/v1/agents/credentials \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/agents/credentials
POST agents credentials
```bash
curl -X POST https://localhost:20128/api/v1/agents/credentials \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/agents/health
GET agents health
```bash
curl https://localhost:20128/api/v1/agents/health \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/agents/tasks
GET agents tasks
```bash
curl https://localhost:20128/api/v1/agents/tasks \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/agents/tasks
POST agents tasks
```bash
curl -X POST https://localhost:20128/api/v1/agents/tasks \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/v1/agents/tasks
DELETE agents tasks
```bash
curl -X DELETE https://localhost:20128/api/v1/agents/tasks \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/agents/tasks/{id}
GET agents tasks <id>
```bash
curl https://localhost:20128/api/v1/agents/tasks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/agents/tasks/{id}
POST agents tasks <id>
```bash
curl -X POST https://localhost:20128/api/v1/agents/tasks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/v1/agents/tasks/{id}
DELETE agents tasks <id>
```bash
curl -X DELETE https://localhost:20128/api/v1/agents/tasks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/antigravity
POST antigravity
```bash
curl -X POST https://localhost:20128/api/v1/antigravity \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/auto-combo/{channel}/candidates
GET auto combo <channel> candidates
```bash
curl https://localhost:20128/api/v1/auto-combo/{channel}/candidates \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/batches
GET batches
```bash
curl https://localhost:20128/api/v1/batches \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/batches
POST batches
```bash
curl -X POST https://localhost:20128/api/v1/batches \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/batches/{id}
GET batches <id>
```bash
curl https://localhost:20128/api/v1/batches/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/v1/batches/{id}
DELETE batches <id>
```bash
curl -X DELETE https://localhost:20128/api/v1/batches/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/batches/{id}/cancel
POST batches <id> cancel
```bash
curl -X POST https://localhost:20128/api/v1/batches/{id}/cancel \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/v1/batches/delete-completed
DELETE batches delete completed
```bash
curl -X DELETE https://localhost:20128/api/v1/batches/delete-completed \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/classify
POST classify
```bash
curl -X POST https://localhost:20128/api/v1/classify \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/combos
GET combos
```bash
curl https://localhost:20128/api/v1/combos \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/completions
POST completions
```bash
curl -X POST https://localhost:20128/api/v1/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/files
GET files
```bash
curl https://localhost:20128/api/v1/files \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/files
POST files
```bash
curl -X POST https://localhost:20128/api/v1/files \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/files/{id}
GET files <id>
```bash
curl https://localhost:20128/api/v1/files/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/v1/files/{id}
DELETE files <id>
```bash
curl -X DELETE https://localhost:20128/api/v1/files/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/files/{id}/content
GET files <id> content
```bash
curl https://localhost:20128/api/v1/files/{id}/content \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/images/edits
POST images edits
```bash
curl -X POST https://localhost:20128/api/v1/images/edits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/images/upscale
GET images upscale
```bash
curl https://localhost:20128/api/v1/images/upscale \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/images/upscale
POST images upscale
```bash
curl -X POST https://localhost:20128/api/v1/images/upscale \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/issues/report
POST issues report
```bash
curl -X POST https://localhost:20128/api/v1/issues/report \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/management/proxies
GET management proxies
```bash
curl https://localhost:20128/api/v1/management/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/management/proxies
POST management proxies
```bash
curl -X POST https://localhost:20128/api/v1/management/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PATCH /api/v1/management/proxies
PATCH management proxies
```bash
curl -X PATCH https://localhost:20128/api/v1/management/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/v1/management/proxies
DELETE management proxies
```bash
curl -X DELETE https://localhost:20128/api/v1/management/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/management/proxies/assignments
GET management proxies assignments
```bash
curl https://localhost:20128/api/v1/management/proxies/assignments \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/v1/management/proxies/assignments
PUT management proxies assignments
```bash
curl -X PUT https://localhost:20128/api/v1/management/proxies/assignments \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/v1/management/proxies/bulk-assign
PUT management proxies bulk assign
```bash
curl -X PUT https://localhost:20128/api/v1/management/proxies/bulk-assign \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/management/proxies/health
GET management proxies health
```bash
curl https://localhost:20128/api/v1/management/proxies/health \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/me/status
GET me status
```bash
curl https://localhost:20128/api/v1/me/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/muse-code/models
GET muse code models
```bash
curl https://localhost:20128/api/v1/muse-code/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/music/generations
GET music generations
```bash
curl https://localhost:20128/api/v1/music/generations \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/music/generations
POST music generations
```bash
curl -X POST https://localhost:20128/api/v1/music/generations \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/providers/{provider}/limits
GET providers <provider> limits
```bash
curl https://localhost:20128/api/v1/providers/{provider}/limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/v1/providers/{provider}/limits
PUT providers <provider> limits
```bash
curl -X PUT https://localhost:20128/api/v1/providers/{provider}/limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/quotas/check
GET quotas check
```bash
curl https://localhost:20128/api/v1/quotas/check \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/registered-keys
GET registered keys
```bash
curl https://localhost:20128/api/v1/registered-keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/registered-keys
POST registered keys
```bash
curl -X POST https://localhost:20128/api/v1/registered-keys \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/registered-keys/{id}
GET registered keys <id>
```bash
curl https://localhost:20128/api/v1/registered-keys/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/v1/registered-keys/{id}
DELETE registered keys <id>
```bash
curl -X DELETE https://localhost:20128/api/v1/registered-keys/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/registered-keys/{id}/revoke
POST registered keys <id> revoke
```bash
curl -X POST https://localhost:20128/api/v1/registered-keys/{id}/revoke \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/relay/chat/completions
POST relay chat completions
```bash
curl -X POST https://localhost:20128/api/v1/relay/chat/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/relay/chat/completions/bifrost
POST relay chat completions bifrost
```bash
curl -X POST https://localhost:20128/api/v1/relay/chat/completions/bifrost \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/responses/{path}
POST responses <path>
```bash
curl -X POST https://localhost:20128/api/v1/responses/{path} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/search/analytics
GET search analytics
```bash
curl https://localhost:20128/api/v1/search/analytics \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/segment
POST segment
```bash
curl -X POST https://localhost:20128/api/v1/segment \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/video-bridge/drilldown
GET video bridge drilldown
```bash
curl https://localhost:20128/api/v1/video-bridge/drilldown \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/v1/video-bridge/drilldown
DELETE video bridge drilldown
```bash
curl -X DELETE https://localhost:20128/api/v1/video-bridge/drilldown \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/videos/generations
GET videos generations
```bash
curl https://localhost:20128/api/v1/videos/generations \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/videos/generations
POST videos generations
```bash
curl -X POST https://localhost:20128/api/v1/videos/generations \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/{token}
GET vscode <token>
```bash
curl https://localhost:20128/api/v1/vscode/{token} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/{token}/api/chat
POST vscode <token> api chat
```bash
curl -X POST https://localhost:20128/api/v1/vscode/{token}/api/chat \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/vscode/{token}/api/show
POST vscode <token> api show
```bash
curl -X POST https://localhost:20128/api/v1/vscode/{token}/api/show \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/{token}/api/tags
GET vscode <token> api tags
```bash
curl https://localhost:20128/api/v1/vscode/{token}/api/tags \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/vscode/{token}/api/version
GET vscode <token> api version
```bash
curl https://localhost:20128/api/v1/vscode/{token}/api/version \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/{token}/chat/completions
POST vscode <token> chat completions
```bash
curl -X POST https://localhost:20128/api/v1/vscode/{token}/chat/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/{token}/combos
GET vscode <token> combos
```bash
curl https://localhost:20128/api/v1/vscode/{token}/combos \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/vscode/{token}/models
GET vscode <token> models
```bash
curl https://localhost:20128/api/v1/vscode/{token}/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/{token}/responses
POST vscode <token> responses
```bash
curl -X POST https://localhost:20128/api/v1/vscode/{token}/responses \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/vscode/{token}/v1/chat/completions
POST vscode <token> v1 chat completions
```bash
curl -X POST https://localhost:20128/api/v1/vscode/{token}/v1/chat/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/{token}/v1/models
GET vscode <token> v1 models
```bash
curl https://localhost:20128/api/v1/vscode/{token}/v1/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/vscode/combos/{token}/{{slug}}
GET vscode combos <token> <{slug>}
```bash
curl https://localhost:20128/api/v1/vscode/combos/{token}/{{slug}} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/combos/{token}/{{slug}}
POST vscode combos <token> <{slug>}
```bash
curl -X POST https://localhost:20128/api/v1/vscode/combos/{token}/{{slug}} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/raw/{token}
GET vscode raw <token>
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/raw/{token}/api/chat
POST vscode raw <token> api chat
```bash
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/api/chat \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/vscode/raw/{token}/api/show
POST vscode raw <token> api show
```bash
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/api/show \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/raw/{token}/api/tags
GET vscode raw <token> api tags
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token}/api/tags \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/vscode/raw/{token}/api/version
GET vscode raw <token> api version
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token}/api/version \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/raw/{token}/chat/completions
POST vscode raw <token> chat completions
```bash
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/chat/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/raw/{token}/combos
GET vscode raw <token> combos
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token}/combos \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/v1/vscode/raw/{token}/models
GET vscode raw <token> models
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token}/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/vscode/raw/{token}/responses
POST vscode raw <token> responses
```bash
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/responses \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/v1/vscode/raw/{token}/v1/chat/completions
POST vscode raw <token> v1 chat completions
```bash
curl -X POST https://localhost:20128/api/v1/vscode/raw/{token}/v1/chat/completions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/vscode/raw/{token}/v1/models
GET vscode raw <token> v1 models
```bash
curl https://localhost:20128/api/v1/vscode/raw/{token}/v1/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/v1/web/fetch
POST web fetch
```bash
curl -X POST https://localhost:20128/api/v1/web/fetch \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -14,91 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
### GET /api/mcp/audit
GET mcp audit
```bash
curl https://localhost:20128/api/mcp/audit \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/mcp/audit/stats
GET mcp audit stats
```bash
curl https://localhost:20128/api/mcp/audit/stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/mcp/sse
GET mcp sse
```bash
curl https://localhost:20128/api/mcp/sse \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/mcp/sse
POST mcp sse
```bash
curl -X POST https://localhost:20128/api/mcp/sse \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/mcp/status
GET mcp status
```bash
curl https://localhost:20128/api/mcp/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/mcp/stream
GET mcp stream
```bash
curl https://localhost:20128/api/mcp/stream \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/mcp/stream
POST mcp stream
```bash
curl -X POST https://localhost:20128/api/mcp/stream \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/mcp/stream
DELETE mcp stream
```bash
curl -X DELETE https://localhost:20128/api/mcp/stream \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/mcp/tools
GET mcp tools
```bash
curl https://localhost:20128/api/mcp/tools \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
_No endpoints mapped for this area yet._
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -54,46 +54,6 @@ curl https://localhost:20128/api/models/catalog \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/models/openrouter-catalog
GET models openrouter catalog
```bash
curl https://localhost:20128/api/models/openrouter-catalog \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/models/test
POST models test
```bash
curl -X POST https://localhost:20128/api/models/test \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/models/test-all
POST models test all
```bash
curl -X POST https://localhost:20128/api/models/test-all \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/v1/models/{model}
GET models <model>
```bash
curl https://localhost:20128/api/v1/models/{model} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -229,526 +229,6 @@ curl https://localhost:20128/api/provider-models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/providers/{id}/cc-alias
GET providers <id> cc alias
```bash
curl https://localhost:20128/api/providers/{id}/cc-alias \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/providers/{id}/cc-alias
PUT providers <id> cc alias
```bash
curl -X PUT https://localhost:20128/api/providers/{id}/cc-alias \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/{id}/chatgpt-web-codex-doctor
GET providers <id> chatgpt web codex doctor
```bash
curl https://localhost:20128/api/providers/{id}/chatgpt-web-codex-doctor \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/{id}/claude-auth/apply-local
POST providers <id> claude auth apply local
```bash
curl -X POST https://localhost:20128/api/providers/{id}/claude-auth/apply-local \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/claude-auth/export
POST providers <id> claude auth export
```bash
curl -X POST https://localhost:20128/api/providers/{id}/claude-auth/export \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/codex-auth/apply-local
POST providers <id> codex auth apply local
```bash
curl -X POST https://localhost:20128/api/providers/{id}/codex-auth/apply-local \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/codex-auth/export
POST providers <id> codex auth export
```bash
curl -X POST https://localhost:20128/api/providers/{id}/codex-auth/export \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/{id}/interception-rules
GET providers <id> interception rules
```bash
curl https://localhost:20128/api/providers/{id}/interception-rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/providers/{id}/interception-rules
PUT providers <id> interception rules
```bash
curl -X PUT https://localhost:20128/api/providers/{id}/interception-rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/providers/{id}/interception-rules
DELETE providers <id> interception rules
```bash
curl -X DELETE https://localhost:20128/api/providers/{id}/interception-rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/{id}/login
POST providers <id> login
```bash
curl -X POST https://localhost:20128/api/providers/{id}/login \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/{id}/param-filters
GET providers <id> param filters
```bash
curl https://localhost:20128/api/providers/{id}/param-filters \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/providers/{id}/param-filters
PUT providers <id> param filters
```bash
curl -X PUT https://localhost:20128/api/providers/{id}/param-filters \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/providers/{id}/param-filters
DELETE providers <id> param filters
```bash
curl -X DELETE https://localhost:20128/api/providers/{id}/param-filters \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/{id}/refresh
POST providers <id> refresh
```bash
curl -X POST https://localhost:20128/api/providers/{id}/refresh \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/refresh-cursor
POST providers <id> refresh cursor
```bash
curl -X POST https://localhost:20128/api/providers/{id}/refresh-cursor \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/refresh-token
POST providers <id> refresh token
```bash
curl -X POST https://localhost:20128/api/providers/{id}/refresh-token \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/{id}/sync-models
POST providers <id> sync models
```bash
curl -X POST https://localhost:20128/api/providers/{id}/sync-models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/bulk
POST providers bulk
```bash
curl -X POST https://localhost:20128/api/providers/bulk \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/bulk-web-session
POST providers bulk web session
```bash
curl -X POST https://localhost:20128/api/providers/bulk-web-session \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/claude-auth/import
POST providers claude auth import
```bash
curl -X POST https://localhost:20128/api/providers/claude-auth/import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/claude-auth/import-bulk
POST providers claude auth import bulk
```bash
curl -X POST https://localhost:20128/api/providers/claude-auth/import-bulk \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/claude-auth/zip-extract
POST providers claude auth zip extract
```bash
curl -X POST https://localhost:20128/api/providers/claude-auth/zip-extract \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/codex-auth/import
POST providers codex auth import
```bash
curl -X POST https://localhost:20128/api/providers/codex-auth/import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/codex-auth/import-bulk
POST providers codex auth import bulk
```bash
curl -X POST https://localhost:20128/api/providers/codex-auth/import-bulk \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/codex-auth/zip-extract
POST providers codex auth zip extract
```bash
curl -X POST https://localhost:20128/api/providers/codex-auth/zip-extract \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/command-code/auth/apply
POST providers command code auth apply
```bash
curl -X POST https://localhost:20128/api/providers/command-code/auth/apply \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/command-code/auth/callback
POST providers command code auth callback
```bash
curl -X POST https://localhost:20128/api/providers/command-code/auth/callback \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/command-code/auth/start
POST providers command code auth start
```bash
curl -X POST https://localhost:20128/api/providers/command-code/auth/start \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/command-code/auth/status
GET providers command code auth status
```bash
curl https://localhost:20128/api/providers/command-code/auth/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/command-code/auth/status
POST providers command code auth status
```bash
curl -X POST https://localhost:20128/api/providers/command-code/auth/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/expiration
GET providers expiration
```bash
curl https://localhost:20128/api/providers/expiration \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/providers/free-onboarding
GET providers free onboarding
```bash
curl https://localhost:20128/api/providers/free-onboarding \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/free-onboarding
POST providers free onboarding
```bash
curl -X POST https://localhost:20128/api/providers/free-onboarding \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/health-autopilot
GET providers health autopilot
```bash
curl https://localhost:20128/api/providers/health-autopilot \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/health-autopilot/actions
POST providers health autopilot actions
```bash
curl -X POST https://localhost:20128/api/providers/health-autopilot/actions \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/health-matrix
GET providers health matrix
```bash
curl https://localhost:20128/api/providers/health-matrix \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/import
POST providers import
```bash
curl -X POST https://localhost:20128/api/providers/import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/openrouter-stats
GET providers openrouter stats
```bash
curl https://localhost:20128/api/providers/openrouter-stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/providers/quota-windows
GET providers quota windows
```bash
curl https://localhost:20128/api/providers/quota-windows \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/volcengine-plan/connect
POST providers volcengine plan connect
```bash
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/volcengine-plan/connect/{sessionId}/cancel
POST providers volcengine plan connect <sessionId> cancel
```bash
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/cancel \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/volcengine-plan/connect/{sessionId}/code
POST providers volcengine plan connect <sessionId> code
```bash
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/code \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/volcengine-plan/connect/{sessionId}/identity
POST providers volcengine plan connect <sessionId> identity
```bash
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/identity \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/volcengine-plan/connect/{sessionId}/resend
POST providers volcengine plan connect <sessionId> resend
```bash
curl -X POST https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/resend \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/providers/volcengine-plan/connect/{sessionId}/status
GET providers volcengine plan connect <sessionId> status
```bash
curl https://localhost:20128/api/providers/volcengine-plan/connect/{sessionId}/status \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/providers/web-session-contract
GET providers web session contract
```bash
curl https://localhost:20128/api/providers/web-session-contract \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/providers/zed/discover
POST providers zed discover
```bash
curl -X POST https://localhost:20128/api/providers/zed/discover \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/zed/import
POST providers zed import
```bash
curl -X POST https://localhost:20128/api/providers/zed/import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/providers/zed/manual-import
POST providers zed manual import
```bash
curl -X POST https://localhost:20128/api/providers/zed/manual-import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -25,15 +25,6 @@ curl https://localhost:20128/api/monitoring/health \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/provider-metrics
GET provider metrics
```bash
curl https://localhost:20128/api/provider-metrics \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -383,981 +383,6 @@ curl -X POST https://localhost:20128/api/settings/purge-usage-history \
-d '{}'
```
### GET /api/settings/authz-inventory
GET settings authz inventory
```bash
curl https://localhost:20128/api/settings/authz-inventory \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/auto-disable-accounts
GET settings auto disable accounts
```bash
curl https://localhost:20128/api/settings/auto-disable-accounts \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/auto-disable-accounts
PUT settings auto disable accounts
```bash
curl -X PUT https://localhost:20128/api/settings/auto-disable-accounts \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/background-degradation
GET settings background degradation
```bash
curl https://localhost:20128/api/settings/background-degradation \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/background-degradation
POST settings background degradation
```bash
curl -X POST https://localhost:20128/api/settings/background-degradation \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/settings/background-degradation
PUT settings background degradation
```bash
curl -X PUT https://localhost:20128/api/settings/background-degradation \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/cache-config
GET settings cache config
```bash
curl https://localhost:20128/api/settings/cache-config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/cache-config
PUT settings cache config
```bash
curl -X PUT https://localhost:20128/api/settings/cache-config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/cache-metrics
GET settings cache metrics
```bash
curl https://localhost:20128/api/settings/cache-metrics \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/settings/cache-metrics
DELETE settings cache metrics
```bash
curl -X DELETE https://localhost:20128/api/settings/cache-metrics \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/cc-discovery-metrics
GET settings cc discovery metrics
```bash
curl https://localhost:20128/api/settings/cc-discovery-metrics \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/compression/rules
GET settings compression rules
```bash
curl https://localhost:20128/api/settings/compression/rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/compression/run-telemetry
GET settings compression run telemetry
```bash
curl https://localhost:20128/api/settings/compression/run-telemetry \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/database
GET settings database
```bash
curl https://localhost:20128/api/settings/database \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/database
PUT settings database
```bash
curl -X PUT https://localhost:20128/api/settings/database \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PATCH /api/settings/database
PATCH settings database
```bash
curl -X PATCH https://localhost:20128/api/settings/database \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/database/refresh-stats
POST settings database refresh stats
```bash
curl -X POST https://localhost:20128/api/settings/database/refresh-stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/database/vacuum
GET settings database vacuum
```bash
curl https://localhost:20128/api/settings/database/vacuum \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/database/vacuum
POST settings database vacuum
```bash
curl -X POST https://localhost:20128/api/settings/database/vacuum \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/export-json
GET settings export json
```bash
curl https://localhost:20128/api/settings/export-json \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/favicon
GET settings favicon
```bash
curl https://localhost:20128/api/settings/favicon \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/feature-flags
GET settings feature flags
```bash
curl https://localhost:20128/api/settings/feature-flags \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/feature-flags
PUT settings feature flags
```bash
curl -X PUT https://localhost:20128/api/settings/feature-flags \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/feature-flags
DELETE settings feature flags
```bash
curl -X DELETE https://localhost:20128/api/settings/feature-flags \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/free-proxies
GET settings free proxies
```bash
curl https://localhost:20128/api/settings/free-proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### DELETE /api/settings/free-proxies
DELETE settings free proxies
```bash
curl -X DELETE https://localhost:20128/api/settings/free-proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/free-proxies/{id}/add-to-pool
POST settings free proxies <id> add to pool
```bash
curl -X POST https://localhost:20128/api/settings/free-proxies/{id}/add-to-pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/free-proxies/bulk-add-to-pool
POST settings free proxies bulk add to pool
```bash
curl -X POST https://localhost:20128/api/settings/free-proxies/bulk-add-to-pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/free-proxies/stats
GET settings free proxies stats
```bash
curl https://localhost:20128/api/settings/free-proxies/stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/free-proxies/sync
POST settings free proxies sync
```bash
curl -X POST https://localhost:20128/api/settings/free-proxies/sync \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/import-json
POST settings import json
```bash
curl -X POST https://localhost:20128/api/settings/import-json \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/lkgp-cache
DELETE settings lkgp cache
```bash
curl -X DELETE https://localhost:20128/api/settings/lkgp-cache \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/local-corpus
GET settings local corpus
```bash
curl https://localhost:20128/api/settings/local-corpus \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/local-corpus
POST settings local corpus
```bash
curl -X POST https://localhost:20128/api/settings/local-corpus \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/local-corpus
DELETE settings local corpus
```bash
curl -X DELETE https://localhost:20128/api/settings/local-corpus \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/mitm
GET settings mitm
```bash
curl https://localhost:20128/api/settings/mitm \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/mitm
POST settings mitm
```bash
curl -X POST https://localhost:20128/api/settings/mitm \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/settings/mitm
PUT settings mitm
```bash
curl -X PUT https://localhost:20128/api/settings/mitm \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/model-aliases
GET settings model aliases
```bash
curl https://localhost:20128/api/settings/model-aliases \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/model-aliases
POST settings model aliases
```bash
curl -X POST https://localhost:20128/api/settings/model-aliases \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/settings/model-aliases
PUT settings model aliases
```bash
curl -X PUT https://localhost:20128/api/settings/model-aliases \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/model-aliases
DELETE settings model aliases
```bash
curl -X DELETE https://localhost:20128/api/settings/model-aliases \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/models-dev
GET settings models dev
```bash
curl https://localhost:20128/api/settings/models-dev \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/models-dev
POST settings models dev
```bash
curl -X POST https://localhost:20128/api/settings/models-dev \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/notion
GET settings notion
```bash
curl https://localhost:20128/api/settings/notion \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/notion
POST settings notion
```bash
curl -X POST https://localhost:20128/api/settings/notion \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/notion
DELETE settings notion
```bash
curl -X DELETE https://localhost:20128/api/settings/notion \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/obsidian
GET settings obsidian
```bash
curl https://localhost:20128/api/settings/obsidian \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/obsidian
POST settings obsidian
```bash
curl -X POST https://localhost:20128/api/settings/obsidian \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/obsidian
DELETE settings obsidian
```bash
curl -X DELETE https://localhost:20128/api/settings/obsidian \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/obsidian/webdav
GET settings obsidian webdav
```bash
curl https://localhost:20128/api/settings/obsidian/webdav \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/obsidian/webdav
POST settings obsidian webdav
```bash
curl -X POST https://localhost:20128/api/settings/obsidian/webdav \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/obsidian/webdav
DELETE settings obsidian webdav
```bash
curl -X DELETE https://localhost:20128/api/settings/obsidian/webdav \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/settings/oneproxy
GET settings oneproxy
```bash
curl https://localhost:20128/api/settings/oneproxy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/oneproxy
POST settings oneproxy
```bash
curl -X POST https://localhost:20128/api/settings/oneproxy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/oneproxy
DELETE settings oneproxy
```bash
curl -X DELETE https://localhost:20128/api/settings/oneproxy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/oneproxy/rotate
POST settings oneproxy rotate
```bash
curl -X POST https://localhost:20128/api/settings/oneproxy/rotate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/proxies
GET settings proxies
```bash
curl https://localhost:20128/api/settings/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/proxies
POST settings proxies
```bash
curl -X POST https://localhost:20128/api/settings/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PATCH /api/settings/proxies
PATCH settings proxies
```bash
curl -X PATCH https://localhost:20128/api/settings/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/proxies
DELETE settings proxies
```bash
curl -X DELETE https://localhost:20128/api/settings/proxies \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/proxies/{id}/repair-relay
POST settings proxies <id> repair relay
```bash
curl -X POST https://localhost:20128/api/settings/proxies/{id}/repair-relay \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/proxies/assignments
GET settings proxies assignments
```bash
curl https://localhost:20128/api/settings/proxies/assignments \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/proxies/assignments
PUT settings proxies assignments
```bash
curl -X PUT https://localhost:20128/api/settings/proxies/assignments \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxies/auto-test
POST settings proxies auto test
```bash
curl -X POST https://localhost:20128/api/settings/proxies/auto-test \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxies/batch-activate
POST settings proxies batch activate
```bash
curl -X POST https://localhost:20128/api/settings/proxies/batch-activate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxies/batch-delete
POST settings proxies batch delete
```bash
curl -X POST https://localhost:20128/api/settings/proxies/batch-delete \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/settings/proxies/bulk-assign
PUT settings proxies bulk assign
```bash
curl -X PUT https://localhost:20128/api/settings/proxies/bulk-assign \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxies/bulk-import
POST settings proxies bulk import
```bash
curl -X POST https://localhost:20128/api/settings/proxies/bulk-import \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/proxies/egress
GET settings proxies egress
```bash
curl https://localhost:20128/api/settings/proxies/egress \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/proxies/egress
POST settings proxies egress
```bash
curl -X POST https://localhost:20128/api/settings/proxies/egress \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/proxies/health
GET settings proxies health
```bash
curl https://localhost:20128/api/settings/proxies/health \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/proxies/migrate
POST settings proxies migrate
```bash
curl -X POST https://localhost:20128/api/settings/proxies/migrate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/proxies/pool
GET settings proxies pool
```bash
curl https://localhost:20128/api/settings/proxies/pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/proxies/pool
PUT settings proxies pool
```bash
curl -X PUT https://localhost:20128/api/settings/proxies/pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PATCH /api/settings/proxies/pool
PATCH settings proxies pool
```bash
curl -X PATCH https://localhost:20128/api/settings/proxies/pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/proxies/pool
DELETE settings proxies pool
```bash
curl -X DELETE https://localhost:20128/api/settings/proxies/pool \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/proxy/cloudflare-deploy
POST settings proxy cloudflare deploy
```bash
curl -X POST https://localhost:20128/api/settings/proxy/cloudflare-deploy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxy/deno-deploy
POST settings proxy deno deploy
```bash
curl -X POST https://localhost:20128/api/settings/proxy/deno-deploy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/proxy/vercel-deploy
POST settings proxy vercel deploy
```bash
curl -X POST https://localhost:20128/api/settings/proxy/vercel-deploy \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/purge-call-logs
POST settings purge call logs
```bash
curl -X POST https://localhost:20128/api/settings/purge-call-logs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/purge-detailed-logs
POST settings purge detailed logs
```bash
curl -X POST https://localhost:20128/api/settings/purge-detailed-logs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/purge-logs
POST settings purge logs
```bash
curl -X POST https://localhost:20128/api/settings/purge-logs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/settings/purge-quota-snapshots
POST settings purge quota snapshots
```bash
curl -X POST https://localhost:20128/api/settings/purge-quota-snapshots \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/quota/state
GET settings quota state
```bash
curl https://localhost:20128/api/settings/quota/state \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/quota/state
POST settings quota state
```bash
curl -X POST https://localhost:20128/api/settings/quota/state \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/reasoning-routing-rules
GET settings reasoning routing rules
```bash
curl https://localhost:20128/api/settings/reasoning-routing-rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/reasoning-routing-rules
POST settings reasoning routing rules
```bash
curl -X POST https://localhost:20128/api/settings/reasoning-routing-rules \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/reasoning-routing-rules/{id}
GET settings reasoning routing rules <id>
```bash
curl https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PATCH /api/settings/reasoning-routing-rules/{id}
PATCH settings reasoning routing rules <id>
```bash
curl -X PATCH https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/settings/reasoning-routing-rules/{id}
DELETE settings reasoning routing rules <id>
```bash
curl -X DELETE https://localhost:20128/api/settings/reasoning-routing-rules/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/reasoning-routing-rules/simulate
POST settings reasoning routing rules simulate
```bash
curl -X POST https://localhost:20128/api/settings/reasoning-routing-rules/simulate \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/task-routing
GET settings task routing
```bash
curl https://localhost:20128/api/settings/task-routing \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/settings/task-routing
POST settings task routing
```bash
curl -X POST https://localhost:20128/api/settings/task-routing \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### PUT /api/settings/task-routing
PUT settings task routing
```bash
curl -X PUT https://localhost:20128/api/settings/task-routing \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/settings/tier-config
GET settings tier config
```bash
curl https://localhost:20128/api/settings/tier-config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/settings/tier-config
PUT settings tier config
```bash
curl -X PUT https://localhost:20128/api/settings/tier-config \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -93,44 +93,6 @@ curl -X POST https://localhost:20128/api/sync/initialize \
-d '{}'
```
### GET /api/sync/bundle
GET sync bundle
```bash
curl https://localhost:20128/api/sync/bundle \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/sync/tokens
GET sync tokens
```bash
curl https://localhost:20128/api/sync/tokens \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/sync/tokens
POST sync tokens
```bash
curl -X POST https://localhost:20128/api/sync/tokens \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/sync/tokens/{id}
DELETE sync tokens <id>
```bash
curl -X DELETE https://localhost:20128/api/sync/tokens/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -137,192 +137,6 @@ curl https://localhost:20128/api/usage/model-latency-stats \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/budget/bulk
GET usage budget bulk
```bash
curl https://localhost:20128/api/usage/budget/bulk \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/codex-reset-credit
GET usage codex reset credit
```bash
curl https://localhost:20128/api/usage/codex-reset-credit \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/usage/codex-reset-credit
POST usage codex reset credit
```bash
curl -X POST https://localhost:20128/api/usage/codex-reset-credit \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/usage/combo-forecast
GET usage combo forecast
```bash
curl https://localhost:20128/api/usage/combo-forecast \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/combo-health
GET usage combo health
```bash
curl https://localhost:20128/api/usage/combo-health \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/combo-health-autopilot
GET usage combo health autopilot
```bash
curl https://localhost:20128/api/usage/combo-health-autopilot \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/combo-health-dashboard
GET usage combo health dashboard
```bash
curl https://localhost:20128/api/usage/combo-health-dashboard \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/combo-scoring-inspector
GET usage combo scoring inspector
```bash
curl https://localhost:20128/api/usage/combo-scoring-inspector \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/combo-trace/{id}
GET usage combo trace <id>
```bash
curl https://localhost:20128/api/usage/combo-trace/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/om-usage
GET usage om usage
```bash
curl https://localhost:20128/api/usage/om-usage \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/provider-limits
GET usage provider limits
```bash
curl https://localhost:20128/api/usage/provider-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/usage/provider-limits
POST usage provider limits
```bash
curl -X POST https://localhost:20128/api/usage/provider-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/usage/provider-window-costs
GET usage provider window costs
```bash
curl https://localhost:20128/api/usage/provider-window-costs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/quota
GET usage quota
```bash
curl https://localhost:20128/api/usage/quota \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/requests-by-provider-date
GET usage requests by provider date
```bash
curl https://localhost:20128/api/usage/requests-by-provider-date \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/route-explain/{id}
GET usage route explain <id>
```bash
curl https://localhost:20128/api/usage/route-explain/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/token-limits
GET usage token limits
```bash
curl https://localhost:20128/api/usage/token-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/usage/token-limits
POST usage token limits
```bash
curl -X POST https://localhost:20128/api/usage/token-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/usage/token-limits
DELETE usage token limits
```bash
curl -X DELETE https://localhost:20128/api/usage/token-limits \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/usage/utilization
GET usage utilization
```bash
curl https://localhost:20128/api/usage/utilization \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -620,46 +620,6 @@ curl https://localhost:20128/api/services/{name}/logs \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/services/9router/models
GET services 9router models
```bash
curl https://localhost:20128/api/services/9router/models \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/services/9router/provider-expose
POST services 9router provider expose
```bash
curl -X POST https://localhost:20128/api/services/9router/provider-expose \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/services/cliproxy/accounts
GET services cliproxy accounts
```bash
curl https://localhost:20128/api/services/cliproxy/accounts \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/services/cliproxy/provider-expose
POST services cliproxy provider expose
```bash
curl -X POST https://localhost:20128/api/services/cliproxy/provider-expose \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -14,86 +14,7 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
### GET /api/webhooks
GET webhooks
```bash
curl https://localhost:20128/api/webhooks \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/webhooks
POST webhooks
```bash
curl -X POST https://localhost:20128/api/webhooks \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### GET /api/webhooks/{id}
GET webhooks <id>
```bash
curl https://localhost:20128/api/webhooks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### PUT /api/webhooks/{id}
PUT webhooks <id>
```bash
curl -X PUT https://localhost:20128/api/webhooks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### DELETE /api/webhooks/{id}
DELETE webhooks <id>
```bash
curl -X DELETE https://localhost:20128/api/webhooks/{id} \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### GET /api/webhooks/{id}/deliveries
GET webhooks <id> deliveries
```bash
curl https://localhost:20128/api/webhooks/{id}/deliveries \
-H "Authorization: Bearer $OMNIROUTE_TOKEN"
```
### POST /api/webhooks/{id}/test
POST webhooks <id> test
```bash
curl -X POST https://localhost:20128/api/webhooks/{id}/test \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
### POST /api/webhooks/validate-url
POST webhooks validate url
```bash
curl -X POST https://localhost:20128/api/webhooks/validate-url \
-H "Authorization: Bearer $OMNIROUTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
_No endpoints mapped for this area yet._
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -2,7 +2,7 @@
import { useTranslations } from "next-intl";
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { useState, useEffect, useMemo, useCallback, useRef, useSyncExternalStore } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
@@ -106,6 +106,12 @@ const INLINE_LINK = "text-primary hover:underline";
const DOCS_LINK =
"hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors";
// Stable no-op subscription for useSyncExternalStore reads of never-changing
// browser globals (location.origin does not change without a full navigation).
function emptySubscribe() {
return () => {};
}
export default function HomePageClient({ machineId }: HomePageClientProps) {
const router = useRouter();
const isElectron = useIsElectron();
@@ -115,7 +121,13 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
const [providerConnections, setProviderConnections] = useState([]);
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
const [baseUrl, setBaseUrl] = useState("/v1");
// useSyncExternalStore keeps SSR/hydration consistent ("/v1" on the server,
// the real origin after hydration) without a setState-in-effect round-trip.
const baseUrl = useSyncExternalStore(
emptySubscribe,
() => `${globalThis.location.origin}/v1`,
() => "/v1"
);
const [selectedProvider, setSelectedProvider] = useState(null);
const [providerMetrics, setProviderMetrics] = useState<Record<string, ProviderMetricSummary>>({});
const [providerTopology, setProviderTopology] = useState({ lastProvider: "", errorProvider: "" });
@@ -135,36 +147,39 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
// Platform detection and download links for Electron
const platform =
typeof globalThis.window === "undefined" ? undefined : globalThis.window.electronAPI?.platform;
// Destructured to locals: `versionInfo?.current` in a dependency array trips
// the lint heuristic that treats any `.current` access as a mutable ref read.
const installedVersion = versionInfo?.current || "";
const latestVersion = versionInfo?.latest || "";
const electronDownload = useMemo(() => {
const latest = versionInfo?.latest || "";
const cleanLatest = latest.replace(/^v/, "");
const cleanLatest = latestVersion.replace(/^v/, "");
if (platform === "darwin") {
return {
label: t("downloadDmg"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute-${cleanLatest}.dmg`,
desc: t("downloadDmgDescription", { version: versionInfo?.current || "" }),
desc: t("downloadDmgDescription", { version: installedVersion }),
};
}
if (platform === "win32") {
return {
label: t("downloadExe"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute.Setup.${cleanLatest}.exe`,
desc: t("downloadExeDescription", { version: versionInfo?.current || "" }),
desc: t("downloadExeDescription", { version: installedVersion }),
};
}
if (platform === "linux") {
return {
label: t("downloadAppImage"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute-${cleanLatest}.AppImage`,
desc: t("downloadAppImageDescription", { version: versionInfo?.current || "" }),
desc: t("downloadAppImageDescription", { version: installedVersion }),
};
}
return {
label: t("downloadUpdate"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/tag/v${cleanLatest}`,
desc: t("downloadUpdateDescription", { version: versionInfo?.current || "" }),
desc: t("downloadUpdateDescription", { version: installedVersion }),
};
}, [platform, t, versionInfo?.latest, versionInfo?.current]);
}, [platform, t, latestVersion, installedVersion]);
// Electron internal auto-updater state and listeners
const [electronUpdateStatus, setElectronUpdateStatus] = useState<{
@@ -234,12 +249,6 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
});
}, []);
useEffect(() => {
if (typeof globalThis.window !== "undefined") {
setBaseUrl(`${globalThis.location.origin}/v1`);
}
}, []);
const fetchData = useCallback(async () => {
try {
const [provRes, modelsRes, versionRes] = await Promise.all([
@@ -267,7 +276,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
}, []);
useEffect(() => {
fetchData();
void (async () => {
await fetchData();
})();
}, [fetchData]);
// Fetch provider nodes for display labels (compat providers)

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl";
import type { AuditLogEntry } from "@/lib/compliance/index";
import ActivityFeed from "./components/ActivityFeed";
@@ -14,7 +14,9 @@ export default function ActivityFeedClient() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [category, setCategory] = useState<EventCategory>("all");
const referenceNowMs = useRef<number>(Date.now());
// State (not a ref) because it is rendered: refs cannot be read during
// render, and Date.now() cannot run there either — the fetch settles it.
const [referenceNowMs, setReferenceNowMs] = useState<number>(0);
const fetchEntries = useCallback(async () => {
setLoading(true);
@@ -30,7 +32,7 @@ export default function ActivityFeedClient() {
}
const data = (await res.json()) as AuditLogEntry[];
// Reset reference time on fresh load so relative timestamps are stable
referenceNowMs.current = Date.now();
setReferenceNowMs(Date.now());
setAllEntries(Array.isArray(data) ? data : []);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : t("fetchFailed");
@@ -41,7 +43,9 @@ export default function ActivityFeedClient() {
}, [t]);
useEffect(() => {
fetchEntries();
void (async () => {
await fetchEntries();
})();
}, [fetchEntries]);
const filtered =
@@ -114,7 +118,7 @@ export default function ActivityFeedClient() {
<span className="text-sm">{t("loadingActivity")}</span>
</div>
) : (
<ActivityFeed entries={filtered} referenceNowMs={referenceNowMs.current} />
<ActivityFeed entries={filtered} referenceNowMs={referenceNowMs} />
)}
</div>
</div>

View File

@@ -95,7 +95,9 @@ export default function CacheHealthTab() {
}, []);
useEffect(() => {
void load(range);
void (async () => {
await load(range);
})();
}, [load, range]);
if (loading) return <Skeleton className="h-64 w-full" />;
@@ -179,8 +181,8 @@ export default function CacheHealthTab() {
{text(t, "cacheHealthConcentration", "Where the writes are concentrated")}
</h3>
<span className="text-xs text-text-muted">
{text(t, "cacheHealthThreshold", "outlier above")} {compact(data.heavyWriteThreshold)}{" "}
{text(t, "cacheHealthTokens", "tokens")}
{text(t, "cacheHealthThreshold", "outlier above")}{" "}
{compact(data.heavyWriteThreshold)} {text(t, "cacheHealthTokens", "tokens")}
</span>
</div>
<p className="text-sm text-text-main">
@@ -216,7 +218,9 @@ export default function CacheHealthTab() {
<table className="w-full min-w-[560px] text-sm">
<thead>
<tr className="border-b border-border text-left text-xs uppercase text-text-muted">
<th className="pb-2 pr-4 font-medium">{text(t, "cacheHealthModel", "Model")}</th>
<th className="pb-2 pr-4 font-medium">
{text(t, "cacheHealthModel", "Model")}
</th>
<th className="pb-2 pr-4 text-right font-medium">
{text(t, "cacheHealthCalls", "Calls")}
</th>

View File

@@ -852,7 +852,9 @@ export default function ComboHealthTab() {
useEffect(() => {
const controller = new AbortController();
fetchData(controller, false);
void (async () => {
await fetchData(controller, false);
})();
return () => controller.abort();
}, [fetchData]);

View File

@@ -131,7 +131,9 @@ export default function ProviderUtilizationTab() {
useEffect(() => {
const controller = new AbortController();
fetchUtilization(range, aggregateBy, controller.signal);
void (async () => {
await fetchUtilization(range, aggregateBy, controller.signal);
})();
return () => controller.abort();
}, [fetchUtilization, range, aggregateBy]);
@@ -340,12 +342,8 @@ export default function ProviderUtilizationTab() {
<ProviderIcon providerId={providerPart} size={22} />
</div>
<div>
<p className="text-sm font-semibold text-text-main">
{cardTitle}
</p>
<p className="text-xs text-text-muted">
{cardSubtitle}
</p>
<p className="text-sm font-semibold text-text-main">{cardTitle}</p>
<p className="text-xs text-text-muted">{cardSubtitle}</p>
</div>
</div>
<span

View File

@@ -522,14 +522,18 @@ export default function RouteExplainabilityTab({
useEffect(() => {
const controller = new AbortController();
fetchLogs(controller.signal);
void (async () => {
await fetchLogs(controller.signal);
})();
return () => controller.abort();
}, [fetchLogs]);
useEffect(() => {
if (!selectedId) return;
const controller = new AbortController();
fetchExplanation(selectedId, controller.signal);
void (async () => {
await fetchExplanation(selectedId, controller.signal);
})();
return () => controller.abort();
}, [fetchExplanation, selectedId]);

View File

@@ -64,7 +64,9 @@ export default function A2aAuditTab() {
}, [offset, skillFilter, stateFilter]);
useEffect(() => {
void fetchTasks();
void (async () => {
await fetchTasks();
})();
}, [fetchTasks]);
return (

View File

@@ -110,7 +110,9 @@ export default function ComplianceTab() {
}, [actor, eventType, from, offset, t, to]);
useEffect(() => {
void fetchEntries();
void (async () => {
await fetchEntries();
})();
}, [fetchEntries]);
const visibleEntries = useMemo(() => {
@@ -330,7 +332,9 @@ export default function ComplianceTab() {
</td>
<td className="px-4 py-3">
<span className="rounded-md border border-border bg-surface px-2 py-1 font-mono text-xs text-text-main">
{t.has(`eventTypes.${entry.action}`) ? t(`eventTypes.${entry.action}`) : entry.action}
{t.has(`eventTypes.${entry.action}`)
? t(`eventTypes.${entry.action}`)
: entry.action}
</span>
</td>
<td className="px-4 py-3">

View File

@@ -56,7 +56,9 @@ export default function McpAuditTab() {
}, []);
useEffect(() => {
void fetchStats();
void (async () => {
await fetchStats();
})();
}, [fetchStats]);
const fetchAudit = useCallback(async () => {
@@ -86,7 +88,9 @@ export default function McpAuditTab() {
}, [offset, successFilter, t, toolFilter]);
useEffect(() => {
void fetchAudit();
void (async () => {
await fetchAudit();
})();
}, [fetchAudit]);
return (

View File

@@ -63,7 +63,9 @@ export default function CacheEntriesTab() {
);
useEffect(() => {
fetchEntries();
void (async () => {
await fetchEntries();
})();
}, [fetchEntries]);
const handleDelete = async (signature: string) => {

View File

@@ -130,9 +130,12 @@ export default function ReasoningCacheTab() {
const [loading, setLoading] = useState(true);
const [clearing, setClearing] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>(null);
// Snapshot of "now" taken when the data lands (never during render — the
// purity rule bars Date.now() there); entries only render after a fetch.
const [nowMs, setNowMs] = useState(0);
const timeAgo = (dateStr: string): string => {
const diff = Date.now() - new Date(dateStr).getTime();
const diff = nowMs - new Date(dateStr).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return t("justNow");
if (minutes < 60) return t("minutesAgo", { minutes });
@@ -147,6 +150,7 @@ export default function ReasoningCacheTab() {
const res = await fetch("/api/cache/reasoning");
if (res.ok) {
const json: ReasoningCacheData = await res.json();
setNowMs(Date.now());
setData(json);
}
} catch (error) {
@@ -157,7 +161,9 @@ export default function ReasoningCacheTab() {
}, []);
useEffect(() => {
void fetchData();
void (async () => {
await fetchData();
})();
const id = setInterval(() => void fetchData(), REFRESH_INTERVAL_MS);
return () => clearInterval(id);
}, [fetchData]);

View File

@@ -374,7 +374,9 @@ export default function CachePage() {
}, []);
useEffect(() => {
void fetchStats();
void (async () => {
await fetchStats();
})();
const id = setInterval(() => void fetchStats(), REFRESH_INTERVAL_MS);
return () => clearInterval(id);
}, [fetchStats]);

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState } from "react";
import { Modal, Button, Input, Select } from "@/shared/components";
import { useTranslations } from "next-intl";
@@ -29,7 +29,14 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
// Adjust-during-render (React docs pattern): when the modal (re)opens for a
// memory, seed the form fields from it before painting — no effect round-trip.
const [prevSync, setPrevSync] = useState<{ memory: Memory | null; isOpen: boolean }>({
memory: null,
isOpen: false,
});
if (memory !== prevSync.memory || isOpen !== prevSync.isOpen) {
setPrevSync({ memory, isOpen });
if (memory && isOpen) {
setType(memory.type);
setKey(memory.key);
@@ -38,7 +45,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
setMetadataError("");
setError("");
}
}, [memory, isOpen]);
}
const handleMetadataChange = (value: string) => {
setMetadataStr(value);
@@ -151,9 +158,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
metadataError ? "border-red-500" : "border-border"
}`}
/>
{metadataError && (
<p className="text-xs text-red-400 mt-1">{metadataError}</p>
)}
{metadataError && <p className="text-xs text-red-400 mt-1">{metadataError}</p>}
</div>
</div>
</Modal>

View File

@@ -40,7 +40,8 @@ export default function QdrantConfigCard() {
collection?: { exists: boolean; vectorSize?: number; vectorName?: string | null };
} | null>(null);
const [searchValidated, setSearchValidated] = useState(false);
const [tutorialOpen, setTutorialOpen] = useState(false); const [checking, setChecking] = useState(false);
const [tutorialOpen, setTutorialOpen] = useState(false);
const [checking, setChecking] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<
@@ -109,7 +110,8 @@ export default function QdrantConfigCard() {
// invalidate in-flight checks so they cannot overwrite the new state.
healthSeqRef.current += 1;
setHealth(null);
setSearchValidated(false); setQdrant(next);
setSearchValidated(false);
setQdrant(next);
setSaving(true);
setSaveStatus("");
try {
@@ -153,7 +155,8 @@ export default function QdrantConfigCard() {
setSaving(false);
}
},
[qdrant, checkHealth] );
[qdrant, checkHealth]
);
// Auto-check on mount once settings load: without this the status badge
// renders red after a page refresh because `health` starts as null and the
@@ -161,7 +164,9 @@ export default function QdrantConfigCard() {
// connection button still drives the same check manually.
useEffect(() => {
if (!loading && qdrant.enabled && health === null) {
void checkHealth();
void (async () => {
await checkHealth();
})();
}
}, [loading, qdrant.enabled, health, checkHealth]);
@@ -245,7 +250,8 @@ export default function QdrantConfigCard() {
? "text-text-muted"
: health.ok
? "text-emerald-500"
: "text-red-500" }`}
: "text-red-500"
}`}
>
<span
className={`inline-block w-2.5 h-2.5 rounded-full ${

View File

@@ -190,9 +190,7 @@ export default function MemoriesTab() {
else skipped++;
}
fetchMemories();
setImportStatus(
t("importResult", { imported, skipped }),
);
setImportStatus(t("importResult", { imported, skipped }));
} catch {
setImportStatus(t("importError"));
} finally {
@@ -239,7 +237,9 @@ export default function MemoriesTab() {
// Auto-run health check on mount + poll every 30s, so the indicator reflects
// engine health without requiring a manual click.
useEffect(() => {
void checkHealth();
void (async () => {
await checkHealth();
})();
const id = setInterval(() => {
void checkHealth();
}, 30_000);
@@ -260,8 +260,9 @@ export default function MemoriesTab() {
body: JSON.stringify({ dryRun: true, olderThanDays: 30 }),
});
const data = await res.json().catch(() => null);
const candidates: string[] =
Array.isArray(data?.candidates) ? data.candidates.map((c: { key?: string }) => c?.key ?? String(c)) : [];
const candidates: string[] = Array.isArray(data?.candidates)
? data.candidates.map((c: { key?: string }) => c?.key ?? String(c))
: [];
setSummarizeCandidates(candidates);
setSummarizeDialogOpen(true);
} catch {
@@ -289,8 +290,7 @@ export default function MemoriesTab() {
}
};
const showHitRate =
(stats.cacheStats?.hits ?? 0) + (stats.cacheStats?.misses ?? 0) > 0;
const showHitRate = (stats.cacheStats?.hits ?? 0) + (stats.cacheStats?.misses ?? 0) > 0;
if (isLoading) {
return (
@@ -401,9 +401,7 @@ export default function MemoriesTab() {
info
</span>
</div>
<div className="text-2xl font-bold">
{((stats.hitRate ?? 0) * 100).toFixed(1)}%
</div>
<div className="text-2xl font-bold">{((stats.hitRate ?? 0) * 100).toFixed(1)}%</div>
</div>
</Card>
)}
@@ -448,12 +446,8 @@ export default function MemoriesTab() {
<span className="material-symbols-outlined text-[40px] text-text-muted mb-3">
psychology
</span>
<p className="text-sm font-medium text-text-main mb-1">
{t("emptyState.title")}
</p>
<p className="text-xs text-text-muted max-w-xs">
{t("emptyState.description")}
</p>
<p className="text-sm font-medium text-text-main mb-1">{t("emptyState.title")}</p>
<p className="text-xs text-text-muted max-w-xs">{t("emptyState.description")}</p>
<Button className="mt-4" size="sm" onClick={() => setAddDialogOpen(true)}>
{t("addMemory")}
</Button>
@@ -477,7 +471,9 @@ export default function MemoriesTab() {
<td className="py-2 px-4">
<Badge
variant={getTypeColor(memory.type)}
title={t(TYPE_TOOLTIPS[memory.type]?.replace("memory.", "") ?? memory.type)}
title={t(
TYPE_TOOLTIPS[memory.type]?.replace("memory.", "") ?? memory.type
)}
>
{t(memory.type)}
</Badge>
@@ -665,7 +661,10 @@ export default function MemoriesTab() {
</p>
<ul className="space-y-1 max-h-48 overflow-y-auto">
{summarizeCandidates.map((key, i) => (
<li key={i} className="text-xs font-mono text-text-main truncate px-2 py-1 bg-surface/30 rounded">
<li
key={i}
className="text-xs font-mono text-text-main truncate px-2 py-1 bg-surface/30 rounded"
>
{key}
</li>
))}

View File

@@ -39,7 +39,9 @@ export function useEngineStatus(refreshIntervalMs = 5000): UseEngineStatusResult
useEffect(() => {
mounted.current = true;
void fetchOnce();
void (async () => {
await fetchOnce();
})();
if (!refreshIntervalMs || refreshIntervalMs <= 0) {
return () => {
mounted.current = false;

View File

@@ -40,7 +40,9 @@ export function useMemorySettings(): UseMemorySettingsResult {
useEffect(() => {
mounted.current = true;
void fetchOnce();
void (async () => {
await fetchOnce();
})();
return () => {
mounted.current = false;
};

View File

@@ -0,0 +1,157 @@
"use client";
/** Polls the 3 agent sources (allSettled), listens to the `requests` WS channel as a refetch trigger. */
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLiveDashboard } from "@/hooks/useLiveDashboard";
import type { CloudAgentTask } from "@/lib/cloudAgent/types";
import type { A2ATask } from "@/lib/a2a/taskManager";
import type { FleetSnapshot } from "@/lib/conductor/hubProxy";
import { fromCloudAgent } from "../model/fromCloudAgent";
import { fromA2A } from "../model/fromA2A";
import { fromConductor } from "../model/fromConductor";
import { mergeSnapshot } from "../model/mergeSnapshot";
import type { OrchSnapshot, SourceStatus } from "../model/orchestrationTypes";
export const POLL_MS = 5_000;
export const WS_REFETCH_DEBOUNCE_MS = 1_000;
interface Raw {
cloudAgent: CloudAgentTask[];
a2a: A2ATask[];
conductor: FleetSnapshot;
}
const EMPTY_RAW: Raw = {
cloudAgent: [],
a2a: [],
conductor: { offline: true, runners: [], tasks: [] },
};
async function fetchJson<T>(url: string, signal: AbortSignal): Promise<T> {
const res = await fetch(url, { signal, cache: "no-store" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
/** Builds the 3-source status list from a `Promise.allSettled` triple. */
function buildSourceStatuses(
ca: PromiseSettledResult<{ data: CloudAgentTask[] }>,
a2a: PromiseSettledResult<{ tasks: A2ATask[] }>,
cond: PromiseSettledResult<FleetSnapshot>,
nowIso: string
): SourceStatus[] {
const next: SourceStatus[] = [];
if (ca.status === "fulfilled") next.push({ source: "cloud-agent", ok: true });
else
next.push({
source: "cloud-agent",
ok: false,
error: String(ca.reason),
staleSince: nowIso,
});
if (a2a.status === "fulfilled") next.push({ source: "a2a", ok: true });
else next.push({ source: "a2a", ok: false, error: String(a2a.reason), staleSince: nowIso });
if (cond.status === "fulfilled") {
next.push({ source: "conductor", ok: true, offline: cond.value.offline });
} else
next.push({
source: "conductor",
ok: false,
error: String(cond.reason),
staleSince: nowIso,
});
return next;
}
export function useOrchestrationSnapshot() {
// `raw` and `polledAt` are React state (not refs) so the merge below reads them
// during render like any other state — a ref read during render trips the
// `react-hooks/refs` lint rule, and computing `Date.now()` inline in the memo
// factory trips `react-hooks/purity`. Sampling `Date.now()` once per poll (inside
// the effect, not during render) keeps `mergeSnapshot`'s staleness math correct
// without either violation.
const [raw, setRaw] = useState<Raw>(EMPTY_RAW);
const [statuses, setStatuses] = useState<SourceStatus[]>([]);
// Lazy initializer (not a literal 0) so the pre-first-poll render already has a
// real timestamp — with `0` the very first `mergeSnapshot` call stamped
// `generatedAt` as the 1970 epoch. Safe: with EMPTY_RAW there is nothing to
// staleness-filter at mount, so seeding `Date.now()` here changes no behavior.
const [polledAt, setPolledAt] = useState<number>(() => Date.now());
const [isLoading, setIsLoading] = useState(true);
const [showCompleted, setShowCompleted] = useState(false);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Populated by the mount effect below; lets `refetch()` (and the WS debounce
// trigger) reach the same poll loop without hoisting it out of the effect —
// hoisting to a top-level `useCallback` invoked from the effect body trips
// `react-hooks/set-state-in-effect`.
const pollRef = useRef<() => void>(() => {});
useEffect(() => {
const controller = new AbortController();
const poll = async () => {
const [ca, a2a, cond] = await Promise.allSettled([
fetchJson<{ data: CloudAgentTask[] }>("/api/v1/agents/tasks?limit=100", controller.signal),
fetchJson<{ tasks: A2ATask[] }>("/api/a2a/tasks?limit=200", controller.signal),
fetchJson<FleetSnapshot>("/api/conductor/fleet", controller.signal),
]);
if (controller.signal.aborted) return;
const nowMs = Date.now();
const nowIso = new Date(nowMs).toISOString();
const next = buildSourceStatuses(ca, a2a, cond, nowIso);
// Failed sources keep the previously stored slice — only overwrite what
// actually resolved this round ("last good data" contract from the brief).
setRaw((prev) => ({
cloudAgent: ca.status === "fulfilled" ? ca.value.data : prev.cloudAgent,
a2a: a2a.status === "fulfilled" ? a2a.value.tasks : prev.a2a,
conductor: cond.status === "fulfilled" ? cond.value : prev.conductor,
}));
setStatuses(next);
setPolledAt(nowMs);
setIsLoading(false);
};
pollRef.current = () => void poll();
void poll();
const id = setInterval(() => void poll(), POLL_MS);
return () => {
clearInterval(id);
controller.abort();
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
}
};
}, []);
const refetch = useCallback(() => {
pollRef.current();
}, []);
useLiveDashboard({
channels: ["requests"],
onEvent: (payload) => {
if (payload.channel !== "requests") return;
if (debounceRef.current) return; // debounce burst → one refetch
debounceRef.current = setTimeout(() => {
debounceRef.current = null;
refetch();
}, WS_REFETCH_DEBOUNCE_MS);
},
});
const snapshot: OrchSnapshot = useMemo(
() =>
mergeSnapshot(
{
cloudAgent: fromCloudAgent(raw.cloudAgent),
a2a: fromA2A(raw.a2a),
conductor: fromConductor(raw.conductor),
},
statuses,
{ now: polledAt, showCompleted }
),
[raw, statuses, showCompleted, polledAt]
);
return { snapshot, isLoading, showCompleted, setShowCompleted, refetch };
}

View File

@@ -0,0 +1,53 @@
/** A2A tasks → unified orchestration nodes. Pure. */
import type { A2ATask } from "@/lib/a2a/taskManager";
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
const STATE_MAP: Record<string, OrchState> = {
submitted: "queued",
working: "running",
completed: "succeeded",
failed: "failed",
cancelled: "cancelled",
};
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
function truncate(s: string, n = 60): string {
return s.length > n ? `${s.slice(0, n - 1)}` : s;
}
export function fromA2A(tasks: A2ATask[]): { nodes: OrchNode[]; edges: OrchEdge[] } {
if (tasks.length === 0) return { nodes: [], edges: [] };
const nodes: OrchNode[] = [];
const edges: OrchEdge[] = [];
const counts: Partial<Record<OrchState, number>> = {};
for (const t of tasks) {
const mapped = STATE_MAP[t.state];
const state: OrchState = mapped ?? "failed";
counts[state] = (counts[state] ?? 0) + 1;
const id = `a2a:${t.id}`;
const firstUser = t.input.messages.find((m) => m.role === "user")?.content ?? "";
nodes.push({
id,
kind: "work",
source: "a2a",
state,
label: t.skill,
sublabel: mapped ? truncate(firstUser) : `unknown state: ${String(t.state)}`,
startedAt: t.createdAt,
updatedAt: t.updatedAt,
endedAt: TERMINAL.has(state) ? t.updatedAt : undefined,
raw: t,
});
edges.push({
id: `e:source:a2a→${id}`,
from: "source:a2a",
to: id,
kind: "owns",
active: state === "running",
});
}
nodes.unshift({ id: "source:a2a", kind: "source", source: "a2a", label: "A2A", counts });
return { nodes, edges };
}

View File

@@ -0,0 +1,75 @@
/** Cloud Agent tasks → unified orchestration nodes. Pure. */
import type { CloudAgentTask } from "@/lib/cloudAgent/types";
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
const STATE_MAP: Record<string, OrchState> = {
queued: "queued",
running: "running",
awaiting_approval: "waiting_approval",
completed: "succeeded",
failed: "failed",
cancelled: "cancelled",
};
function truncate(s: string, n = 60): string {
return s.length > n ? `${s.slice(0, n - 1)}` : s;
}
export function fromCloudAgent(tasks: CloudAgentTask[]): { nodes: OrchNode[]; edges: OrchEdge[] } {
if (tasks.length === 0) return { nodes: [], edges: [] };
const nodes: OrchNode[] = [];
const edges: OrchEdge[] = [];
const counts: Partial<Record<OrchState, number>> = {};
for (const t of tasks) {
const mapped = STATE_MAP[t.status];
const state: OrchState = mapped ?? "failed";
counts[state] = (counts[state] ?? 0) + 1;
const id = `cloud-agent:${t.id}`;
const active = state === "running";
nodes.push({
id,
kind: "work",
source: "cloud-agent",
state,
label: truncate(t.prompt),
sublabel: mapped ? t.providerId : `${t.providerId} — unknown status: ${String(t.status)}`,
startedAt: t.createdAt,
updatedAt: t.updatedAt,
endedAt: t.completedAt,
cost: t.result?.cost,
raw: t,
});
edges.push({
id: `e:source:cloud-agent→${id}`,
from: "source:cloud-agent",
to: id,
kind: "owns",
active,
});
const last = t.activities[t.activities.length - 1];
if (active && last) {
const actId = `${id}:activity`;
nodes.push({
id: actId,
kind: "activity",
source: "cloud-agent",
state,
label: truncate(last.content),
sublabel: last.type,
updatedAt: last.timestamp,
});
edges.push({ id: `e:${id}${actId}`, from: id, to: actId, kind: "owns", active: true });
}
}
nodes.unshift({
id: "source:cloud-agent",
kind: "source",
source: "cloud-agent",
label: "Cloud Agent",
counts,
});
return { nodes, edges };
}

View File

@@ -0,0 +1,148 @@
/** Conductor fleet snapshot → unified orchestration nodes. Pure. */
import type { FleetRunner, FleetSnapshot, FleetTask } from "@/lib/conductor/hubProxy";
import type { OrchEdge, OrchNode, OrchState } from "./orchestrationTypes";
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
function mapHubStatus(status: string): OrchState | null {
const s = status.toLowerCase();
if (s === "queued" || s === "pending") return "queued";
if (s === "running" || s === "working" || s === "scheduled") return "running";
if (s === "done" || s === "completed" || s === "succeeded") return "succeeded";
if (s === "failed" || s === "error") return "failed";
if (s === "cancelled" || s === "canceled") return "cancelled";
return null;
}
function taskNode(t: FleetTask, kind: "work" | "activity"): OrchNode {
const mapped = mapHubStatus(t.status);
const state: OrchState = mapped ?? "failed";
return {
id: `conductor:task:${t.id}`,
kind,
source: "conductor",
state,
label: t.summary ?? t.id,
sublabel: mapped ? (t.repo ?? t.mode) : `unknown status: ${t.status}`,
updatedAt: t.updated_at ?? undefined,
// FleetTask has no dedicated completion timestamp — updated_at is the closest
// proxy, same pattern as fromA2A.ts (A2ATask has no completedAt either).
endedAt: TERMINAL.has(state) ? (t.updated_at ?? undefined) : undefined,
raw: t,
};
}
/**
* Tasks whose runner actually exists in `snap.runners` AND is currently "running" — those
* get "absorbed" into that runner's ActivityNode instead of getting their own work node.
* A running task pointing at a runner id that has since deregistered falls through to the
* normal work-node loop instead of being silently skipped as "already an activity".
*/
function computeActiveByRunner(snap: FleetSnapshot): Map<string, FleetTask> {
const runnerIds = new Set(snap.runners.map((r) => r.id));
const activeByRunner = new Map<string, FleetTask>();
for (const t of snap.tasks) {
if (t.runner && runnerIds.has(t.runner) && mapHubStatus(t.status) === "running") {
activeByRunner.set(t.runner, t);
}
}
return activeByRunner;
}
function runnerState(r: FleetRunner, activeTask: FleetTask | undefined): OrchState {
if (!r.online) return "failed";
if (r.draining) return "cancelled";
return activeTask ? "running" : "queued";
}
/** One work node per runner, plus an activity node for its currently-active task. */
function runnerWorkNodes(
snap: FleetSnapshot,
activeByRunner: Map<string, FleetTask>,
bump: (s: OrchState) => void
): { nodes: OrchNode[]; edges: OrchEdge[] } {
const nodes: OrchNode[] = [];
const edges: OrchEdge[] = [];
for (const r of snap.runners) {
const id = `conductor:runner:${r.id}`;
const activeTask = activeByRunner.get(r.id);
const state = runnerState(r, activeTask);
bump(state);
nodes.push({
id,
kind: "work",
source: "conductor",
state,
label: r.name,
sublabel: r.clis.join(", "),
raw: r,
});
edges.push({
id: `e:source:conductor→${id}`,
from: "source:conductor",
to: id,
kind: "owns",
active: state === "running",
});
if (activeTask) {
nodes.push(taskNode(activeTask, "activity"));
edges.push({
id: `e:${id}→conductor:task:${activeTask.id}`,
from: id,
to: `conductor:task:${activeTask.id}`,
kind: "owns",
active: true,
});
}
}
return { nodes, edges };
}
/** Work nodes for tasks not already absorbed as a runner's activity node. */
function remainingTaskWorkNodes(
snap: FleetSnapshot,
activeByRunner: Map<string, FleetTask>,
bump: (s: OrchState) => void
): { nodes: OrchNode[]; edges: OrchEdge[] } {
const nodes: OrchNode[] = [];
const edges: OrchEdge[] = [];
for (const t of snap.tasks) {
if (t.runner && activeByRunner.get(t.runner)?.id === t.id) continue; // already an activity
const node = taskNode(t, "work");
bump(node.state as OrchState);
nodes.push(node);
edges.push({
id: `e:source:conductor→${node.id}`,
from: "source:conductor",
to: node.id,
kind: "owns",
active: node.state === "running",
});
}
return { nodes, edges };
}
export function fromConductor(snap: FleetSnapshot): { nodes: OrchNode[]; edges: OrchEdge[] } {
if (snap.offline || (snap.runners.length === 0 && snap.tasks.length === 0)) {
return { nodes: [], edges: [] };
}
const counts: Partial<Record<OrchState, number>> = {};
const bump = (s: OrchState) => {
counts[s] = (counts[s] ?? 0) + 1;
};
const activeByRunner = computeActiveByRunner(snap);
const runners = runnerWorkNodes(snap, activeByRunner, bump);
const tasks = remainingTaskWorkNodes(snap, activeByRunner, bump);
const nodes = [...runners.nodes, ...tasks.nodes];
const edges = [...runners.edges, ...tasks.edges];
nodes.unshift({
id: "source:conductor",
kind: "source",
source: "conductor",
label: "Conductor",
counts,
});
return { nodes, edges };
}

View File

@@ -0,0 +1,217 @@
/** Merge the three source mappers into one snapshot: root, dedupe, staleness filter, cap. Pure. */
import {
MAX_WORK_NODES,
STALE_COMPLETED_MS,
type OrchEdge,
type OrchNode,
type OrchSnapshot,
type OrchSource,
type OrchState,
type SourceStatus,
} from "./orchestrationTypes";
const TERMINAL: ReadonlySet<OrchState> = new Set(["succeeded", "failed", "cancelled"]);
export interface MergeOptions {
now: number;
showCompleted?: boolean;
}
interface Part {
nodes: OrchNode[];
edges: OrchEdge[];
}
interface NodesAndEdges {
nodes: OrchNode[];
edges: OrchEdge[];
}
function conductorMirrorId(node: OrchNode): string | null {
const raw = node.raw as { metadata?: { conductor?: { task_id?: unknown } } } | undefined;
const id = raw?.metadata?.conductor?.task_id;
return typeof id === "string" ? id : null;
}
/**
* (2) Conductor↔A2A dedupe — key verified in src/lib/conductor/bridge.ts::ensureMirrored.
* Mutates `dropped` in place; returns the (possibly patched) nodes/edges.
*/
function dedupeConductorMirrors(
nodes: OrchNode[],
edges: OrchEdge[],
dropped: Set<string>
): NodesAndEdges {
const conductorTaskIds = new Set(
nodes
.filter((n) => n.source === "conductor" && n.id.startsWith("conductor:task:"))
.map((n) => n.id.slice("conductor:task:".length))
);
const nextNodes = [...nodes];
const nextEdges = [...edges];
for (const n of nextNodes) {
if (n.source !== "a2a" || n.kind !== "work") continue;
const mirror = conductorMirrorId(n);
if (mirror && conductorTaskIds.has(mirror)) {
dropped.add(n.id);
const cIndex = nextNodes.findIndex((c) => c.id === `conductor:task:${mirror}`);
if (cIndex !== -1) {
// Copy rather than mutate — the original object is still referenced by
// parts.conductor.nodes, and this function's contract is Pure.
const cNode: OrchNode = { ...nextNodes[cIndex], mirrorOf: n.id };
nextNodes[cIndex] = cNode;
nextEdges.push({
id: `e:mirror:${cNode.id}`,
from: cNode.id,
to: "source:a2a",
kind: "mirror",
active: false,
});
}
}
}
return { nodes: nextNodes, edges: nextEdges };
}
/** (3) staleness filter — adds stale terminal work/activity node ids to `dropped`. */
function markStaleCompleted(nodes: OrchNode[], now: number, dropped: Set<string>): void {
for (const n of nodes) {
if (n.kind !== "work" && n.kind !== "activity") continue;
if (
n.state &&
TERMINAL.has(n.state) &&
n.endedAt &&
now - Date.parse(n.endedAt) > STALE_COMPLETED_MS
) {
dropped.add(n.id);
}
}
}
/** One source's overflow placeholder node, or null when it fits under `budgetPer`. */
function overflowNodeForSource(
source: OrchSource,
list: OrchNode[],
budgetPer: number,
dropped: Set<string>
): OrchNode | null {
if (list.length <= budgetPer) return null;
list.sort((a, b) => Date.parse(b.updatedAt ?? "0") - Date.parse(a.updatedAt ?? "0"));
const excess = list.slice(budgetPer);
const counts: Partial<Record<OrchState, number>> = {};
for (const n of excess) {
dropped.add(n.id);
if (n.state) counts[n.state] = (counts[n.state] ?? 0) + 1;
}
return {
id: `overflow:${source}`,
kind: "overflow",
source,
label: `+${excess.length} more`,
counts,
// Additive: lets overviewProjection fold true per-state totals into its
// counters even though these nodes no longer render on the canvas
// (operator ruling — spec governs, counters must show TRUE totals).
droppedByState: counts,
};
}
/** (4) cap with per-source overflow, newest kept. */
function capWorkNodesWithOverflow(
nodes: OrchNode[],
edges: OrchEdge[],
dropped: Set<string>
): NodesAndEdges {
const works = nodes.filter((n) => n.kind === "work");
if (works.length <= MAX_WORK_NODES) return { nodes, edges };
const bySource = new Map<OrchSource, OrchNode[]>();
for (const w of works) {
const list = bySource.get(w.source as OrchSource) ?? [];
list.push(w);
bySource.set(w.source as OrchSource, list);
}
const budgetPer = Math.max(1, Math.floor(MAX_WORK_NODES / bySource.size));
const overflowNodes: OrchNode[] = [];
for (const [source, list] of bySource) {
const overflow = overflowNodeForSource(source, list, budgetPer, dropped);
if (overflow) overflowNodes.push(overflow);
}
const nextNodes = nodes.filter((n) => !dropped.has(n.id)).concat(overflowNodes);
const nextEdges = edges.filter((e) => !dropped.has(e.from) && !dropped.has(e.to));
for (const o of overflowNodes) {
nextEdges.push({
id: `e:source:${o.source}${o.id}`,
from: `source:${o.source}`,
to: o.id,
kind: "owns",
active: false,
});
}
return { nodes: nextNodes, edges: nextEdges };
}
/**
* (1) root — link every present SourceNode, plus failed sources so the UI can show them
* stale. Returns nodes with the root prepended.
*/
function buildRootAndSourceEdges(
nodes: OrchNode[],
edges: OrchEdge[],
sources: SourceStatus[]
): NodesAndEdges {
const root: OrchNode = { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" };
const nextNodes = [...nodes];
const nextEdges = [...edges];
const sourceIds = new Set(nextNodes.filter((n) => n.kind === "source").map((n) => n.id));
for (const s of sources) {
// `!s.ok` covers hard failures; `s.offline` also materializes a placeholder
// for a source that reported ok:true but offline:true (e.g. Conductor with
// no hub configured) — otherwise that source never gets a SourceNode at all
// and its "offline" sublabel can never render.
if ((!s.ok || s.offline) && !sourceIds.has(`source:${s.source}`) && s.source !== "routing") {
nextNodes.push({
id: `source:${s.source}`,
kind: "source",
source: s.source,
label: s.source,
sublabel: s.offline ? "offline" : "error",
});
sourceIds.add(`source:${s.source}`);
}
}
for (const id of sourceIds) {
nextEdges.push({
id: `e:orchestrator→${id}`,
from: "orchestrator",
to: id,
kind: "owns",
active: false,
});
}
return { nodes: [root, ...nextNodes], edges: nextEdges };
}
export function mergeSnapshot(
parts: { cloudAgent: Part; a2a: Part; conductor: Part },
sources: SourceStatus[],
opts: MergeOptions
): OrchSnapshot {
let nodes: OrchNode[] = [...parts.cloudAgent.nodes, ...parts.a2a.nodes, ...parts.conductor.nodes];
let edges: OrchEdge[] = [...parts.cloudAgent.edges, ...parts.a2a.edges, ...parts.conductor.edges];
const dropped = new Set<string>();
({ nodes, edges } = dedupeConductorMirrors(nodes, edges, dropped));
if (!opts.showCompleted) {
markStaleCompleted(nodes, opts.now, dropped);
}
nodes = nodes.filter((n) => !dropped.has(n.id));
edges = edges.filter((e) => !dropped.has(e.from) && !dropped.has(e.to));
({ nodes, edges } = capWorkNodesWithOverflow(nodes, edges, dropped));
({ nodes, edges } = buildRootAndSourceEdges(nodes, edges, sources));
return { nodes, edges, sources, generatedAt: new Date(opts.now).toISOString() };
}

View File

@@ -0,0 +1,58 @@
/** OrchSnapshot → @xyflow nodes/edges with a deterministic shallow 3-layer layout. Pure. */
import type { Edge, Node } from "@xyflow/react";
import { edgeStyle } from "@/shared/components/flow/edgeStyles";
import type { OrchNodeKind, OrchSnapshot } from "./orchestrationTypes";
const LAYER_Y: Record<OrchNodeKind, number> = {
orchestrator: 0,
source: 150,
work: 320,
overflow: 320,
activity: 470,
};
const X_GAP = 260;
export function orchestrationToFlow(snap: OrchSnapshot): {
nodes: Node[];
edges: Edge[];
fitKey: string;
} {
const byLayer = new Map<number, string[]>();
for (const n of [...snap.nodes].sort((a, b) => a.id.localeCompare(b.id))) {
const y = LAYER_Y[n.kind];
const ids = byLayer.get(y) ?? [];
ids.push(n.id);
byLayer.set(y, ids);
}
const pos = new Map<string, { x: number; y: number }>();
for (const [y, ids] of byLayer) {
const width = (ids.length - 1) * X_GAP;
ids.forEach((id, i) => pos.set(id, { x: i * X_GAP - width / 2, y }));
}
const stateOf = new Map(snap.nodes.map((n) => [n.id, n.state]));
const nodes: Node[] = snap.nodes.map((n) => ({
id: n.id,
type: n.kind,
position: pos.get(n.id)!,
data: n as unknown as Record<string, unknown>,
}));
const edges: Edge[] = snap.edges.map((e) => {
const target = stateOf.get(e.to);
const style = edgeStyle(e.active, false, target === "failed", target === "succeeded");
return {
id: e.id,
source: e.from,
target: e.to,
animated: e.active,
style: e.kind === "mirror" ? { ...style, strokeDasharray: "6 4" } : style,
};
});
const fitKey = snap.nodes
.filter((n) => n.kind === "work")
.map((n) => n.id)
.sort()
.join("|");
return { nodes, edges, fitKey };
}

View File

@@ -0,0 +1,79 @@
/**
* Pure domain vocabulary for the Orchestration Canvas — no React, no side effects.
* Spec: _tasks/superpowers/specs/2026-08-30-orchestration-canvas-design.md
*/
import { STATUS_HEX } from "@/shared/constants/statusColors";
export type OrchState =
"queued" | "running" | "waiting_approval" | "succeeded" | "failed" | "cancelled";
export type OrchSource = "cloud-agent" | "a2a" | "conductor" | "routing";
export type OrchNodeKind = "orchestrator" | "source" | "work" | "activity" | "overflow";
export interface OrchNode {
id: string; // `${source}:${sourceId}` for work nodes
kind: OrchNodeKind;
source?: OrchSource;
state?: OrchState;
label: string;
sublabel?: string;
startedAt?: string;
updatedAt?: string;
endedAt?: string;
cost?: number;
counts?: Partial<Record<OrchState, number>>;
// Overflow nodes only: per-state counts of the work nodes folded into this
// overflow node when the MAX_WORK_NODES cap engages. overviewProjection folds
// this into its `counts` totals (never into `columns`) so operators still see
// TRUE totals even when the canvas caps the rendered node count.
droppedByState?: Partial<Record<OrchState, number>>;
mirrorOf?: string;
raw?: unknown;
}
export interface OrchEdge {
id: string;
from: string;
to: string;
kind: "owns" | "mirror";
active: boolean; // true while the target work is `running`
}
export interface SourceStatus {
source: OrchSource;
ok: boolean;
offline?: boolean;
error?: string;
staleSince?: string;
}
export interface OrchSnapshot {
nodes: OrchNode[];
edges: OrchEdge[];
sources: SourceStatus[];
generatedAt: string;
}
export const ORCH_STATES = [
"queued",
"running",
"waiting_approval",
"succeeded",
"failed",
"cancelled",
] as const satisfies readonly OrchState[];
const STATE_HEX: Record<OrchState, string> = {
queued: STATUS_HEX.muted,
running: STATUS_HEX.warning,
waiting_approval: STATUS_HEX.approval,
succeeded: STATUS_HEX.success,
failed: STATUS_HEX.error,
cancelled: STATUS_HEX.muted,
};
export function orchStateColor(state: OrchState): string {
return STATE_HEX[state];
}
export const STALE_COMPLETED_MS = 600_000; // completed >10 min ago drop out of the live view
export const MAX_WORK_NODES = 40; // beyond this, per-source overflow nodes take over

View File

@@ -0,0 +1,50 @@
/** OrchSnapshot → overview counters + kanban columns. Pure. */
import {
ORCH_STATES,
type OrchNode,
type OrchSnapshot,
type OrchState,
} from "./orchestrationTypes";
export interface OverviewData {
counts: Record<OrchState, number>;
columns: {
queued: OrchNode[];
running: OrchNode[];
waiting_approval: OrchNode[];
done: OrchNode[];
};
}
export function overviewProjection(snap: OrchSnapshot, comboActive: number): OverviewData {
const counts = Object.fromEntries(ORCH_STATES.map((s) => [s, 0])) as Record<OrchState, number>;
const columns: OverviewData["columns"] = {
queued: [],
running: [],
waiting_approval: [],
done: [],
};
for (const n of snap.nodes) {
// Overflow nodes (MAX_WORK_NODES cap) fold their dropped work nodes' true
// per-state counts into `counts` only — never into `columns`, since those
// nodes are not rendered on the canvas. Counters must show TRUE totals
// even when the canvas caps the rendered node count (operator ruling).
if (n.kind === "overflow" && n.droppedByState) {
for (const s of ORCH_STATES) {
counts[s] += n.droppedByState[s] ?? 0;
}
continue;
}
if (n.kind !== "work" || !n.state) continue;
counts[n.state] += 1;
if (n.state === "queued" || n.state === "running" || n.state === "waiting_approval") {
columns[n.state].push(n);
} else {
columns.done.push(n);
}
}
columns.done.sort((a, b) => Date.parse(b.updatedAt ?? "0") - Date.parse(a.updatedAt ?? "0"));
counts.running += comboActive;
return { counts, columns };
}

View File

@@ -95,7 +95,9 @@ export function RadarCatalogTable({ entries, refreshCatalog, onError }: RadarCat
}, [onError, t]);
useEffect(() => {
void loadState();
void (async () => {
await loadState();
})();
}, [loadState]);
const stateByKey = useMemo(

View File

@@ -67,9 +67,15 @@ export default function RadarIntelPage() {
}, [load, t]);
useEffect(() => {
load()
.catch(() => setError(t("loadFailed")))
.finally(() => setLoading(false));
void (async () => {
try {
await load();
} catch {
setError(t("loadFailed"));
} finally {
setLoading(false);
}
})();
}, [load, t]);
if (flagOff) notFound();

View File

@@ -201,7 +201,9 @@ export default function RadarPage() {
}, [fetchCatalog, fetchReferrals]);
useEffect(() => {
fetchSettings();
void (async () => {
await fetchSettings();
})();
}, [fetchSettings]);
// Sync (defined before handleActivate which depends on it)
@@ -242,7 +244,9 @@ export default function RadarPage() {
if (loading || syncing || optIn !== true || autoSyncFiredRef.current) return;
if (!shouldAutoSyncOnOpen(meta?.fetchedAt ?? null, Date.now())) return;
autoSyncFiredRef.current = true;
void handleSync();
void (async () => {
await handleSync();
})();
}, [loading, syncing, optIn, meta, handleSync]);
// Activate opt-in

View File

@@ -55,7 +55,16 @@ export default function RadarSetupPage() {
const provider = searchParams.get("provider");
const [setupData, setSetupData] = useState<ProviderSetupData | null>(null);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(provider !== null);
// Adjust-during-render when the provider query param changes (React docs
// pattern): a null provider has nothing to load, any other transition
// restarts the loading state before the fetch effect fires.
const [prevProvider, setPrevProvider] = useState(provider);
if (provider !== prevProvider) {
setPrevProvider(provider);
setLoading(provider !== null);
}
const [error, setError] = useState("");
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null);
@@ -63,7 +72,6 @@ export default function RadarSetupPage() {
// Fetch catalog to find the provider's setup data
useEffect(() => {
if (!provider) {
setLoading(false);
return;
}
@@ -123,12 +131,13 @@ export default function RadarSetupPage() {
}, [provider, t]);
// Test connection — uses the EXISTING connection-test endpoint
const connectionId = setupData?.connectionId ?? null;
const handleTestConnection = useCallback(async () => {
if (!setupData?.connectionId) return;
if (!connectionId) return;
setTesting(true);
setTestResult(null);
try {
const res = await fetch(`/api/providers/${encodeURIComponent(setupData.connectionId)}/test`, {
const res = await fetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
@@ -147,7 +156,7 @@ export default function RadarSetupPage() {
} finally {
setTesting(false);
}
}, [setupData?.connectionId, t]);
}, [connectionId, t]);
if (!provider) {
return (

View File

@@ -490,18 +490,18 @@ export default function EvalsTab() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (targetOptions.length === 0) return;
if (targetOptions.some((option) => option.key === selectedTargetKey)) return;
// Adjust-during-render (React docs pattern): keep the selected target inside
// the current option set, and never compare a target against itself. Both
// guards self-extinguish after their setState, so the re-render settles.
if (
targetOptions.length > 0 &&
!targetOptions.some((option) => option.key === selectedTargetKey)
) {
setSelectedTargetKey(targetOptions[0]?.key || "suite-default:__default__");
}, [selectedTargetKey, targetOptions]);
useEffect(() => {
if (!compareTargetKey) return;
if (compareTargetKey === selectedTargetKey) {
setCompareTargetKey("");
}
}, [compareTargetKey, selectedTargetKey]);
}
if (compareTargetKey && compareTargetKey === selectedTargetKey) {
setCompareTargetKey("");
}
const filteredSuites = !search.trim()
? suites
@@ -1846,7 +1846,11 @@ export default function EvalsTab() {
);
}
const HeroSection = memo(function HeroSection({ t }: { t: (key: string, values?: Record<string, unknown>) => string }) {
const HeroSection = memo(function HeroSection({
t,
}: {
t: (key: string, values?: Record<string, unknown>) => string;
}) {
return (
<Card className="p-0 overflow-hidden">
<div

View File

@@ -41,6 +41,23 @@ interface ResetCreditRequestState {
tr: TranslateUsage;
}
// Module-level so the ref-store mutation stays outside any hook body — the
// immutability rule bars in-callback writes to `state.idempotencyKeysRef.current`.
function resetIdempotencyKeys(keys: React.MutableRefObject<Record<string, string>>): void {
keys.current = {};
}
function ensureIdempotencyKey(
keys: React.MutableRefObject<Record<string, string>>,
selectionToken: string
): string {
const existing = keys.current[selectionToken];
if (existing) return existing;
const created = createIdempotencyKey();
keys.current[selectionToken] = created;
return created;
}
function createIdempotencyKey(): string {
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID()
@@ -112,9 +129,7 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
async (selectionToken: string) => {
const picker = state.resetCreditPicker;
if (!picker || state.redeemingResetCreditId || !selectionToken) return;
const idempotencyKey =
state.idempotencyKeysRef.current[selectionToken] ??
(state.idempotencyKeysRef.current[selectionToken] = createIdempotencyKey());
const idempotencyKey = ensureIdempotencyKey(state.idempotencyKeysRef, selectionToken);
state.setRedeemingResetCreditId(picker.connectionId);
state.setErrors((prev) => ({ ...prev, [picker.connectionId]: null }));
try {
@@ -145,7 +160,7 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
[picker.connectionId]: new Date().toISOString(),
}));
state.setResetCreditPicker(null);
state.idempotencyKeysRef.current = {};
resetIdempotencyKeys(state.idempotencyKeysRef);
notify.success(state.tr("resetCreditRedeemed", "Reset redeemed"));
} catch (error) {
const message = getRequestErrorMessage(

View File

@@ -22,7 +22,9 @@ export default function RateLimitStatus() {
}, []);
useEffect(() => {
load();
void (async () => {
await load();
})();
const interval = setInterval(load, 10000);
return () => clearInterval(interval);
}, [load]);

View File

@@ -49,7 +49,9 @@ export default function SessionsTab() {
}, []);
useEffect(() => {
loadSessions();
void (async () => {
await loadSessions();
})();
const interval = setInterval(loadSessions, 5000);
return () => clearInterval(interval);
}, [loadSessions]);

View File

@@ -161,7 +161,9 @@ export default function ProviderQuotaWidget({
const [refreshingAll, setRefreshingAll] = useState(false);
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
const refreshingAllRef = useRef(false);
const lastRefreshAllAtRef = useRef(Date.now());
// State (not a ref): the countdown renders it, and refs cannot be read during
// render nor initialized with Date.now() (purity rule).
const [lastRefreshAllAt, setLastRefreshAllAt] = useState(() => Date.now());
const autoRefreshIntervalMs = autoRefreshInterval > 0 ? autoRefreshInterval * 1000 : 0;
const [autoRefreshClock, setAutoRefreshClock] = useState(() => Date.now());
@@ -188,14 +190,16 @@ export default function ProviderQuotaWidget({
}, []);
useEffect(() => {
void loadData();
void (async () => {
await loadData();
})();
}, [loadData]);
const refreshAll = useCallback(async () => {
if (refreshingAllRef.current) return;
refreshingAllRef.current = true;
const now = Date.now();
lastRefreshAllAtRef.current = now;
setLastRefreshAllAt(now);
setAutoRefreshClock(now);
setRefreshingAll(true);
try {
@@ -235,10 +239,12 @@ export default function ProviderQuotaWidget({
if (document.visibilityState !== "visible") return;
if (refreshingAllRef.current) return;
if (autoRefreshClock - lastRefreshAllAtRef.current >= autoRefreshIntervalMs) {
void refreshAll();
if (autoRefreshClock - lastRefreshAllAt >= autoRefreshIntervalMs) {
void (async () => {
await refreshAll();
})();
}
}, [autoRefreshClock, autoRefreshIntervalMs, refreshAll]);
}, [autoRefreshClock, lastRefreshAllAt, autoRefreshIntervalMs, refreshAll]);
const providerGroups = useMemo(() => {
const groups = new Map<string, Connection[]>();
@@ -286,10 +292,7 @@ export default function ProviderQuotaWidget({
? tr("refreshing", "Refreshing")
: autoRefreshIntervalMs > 0
? `${tr("autoRefreshing", "Auto-refreshing")} ${formatAutoRefreshCountdown(
Math.max(
0,
autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAtRef.current)
)
Math.max(0, autoRefreshIntervalMs - (autoRefreshClock - lastRefreshAllAt))
)}`
: tr("forceRefresh", "Refresh now")}
</button>

View File

@@ -1432,13 +1432,13 @@ async function buildUnifiedModelsResponseCore(
return activeAliases.has(alias) || activeAliases.has(provider);
};
const hasEquivalentSpecialtyModel = (
const findEquivalentSpecialtyModel = (
providerId: string,
rawModelId: string,
type: string,
scopedModelId: string
) =>
models.some((model: any) => {
models.find((model: any) => {
if (model?.id === scopedModelId) return true;
if (model?.owned_by !== providerId || model?.type !== type) return false;
const existingRoot =
@@ -1450,6 +1450,13 @@ async function buildUnifiedModelsResponseCore(
return existingRoot === rawModelId;
});
const hasEquivalentSpecialtyModel = (
providerId: string,
rawModelId: string,
type: string,
scopedModelId: string
) => findEquivalentSpecialtyModel(providerId, rawModelId, type, scopedModelId) !== undefined;
// Helper: strip the provider prefix from a specialty model ID to get the
// provider-relative path (e.g. "openrouter/google/chirp-3" -> "google/chirp-3").
// This is the correct key used by the hidden-model lookup — using .split("/").pop()
@@ -1464,7 +1471,22 @@ async function buildUnifiedModelsResponseCore(
const rawModelId = getSpecialtyModelRelativeId(embModel.id, embModel.provider);
if (!providerSupportsModel(embModel.provider, rawModelId)) continue;
if (isModelHiddenBulk(embModel.provider, rawModelId)) continue;
if (hasEquivalentSpecialtyModel(embModel.provider, rawModelId, "embedding", embModel.id)) {
const existingEmbedding = findEquivalentSpecialtyModel(
embModel.provider,
rawModelId,
"embedding",
embModel.id
);
if (existingEmbedding) {
// Discovery publishes no vector width, so the registry is the authority.
if (embModel.dimensions !== undefined) {
existingEmbedding.dimensions = embModel.dimensions;
}
// A provider that does not report its endpoints leaves the model unclassified. Being in
// the embedding registry is that statement, so make it rather than leave it untyped.
if (!existingEmbedding.type) {
existingEmbedding.type = "embedding";
}
continue;
}
models.push({

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, getLocale, getTranslations } from "next-intl/server";
import { RTL_LOCALES } from "@/i18n/config";
import { normalizeComplianceEventTypes } from "@/i18n/request";
import { getSettings } from "@/lib/db/settings";
import { getRootLayoutSettings } from "@/lib/db/rootLayoutSettings";
import type { Viewport } from "next";
import { PwaRegister } from "@/shared/components/PwaRegister";
import { LocaleAutoDetect } from "@/shared/components/LocaleAutoDetect";
@@ -22,9 +22,9 @@ export const viewport: Viewport = {
};
export async function generateMetadata() {
const settings = await getSettings();
const instanceName = settings?.instanceName || "OmniRoute";
const customFaviconUrl = settings?.customFaviconUrl || settings?.customFaviconBase64;
const settings = await getRootLayoutSettings();
const instanceName = settings.instanceName;
const customFaviconUrl = settings.customFaviconUrl || settings.customFaviconBase64;
return {
title: `${instanceName} — AI Gateway for Multi-Provider LLMs`,

View File

@@ -319,7 +319,7 @@ export async function registerNodejs(): Promise<void> {
process.title = renameProcessTitle(process.title);
// Initialize proxy fetch patch FIRST (before any HTTP requests)
await import("@omniroute/open-sse/index.ts");
await import("@omniroute/open-sse/utils/proxyFetch.ts");
console.log("[STARTUP] Global fetch proxy patch initialized");
// Register quota fetchers early so combo routing can use real quota-aware

View File

@@ -1,29 +1,6 @@
import type { SupportedBatchEndpoint } from "@/shared/constants/batchEndpoints";
type BatchRouteHandler = (request: Request) => Promise<Response> | Response;
const handlerLoaders: Record<SupportedBatchEndpoint, () => Promise<BatchRouteHandler>> = {
"/v1/responses": async () => (await import("@/app/api/v1/responses/route")).POST,
"/v1/chat/completions": async () => (await import("@/app/api/v1/chat/completions/route")).POST,
"/v1/embeddings": async () => (await import("@/app/api/v1/embeddings/route")).POST,
"/v1/completions": async () => (await import("@/app/api/v1/completions/route")).POST,
"/v1/moderations": async () => (await import("@/app/api/v1/moderations/route")).POST,
"/v1/images/generations": async () =>
(await import("@/app/api/v1/images/generations/route")).POST,
"/v1/videos/generations": async () =>
(await import("@/app/api/v1/videos/generations/route")).POST,
};
const handlerCache = new Map<SupportedBatchEndpoint, BatchRouteHandler>();
async function getHandler(endpoint: SupportedBatchEndpoint): Promise<BatchRouteHandler> {
const cached = handlerCache.get(endpoint);
if (cached) return cached;
const handler = await handlerLoaders[endpoint]();
handlerCache.set(endpoint, handler);
return handler;
}
import { getRuntimePorts } from "@/lib/runtime/ports";
import { normalizeBasePath } from "@/shared/utils/basePath";
async function dispatchBatchApiRequest({
endpoint,
@@ -39,13 +16,17 @@ async function dispatchBatchApiRequest({
headers.set("Authorization", `Bearer ${apiKey}`);
}
const handler = await getHandler(endpoint);
const request = new Request(`http://localhost${endpoint}`, {
const { dashboardPort } = getRuntimePorts();
const basePath = normalizeBasePath(process.env.OMNIROUTE_BASE_PATH);
const url = `http://127.0.0.1:${dashboardPort}${basePath}${endpoint}`;
return await globalThis.fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
// Never follow a redirect while carrying the stored batch API key.
redirect: "error",
});
return await handler(request);
}
export const dispatch = {

View File

@@ -67,6 +67,7 @@ export const MODE_PACK_OPTIONS = [
export const ROUTER_STRATEGY_OPTIONS = [
{ id: "rules", label: "Rules (6-Factor Scoring)" },
{ id: "score", label: "Highest Weighted Score" },
{ id: "cost", label: "Cost Optimized" },
{ id: "latency", label: "Latency Optimized" },
{ id: "sla-aware", label: "SLA-aware" },

View File

@@ -40,6 +40,7 @@ import { invalidateDbCache } from "./readCache";
import { rowToCamel } from "./caseMapping";
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
import { parseModelAccessMode } from "./apiKeys/modelAccessMode";
import { getExistingDbInstance as getDb, setDbInstance as setDb } from "./singleton";
// Re-exported so existing call sites that pull these helpers off the core module keep working.
export { toSnakeCase, toCamelCase, objToSnake, rowToCamel, cleanNulls } from "./caseMapping";
import {
@@ -514,7 +515,6 @@ const SCHEMA_SQL = `
// Module-level `let` resets on every webpack recompile, causing connection leaks.
declare global {
var __omnirouteDb: SqliteAdapter | undefined;
// Cycle-breaker counter for the probe-failed/restore cascade. Survives
// Next.js HMR re-evaluations so concurrent subsystems all see the same
// count and we abort with a clear error instead of looping forever.
@@ -529,18 +529,6 @@ declare global {
var __omnirouteDbOomFailureCount: number | undefined;
}
function getDb(): SqliteDatabase | null {
return globalThis.__omnirouteDb ?? null;
}
function setDb(db: SqliteDatabase | null): void {
if (db) {
globalThis.__omnirouteDb = db;
} else {
delete globalThis.__omnirouteDb;
}
}
function checkpointDb(db: SqliteDatabase, mode: CheckpointMode = "TRUNCATE"): boolean {
if (isCloud || isBuildPhase || !SQLITE_FILE) return false;
db.pragma(`wal_checkpoint(${mode})`);

View File

@@ -0,0 +1,62 @@
import { getExistingDbInstance } from "./singleton";
export interface RootLayoutSettings {
instanceName: string;
customFaviconUrl: string;
customFaviconBase64: string;
}
type RootLayoutSettingKey = keyof RootLayoutSettings;
type SettingsRow = {
key?: unknown;
value?: unknown;
};
const ROOT_LAYOUT_SETTING_KEYS = [
"instanceName",
"customFaviconUrl",
"customFaviconBase64",
] as const satisfies readonly RootLayoutSettingKey[];
const ROOT_LAYOUT_SETTING_KEY_SET = new Set<string>(ROOT_LAYOUT_SETTING_KEYS);
const DEFAULT_ROOT_LAYOUT_SETTINGS: RootLayoutSettings = {
instanceName: "OmniRoute",
customFaviconUrl: "",
customFaviconBase64: "",
};
function parseStoredString(value: unknown): string | null {
if (typeof value !== "string") return null;
try {
const parsed = JSON.parse(value) as unknown;
return typeof parsed === "string" ? parsed : null;
} catch {
return null;
}
}
/** Read only the settings needed while compiling and rendering the root layout. */
export async function getRootLayoutSettings(): Promise<RootLayoutSettings> {
const db = getExistingDbInstance();
if (!db) return { ...DEFAULT_ROOT_LAYOUT_SETTINGS };
const rows = db
.prepare(
`SELECT key, value FROM key_value
WHERE namespace = 'settings' AND key IN (?, ?, ?)`
)
.all(...ROOT_LAYOUT_SETTING_KEYS) as SettingsRow[];
const settings = { ...DEFAULT_ROOT_LAYOUT_SETTINGS };
for (const row of rows) {
if (typeof row.key !== "string" || !ROOT_LAYOUT_SETTING_KEY_SET.has(row.key)) continue;
const value = parseStoredString(row.value);
if (value === null) continue;
settings[row.key as RootLayoutSettingKey] = value;
}
if (!settings.instanceName) settings.instanceName = DEFAULT_ROOT_LAYOUT_SETTINGS.instanceName;
return settings;
}

19
src/lib/db/singleton.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { SqliteAdapter } from "./adapters/types";
declare global {
var __omnirouteDb: SqliteAdapter | undefined;
}
/** Read the process-wide DB handle without initializing storage. */
export function getExistingDbInstance(): SqliteAdapter | null {
return globalThis.__omnirouteDb ?? null;
}
/** Replace the process-wide DB handle while preserving it across Next.js HMR. */
export function setDbInstance(db: SqliteAdapter | null): void {
if (db) {
globalThis.__omnirouteDb = db;
} else {
delete globalThis.__omnirouteDb;
}
}

View File

@@ -19,7 +19,15 @@ const SHUTDOWN_TIMEOUT_MS = parseInt(process.env.SHUTDOWN_TIMEOUT_MS || "30000",
declare global {
var __omnirouteShutdown:
{ init: boolean; shuttingDown: boolean; activeRequests: number } | undefined;
| {
init: boolean;
shuttingDown: boolean;
activeRequests: number;
shutdownPromise?: Promise<void>;
}
| undefined;
var __omnirouteRequestShutdown: ((signal: string) => Promise<void>) | undefined;
var __omnirouteCustomServerOwnsShutdown: boolean | undefined;
}
function getShutdownState() {
@@ -102,12 +110,14 @@ async function cleanup(): Promise<void> {
{ closeDbInstance },
{ flushSpendBatchWriter },
{ closeLogRotation },
{ closeSharedLoggerResource },
{ closeCallLogSaves },
] = await Promise.all([
import("@omniroute/open-sse/mcp-server/audit.ts"),
import("@/lib/db/core"),
import("@/lib/spend/batchWriter"),
import("@/lib/logRotation"),
import("@/shared/utils/loggerResource"),
import("@/lib/usage/callLogs"),
]);
const flushResult = await flushSpendBatchWriter();
@@ -123,9 +133,6 @@ async function cleanup(): Promise<void> {
if (closeDbInstance()) {
console.log("[Shutdown] SQLite database checkpointed and closed.");
}
closeLogRotation();
console.log("[Shutdown] Log rotation timer stopped.");
// Tear down any persistent VNC login browser containers so they don't leak
// past the server process. Best-effort; no-op if the feature was never used
// or the docker CLI is unavailable.
@@ -147,41 +154,62 @@ async function cleanup(): Promise<void> {
} catch {
/* feature unused */
}
await closeSharedLoggerResource();
closeLogRotation();
console.log("[Shutdown] Logger transport and log rotation stopped.");
} catch (err) {
console.error("[Shutdown] Error during cleanup:", (err as Error).message);
}
}
/**
* Start the process-wide shutdown sequence, or join the sequence already in progress.
*/
export function requestGracefulShutdown(signal: string): Promise<void> {
const state = getShutdownState();
if (state.shutdownPromise) return state.shutdownPromise;
state.shuttingDown = true;
markServerStopping();
state.shutdownPromise = (async () => {
console.log(`\n[Shutdown] Received ${signal}. Draining ${state.activeRequests} request(s)...`);
await waitForDrain();
await cleanup();
console.log("[Shutdown] Bye.");
})();
return state.shutdownPromise;
}
/**
* Initialize graceful shutdown handlers.
* Should be called once during server startup.
*/
export function initGracefulShutdown(): void {
const state = getShutdownState();
globalThis.__omnirouteRequestShutdown ??= requestGracefulShutdown;
if (state.init) return;
state.init = true;
const shutdown = async (signal: string) => {
if (state.shuttingDown) return;
state.shuttingDown = true;
markServerStopping();
if (globalThis.__omnirouteCustomServerOwnsShutdown) {
console.log("[Shutdown] Cleanup registered with the custom server shutdown owner.");
return;
}
console.log(`\n[Shutdown] Received ${signal}. Draining ${state.activeRequests} request(s)...`);
await waitForDrain();
await cleanup();
console.log("[Shutdown] Bye.");
process.exit(0);
const shutdown = (signal: string) => {
void globalThis.__omnirouteRequestShutdown?.(signal).then(() => process.exit(0));
};
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
// #8045: on Windows, closing the console window delivers CTRL_CLOSE_EVENT, which
// Node/libuv maps to a JS-visible "SIGHUP" event — without this listener, closing
// the window never runs cleanup() (WAL checkpoint + closeDbInstance()), leaving
// storage.sqlite's WAL un-checkpointed for the next launch.
process.on("SIGHUP", () => shutdown("SIGHUP"));
process.on("SIGHUP", () => void shutdown("SIGHUP"));
console.log("[Shutdown] Graceful shutdown handlers registered.");
}

View File

@@ -42,8 +42,18 @@ export function getAppLogRotationCheckInterval(): number {
);
}
/** Module-level timer handle — cleared by closeLogRotation(). */
let rotationTimer: ReturnType<typeof setInterval> | null = null;
interface LogRotationState {
timer: ReturnType<typeof setInterval> | null;
}
declare global {
var __omnirouteLogRotationState: LogRotationState | undefined;
}
/** Process-wide state survives Next.js development HMR and split server chunks. */
function getLogRotationState(): LogRotationState {
return (globalThis.__omnirouteLogRotationState ??= { timer: null });
}
export function getLogConfig() {
const logToFile = getAppLogToFile();
@@ -172,6 +182,9 @@ export function cleanupOverflowLogs(logFilePath: string, maxFiles: number): void
* Call closeLogRotation() during application shutdown to clear the timer.
*/
export function initLogRotation(): void {
const state = getLogRotationState();
if (state.timer !== null) return;
const config = getLogConfig();
if (!config.logToFile) return;
@@ -181,7 +194,7 @@ export function initLogRotation(): void {
cleanupOverflowLogs(config.logFilePath, config.maxFiles);
const intervalMs = getAppLogRotationCheckInterval();
rotationTimer = setInterval(
state.timer = setInterval(
(filePath: string, maxSize: number, maxFiles: number) => {
rotateIfNeeded(filePath, maxSize);
cleanupOverflowLogs(filePath, maxFiles);
@@ -191,7 +204,7 @@ export function initLogRotation(): void {
config.maxFileSize,
config.maxFiles
);
rotationTimer.unref?.();
state.timer.unref?.();
}
/**
@@ -199,8 +212,9 @@ export function initLogRotation(): void {
* Idempotent — safe to call multiple times.
*/
export function closeLogRotation(): void {
if (rotationTimer !== null) {
clearInterval(rotationTimer);
rotationTimer = null;
const state = getLogRotationState();
if (state.timer !== null) {
clearInterval(state.timer);
state.timer = null;
}
}

View File

@@ -9,6 +9,7 @@ import {
joinClaudeCodeCompatibleUrl,
joinBaseUrlAndPath,
} from "@omniroute/open-sse/services/claudeCodeCompatible.ts";
import { getDefaultExecutor } from "@omniroute/open-sse/executors/defaultResolver.ts";
import {
addModelsSuffix,
normalizeAnthropicBaseUrl,
@@ -137,8 +138,7 @@ export async function validateClaudeOAuthInline({
typeof override === "string" && override ? override : modelId || "claude-haiku-4-5-20251001";
try {
const { getExecutor } = await import("@omniroute/open-sse/executors/index.ts");
const executed = await (await getExecutor("claude")).execute({
const executed = await getDefaultExecutor("claude").execute({
model: testModelId,
body: {
model: testModelId,

View File

@@ -22,13 +22,12 @@
import { logger } from "@omniroute/open-sse/utils/logger.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
import type { BaseExecutor } from "@omniroute/open-sse/executors/base";
import { getCodexUsage } from "@omniroute/open-sse/services/usage/codex.ts";
import { getSettings } from "@/lib/db/settings";
import { getProviderConnections, updateProviderConnection } from "@/lib/db/providers";
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
import { refreshAndUpdateCredentials } from "@/lib/usage/providerLimits";
import { refreshAndUpdateCredentialsWithResolver } from "@/lib/usage/providerLimits/credentialRefresh";
import { getCircuitBreaker } from "@/shared/utils/circuitBreaker";
import {
QUOTA_AUTOPING_FAILURE_COOLDOWN_MS,
@@ -63,8 +62,11 @@ export interface QuotaAutoPingDeps {
refreshAndUpdateCredentials: (
connection: QuotaAutoPingConnection
) => Promise<{ connection: QuotaAutoPingConnection }>;
getCodexUsage: (accessToken?: string, providerSpecificData?: JsonRecord) => Promise<JsonRecord>;
getExecutor: (provider: string) => Promise<BaseExecutor>;
getCodexUsage: (
accessToken?: string,
providerSpecificData?: JsonRecord
) => Promise<JsonRecord>;
getExecutor: (provider: "codex") => Promise<BaseExecutor>;
canExecuteProvider: (provider: string) => boolean;
isConnectionUnavailableToAuxiliaryActivity: (connectionId: string) => Promise<boolean>;
}
@@ -79,15 +81,33 @@ export function createQuotaAutoPingState(): QuotaAutoPingState {
return { running: false, resetCache: {}, failureCache: {} };
}
let codexExecutorPromise: Promise<BaseExecutor> | null = null;
async function loadQuotaAutoPingExecutor(provider: string): Promise<BaseExecutor> {
if (provider !== "codex") {
throw new Error(`Quota auto-ping does not support provider "${provider}"`);
}
try {
codexExecutorPromise ??= import("@omniroute/open-sse/executors/codex.ts").then(
({ CodexExecutor }) => new CodexExecutor()
);
return await codexExecutorPromise;
} catch (error) {
codexExecutorPromise = null;
throw error;
}
}
export function createDefaultQuotaAutoPingDeps(): QuotaAutoPingDeps {
return {
getSettings,
getProviderConnections,
updateProviderConnection,
refreshAndUpdateCredentials: async (connection) =>
refreshAndUpdateCredentials(connection as never),
refreshAndUpdateCredentialsWithResolver(connection, loadQuotaAutoPingExecutor),
getCodexUsage,
getExecutor,
getExecutor: loadQuotaAutoPingExecutor,
canExecuteProvider: (provider) => getCircuitBreaker(provider).canExecute(),
isConnectionUnavailableToAuxiliaryActivity,
};

View File

@@ -18,13 +18,10 @@ import { clearRecoveredProviderState } from "@/sse/services/auth";
import { getMachineId } from "@/shared/utils/machine";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
import { mergeProviderLimitsCacheEntry, toProviderLimitsCacheEntry } from "./providerLimitsCache";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
import { getCredentialRefreshExecutor } from "@omniroute/open-sse/executors/credential.ts";
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
import { cooldownUntilMs } from "@omniroute/open-sse/services/accountFallback.ts";
import {
rotationGroupFor,
serializeRefresh,
} from "@omniroute/open-sse/services/refreshSerializer.ts";
import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts";
import {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
@@ -42,29 +39,15 @@ import {
sanitizeUsageQuotasForProvider,
} from "./providerLimits/quotaNormalize";
import { syncInChunksWithSpacing } from "./providerLimits/chunkedSpacingSync";
import {
refreshAndUpdateCredentialsWithResolver,
type CredentialRefreshOptions,
type ProviderConnectionLike,
} from "./providerLimits/credentialRefresh";
export { shouldAttemptRotatingRefresh } from "./providerLimits/credentialRefresh";
type JsonRecord = Record<string, unknown>;
type SyncSource = "manual" | "scheduled";
interface ProviderConnectionLike {
id: string;
provider: string;
authType?: string;
accessToken?: string;
refreshToken?: string;
expiresAt?: string;
tokenExpiresAt?: string;
providerSpecificData?: JsonRecord;
testStatus?: string;
isActive?: boolean;
lastError?: string | null;
lastErrorAt?: string | null;
lastErrorType?: string | null;
lastErrorSource?: string | null;
errorCode?: string | number | null;
rateLimitedUntil?: string | null;
backoffLevel?: number;
}
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"glm",
"glm-cn",
@@ -218,122 +201,15 @@ async function syncToCloudIfEnabled() {
}
}
/**
* Whether the quota path may refresh this provider's token. Exported for testing.
*
* Rotating-refresh providers (Codex/OpenAI share one Auth0 client_id, etc.) mint a
* single-use refresh_token on every refresh. The BULK quota-sync path runs many
* connections concurrently; refreshing sibling accounts in parallel makes Auth0
* revoke the whole token family (openai/codex#9648) and kills every account but
* the last (#3019). So the bulk path never refreshes rotating providers
* (`allowRotatingRefresh` falsy). The on-demand, per-connection path opts in and
* is made safe by `serializeRefresh` (one token mint at a time per rotation group,
* so even N concurrent per-account requests can never refresh siblings in
* parallel). Non-rotating providers are always eligible.
*/
export function shouldAttemptRotatingRefresh(
provider: string,
allowRotatingRefresh: boolean | undefined
): boolean {
if (rotationGroupFor(provider) === null) return true;
return allowRotatingRefresh === true;
}
export async function refreshAndUpdateCredentials(
connection: ProviderConnectionLike,
opts: { allowRotatingRefresh?: boolean; force?: boolean } = {}
opts: CredentialRefreshOptions = {}
) {
if (!shouldAttemptRotatingRefresh(connection.provider, opts.allowRotatingRefresh)) {
return { connection, refreshed: false };
}
const executor = await getExecutor(connection.provider);
const credentials = {
connectionId: connection.id,
accessToken: connection.accessToken,
refreshToken: connection.refreshToken,
expiresAt: connection.tokenExpiresAt || connection.expiresAt || null,
providerSpecificData: connection.providerSpecificData,
copilotToken: connection.providerSpecificData?.copilotToken,
copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt,
};
// `force` is used ONLY on the reactive 401 recovery path (a usage fetch came
// back unauthorized) — it bypasses the proactive `needsRefresh` heuristic so
// imported accounts (expiresAt=null, where needsRefresh is always false) can
// still re-mint. The mint stays serialized per rotation group; this never
// refreshes proactively from the bulk path (#3019 guard above is unchanged).
if (!opts.force && !executor.needsRefresh(credentials)) {
return { connection, refreshed: false };
}
// Serialize the actual token mint per rotation group so two sibling accounts
// never hit Auth0 concurrently (passthrough for non-rotating providers).
const refreshResult = (await serializeRefresh(connection.provider, () =>
executor.refreshCredentials(credentials, console)
)) as
| (JsonRecord & {
accessToken?: string;
refreshToken?: string;
expiresIn?: number;
expiresAt?: string;
copilotToken?: string;
copilotTokenExpiresAt?: string;
})
| null;
if (!refreshResult) {
// Refresh failed but we still have an accessToken — fall back to the
// existing token for ANY OAuth provider (graceful degradation) instead of
// hard-failing. Previously this was qualified to `provider === "github"`,
// which left every other provider stuck on a transient refresh failure even
// when a usable access token was still on hand.
if (connection.accessToken) {
return { connection, refreshed: false };
}
throw withStatus(
new Error("Failed to refresh credentials. Please re-authorize the connection."),
401
);
}
const updateData: JsonRecord = {
updatedAt: new Date().toISOString(),
};
if (refreshResult.accessToken) {
updateData.accessToken = refreshResult.accessToken;
}
if (refreshResult.refreshToken) {
updateData.refreshToken = refreshResult.refreshToken;
}
if (refreshResult.expiresIn) {
const expiresAt = new Date(Date.now() + refreshResult.expiresIn * 1000).toISOString();
updateData.expiresAt = expiresAt;
updateData.tokenExpiresAt = expiresAt;
} else if (refreshResult.expiresAt) {
updateData.expiresAt = refreshResult.expiresAt;
updateData.tokenExpiresAt = refreshResult.expiresAt;
}
if (refreshResult.copilotToken || refreshResult.copilotTokenExpiresAt) {
updateData.providerSpecificData = {
...(connection.providerSpecificData || {}),
copilotToken: refreshResult.copilotToken,
copilotTokenExpiresAt: refreshResult.copilotTokenExpiresAt,
};
}
await updateProviderConnection(connection.id, updateData);
return {
connection: {
...connection,
...updateData,
providerSpecificData:
(updateData.providerSpecificData as JsonRecord | undefined) ||
connection.providerSpecificData,
},
refreshed: true,
};
return refreshAndUpdateCredentialsWithResolver(
connection,
getCredentialRefreshExecutor,
opts
);
}
function isUsageAuthError(message: unknown): boolean {

View File

@@ -0,0 +1,150 @@
import { updateProviderConnection } from "@/lib/db/providers";
import type { BaseExecutor } from "@omniroute/open-sse/executors/base";
import {
rotationGroupFor,
serializeRefresh,
} from "@omniroute/open-sse/services/refreshSerializer.ts";
type JsonRecord = Record<string, unknown>;
type CredentialRefreshResult = JsonRecord & {
accessToken?: string;
refreshToken?: string;
expiresIn?: number;
expiresAt?: string;
copilotToken?: string;
copilotTokenExpiresAt?: string;
};
export interface ProviderConnectionLike {
id: string;
provider: string;
authType?: string;
accessToken?: string;
refreshToken?: string;
expiresAt?: string | null;
tokenExpiresAt?: string | null;
providerSpecificData?: JsonRecord;
testStatus?: string;
isActive?: boolean;
lastError?: string | null;
lastErrorAt?: string | null;
lastErrorType?: string | null;
lastErrorSource?: string | null;
errorCode?: string | number | null;
rateLimitedUntil?: string | null;
backoffLevel?: number;
}
export interface CredentialRefreshOptions {
allowRotatingRefresh?: boolean;
force?: boolean;
}
export type CredentialExecutorResolver = (provider: string) => Promise<BaseExecutor>;
function withStatus(error: Error, status: number): Error & { status: number } {
return Object.assign(error, { status });
}
/**
* Whether the quota path may refresh this provider's token.
*
* Rotating-refresh providers mint a single-use refresh token on every refresh,
* so bulk quota sync must not refresh siblings concurrently. The on-demand path
* explicitly opts in and remains serialized per rotation group.
*/
export function shouldAttemptRotatingRefresh(
provider: string,
allowRotatingRefresh: boolean | undefined
): boolean {
if (rotationGroupFor(provider) === null) return true;
return allowRotatingRefresh === true;
}
function buildCredentialUpdateData(
connection: ProviderConnectionLike,
refreshResult: CredentialRefreshResult
): JsonRecord {
const updateData: JsonRecord = {
updatedAt: new Date().toISOString(),
};
if (refreshResult.accessToken) {
updateData.accessToken = refreshResult.accessToken;
}
if (refreshResult.refreshToken) {
updateData.refreshToken = refreshResult.refreshToken;
}
if (refreshResult.expiresIn) {
const expiresAt = new Date(Date.now() + refreshResult.expiresIn * 1000).toISOString();
updateData.expiresAt = expiresAt;
updateData.tokenExpiresAt = expiresAt;
} else if (refreshResult.expiresAt) {
updateData.expiresAt = refreshResult.expiresAt;
updateData.tokenExpiresAt = refreshResult.expiresAt;
}
if (refreshResult.copilotToken || refreshResult.copilotTokenExpiresAt) {
updateData.providerSpecificData = {
...(connection.providerSpecificData || {}),
copilotToken: refreshResult.copilotToken,
copilotTokenExpiresAt: refreshResult.copilotTokenExpiresAt,
};
}
return updateData;
}
/** Refresh and persist credentials using a caller-supplied executor resolver. */
export async function refreshAndUpdateCredentialsWithResolver(
connection: ProviderConnectionLike,
resolveExecutor: CredentialExecutorResolver,
opts: CredentialRefreshOptions = {}
) {
if (!shouldAttemptRotatingRefresh(connection.provider, opts.allowRotatingRefresh)) {
return { connection, refreshed: false };
}
const executor = await resolveExecutor(connection.provider);
const credentials = {
connectionId: connection.id,
accessToken: connection.accessToken,
refreshToken: connection.refreshToken,
expiresAt: connection.tokenExpiresAt || connection.expiresAt || null,
providerSpecificData: connection.providerSpecificData,
copilotToken: connection.providerSpecificData?.copilotToken,
copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt,
};
if (!opts.force && !executor.needsRefresh(credentials)) {
return { connection, refreshed: false };
}
const refreshResult = (await serializeRefresh(connection.provider, () =>
executor.refreshCredentials(credentials, console)
)) as CredentialRefreshResult | null;
if (!refreshResult) {
if (connection.accessToken) {
return { connection, refreshed: false };
}
throw withStatus(
new Error("Failed to refresh credentials. Please re-authorize the connection."),
401
);
}
const updateData = buildCredentialUpdateData(connection, refreshResult);
await updateProviderConnection(connection.id, updateData);
return {
connection: {
...connection,
...updateData,
providerSpecificData:
(updateData.providerSpecificData as JsonRecord | undefined) ||
connection.providerSpecificData,
},
refreshed: true,
};
}

View File

@@ -55,6 +55,16 @@ import {
// Reduced from 300 → 50 to avoid browser freeze and network saturation.
const PAGE_SIZE = 50;
// Column sort toggle mapping: clicking a column header toggles asc/desc.
const COLUMN_SORT_MAP = {
status: { desc: "status_desc", asc: "status_asc" },
model: { desc: "model_desc", asc: "model_asc" },
tokens: { desc: "tokens_desc", asc: "tokens_asc" },
tps: { desc: "tps_desc", asc: "tps_asc" },
duration: { desc: "duration_desc", asc: "duration_asc" },
time: { desc: "newest", asc: "oldest" },
} as const;
function getLogTotalTokens(log) {
return (log?.tokens?.in || 0) + (log?.tokens?.out || 0);
}
@@ -147,17 +157,8 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
const [groupedView, setGroupedView] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
// Column sort toggle: clicking a column header toggles asc/desc
const columnSortMap = {
status: { desc: "status_desc", asc: "status_asc" },
model: { desc: "model_desc", asc: "model_asc" },
tokens: { desc: "tokens_desc", asc: "tokens_asc" },
tps: { desc: "tps_desc", asc: "tps_asc" },
duration: { desc: "duration_desc", asc: "duration_asc" },
time: { desc: "newest", asc: "oldest" },
};
const toggleSort = useCallback((column: string) => {
const mapping = columnSortMap[column as keyof typeof columnSortMap];
const mapping = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
if (!mapping) return;
setSortBy((prev) => {
if (prev === mapping.desc) return mapping.asc;
@@ -166,7 +167,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
}, []);
const getSortIndicator = useCallback(
(column: string) => {
const mapping = columnSortMap[column as keyof typeof columnSortMap];
const mapping = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
if (!mapping) return "";
if (sortBy === mapping.desc) return " ↓";
if (sortBy === mapping.asc) return " ↑";
@@ -535,89 +536,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
// endpoint until the row appears.
const router = useRouter();
const openDetail = async (logEntry) => {
// Guard: if no valid id provided, close instead of opening an empty modal
if (!logEntry?.id) {
try {
closeDetail();
} catch {}
return;
}
const requestToken = `${logEntry.id}:${Date.now()}:${Math.random()}`;
detailRequestRef.current = requestToken;
const isCurrentDetailRequest = () => detailRequestRef.current === requestToken;
setSelectedLog(logEntry);
try {
const url = new URL(globalThis.location.href);
url.searchParams.set("id", logEntry.id);
router.replace(url.pathname + url.search, { scroll: false });
} catch (e) {
// ignore navigation errors
}
setDetailLoading(true);
setDetailData(null);
try {
const res = await fetch(`/api/logs/${logEntry.id}`, { cache: "no-store" });
if (res.ok) {
const data = await res.json();
if (!isCurrentDetailRequest()) return;
const dataHasPipeline =
data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0;
setDetailData((prev: { pipelinePayloads: any }) => ({
...prev,
...data,
pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads,
}));
// ensure the modal summary reflects the fetched call log summary
if (data && typeof data === "object") {
setSelectedLog((prev: any) => ({
...prev,
...data,
active: data.active === true,
}));
}
} else {
// A deep-linked id can legitimately 404 while the request is still
// finalizing. Keep the modal open and poll /api/logs/[id] instead of
// falling back to an in-memory active-request endpoint.
if (!isCurrentDetailRequest()) return;
if (res.status === 404) {
if (logEntry.pendingLookup || logEntry.active) {
setSelectedLog((prev: { method: any; path: any }) => ({
...prev,
id: logEntry.id,
status: 0,
method: prev?.method,
path: prev?.path || "",
}));
setDetailData({ detailState: "pending" });
return;
}
try {
console.warn("Log not found:", logEntry.id);
} catch {}
try {
closeDetail();
} catch {}
return;
}
// other errors: show a minimal error indicator by setting detailData to an error object
try {
const body = await res.text().catch(() => null);
if (!isCurrentDetailRequest()) return;
setDetailData({ error: `Failed to fetch log (status ${res.status})`, body });
} catch {}
}
} catch (error) {
console.error("Failed to fetch log detail:", error);
} finally {
if (isCurrentDetailRequest()) setDetailLoading(false);
}
};
const closeDetail = () => {
const closeDetail = useCallback(() => {
detailRequestRef.current = "";
setSelectedLog(null);
setDetailData(null);
@@ -629,7 +548,92 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
} catch (e) {
// ignore navigation errors
}
};
}, [router]);
const openDetail = useCallback(
async (logEntry) => {
// Guard: if no valid id provided, close instead of opening an empty modal
if (!logEntry?.id) {
try {
closeDetail();
} catch {}
return;
}
const requestToken = `${logEntry.id}:${Date.now()}:${Math.random()}`;
detailRequestRef.current = requestToken;
const isCurrentDetailRequest = () => detailRequestRef.current === requestToken;
setSelectedLog(logEntry);
try {
const url = new URL(globalThis.location.href);
url.searchParams.set("id", logEntry.id);
router.replace(url.pathname + url.search, { scroll: false });
} catch (e) {
// ignore navigation errors
}
setDetailLoading(true);
setDetailData(null);
try {
const res = await fetch(`/api/logs/${logEntry.id}`, { cache: "no-store" });
if (res.ok) {
const data = await res.json();
if (!isCurrentDetailRequest()) return;
const dataHasPipeline =
data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0;
setDetailData((prev: { pipelinePayloads: any }) => ({
...prev,
...data,
pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads,
}));
// ensure the modal summary reflects the fetched call log summary
if (data && typeof data === "object") {
setSelectedLog((prev: any) => ({
...prev,
...data,
active: data.active === true,
}));
}
} else {
// A deep-linked id can legitimately 404 while the request is still
// finalizing. Keep the modal open and poll /api/logs/[id] instead of
// falling back to an in-memory active-request endpoint.
if (!isCurrentDetailRequest()) return;
if (res.status === 404) {
if (logEntry.pendingLookup || logEntry.active) {
setSelectedLog((prev: { method: any; path: any }) => ({
...prev,
id: logEntry.id,
status: 0,
method: prev?.method,
path: prev?.path || "",
}));
setDetailData({ detailState: "pending" });
return;
}
try {
console.warn("Log not found:", logEntry.id);
} catch {}
try {
closeDetail();
} catch {}
return;
}
// other errors: show a minimal error indicator by setting detailData to an error object
try {
const body = await res.text().catch(() => null);
if (!isCurrentDetailRequest()) return;
setDetailData({ error: `Failed to fetch log (status ${res.status})`, body });
} catch {}
}
} catch (error) {
console.error("Failed to fetch log detail:", error);
} finally {
if (isCurrentDetailRequest()) setDetailLoading(false);
}
},
[closeDetail, router]
);
const sortedLogsForNav = useMemo(() => sortedLogs, [sortedLogs]);
@@ -654,7 +658,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
console.error("Failed to open initial log id:", error_);
});
}
}, [initialSelectedId]);
}, [initialSelectedId, openDetail]);
useEffect(() => {
const isActive = selectedLog?.active === true;
@@ -765,7 +769,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
pendingBoundaryNavRef.current = "prev";
fetchLogs(false);
}
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
const handleNext = useCallback(() => {
const idx = currentLogIndex;
@@ -781,7 +785,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
pendingBoundaryNavRef.current = "next";
fetchLogs(false);
}
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
// Resolves a pending boundary nav (see handlePrev/handleNext) once a
// triggered fetchLogs() resync has landed in sortedLogsForNav. Only fires

View File

@@ -37,6 +37,7 @@ export type AnyRoutingStrategyValue = RoutingStrategyValue | InternalRoutingStra
export const AUTO_ROUTING_STRATEGY_VALUES = [
"rules",
"score",
"cost",
"eco",
"latency",

View File

@@ -14,4 +14,6 @@ export const STATUS_HEX = {
warning: "#f59e0b",
error: "#ef4444",
muted: "#6b7280",
/** Human-approval gate (waiting_approval) — violet, matching the industry de-facto palette. */
approval: "#8b5cf6",
} as const;

View File

@@ -42,6 +42,13 @@ export function isClientAbortError(err) {
const e = /** @type {NodeJS.ErrnoException} */ (err);
// Node emits `Error: aborted` (no code) from http.Server#abortIncoming.
if (e.message === "aborted" || e.message === "Aborted") return true;
// OmniRoute's SSE teardown aborts in-flight legs with
// `Error [AbortError]: request_signal_aborted` on client disconnects
// (open-sse/utils/streamHandler.ts), and fetch/DOM cancellation surfaces as
// `AbortError` with an abort-flavoured message. Same benign class as
// `Error: aborted` — an emitter-left 'error' event on any of these used to
// kill the process (#fix-dev-server-aborted).
if (e.name === "AbortError" && /abort/i.test(String(e.message))) return true;
switch (e.code) {
case "ERR_STREAM_PREMATURE_CLOSE":
case "ECONNRESET":

View File

@@ -18,6 +18,10 @@ import { resolve } from "path";
import { getLogConfig, initLogRotation } from "@/lib/logRotation";
import { getAppLogLevel } from "@/lib/logEnv";
import { redactLogArgs } from "@/shared/utils/logRedaction";
import {
getOrCreateSharedLoggerResource,
type SharedLoggerResource,
} from "@/shared/utils/loggerResource";
const isDev = process.env.NODE_ENV !== "production";
@@ -61,7 +65,7 @@ function getTransportCompatibleConfig(): pino.LoggerOptions {
* vanished, so failed writes are dropped (best-effort stderr notice) instead of
* escalating.
*/
function buildFileTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
function buildTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
const stream = pino.transport({ targets });
stream.on("error", (err: unknown) => {
try {
@@ -75,11 +79,65 @@ function buildFileTransportStream(targets: NonNullable<pino.TransportMultiOption
return stream;
}
interface OwnedLogStream {
flushSync?: () => void;
end?: () => void;
once?: (event: string, listener: () => void) => unknown;
}
async function closeOwnedStream(stream: OwnedLogStream | null): Promise<void> {
if (!stream) return;
try {
stream.flushSync?.();
} catch {
// Best-effort shutdown: a missing log destination must not block process exit.
}
if (!stream.end) return;
await new Promise<void>((resolveClose) => {
let resolved = false;
const finish = () => {
if (resolved) return;
resolved = true;
resolveClose();
};
const fallback = setTimeout(finish, 1_000);
stream.once?.("close", () => {
clearTimeout(fallback);
finish();
});
try {
stream.end?.();
if (!stream.once) {
clearTimeout(fallback);
finish();
}
} catch {
clearTimeout(fallback);
finish();
}
});
}
function createLoggerResource(
logger: pino.Logger,
stream: OwnedLogStream | null
): SharedLoggerResource {
return {
logger,
close: () => closeOwnedStream(stream),
};
}
/**
* Build the logger with optional file transport.
* Uses pino transport targets for all destinations.
*/
function buildLogger(): pino.Logger {
function buildLoggerResource(): SharedLoggerResource {
const logConfig = getLogConfig();
const logLevel = (baseConfig.level as string) || "info";
const transportConfig = getTransportCompatibleConfig();
@@ -95,7 +153,7 @@ function buildLogger(): pino.Logger {
if (isDev) {
// Dev: pino-pretty → stdout, JSON → file
const stream = buildFileTransportStream([
const stream = buildTransportStream([
{
target: "pino-pretty",
options: {
@@ -113,12 +171,12 @@ function buildLogger(): pino.Logger {
level: logLevel,
},
]);
return pino(transportConfig, stream);
return createLoggerResource(pino(transportConfig, stream), stream);
}
// Production: JSON → stdout + JSON → file
{
const stream = buildFileTransportStream([
const stream = buildTransportStream([
{
target: "pino/file",
options: { destination: 1 }, // stdout
@@ -130,7 +188,7 @@ function buildLogger(): pino.Logger {
level: logLevel,
},
]);
return pino(transportConfig, stream);
return createLoggerResource(pino(transportConfig, stream), stream);
}
} catch (err) {
// Log the actual error for diagnostics (issue #165)
@@ -156,12 +214,15 @@ function buildLogger(): pino.Logger {
});
// Production fallback: JSON to both stdout and file via multistream
return pino(
baseConfig,
pino.multistream([
{ stream: process.stdout, level: logLevel as pino.Level },
{ stream: fileDestination, level: logLevel as pino.Level },
])
return createLoggerResource(
pino(
baseConfig,
pino.multistream([
{ stream: process.stdout, level: logLevel as pino.Level },
{ stream: fileDestination, level: logLevel as pino.Level },
])
),
fileDestination
);
} catch (fallbackErr) {
try {
@@ -175,9 +236,8 @@ function buildLogger(): pino.Logger {
// Console-only (no file logging)
if (isDev) {
return pino({
...baseConfig,
transport: {
const stream = buildTransportStream([
{
target: "pino-pretty",
options: {
colorize: true,
@@ -185,14 +245,18 @@ function buildLogger(): pino.Logger {
ignore: "pid,hostname,service",
messageFormat: "[{module}] {msg}",
},
level: logLevel,
},
});
]);
return createLoggerResource(pino(transportConfig, stream), stream);
}
return pino(baseConfig);
return createLoggerResource(pino(baseConfig), null);
}
export const logger = buildLogger();
const sharedLoggerResource = getOrCreateSharedLoggerResource(buildLoggerResource);
export const logger = sharedLoggerResource.logger;
/**
* Create a child logger with a module tag.

Some files were not shown because too many files have changed in this diff Show More