Compare commits

..

2 Commits

Author SHA1 Message Date
diegosouzapw
aabfc59078 docs(api): security tiers on generated paths, regenerated API skills, size baseline
The first CI round caught three real contract gaps in the generated coverage:

- Generated operations on LOCAL_ONLY routes now carry x-loopback-only (and
  x-always-protected for ALWAYS_PROTECTED_API_PATHS), resolved through the real
  src/server/authz/routeGuard.ts at generation time. The
  openapi-security-tiers guard now also accepts LOCAL_ONLY_API_PATTERNS —
  param-shaped routes (/api/providers/{id}/login) are classified by regex in
  the runtime and were invisible to the prefix-only check.
- The API agent skills are generated FROM the spec: 18 SKILL.md files
  regenerated via generate-agent-skills --apply so the generator stays 46/46.
- src/app/docs/lib/openapi.generated.ts grew with the spec (171 -> 1347 lines,
  emitted by gen-openapi-module): frozen in file-size-baseline.json with a
  _rebaseline justification — shrink by slimming the spec, never by editing
  the generated module.
2026-09-01 00:27:53 -03:00
diegosouzapw
3113e571fc docs(api): document every implemented route in openapi.yaml (276 -> 692 paths)
Follow-up nº 3 of the 2026-08-31 docs audit: 416 implemented routes had no
OpenAPI entry (gamification, radar, skills, webhooks, mcp, a2a, tunnels,
version-manager and plugins were absent entirely). Adds a minimal, honest
entry for each — real methods parsed from every route.ts's exports, a group
tag and a neutral path-derived summary; no invented semantics. Rich schemas
remain hand-curated in the existing entries.

Generated by scripts/ad-hoc/gen-openapi-missing-paths.mjs, which enumerates
routes with the same lib check:api-docs-refs uses — the spec now covers
692/692 real routes and the gate verifies every spec path has a real route.
2026-08-31 14:37:12 -03:00
126 changed files with 10432 additions and 3894 deletions

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
- 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

@@ -1 +0,0 @@
- 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,6 +829,12 @@
"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": {
@@ -844,16 +850,62 @@
"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
@@ -874,6 +926,24 @@
"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
@@ -990,6 +1060,31 @@
"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
@@ -1090,6 +1185,29 @@
"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
@@ -1258,6 +1376,11 @@
"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
@@ -1268,6 +1391,11 @@
"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
@@ -1276,11 +1404,17 @@
"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": {
@@ -1303,6 +1437,17 @@
"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
@@ -2153,6 +2298,9 @@
"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,5 +1,6 @@
{
"_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).",
@@ -495,7 +496,8 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"open-sse/executors/commandCode.ts": 1271
"open-sse/executors/commandCode.ts": 1271,
"src/app/docs/lib/openapi.generated.ts": 1347
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -414,8 +414,6 @@ 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
@@ -424,7 +422,7 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
### Router strategies in detail
The auto-combo engine exposes 6 pluggable **RouterStrategy** implementations that
The auto-combo engine exposes 5 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,37 +570,3 @@ 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: 3072,
dimensions: 768,
},
{
id: "google/gemini-embedding-2",

View File

@@ -1,54 +0,0 @@
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

@@ -1,13 +0,0 @@
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 { getDefaultExecutor } from "./defaultResolver.ts";
import { DefaultExecutor } from "./default.ts";
// R0.3 — declarative built-in table, made LAZY by #11220.
//
@@ -207,6 +207,8 @@ 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,
@@ -249,7 +251,8 @@ export async function getExecutor(provider: string): Promise<BaseExecutor> {
(err as Error & { status?: number }).status = 400;
throw err;
}
return getDefaultExecutor(provider);
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
return defaultCache.get(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, isClaudeCodeOriginatedHeaders } from "../config/codexIdentity.ts";
import { getCodexClientSessionId, isCodexOriginatedHeaders } from "../config/codexIdentity.ts";
import {
noteCodexTurnStateProvenance,
readCodexTurnStateHeader,
@@ -981,14 +981,8 @@ 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 || isClaudeCodeClient) &&
(settings.echoRequestedModelName === true || isCodexResponsesEcho) &&
typeof requestedModel === "string" &&
requestedModel
? requestedModel
@@ -5471,7 +5465,6 @@ export async function handleChatCore({
const streamReadiness = await ensureStreamReadiness(providerResponse, {
timeoutMs: streamReadinessPolicy.timeoutMs,
maxTimeoutMs: streamReadinessPolicy.maxTimeoutMs,
provider,
model,
log,

View File

@@ -4,14 +4,13 @@
* 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, ScoringWeights } from "./scoring.ts";
import type { ProviderCandidate, ScoredProvider } from "./scoring.ts";
import { scorePool } from "./scoring.ts";
import { getTaskFitness } from "./taskFitness.ts";
import { clamp01 } from "../../utils/number.ts";
@@ -33,8 +32,6 @@ export interface RoutingContext {
lastKnownGoodProvider?: string;
lkgpEnabled?: boolean;
sla?: SlaRoutingPolicy;
weights?: ScoringWeights;
explorationRate?: number;
}
export interface RoutingDecision {
@@ -111,38 +108,6 @@ 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 {
@@ -372,14 +337,12 @@ class LKGPStrategyImpl implements RouterStrategy {
const strategyRegistry = new Map<string, RouterStrategy>();
const rulesStrategy = new RulesStrategyImpl();
const scoreStrategy = new ScoreStrategyImpl();
const costStrategy = new CostStrategyImpl();
const latencyStrategy = new LatencyStrategyImpl();
const slaStrategy = new SLAStrategyImpl();
const lkgpStrategy = new LKGPStrategyImpl();
strategyRegistry.set("rules", rulesStrategy);
strategyRegistry.set("score", scoreStrategy);
strategyRegistry.set("cost", costStrategy);
strategyRegistry.set("eco", costStrategy); // alias
strategyRegistry.set("latency", latencyStrategy);

View File

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

View File

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

View File

@@ -43,19 +43,7 @@ export function resolveReasoningTransport(
): ReasoningTransport {
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
const transport = REASONING_TRANSPORTS.get(normalized);
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";
return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext");
}
function asRecord(value: unknown): JsonRecord | null {

View File

@@ -40,7 +40,6 @@ import {
normalizeResponsesReasoningEffort,
RESPONSES_STORE_MARKER,
} from "./request/openai-responses/helpers.ts";
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
bootstrapTranslatorRegistry();
export { register } from "./registry.ts";
@@ -576,14 +575,6 @@ 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,15 +220,12 @@ 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;
@@ -301,13 +298,12 @@ 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(resultPayload),
content: JSON.stringify(
part.functionResponse.response?.result || part.functionResponse.response || {}
),
});
}
}
@@ -320,7 +316,9 @@ 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,13 +147,12 @@ 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(resultPayload),
content: JSON.stringify(
part.functionResponse.response?.result || part.functionResponse.response || {}
),
};
}
}

View File

@@ -471,9 +471,6 @@ 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;
@@ -492,14 +489,7 @@ export async function ensureStreamReadiness(
};
const startedAt = Date.now();
const effectiveTimeoutMs = Math.max(0, Math.floor(options.timeoutMs));
// 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;
const deadline = startedAt + effectiveTimeoutMs;
let handedOffReader = false;
const buildReadyResponse = () =>
@@ -510,7 +500,7 @@ export async function ensureStreamReadiness(
});
const timeoutReason = () =>
`Stream produced no non-ping SSE event within ${deadline - startedAt}ms (max=${maxDeadline - startedAt}ms)`;
`Stream produced no non-ping SSE event within ${effectiveTimeoutMs}ms`;
try {
while (true) {
@@ -603,22 +593,6 @@ 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,7 +14,6 @@ export type StreamReadinessPolicyInput = {
export type StreamReadinessPolicyResult = {
timeoutMs: number;
baseTimeoutMs: number;
maxTimeoutMs: number;
reasons: string[];
};
@@ -122,7 +121,7 @@ export function resolveStreamReadinessTimeout(
): StreamReadinessPolicyResult {
const baseTimeoutMs = Math.max(0, Math.floor(input.baseTimeoutMs || 0));
if (baseTimeoutMs <= 0) {
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, maxTimeoutMs: baseTimeoutMs, reasons: ["disabled"] };
return { timeoutMs: baseTimeoutMs, baseTimeoutMs, reasons: ["disabled"] };
}
const maxTimeoutMs = Math.max(baseTimeoutMs, input.maxTimeoutMs ?? DEFAULT_MAX_TIMEOUT_MS);
@@ -198,5 +197,5 @@ export function resolveStreamReadinessTimeout(
timeoutMs = Math.min(timeoutMs, maxTimeoutMs);
if (timeoutMs === baseTimeoutMs) reasons.push("base");
return { timeoutMs, baseTimeoutMs, maxTimeoutMs, reasons };
return { timeoutMs, baseTimeoutMs, reasons };
}

View File

@@ -0,0 +1,129 @@
#!/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,11 +137,6 @@ 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):
@@ -236,7 +231,6 @@ 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,6 +74,142 @@ 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,6 +69,24 @@ 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,6 +52,42 @@ 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,6 +385,372 @@ 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,6 +131,46 @@ 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,6 +43,48 @@ 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,6 +74,24 @@ 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,7 +14,46 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
_No endpoints mapped for this area yet._
### 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 '{}'
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -462,6 +462,898 @@ 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,7 +14,91 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
_No endpoints mapped for this area yet._
### 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"
```
## Payloads
See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas.

View File

@@ -54,6 +54,46 @@ 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,6 +229,526 @@ 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,6 +25,15 @@ 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,6 +383,981 @@ 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,6 +93,44 @@ 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,6 +137,192 @@ 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,6 +620,46 @@ 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,7 +14,86 @@ All requests require a valid Bearer token or session cookie. Obtain a token via
## Endpoints
_No endpoints mapped for this area yet._
### 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 '{}'
```
## 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, useSyncExternalStore } from "react";
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
@@ -106,12 +106,6 @@ 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();
@@ -121,13 +115,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
const [providerConnections, setProviderConnections] = useState([]);
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
// 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 [baseUrl, setBaseUrl] = useState("/v1");
const [selectedProvider, setSelectedProvider] = useState(null);
const [providerMetrics, setProviderMetrics] = useState<Record<string, ProviderMetricSummary>>({});
const [providerTopology, setProviderTopology] = useState({ lastProvider: "", errorProvider: "" });
@@ -147,39 +135,36 @@ 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 cleanLatest = latestVersion.replace(/^v/, "");
const latest = versionInfo?.latest || "";
const cleanLatest = latest.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: installedVersion }),
desc: t("downloadDmgDescription", { version: versionInfo?.current || "" }),
};
}
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: installedVersion }),
desc: t("downloadExeDescription", { version: versionInfo?.current || "" }),
};
}
if (platform === "linux") {
return {
label: t("downloadAppImage"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/download/v${cleanLatest}/OmniRoute-${cleanLatest}.AppImage`,
desc: t("downloadAppImageDescription", { version: installedVersion }),
desc: t("downloadAppImageDescription", { version: versionInfo?.current || "" }),
};
}
return {
label: t("downloadUpdate"),
url: `https://github.com/diegosouzapw/OmniRoute/releases/tag/v${cleanLatest}`,
desc: t("downloadUpdateDescription", { version: installedVersion }),
desc: t("downloadUpdateDescription", { version: versionInfo?.current || "" }),
};
}, [platform, t, latestVersion, installedVersion]);
}, [platform, t, versionInfo?.latest, versionInfo?.current]);
// Electron internal auto-updater state and listeners
const [electronUpdateStatus, setElectronUpdateStatus] = useState<{
@@ -249,6 +234,12 @@ 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([
@@ -276,9 +267,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
}, []);
useEffect(() => {
void (async () => {
await fetchData();
})();
fetchData();
}, [fetchData]);
// Fetch provider nodes for display labels (compat providers)

View File

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

View File

@@ -95,9 +95,7 @@ export default function CacheHealthTab() {
}, []);
useEffect(() => {
void (async () => {
await load(range);
})();
void load(range);
}, [load, range]);
if (loading) return <Skeleton className="h-64 w-full" />;
@@ -181,8 +179,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">
@@ -218,9 +216,7 @@ 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,9 +852,7 @@ export default function ComboHealthTab() {
useEffect(() => {
const controller = new AbortController();
void (async () => {
await fetchData(controller, false);
})();
fetchData(controller, false);
return () => controller.abort();
}, [fetchData]);

View File

@@ -131,9 +131,7 @@ export default function ProviderUtilizationTab() {
useEffect(() => {
const controller = new AbortController();
void (async () => {
await fetchUtilization(range, aggregateBy, controller.signal);
})();
fetchUtilization(range, aggregateBy, controller.signal);
return () => controller.abort();
}, [fetchUtilization, range, aggregateBy]);
@@ -342,8 +340,12 @@ 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,18 +522,14 @@ export default function RouteExplainabilityTab({
useEffect(() => {
const controller = new AbortController();
void (async () => {
await fetchLogs(controller.signal);
})();
fetchLogs(controller.signal);
return () => controller.abort();
}, [fetchLogs]);
useEffect(() => {
if (!selectedId) return;
const controller = new AbortController();
void (async () => {
await fetchExplanation(selectedId, controller.signal);
})();
fetchExplanation(selectedId, controller.signal);
return () => controller.abort();
}, [fetchExplanation, selectedId]);

View File

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

View File

@@ -110,9 +110,7 @@ export default function ComplianceTab() {
}, [actor, eventType, from, offset, t, to]);
useEffect(() => {
void (async () => {
await fetchEntries();
})();
void fetchEntries();
}, [fetchEntries]);
const visibleEntries = useMemo(() => {
@@ -332,9 +330,7 @@ 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,9 +56,7 @@ export default function McpAuditTab() {
}, []);
useEffect(() => {
void (async () => {
await fetchStats();
})();
void fetchStats();
}, [fetchStats]);
const fetchAudit = useCallback(async () => {
@@ -88,9 +86,7 @@ export default function McpAuditTab() {
}, [offset, successFilter, t, toolFilter]);
useEffect(() => {
void (async () => {
await fetchAudit();
})();
void fetchAudit();
}, [fetchAudit]);
return (

View File

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

View File

@@ -130,12 +130,9 @@ 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 = nowMs - new Date(dateStr).getTime();
const diff = Date.now() - new Date(dateStr).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return t("justNow");
if (minutes < 60) return t("minutesAgo", { minutes });
@@ -150,7 +147,6 @@ 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) {
@@ -161,9 +157,7 @@ export default function ReasoningCacheTab() {
}, []);
useEffect(() => {
void (async () => {
await fetchData();
})();
void fetchData();
const id = setInterval(() => void fetchData(), REFRESH_INTERVAL_MS);
return () => clearInterval(id);
}, [fetchData]);

View File

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

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { Modal, Button, Input, Select } from "@/shared/components";
import { useTranslations } from "next-intl";
@@ -29,14 +29,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState("");
// 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 });
useEffect(() => {
if (memory && isOpen) {
setType(memory.type);
setKey(memory.key);
@@ -45,7 +38,7 @@ export default function EditMemoryModal({ memory, isOpen, onClose, onSaved }: Pr
setMetadataError("");
setError("");
}
}
}, [memory, isOpen]);
const handleMetadataChange = (value: string) => {
setMetadataStr(value);
@@ -158,7 +151,9 @@ 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,8 +40,7 @@ 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<
@@ -110,8 +109,7 @@ 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 {
@@ -155,8 +153,7 @@ 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
@@ -164,9 +161,7 @@ export default function QdrantConfigCard() {
// connection button still drives the same check manually.
useEffect(() => {
if (!loading && qdrant.enabled && health === null) {
void (async () => {
await checkHealth();
})();
void checkHealth();
}
}, [loading, qdrant.enabled, health, checkHealth]);
@@ -250,8 +245,7 @@ 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,7 +190,9 @@ export default function MemoriesTab() {
else skipped++;
}
fetchMemories();
setImportStatus(t("importResult", { imported, skipped }));
setImportStatus(
t("importResult", { imported, skipped }),
);
} catch {
setImportStatus(t("importError"));
} finally {
@@ -237,9 +239,7 @@ 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 (async () => {
await checkHealth();
})();
void checkHealth();
const id = setInterval(() => {
void checkHealth();
}, 30_000);
@@ -260,9 +260,8 @@ 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 {
@@ -290,7 +289,8 @@ 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,7 +401,9 @@ 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>
)}
@@ -446,8 +448,12 @@ 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>
@@ -471,9 +477,7 @@ 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>
@@ -661,10 +665,7 @@ 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,9 +39,7 @@ export function useEngineStatus(refreshIntervalMs = 5000): UseEngineStatusResult
useEffect(() => {
mounted.current = true;
void (async () => {
await fetchOnce();
})();
void fetchOnce();
if (!refreshIntervalMs || refreshIntervalMs <= 0) {
return () => {
mounted.current = false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,16 +55,7 @@ export default function RadarSetupPage() {
const provider = searchParams.get("provider");
const [setupData, setSetupData] = useState<ProviderSetupData | null>(null);
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 [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null);
@@ -72,6 +63,7 @@ export default function RadarSetupPage() {
// Fetch catalog to find the provider's setup data
useEffect(() => {
if (!provider) {
setLoading(false);
return;
}
@@ -131,13 +123,12 @@ export default function RadarSetupPage() {
}, [provider, t]);
// Test connection — uses the EXISTING connection-test endpoint
const connectionId = setupData?.connectionId ?? null;
const handleTestConnection = useCallback(async () => {
if (!connectionId) return;
if (!setupData?.connectionId) return;
setTesting(true);
setTestResult(null);
try {
const res = await fetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, {
const res = await fetch(`/api/providers/${encodeURIComponent(setupData.connectionId)}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
@@ -156,7 +147,7 @@ export default function RadarSetupPage() {
} finally {
setTesting(false);
}
}, [connectionId, t]);
}, [setupData?.connectionId, t]);
if (!provider) {
return (

View File

@@ -490,18 +490,18 @@ export default function EvalsTab() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 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)
) {
useEffect(() => {
if (targetOptions.length === 0) return;
if (targetOptions.some((option) => option.key === selectedTargetKey)) return;
setSelectedTargetKey(targetOptions[0]?.key || "suite-default:__default__");
}
if (compareTargetKey && compareTargetKey === selectedTargetKey) {
setCompareTargetKey("");
}
}, [selectedTargetKey, targetOptions]);
useEffect(() => {
if (!compareTargetKey) return;
if (compareTargetKey === selectedTargetKey) {
setCompareTargetKey("");
}
}, [compareTargetKey, selectedTargetKey]);
const filteredSuites = !search.trim()
? suites
@@ -1846,11 +1846,7 @@ 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,23 +41,6 @@ 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()
@@ -129,7 +112,9 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
async (selectionToken: string) => {
const picker = state.resetCreditPicker;
if (!picker || state.redeemingResetCreditId || !selectionToken) return;
const idempotencyKey = ensureIdempotencyKey(state.idempotencyKeysRef, selectionToken);
const idempotencyKey =
state.idempotencyKeysRef.current[selectionToken] ??
(state.idempotencyKeysRef.current[selectionToken] = createIdempotencyKey());
state.setRedeemingResetCreditId(picker.connectionId);
state.setErrors((prev) => ({ ...prev, [picker.connectionId]: null }));
try {
@@ -160,7 +145,7 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
[picker.connectionId]: new Date().toISOString(),
}));
state.setResetCreditPicker(null);
resetIdempotencyKeys(state.idempotencyKeysRef);
state.idempotencyKeysRef.current = {};
notify.success(state.tr("resetCreditRedeemed", "Reset redeemed"));
} catch (error) {
const message = getRequestErrorMessage(

View File

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

View File

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

View File

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

View File

@@ -1432,13 +1432,13 @@ async function buildUnifiedModelsResponseCore(
return activeAliases.has(alias) || activeAliases.has(provider);
};
const findEquivalentSpecialtyModel = (
const hasEquivalentSpecialtyModel = (
providerId: string,
rawModelId: string,
type: string,
scopedModelId: string
) =>
models.find((model: any) => {
models.some((model: any) => {
if (model?.id === scopedModelId) return true;
if (model?.owned_by !== providerId || model?.type !== type) return false;
const existingRoot =
@@ -1450,13 +1450,6 @@ 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()
@@ -1471,22 +1464,7 @@ async function buildUnifiedModelsResponseCore(
const rawModelId = getSpecialtyModelRelativeId(embModel.id, embModel.provider);
if (!providerSupportsModel(embModel.provider, rawModelId)) continue;
if (isModelHiddenBulk(embModel.provider, rawModelId)) continue;
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";
}
if (hasEquivalentSpecialtyModel(embModel.provider, rawModelId, "embedding", embModel.id)) {
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 { getRootLayoutSettings } from "@/lib/db/rootLayoutSettings";
import { getSettings } from "@/lib/db/settings";
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 getRootLayoutSettings();
const instanceName = settings.instanceName;
const customFaviconUrl = settings.customFaviconUrl || settings.customFaviconBase64;
const settings = await getSettings();
const instanceName = settings?.instanceName || "OmniRoute";
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/utils/proxyFetch.ts");
await import("@omniroute/open-sse/index.ts");
console.log("[STARTUP] Global fetch proxy patch initialized");
// Register quota fetchers early so combo routing can use real quota-aware

View File

@@ -1,6 +1,29 @@
import type { SupportedBatchEndpoint } from "@/shared/constants/batchEndpoints";
import { getRuntimePorts } from "@/lib/runtime/ports";
import { normalizeBasePath } from "@/shared/utils/basePath";
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;
}
async function dispatchBatchApiRequest({
endpoint,
@@ -16,17 +39,13 @@ async function dispatchBatchApiRequest({
headers.set("Authorization", `Bearer ${apiKey}`);
}
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, {
const handler = await getHandler(endpoint);
const request = new Request(`http://localhost${endpoint}`, {
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,7 +67,6 @@ 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,7 +40,6 @@ 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 {
@@ -515,6 +514,7 @@ 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,6 +529,18 @@ 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

@@ -1,62 +0,0 @@
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;
}

View File

@@ -1,19 +0,0 @@
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,15 +19,7 @@ const SHUTDOWN_TIMEOUT_MS = parseInt(process.env.SHUTDOWN_TIMEOUT_MS || "30000",
declare global {
var __omnirouteShutdown:
| {
init: boolean;
shuttingDown: boolean;
activeRequests: number;
shutdownPromise?: Promise<void>;
}
| undefined;
var __omnirouteRequestShutdown: ((signal: string) => Promise<void>) | undefined;
var __omnirouteCustomServerOwnsShutdown: boolean | undefined;
{ init: boolean; shuttingDown: boolean; activeRequests: number } | undefined;
}
function getShutdownState() {
@@ -110,14 +102,12 @@ 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();
@@ -133,6 +123,9 @@ 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.
@@ -154,62 +147,41 @@ 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;
if (globalThis.__omnirouteCustomServerOwnsShutdown) {
console.log("[Shutdown] Cleanup registered with the custom server shutdown owner.");
return;
}
const shutdown = async (signal: string) => {
if (state.shuttingDown) return;
state.shuttingDown = true;
markServerStopping();
const shutdown = (signal: string) => {
void globalThis.__omnirouteRequestShutdown?.(signal).then(() => process.exit(0));
console.log(`\n[Shutdown] Received ${signal}. Draining ${state.activeRequests} request(s)...`);
await waitForDrain();
await cleanup();
console.log("[Shutdown] Bye.");
process.exit(0);
};
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => 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", () => void shutdown("SIGHUP"));
process.on("SIGHUP", () => shutdown("SIGHUP"));
console.log("[Shutdown] Graceful shutdown handlers registered.");
}

View File

@@ -42,18 +42,8 @@ export function getAppLogRotationCheckInterval(): number {
);
}
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 });
}
/** Module-level timer handle — cleared by closeLogRotation(). */
let rotationTimer: ReturnType<typeof setInterval> | null = null;
export function getLogConfig() {
const logToFile = getAppLogToFile();
@@ -182,9 +172,6 @@ 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;
@@ -194,7 +181,7 @@ export function initLogRotation(): void {
cleanupOverflowLogs(config.logFilePath, config.maxFiles);
const intervalMs = getAppLogRotationCheckInterval();
state.timer = setInterval(
rotationTimer = setInterval(
(filePath: string, maxSize: number, maxFiles: number) => {
rotateIfNeeded(filePath, maxSize);
cleanupOverflowLogs(filePath, maxFiles);
@@ -204,7 +191,7 @@ export function initLogRotation(): void {
config.maxFileSize,
config.maxFiles
);
state.timer.unref?.();
rotationTimer.unref?.();
}
/**
@@ -212,9 +199,8 @@ export function initLogRotation(): void {
* Idempotent — safe to call multiple times.
*/
export function closeLogRotation(): void {
const state = getLogRotationState();
if (state.timer !== null) {
clearInterval(state.timer);
state.timer = null;
if (rotationTimer !== null) {
clearInterval(rotationTimer);
rotationTimer = null;
}
}

View File

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

View File

@@ -22,12 +22,13 @@
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 { refreshAndUpdateCredentialsWithResolver } from "@/lib/usage/providerLimits/credentialRefresh";
import { refreshAndUpdateCredentials } from "@/lib/usage/providerLimits";
import { getCircuitBreaker } from "@/shared/utils/circuitBreaker";
import {
QUOTA_AUTOPING_FAILURE_COOLDOWN_MS,
@@ -62,11 +63,8 @@ export interface QuotaAutoPingDeps {
refreshAndUpdateCredentials: (
connection: QuotaAutoPingConnection
) => Promise<{ connection: QuotaAutoPingConnection }>;
getCodexUsage: (
accessToken?: string,
providerSpecificData?: JsonRecord
) => Promise<JsonRecord>;
getExecutor: (provider: "codex") => Promise<BaseExecutor>;
getCodexUsage: (accessToken?: string, providerSpecificData?: JsonRecord) => Promise<JsonRecord>;
getExecutor: (provider: string) => Promise<BaseExecutor>;
canExecuteProvider: (provider: string) => boolean;
isConnectionUnavailableToAuxiliaryActivity: (connectionId: string) => Promise<boolean>;
}
@@ -81,33 +79,15 @@ 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) =>
refreshAndUpdateCredentialsWithResolver(connection, loadQuotaAutoPingExecutor),
refreshAndUpdateCredentials(connection as never),
getCodexUsage,
getExecutor: loadQuotaAutoPingExecutor,
getExecutor,
canExecuteProvider: (provider) => getCircuitBreaker(provider).canExecute(),
isConnectionUnavailableToAuxiliaryActivity,
};

View File

@@ -18,10 +18,13 @@ 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 { getCredentialRefreshExecutor } from "@omniroute/open-sse/executors/credential.ts";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
import { cooldownUntilMs } from "@omniroute/open-sse/services/accountFallback.ts";
import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts";
import {
rotationGroupFor,
serializeRefresh,
} from "@omniroute/open-sse/services/refreshSerializer.ts";
import {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
@@ -39,15 +42,29 @@ 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",
@@ -201,15 +218,122 @@ 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: CredentialRefreshOptions = {}
opts: { allowRotatingRefresh?: boolean; force?: boolean } = {}
) {
return refreshAndUpdateCredentialsWithResolver(
connection,
getCredentialRefreshExecutor,
opts
);
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,
};
}
function isUsageAuthError(message: unknown): boolean {

View File

@@ -1,150 +0,0 @@
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,16 +55,6 @@ 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);
}
@@ -157,8 +147,17 @@ 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 = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
const mapping = columnSortMap[column as keyof typeof columnSortMap];
if (!mapping) return;
setSortBy((prev) => {
if (prev === mapping.desc) return mapping.asc;
@@ -167,7 +166,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
}, []);
const getSortIndicator = useCallback(
(column: string) => {
const mapping = COLUMN_SORT_MAP[column as keyof typeof COLUMN_SORT_MAP];
const mapping = columnSortMap[column as keyof typeof columnSortMap];
if (!mapping) return "";
if (sortBy === mapping.desc) return " ↓";
if (sortBy === mapping.asc) return " ↑";
@@ -536,7 +535,89 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
// endpoint until the row appears.
const router = useRouter();
const closeDetail = useCallback(() => {
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 = () => {
detailRequestRef.current = "";
setSelectedLog(null);
setDetailData(null);
@@ -548,92 +629,7 @@ 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]);
@@ -658,7 +654,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
console.error("Failed to open initial log id:", error_);
});
}
}, [initialSelectedId, openDetail]);
}, [initialSelectedId]);
useEffect(() => {
const isActive = selectedLog?.active === true;
@@ -769,7 +765,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
pendingBoundaryNavRef.current = "prev";
fetchLogs(false);
}
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
const handleNext = useCallback(() => {
const idx = currentLogIndex;
@@ -785,7 +781,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
pendingBoundaryNavRef.current = "next";
fetchLogs(false);
}
}, [currentLogIndex, sortedLogsForNav, fetchLogs, openDetail]);
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
// Resolves a pending boundary nav (see handlePrev/handleNext) once a
// triggered fetchLogs() resync has landed in sortedLogsForNav. Only fires

View File

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

View File

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

View File

@@ -42,13 +42,6 @@ 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,10 +18,6 @@ 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";
@@ -65,7 +61,7 @@ function getTransportCompatibleConfig(): pino.LoggerOptions {
* vanished, so failed writes are dropped (best-effort stderr notice) instead of
* escalating.
*/
function buildTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
function buildFileTransportStream(targets: NonNullable<pino.TransportMultiOptions["targets"]>) {
const stream = pino.transport({ targets });
stream.on("error", (err: unknown) => {
try {
@@ -79,65 +75,11 @@ function buildTransportStream(targets: NonNullable<pino.TransportMultiOptions["t
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 buildLoggerResource(): SharedLoggerResource {
function buildLogger(): pino.Logger {
const logConfig = getLogConfig();
const logLevel = (baseConfig.level as string) || "info";
const transportConfig = getTransportCompatibleConfig();
@@ -153,7 +95,7 @@ function buildLoggerResource(): SharedLoggerResource {
if (isDev) {
// Dev: pino-pretty → stdout, JSON → file
const stream = buildTransportStream([
const stream = buildFileTransportStream([
{
target: "pino-pretty",
options: {
@@ -171,12 +113,12 @@ function buildLoggerResource(): SharedLoggerResource {
level: logLevel,
},
]);
return createLoggerResource(pino(transportConfig, stream), stream);
return pino(transportConfig, stream);
}
// Production: JSON → stdout + JSON → file
{
const stream = buildTransportStream([
const stream = buildFileTransportStream([
{
target: "pino/file",
options: { destination: 1 }, // stdout
@@ -188,7 +130,7 @@ function buildLoggerResource(): SharedLoggerResource {
level: logLevel,
},
]);
return createLoggerResource(pino(transportConfig, stream), stream);
return pino(transportConfig, stream);
}
} catch (err) {
// Log the actual error for diagnostics (issue #165)
@@ -214,15 +156,12 @@ function buildLoggerResource(): SharedLoggerResource {
});
// Production fallback: JSON to both stdout and file via multistream
return createLoggerResource(
pino(
baseConfig,
pino.multistream([
{ stream: process.stdout, level: logLevel as pino.Level },
{ stream: fileDestination, level: logLevel as pino.Level },
])
),
fileDestination
return pino(
baseConfig,
pino.multistream([
{ stream: process.stdout, level: logLevel as pino.Level },
{ stream: fileDestination, level: logLevel as pino.Level },
])
);
} catch (fallbackErr) {
try {
@@ -236,8 +175,9 @@ function buildLoggerResource(): SharedLoggerResource {
// Console-only (no file logging)
if (isDev) {
const stream = buildTransportStream([
{
return pino({
...baseConfig,
transport: {
target: "pino-pretty",
options: {
colorize: true,
@@ -245,18 +185,14 @@ function buildLoggerResource(): SharedLoggerResource {
ignore: "pid,hostname,service",
messageFormat: "[{module}] {msg}",
},
level: logLevel,
},
]);
return createLoggerResource(pino(transportConfig, stream), stream);
});
}
return createLoggerResource(pino(baseConfig), null);
return pino(baseConfig);
}
const sharedLoggerResource = getOrCreateSharedLoggerResource(buildLoggerResource);
export const logger = sharedLoggerResource.logger;
export const logger = buildLogger();
/**
* Create a child logger with a module tag.

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