Compare commits

..

12 Commits

Author SHA1 Message Date
diegosouzapw
972744a0c3 fix(combo): always clear the loop-safety timer, not just on the happy path (#11804)
dispatchWithCooldownRetry arms a loop-safety timer (setTimeout, 10 minutes by
default) on every setTry iteration, so a combo that never produces a terminal
response still answers with a 504 instead of hanging. The only clearTimeout in
the whole file sat inside the `if (anySuccess)` branch — the comment said so
verbatim: "clear the safety timer on the happy path".

Every error exit therefore returned the response to the client while leaving a
600s timer pending, its closure retaining orderedTargets and the exhausted
provider/connection sets: all_targets_skipped, all_accounts_inactive, the
aggregated-status return, the final fallback, and the global-timeout branch.
The timer is also re-armed per setTry iteration with no clear in between.

Field evidence from the issue: two requests that failed quality validation
returned 502 to the client immediately, and "Combo loop safety timeout ...
force-terminating" was logged for both exactly 600 seconds later — the leaked
timers firing long after the requests were gone.

Fixed structurally rather than by sprinkling clearTimeout across the five
return sites: the handle is hoisted to function scope and released in a
finally, so a future `return` added to this function cannot silently
reintroduce the leak. The 504 backstop itself is unchanged.

Note the timer already called .unref(), so it never held the event loop open —
this is a memory-retention leak, not a hang.
2026-09-01 00:10:43 -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
108 changed files with 2310 additions and 10429 deletions

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).",

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

@@ -1111,8 +1111,19 @@ async function handleComboChatInner({
let lastError: string | null = null;
let earliestRetryAfter: ComboRetryAfter | null = null;
let lastStatus: number | null = null;
// #11804: the loop-safety timer is armed per setTry iteration but must be
// cleared on EVERY exit path, not just the happy one. Hoisted to function
// scope so the `finally` at the end of this function always reaches it —
// otherwise each error path (all_targets_skipped / all_accounts_inactive /
// aggregated status / final fallback / global timeout) returned to the client
// leaving a 10-minute timer pending, whose closure retains orderedTargets and
// the exhausted provider/connection sets. Field evidence on the issue: the
// client got a 502 immediately, and "Combo loop safety timeout ...
// force-terminating" was logged exactly 600s later.
let activeLoopSafetyTimer: ReturnType<typeof setTimeout> | null = null;
for (let setTry = 0; setTry <= maxSetRetries; setTry++) {
try {
for (let setTry = 0; setTry <= maxSetRetries; setTry++) {
// #1731: Per-set-iteration set of providers whose quota is fully exhausted.
// Reset each retry so providers excluded in a previous attempt get another chance.
const exhaustedProviders = new Set<string>();
@@ -1198,6 +1209,7 @@ async function handleComboChatInner({
);
}, loopSafetyMs);
loopSafetyTimer.unref?.();
activeLoopSafetyTimer = loopSafetyTimer;
});
const runningTasks = new Set<Promise<void>>();
let anySuccess = false;
@@ -2870,11 +2882,20 @@ async function handleComboChatInner({
// Surface the recovery hint with a generic retry recommendation so the client at least
// gets a non-opaque message instead of "Combo routing completed without an upstream response".
recordComboFailure(effectiveSessionId, combo.name);
return errorResponseWithComboDiagnostics(
503,
"Combo routing completed without an upstream response",
buildNoUpstreamResponseDiagnostics(orderedTargets.length)
);
return errorResponseWithComboDiagnostics(
503,
"Combo routing completed without an upstream response",
buildNoUpstreamResponseDiagnostics(orderedTargets.length)
);
} finally {
// #11804: always release the loop-safety timer. Covering every exit path by
// construction here means a future `return` added to this function cannot
// silently reintroduce the leak.
if (activeLoopSafetyTimer) {
clearTimeout(activeLoopSafetyTimer);
activeLoopSafetyTimer = null;
}
}
};
// FASE 2.1: acquire the per-connection concurrency slot for the selected

View File

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

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

@@ -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

@@ -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.

View File

@@ -0,0 +1,32 @@
import type { Logger } from "pino";
export interface SharedLoggerResource {
logger: Logger;
close: () => Promise<void>;
}
declare global {
var __omnirouteLoggerResource: SharedLoggerResource | undefined;
}
/**
* Return the process-wide logger resource, creating it only once.
*
* Next.js development HMR can evaluate logger.ts in more than one server chunk.
* Keeping the resource on globalThis prevents each evaluation from spawning a
* new pino worker transport.
*/
export function getOrCreateSharedLoggerResource(
create: () => SharedLoggerResource
): SharedLoggerResource {
return (globalThis.__omnirouteLoggerResource ??= create());
}
/** Close and forget the shared transport. Idempotent across HMR module copies. */
export async function closeSharedLoggerResource(): Promise<void> {
const resource = globalThis.__omnirouteLoggerResource;
if (!resource) return;
delete globalThis.__omnirouteLoggerResource;
await resource.close();
}

View File

@@ -0,0 +1,155 @@
/**
* Regression test for #11759 — `/v1/models` dropped the vector width and the
* `embedding` type from embedding models that `embeddingRegistry.ts` describes,
* whenever a synced model existed for the same id.
*
* `embeddingRegistry.ts` is the only machine-readable source of two facts: a model's
* vector width, and that it is an embedding model at all. No upstream provider
* publishes either — OpenRouter's catalogue carries no `dimensions`, and OpenAI's
* `/v1/models` returns ids with no capability information.
*
* Two paths lost them:
*
* - Width. The registry loop skipped its entry outright when discovery had already
* produced the model (`hasEquivalentSpecialtyModel(...) continue`), so the surviving
* entry was the discovered one — typed, but with no `dimensions`.
*
* - Type. Classification comes from `sm.supportedEndpoints`, which falls back to
* `["chat"]`. A provider that does not report endpoints therefore yielded
* `modelType === undefined` and the `type` key was omitted entirely, so an embedding
* model read as a chat model.
*
* A consumer that stores vectors needs both: collections are keyed on width, and an
* untyped model cannot be identified as an embedding model. These tests assert the
* merged `/v1/models` output, not the registry in isolation — the registry was always
* correct; the loss happened in `catalog.ts`.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-11759-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
const embeddingRegistry = await import("../../open-sse/config/embeddingRegistry.ts");
/** Both are real registry entries, so the expected widths come from the registry itself. */
const OPENROUTER_MODEL = "qwen/qwen3-embedding-8b";
const OPENAI_MODEL = "text-embedding-3-small";
function registryWidth(providerId: string, modelId: string): number {
const provider = embeddingRegistry.getEmbeddingProvider(providerId);
assert.ok(provider, `embeddingRegistry has no provider "${providerId}"`);
const model = provider!.models.find((m) => m.id === modelId);
assert.ok(model, `embeddingRegistry has no model "${modelId}" on "${providerId}"`);
const { dimensions } = model!;
assert.equal(
typeof dimensions,
"number",
`this test assumes a single advertised width for "${modelId}"`
);
return dimensions as number;
}
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
}
/** An active connection, which is what makes the provider's registry models eligible. */
async function connectProvider(provider: string) {
return (await providersDb.createProviderConnection({
provider,
authType: "apikey",
name: `${provider}-conn`,
apiKey: "sk-test",
isActive: true,
testStatus: "active",
})) as { id: string };
}
async function modelEntry(id: string) {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
assert.equal(response.status, 200);
const body = (await response.json()) as { data: Array<Record<string, unknown>> };
const entry = body.data.find((model) => model.id === id);
assert.ok(
entry,
`expected "${id}" in /v1/models, got ${JSON.stringify(
body.data.filter((m) => String(m.id).includes("embedding")).map((m) => m.id)
)}`
);
return entry!;
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("#11759: a synced embedding model keeps the width the registry states", async () => {
const connection = await connectProvider("openrouter");
// What discovery produces for OpenRouter: typed as an embedding model, and carrying no
// width, because the upstream catalogue does not publish one.
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
{
id: OPENROUTER_MODEL,
name: "Qwen3 Embedding 8B",
source: "imported",
supportedEndpoints: ["embeddings"],
},
]);
const entry = await modelEntry(`openrouter/${OPENROUTER_MODEL}`);
const expected = registryWidth("openrouter", OPENROUTER_MODEL);
assert.equal(
entry.dimensions,
expected,
`the registry states ${expected} for "${OPENROUTER_MODEL}"; the synced entry must not drop it — got ${JSON.stringify(entry.dimensions)}`
);
assert.equal(entry.type, "embedding", "a synced embedding model must stay typed as one");
});
test("#11759: a synced model the embedding registry names is typed as an embedding model", async () => {
const connection = await connectProvider("openai");
// What discovery produces for OpenAI: no `supportedEndpoints`, because `/v1/models`
// returns ids with no capability information, so classification falls back to `["chat"]`
// and the model is emitted with no `type` at all.
await modelsDb.replaceSyncedAvailableModelsForConnection("openai", connection.id, [
{
id: OPENAI_MODEL,
name: "Text Embedding 3 Small",
source: "imported",
},
]);
const entry = await modelEntry(`openai/${OPENAI_MODEL}`);
assert.equal(
entry.type,
"embedding",
`"${OPENAI_MODEL}" is in the embedding registry, so it must not be published untyped — got ${JSON.stringify(entry.type)}`
);
assert.equal(
entry.dimensions,
registryWidth("openai", OPENAI_MODEL),
"an embedding model published without its width cannot be indexed by a consumer"
);
});

View File

@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
const dispatchSourcePath = join(import.meta.dirname, "../../src/lib/batches/dispatch.ts");
const originalFetch = globalThis.fetch;
const originalEnv = {
OMNIROUTE_PORT: process.env.OMNIROUTE_PORT,
PORT: process.env.PORT,
DASHBOARD_PORT: process.env.DASHBOARD_PORT,
OMNIROUTE_BASE_PATH: process.env.OMNIROUTE_BASE_PATH,
};
function restoreEnv(): void {
for (const key of Object.keys(originalEnv) as Array<keyof typeof originalEnv>) {
const value = originalEnv[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
test.afterEach(() => {
globalThis.fetch = originalFetch;
restoreEnv();
});
test("batch dispatch does not pull API route modules into the instrumentation graph", () => {
const source = readFileSync(dispatchSourcePath, "utf8");
assert.doesNotMatch(source, /@\/app\/api\/v1\/.+\/route/);
assert.doesNotMatch(source, /handlerLoaders|BatchRouteHandler/);
});
test("batch dispatch posts to the active dashboard loopback listener", async () => {
process.env.OMNIROUTE_PORT = "24120";
process.env.PORT = "24121";
process.env.DASHBOARD_PORT = "24122";
process.env.OMNIROUTE_BASE_PATH = "/omniroute/";
const calls: Array<{ input: string; init?: RequestInit }> = [];
const upstreamResponse = new Response("accepted", { status: 202 });
globalThis.fetch = async (input, init) => {
calls.push({ input: String(input), init });
return upstreamResponse;
};
const { dispatch } = await import("../../src/lib/batches/dispatch.ts");
const response = await dispatch.dispatchBatchApiRequest({
endpoint: "/v1/chat/completions",
body: { model: "provider/model", messages: [] },
apiKey: "batch-secret",
});
assert.strictEqual(response, upstreamResponse);
assert.equal(calls.length, 1);
assert.equal(calls[0].input, "http://127.0.0.1:24122/omniroute/v1/chat/completions");
assert.equal(calls[0].init?.method, "POST");
assert.equal(calls[0].init?.redirect, "error");
assert.equal(new Headers(calls[0].init?.headers).get("authorization"), "Bearer batch-secret");
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
model: "provider/model",
messages: [],
});
});

View File

@@ -202,6 +202,13 @@ test("Batch API and Processing", async () => {
});
test("Batch handles and counts failures correctly", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ error: { message: "Model not found" } }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
initBatchProcessor();
try {
// 1. Create a file with a request that will fail (invalid provider/model)
@@ -264,6 +271,7 @@ test("Batch handles and counts failures correctly", async () => {
}
} finally {
stopBatchProcessor();
globalThis.fetch = originalFetch;
}
});
@@ -390,6 +398,21 @@ test("Batch rejects input lines whose url does not match the batch endpoint", as
});
test("Batch forces stream: false for all requests", async () => {
const originalFetch = globalThis.fetch;
let dispatchedBody: Record<string, unknown> | null = null;
globalThis.fetch = async (_input, init) => {
dispatchedBody = JSON.parse(String(init?.body));
return new Response(
JSON.stringify({
choices: [{ message: { role: "assistant", content: "batch response" } }],
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
};
initBatchProcessor();
try {
const batchItems = [
@@ -449,8 +472,10 @@ test("Batch forces stream: false for all requests", async () => {
"Should not have JSON parsing error from SSE stream"
);
}
assert.strictEqual(dispatchedBody?.stream, false, "Batch dispatch must disable streaming");
} finally {
stopBatchProcessor();
globalThis.fetch = originalFetch;
}
});

View File

@@ -27,11 +27,12 @@ import path from "node:path";
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-quota-"));
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const { getCredentialRefreshExecutor } =
await import("../../open-sse/executors/credential.ts");
const { refreshAndUpdateCredentials } = await import("../../src/lib/usage/providerLimits.ts");
test("codex: quota-sync must NOT proactively rotate the refresh_token (Auth0 family-revocation cascade guard)", async () => {
const exec = await getExecutor("codex");
const exec = await getCredentialRefreshExecutor("codex");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;
@@ -68,7 +69,7 @@ test("codex: quota-sync must NOT proactively rotate the refresh_token (Auth0 fam
});
test("non-rotating OAuth provider is still refreshed proactively from quota-sync (gate is not over-broad)", async () => {
const exec = await getExecutor("cursor");
const exec = await getCredentialRefreshExecutor("cursor");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;

View File

@@ -0,0 +1,74 @@
/**
* #11804 — the combo loop-safety timer must be cleared on EVERY exit path.
*
* `dispatchWithCooldownRetry` (open-sse/services/combo.ts) arms a
* `setTimeout(..., loopSafetyMs)` — 10 minutes by default — once per `setTry`
* iteration, so a combo that never produces a terminal response still answers
* the client with a 504 instead of hanging forever.
*
* Before this fix the only `clearTimeout` lived inside the `if (anySuccess)`
* branch (the code comment said so verbatim: "clear the safety timer on the
* happy path"). Every error exit — all_targets_skipped, all_accounts_inactive,
* the aggregated-status return, the final fallback, the global-timeout branch —
* returned the response to the client and left a 600s timer pending, its
* closure retaining `orderedTargets` and the exhausted provider/connection
* sets. Field evidence on the issue: a client received 502 immediately and the
* "Combo loop safety timeout ... force-terminating" line was logged exactly
* 600s later, long after the request was gone.
*
* This is a source-level guard rather than a runtime one: driving a real combo
* through each terminal branch needs the full provider/credential/DB stack, and
* the invariant we actually care about ("no exit path may skip the clear") is a
* structural property of the function. The guard fails if someone reintroduces
* a happy-path-only clear or drops the finally.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const comboSrc = readFileSync(resolve(here, "../../open-sse/services/combo.ts"), "utf8");
test("#11804: the loop-safety timer is released in a finally, not only on success", () => {
assert.match(
comboSrc,
/finally\s*\{[^}]*clearTimeout\(activeLoopSafetyTimer\)/s,
"dispatchWithCooldownRetry must clear the loop-safety timer in a finally block so every " +
"exit path (including future ones) releases it"
);
});
test("#11804: the timer handle is reachable from the function-scope cleanup", () => {
// The timer is created inside the `for (setTry...)` loop; the cleanup lives at
// function scope. If the handle is not published to that outer binding, the
// finally silently clears nothing.
assert.match(
comboSrc,
/activeLoopSafetyTimer = loopSafetyTimer/,
"the per-iteration timer must be published to the function-scope handle"
);
const declIdx = comboSrc.indexOf("let activeLoopSafetyTimer");
const loopIdx = comboSrc.indexOf("for (let setTry = 0");
assert.ok(declIdx > 0, "function-scope timer handle must be declared");
assert.ok(
declIdx < loopIdx,
"the handle must be declared OUTSIDE the setTry loop, otherwise each iteration " +
"gets a fresh binding and the previous iteration's timer leaks"
);
});
test("#11804: the safety timeout itself is preserved (fix must not disarm the 504)", () => {
// Guard against 'fixing' the leak by simply never arming the timer.
assert.match(
comboSrc,
/loopSafetyTimer = setTimeout\(/,
"the loop-safety timer must still be armed — the 504 backstop is the reason it exists"
);
assert.match(
comboSrc,
/Combo loop safety timeout/,
"the force-termination path must still exist"
);
});

View File

@@ -15,6 +15,7 @@ const { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor, listExec
await import("../../open-sse/executors/registry.ts");
const { getExecutor, hasSpecializedExecutor, BaseExecutor, DefaultExecutor } =
await import("../../open-sse/executors/index.ts");
const { getDefaultExecutor } = await import("../../open-sse/executors/defaultResolver.ts");
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
@@ -54,3 +55,9 @@ test("registry lookup is exact — Object.prototype names are not executors", as
assert.ok((await getExecutor(name)) instanceof DefaultExecutor, name);
}
});
test("the registry and leaf resolver share fallback executor instances", async () => {
const provider = "default-resolver-test-provider";
assert.equal(await getExecutor(provider), getDefaultExecutor(provider));
assert.equal(getDefaultExecutor(provider), getDefaultExecutor(provider));
});

View File

@@ -1,5 +1,12 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
type GracefulShutdownModule = typeof import("../../src/lib/gracefulShutdown.ts");
const gracefulShutdownUrl = pathToFileURL(join(process.cwd(), "src/lib/gracefulShutdown.ts")).href;
const shutdownSignals = ["SIGTERM", "SIGINT", "SIGHUP"] as const;
// #8045: on Windows, closing the console window delivers CTRL_CLOSE_EVENT, which
// Node/libuv maps to a JS-visible "SIGHUP" event (confirmed: nodejs/node#10165,
@@ -7,17 +14,90 @@ import assert from "node:assert/strict";
// closed"). Before this fix, initGracefulShutdown() only registered SIGTERM/SIGINT,
// so the "close the window" path never ran cleanup() (WAL checkpoint(TRUNCATE) +
// closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next launch.
test("initGracefulShutdown registers a SIGHUP handler (Windows console-close path)", async () => {
const before = process.listenerCount("SIGHUP");
const { initGracefulShutdown } = await import("../../src/lib/gracefulShutdown.ts");
initGracefulShutdown();
const after = process.listenerCount("SIGHUP");
assert.ok(
after > before,
`Expected initGracefulShutdown() to add a SIGHUP listener (before=${before}, after=${after}).`
test("graceful shutdown listeners remain process-singletons across HMR module instances", async () => {
const previousState = globalThis.__omnirouteShutdown;
const previousRequestShutdown = globalThis.__omnirouteRequestShutdown;
const previousCustomServerOwner = globalThis.__omnirouteCustomServerOwnsShutdown;
const listenersBefore = new Map(
shutdownSignals.map((signal) => [signal, process.listeners(signal)] as const)
);
delete globalThis.__omnirouteShutdown;
delete globalThis.__omnirouteRequestShutdown;
delete globalThis.__omnirouteCustomServerOwnsShutdown;
try {
const first = (await import(
`${gracefulShutdownUrl}?phase4=shutdown-a`
)) as GracefulShutdownModule;
const second = (await import(
`${gracefulShutdownUrl}?phase4=shutdown-b`
)) as GracefulShutdownModule;
first.initGracefulShutdown();
assert.equal(globalThis.__omnirouteRequestShutdown, first.requestGracefulShutdown);
for (const signal of shutdownSignals) {
assert.equal(process.listenerCount(signal), listenersBefore.get(signal)!.length + 1);
}
second.initGracefulShutdown();
for (const signal of shutdownSignals) {
assert.equal(
process.listenerCount(signal),
listenersBefore.get(signal)!.length + 1,
`${signal} listener must not be duplicated by HMR re-initialization`
);
}
} finally {
for (const signal of shutdownSignals) {
const previousListeners = listenersBefore.get(signal)!;
for (const listener of process.listeners(signal)) {
if (!previousListeners.includes(listener)) process.removeListener(signal, listener);
}
}
if (previousState === undefined) delete globalThis.__omnirouteShutdown;
else globalThis.__omnirouteShutdown = previousState;
if (previousRequestShutdown === undefined) delete globalThis.__omnirouteRequestShutdown;
else globalThis.__omnirouteRequestShutdown = previousRequestShutdown;
if (previousCustomServerOwner === undefined) {
delete globalThis.__omnirouteCustomServerOwnsShutdown;
} else {
globalThis.__omnirouteCustomServerOwnsShutdown = previousCustomServerOwner;
}
}
});
test("a custom server owner receives cleanup without duplicate process signal listeners", async () => {
const previousState = globalThis.__omnirouteShutdown;
const previousRequestShutdown = globalThis.__omnirouteRequestShutdown;
const previousCustomServerOwner = globalThis.__omnirouteCustomServerOwnsShutdown;
const listenerCounts = new Map(
shutdownSignals.map((signal) => [signal, process.listenerCount(signal)] as const)
);
// Clean up: remove all SIGHUP listeners added by this test so it doesn't leak
// into other test files sharing the same process.
process.removeAllListeners("SIGHUP");
delete globalThis.__omnirouteShutdown;
delete globalThis.__omnirouteRequestShutdown;
globalThis.__omnirouteCustomServerOwnsShutdown = true;
try {
const shutdownModule = (await import(
`${gracefulShutdownUrl}?phase4=custom-owner`
)) as GracefulShutdownModule;
shutdownModule.initGracefulShutdown();
assert.equal(globalThis.__omnirouteRequestShutdown, shutdownModule.requestGracefulShutdown);
for (const signal of shutdownSignals) {
assert.equal(process.listenerCount(signal), listenerCounts.get(signal));
}
} finally {
if (previousState === undefined) delete globalThis.__omnirouteShutdown;
else globalThis.__omnirouteShutdown = previousState;
if (previousRequestShutdown === undefined) delete globalThis.__omnirouteRequestShutdown;
else globalThis.__omnirouteRequestShutdown = previousRequestShutdown;
if (previousCustomServerOwner === undefined) {
delete globalThis.__omnirouteCustomServerOwnsShutdown;
} else {
globalThis.__omnirouteCustomServerOwnsShutdown = previousCustomServerOwner;
}
}
});

View File

@@ -3,6 +3,8 @@
import assert from "node:assert";
import { test } from "node:test";
import { EventEmitter } from "node:events";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
isClientAbortError,
shouldSwallowUncaught,
@@ -115,5 +117,88 @@ test("shouldSwallowUncaught preserves crash semantics for genuine errors", () =>
test("installProcessCrashGuard does not throw on import and is idempotent", () => {
assert.doesNotThrow(() => installProcessCrashGuard(() => {}));
assert.doesNotThrow(() => installProcessCrashGuard(() => {}));
});
test("isClientAbortError matches OmniRoute SSE AbortError shapes (#fix-crash-guard-logger-7)", () => {
// Exact production shape from the 2026-08-31 crash log:
// unhandledRejection: Error [AbortError]: request_signal_aborted
const sseAbort = Object.assign(new Error("request_signal_aborted"), { name: "AbortError" });
assert.equal(isClientAbortError(sseAbort), true, "SSE teardown AbortError must be absorbed");
// fetch / DOMException-style cancellation
const domAbort = new DOMException("This operation was aborted", "AbortError");
assert.equal(isClientAbortError(domAbort), true, "DOMException AbortError must be absorbed");
// A genuine TypeError that merely MENTIONS 'abort' must NOT be absorbed.
const typo = new TypeError("Cannot read properties of undefined (reading 'abort')");
assert.equal(isClientAbortError(typo), false);
});
test("shouldSwallowUncaught absorbs SSE AbortError rejections", () => {
const sseAbort = Object.assign(new Error("request_signal_aborted"), { name: "AbortError" });
assert.equal(shouldSwallowUncaught(sseAbort, "unhandledRejection"), true);
});
// Production crash (2026-08-25 → 08-31, ~170 restarts, exit code 7):
// every real call site installs the guard with NO logger, so the old
// `const logger = log ?? console` default invoked the console OBJECT as a
// function inside the uncaughtException handler → TypeError inside
// process._fatalException → Node exit code 7. These children run the REAL
// production call shape; the process must survive benign aborts and still
// crash on genuine errors.
test("installProcessCrashGuard() with no logger swallows aborts instead of dying (exit-7 regression)", async () => {
const guardPath = fileURLToPath(
new URL("../../src/shared/utils/httpClientAbortGuard.mjs", import.meta.url)
);
const script = `
const { installProcessCrashGuard } = await import(process.argv[1]);
installProcessCrashGuard(); // production call sites pass NO logger
process.emit(
"uncaughtException",
Object.assign(new Error("aborted"), { code: "ECONNRESET" }),
"uncaughtException"
);
process.emit(
"unhandledRejection",
Object.assign(new Error("request_signal_aborted"), { name: "AbortError" }),
Promise.resolve()
);
console.log("ALIVE");
process.exit(0);
`;
const { status, stdout, stderr } = await new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["--input-type=module", "-e", script, guardPath], {
stdio: ["ignore", "pipe", "pipe"],
});
let out = "";
let err = "";
child.stdout.on("data", (d) => (out += d));
child.stderr.on("data", (d) => (err += d));
child.on("close", (status) => resolve({ status, stdout: out, stderr: err }));
child.on("error", reject);
});
assert.equal(status, 0, `child must survive benign aborts; stderr: ${stderr}`);
assert.match(stdout, /ALIVE/);
});
test("installProcessCrashGuard still crashes on genuine errors (no over-swallowing)", async () => {
const guardPath = fileURLToPath(
new URL("../../src/shared/utils/httpClientAbortGuard.mjs", import.meta.url)
);
const script = `
const { installProcessCrashGuard } = await import(process.argv[1]);
installProcessCrashGuard();
process.emit("uncaughtException", new Error("genuine failure"), "uncaughtException");
console.log("SHOULD_NOT_REACH");
`;
const { status, stdout, stderr: _stderr } = await new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["--input-type=module", "-e", script, guardPath], {
stdio: ["ignore", "pipe", "pipe"],
});
let out = "";
let err = "";
child.stdout.on("data", (d) => (out += d));
child.stderr.on("data", (d) => (err += d));
child.on("close", (status) => resolve({ status, stdout: out, stderr: err }));
child.on("error", reject);
});
assert.notEqual(status, 0, "genuine errors must keep crash semantics");
assert.doesNotMatch(stdout, /SHOULD_NOT_REACH/);
});

View File

@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const instrumentationPath = path.join(process.cwd(), "src/instrumentation-node.ts");
const quotaAutoPingPath = path.join(process.cwd(), "src/lib/services/quotaAutoPing.ts");
const credentialRefreshPath = path.join(
process.cwd(),
"src/lib/usage/providerLimits/credentialRefresh.ts"
);
const providerLimitsPath = path.join(process.cwd(), "src/lib/usage/providerLimits.ts");
const credentialExecutorPath = path.join(process.cwd(), "open-sse/executors/credential.ts");
const executorDirectory = path.join(process.cwd(), "open-sse/executors");
const anthropicValidationPath = path.join(
process.cwd(),
"src/lib/providers/validation/anthropicFormat.ts"
);
const defaultExecutorResolverPath = path.join(
process.cwd(),
"open-sse/executors/defaultResolver.ts"
);
test("node instrumentation loads the proxy patch leaf before quota registration", () => {
const source = fs.readFileSync(instrumentationPath, "utf8");
const proxyPatchImport = 'await import("@omniroute/open-sse/utils/proxyFetch.ts")';
const proxyPatchIndex = source.indexOf(proxyPatchImport);
const quotaRegistrationIndex = source.indexOf("await registerQuotaFetchers()");
assert.ok(proxyPatchIndex >= 0, "startup must load the proxyFetch side-effect leaf");
assert.ok(quotaRegistrationIndex > proxyPatchIndex, "proxy patch must run before quota setup");
assert.doesNotMatch(source, /import\("@omniroute\/open-sse\/index\.ts"\)/);
});
test("quota auto-ping lazily loads only the Codex executor", () => {
const source = fs.readFileSync(quotaAutoPingPath, "utf8");
const credentialRefreshSource = fs.readFileSync(credentialRefreshPath, "utf8");
assert.doesNotMatch(source, /open-sse\/executors\/index(?:\.ts)?/);
assert.doesNotMatch(source, /@\/lib\/usage\/providerLimits["']/);
assert.match(source, /import\("@omniroute\/open-sse\/executors\/codex\.ts"\)/);
assert.match(source, /@\/lib\/usage\/providerLimits\/credentialRefresh/);
assert.match(source, /getExecutor: loadQuotaAutoPingExecutor/);
assert.doesNotMatch(credentialRefreshSource, /open-sse\/executors\/index(?:\.ts)?/);
});
test("startup still registers bespoke, batch, and generic quota fetchers", async () => {
const [{ registerQuotaFetchers }, { getQuotaFetcher }] = await Promise.all([
import("../../src/instrumentation-node.ts"),
import("../../open-sse/services/quotaPreflight.ts"),
]);
await registerQuotaFetchers();
for (const provider of [
"agentrouter",
"codex",
"bailian-coding-plan",
"qwen-cloud-token-plan",
"crof",
"deepseek",
"openrouter",
"opencode-go",
"grok-web",
"antigravity",
]) {
assert.equal(typeof getQuotaFetcher(provider), "function", `${provider} quota fetcher missing`);
}
});
test("provider-limit startup uses the refresh-only executor resolver", () => {
const providerLimitsSource = fs.readFileSync(providerLimitsPath, "utf8");
const credentialExecutorSource = fs.readFileSync(credentialExecutorPath, "utf8");
assert.doesNotMatch(providerLimitsSource, /open-sse\/executors\/index(?:\.ts)?/);
assert.match(providerLimitsSource, /open-sse\/executors\/credential\.ts/);
assert.doesNotMatch(credentialExecutorSource, /\.\/index(?:\.ts)?/);
assert.match(credentialExecutorSource, /export async function getCredentialRefreshExecutor/);
});
test("credential resolver covers every executor with custom refresh behavior", () => {
const credentialExecutorSource = fs.readFileSync(credentialExecutorPath, "utf8");
const refreshOverrideFiles = fs
.readdirSync(executorDirectory)
.filter((file) => file.endsWith(".ts") && !["base.ts", "default.ts"].includes(file))
.filter((file) => {
const source = fs.readFileSync(path.join(executorDirectory, file), "utf8");
return /^\s*(?:async\s+)?(?:needsRefresh|refreshCredentials)\s*\(/m.test(source);
});
for (const file of refreshOverrideFiles) {
assert.ok(
credentialExecutorSource.includes(`import("./${file}")`),
`${file} must be registered in the refresh-only resolver`
);
}
});
test("Claude OAuth validation resolves the default executor without the chat registry", () => {
const validationSource = fs.readFileSync(anthropicValidationPath, "utf8");
const resolverSource = fs.readFileSync(defaultExecutorResolverPath, "utf8");
assert.doesNotMatch(validationSource, /open-sse\/executors\/index(?:\.ts)?/);
assert.match(validationSource, /open-sse\/executors\/defaultResolver\.ts/);
assert.doesNotMatch(resolverSource, /\.\/index(?:\.ts)?/);
assert.match(resolverSource, /export function getDefaultExecutor/);
});

View File

@@ -0,0 +1,132 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import pino from "pino";
type LoggerModule = typeof import("../../src/shared/utils/logger.ts");
type LoggerResourceModule = typeof import("../../src/shared/utils/loggerResource.ts");
type LogRotationModule = typeof import("../../src/lib/logRotation.ts");
const loggerUrl = pathToFileURL(join(process.cwd(), "src/shared/utils/logger.ts")).href;
const loggerResourceUrl = pathToFileURL(
join(process.cwd(), "src/shared/utils/loggerResource.ts")
).href;
const logRotationUrl = pathToFileURL(join(process.cwd(), "src/lib/logRotation.ts")).href;
const envKeys = [
"NODE_ENV",
"APP_LOG_TO_FILE",
"APP_LOG_FILE_PATH",
"APP_LOG_LEVEL",
"APP_LOG_ROTATION_CHECK_INTERVAL_MS",
] as const;
function saveEnv(): Record<(typeof envKeys)[number], string | undefined> {
return Object.fromEntries(envKeys.map((key) => [key, process.env[key]])) as Record<
(typeof envKeys)[number],
string | undefined
>;
}
function restoreEnv(saved: ReturnType<typeof saveEnv>): void {
for (const key of envKeys) {
const value = saved[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
function closeLoggerStream(logger: LoggerModule["logger"]): void {
const stream = (logger as unknown as Record<symbol, unknown>)[pino.symbols.streamSym] as
{ flushSync?: () => void; end?: () => void } | undefined;
try {
stream?.flushSync?.();
} catch {}
try {
stream?.end?.();
} catch {}
}
test("logger transport and rotation timer remain process-singletons across HMR module instances", async () => {
const savedEnv = saveEnv();
const testDir = mkdtempSync(join(tmpdir(), "omniroute-logger-singleton-12074-"));
const originalSetInterval = globalThis.setInterval;
let firstLogger: LoggerModule | undefined;
let secondLogger: LoggerModule | undefined;
let firstLoggerResource: LoggerResourceModule | undefined;
let secondLoggerResource: LoggerResourceModule | undefined;
let firstRotation: LogRotationModule | undefined;
let secondRotation: LogRotationModule | undefined;
process.env.NODE_ENV = "production";
process.env.APP_LOG_TO_FILE = "true";
process.env.APP_LOG_FILE_PATH = join(testDir, "application.log");
process.env.APP_LOG_LEVEL = "debug";
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "60000";
try {
firstRotation = (await import(`${logRotationUrl}?phase4=rotation-a`)) as LogRotationModule;
secondRotation = (await import(`${logRotationUrl}?phase4=rotation-b`)) as LogRotationModule;
firstRotation.closeLogRotation();
secondRotation.closeLogRotation();
let intervalCreations = 0;
globalThis.setInterval = ((...args: unknown[]) => {
intervalCreations++;
return Reflect.apply(originalSetInterval, globalThis, args);
}) as typeof setInterval;
firstRotation.initLogRotation();
secondRotation.initLogRotation();
assert.equal(intervalCreations, 1, "HMR reloads must share one log rotation timer");
globalThis.setInterval = originalSetInterval;
firstLoggerResource = (await import(
`${loggerResourceUrl}?phase4=resource-a`
)) as LoggerResourceModule;
secondLoggerResource = (await import(
`${loggerResourceUrl}?phase4=resource-b`
)) as LoggerResourceModule;
firstLogger = (await import(`${loggerUrl}?phase4=logger-a`)) as LoggerModule;
secondLogger = (await import(`${loggerUrl}?phase4=logger-b`)) as LoggerModule;
assert.equal(firstLogger.logger, secondLogger.logger, "HMR reloads must reuse one logger");
const firstStream = (firstLogger.logger as unknown as Record<symbol, unknown>)[
pino.symbols.streamSym
];
const secondStream = (secondLogger.logger as unknown as Record<symbol, unknown>)[
pino.symbols.streamSym
];
assert.equal(firstStream, secondStream, "HMR reloads must reuse one pino transport");
const resource = globalThis.__omnirouteLoggerResource;
assert.ok(resource, "expected the process-wide logger resource to be registered");
const originalClose = resource.close;
let closeCalls = 0;
resource.close = async () => {
closeCalls++;
await originalClose();
};
await firstLoggerResource.closeSharedLoggerResource();
await secondLoggerResource.closeSharedLoggerResource();
assert.equal(closeCalls, 1, "shared logger teardown must be idempotent across HMR modules");
assert.equal(globalThis.__omnirouteLoggerResource, undefined);
} finally {
globalThis.setInterval = originalSetInterval;
firstRotation?.closeLogRotation();
secondRotation?.closeLogRotation();
if (firstLoggerResource) {
await firstLoggerResource.closeSharedLoggerResource();
} else {
if (firstLogger) closeLoggerStream(firstLogger.logger);
if (secondLogger && secondLogger.logger !== firstLogger?.logger) {
closeLoggerStream(secondLogger.logger);
}
}
restoreEnv(savedEnv);
rmSync(testDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});

View File

@@ -7,13 +7,13 @@ import * as yaml from "js-yaml";
const ROOT = process.cwd();
const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml");
const { LOCAL_ONLY_API_PREFIXES, LOCAL_ONLY_API_PATTERNS, ALWAYS_PROTECTED_API_PATHS } =
const { LOCAL_ONLY_API_PREFIXES, ALWAYS_PROTECTED_API_PATHS } =
await import("../../src/server/authz/routeGuard.ts");
const raw: any = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8"));
const paths: Record<string, any> = raw.paths || {};
test("every x-loopback-only path matches a LOCAL_ONLY prefix or pattern in routeGuard.ts", () => {
test("every x-loopback-only path matches a LOCAL_ONLY prefix in routeGuard.ts", () => {
for (const [pathStr, methods] of Object.entries(paths)) {
if (!methods || typeof methods !== "object") continue;
for (const [method, spec] of Object.entries(methods as Record<string, any>)) {
@@ -25,16 +25,10 @@ test("every x-loopback-only path matches a LOCAL_ONLY prefix or pattern in route
return pathStr === norm || pathStr.startsWith(norm + "/");
}
);
// Param-shaped routes (e.g. /api/providers/{id}/login) are classified by
// LOCAL_ONLY_API_PATTERNS regexes rather than a static prefix — the OpenAPI
// {param} placeholder satisfies the same [^/]+ segment the runtime matches.
const matchesPattern = (LOCAL_ONLY_API_PATTERNS as ReadonlyArray<RegExp>).some((re) =>
re.test(pathStr)
);
assert.ok(
matchesPrefix || matchesPattern,
`YAML path "${pathStr}" ${method.toUpperCase()} has x-loopback-only but is NOT in LOCAL_ONLY_API_PREFIXES ` +
`or LOCAL_ONLY_API_PATTERNS. Add it to routeGuard.ts or remove x-loopback-only.`
matchesPrefix,
`YAML path "${pathStr}" ${method.toUpperCase()} has x-loopback-only but is NOT in LOCAL_ONLY_API_PREFIXES. ` +
`Add it to routeGuard.ts LOCAL_ONLY_API_PREFIXES or remove x-loopback-only.`
);
}
}

View File

@@ -22,7 +22,8 @@ process.env.DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-accesstoken-fallback-")
);
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const { getCredentialRefreshExecutor } =
await import("../../open-sse/executors/credential.ts");
const { refreshAndUpdateCredentials } = await import("../../src/lib/usage/providerLimits.ts");
// `gemini` is a non-rotating (no rotation lock group), non-github OAuth provider,
@@ -39,7 +40,7 @@ function geminiConnection() {
}
test("falls back to the existing accessToken for a non-github provider when refreshCredentials returns null", async () => {
const exec = await getExecutor("gemini");
const exec = await getCredentialRefreshExecutor("gemini");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
exec.needsRefresh = () => true; // force the refresh attempt
@@ -65,7 +66,7 @@ test("falls back to the existing accessToken for a non-github provider when refr
});
test("still throws when refresh fails AND there is no accessToken to fall back on", async () => {
const exec = await getExecutor("gemini");
const exec = await getCredentialRefreshExecutor("gemini");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
exec.needsRefresh = () => true;

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