Compare commits

...

12 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
63e4afa321 feat(dashboard): orchestration canvas — unified model + snapshot hook (part 1/2) (#12156)
Modelo puro do Orchestration Canvas (tipos, 3 mappers, mergeSnapshot com dedupe/staleness/cap, projeções flow+overview) + hook de polling com gatilho WS. Ciclo completo: 9 tasks TDD com review por task, review final whole-branch + fixes verificados 6/6, refactor de complexity re-validado (comportamento preservado). CI: 18 pass. Testes: 31 node:test + 2 vitest. Parte 2/2 (UI /dashboard/orchestration) na sequência.
2026-08-31 14:42:20 -03:00
Diego Rodrigues de Sa e Souza
7ca5e1c671 chore(lint): batch 6 of #12146 — memory, radar, audit, analytics, cache, usage, activity, home and RequestLoggerV2 react-hooks violations resolved (#12208)
45 violations across 27 files fixed at the source (no eslint-disable, no new
suppressions; the 45 matching react-hooks/* entries are removed from
config/quality/eslint-suppressions.json):

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

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

* docs: changelog fragment for embedding registry fix

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

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

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

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

* fix(ci): reduce credential refresh complexity

---------

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,8 @@ import path from "node:path";
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-force-refresh-imported-"));
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const { getCredentialRefreshExecutor } =
await import("../../open-sse/executors/credential.ts");
const { refreshAndUpdateCredentials } = await import("../../src/lib/usage/providerLimits.ts");
function importedCodexConnection() {
@@ -33,7 +34,7 @@ function importedCodexConnection() {
}
test("force re-mints an imported rotating account that needsRefresh would skip (#3019 reactive)", async () => {
const exec = await getExecutor("codex");
const exec = await getCredentialRefreshExecutor("codex");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;
@@ -65,7 +66,7 @@ test("force re-mints an imported rotating account that needsRefresh would skip (
});
test("force does NOT override the bulk #3019 guard (no allowRotatingRefresh → no mint)", async () => {
const exec = await getExecutor("codex");
const exec = await getCredentialRefreshExecutor("codex");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;

View File

@@ -0,0 +1,88 @@
import test from "node:test";
import assert from "node:assert/strict";
const { translateRequest } = await import("../../open-sse/translator/index.ts");
const { resolveReasoningTransport, applyReasoningInputPolicy } = await import(
"../../open-sse/services/reasoningInputPolicy.ts"
);
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
test("#12128: resolveReasoningTransport identifies openai-compatible-responses and custom responses variants as opaque transport", () => {
assert.equal(resolveReasoningTransport("openai-compatible-responses-codex"), "opaque");
assert.equal(resolveReasoningTransport("custom-openai-responses"), "opaque");
assert.equal(resolveReasoningTransport("codex-proxy"), "opaque");
assert.equal(resolveReasoningTransport("openai"), "opaque");
assert.equal(resolveReasoningTransport("codex"), "opaque");
});
test("#12128: translateRequest to Responses target strips plaintext reasoning.content for opaque responses providers", () => {
const chatBody = {
model: "gpt-5.6-codex",
messages: [
{ role: "user", content: "hello" },
{
role: "assistant",
content: "Hi there!",
reasoning_content: "Let me think about how to greet the user properly.",
},
{ role: "user", content: "what is 2+2?" },
],
};
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI_RESPONSES,
"gpt-5.6-codex",
chatBody,
false,
null,
"openai-compatible-responses-codex"
) as Record<string, unknown>;
assert.ok(Array.isArray(translated.input), "translated.input must be an array");
const input = translated.input as Record<string, unknown>[];
const reasoningItems = input.filter((item) => item.type === "reasoning");
// For opaque responses targets without encrypted continuation, orphaned plaintext reasoning items
// must either be stripped entirely or have content.length === 0, so strict Codex backends do not 400.
for (const r of reasoningItems) {
assert.ok(
!r.content || (Array.isArray(r.content) && r.content.length === 0),
"reasoning item must not carry non-empty content array to strict responses endpoints"
);
}
});
test("#12128: applyReasoningInputPolicy directly sanitizes replayed plaintext reasoning for openai-compatible-responses", () => {
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
content: [{ type: "reasoning_text", text: "step 1 plan" }],
summary: [],
},
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Done" }],
},
],
};
applyReasoningInputPolicy(body, "responses", {
provider: "openai-compatible-responses-vllm",
onIncompatibleReasoning: "drop",
});
const input = body.input as Record<string, unknown>[];
const reasoningItems = input.filter((item) => item.type === "reasoning");
for (const r of reasoningItems) {
assert.equal(
r.content,
undefined,
"plaintext reasoning content must be dropped for opaque responses providers"
);
}
});

View File

@@ -0,0 +1,111 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-layout-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const core = await import("../../src/lib/db/core.ts");
const { getRootLayoutSettings } = await import("../../src/lib/db/rootLayoutSettings.ts");
function resetStorage(): void {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => {
resetStorage();
delete process.env.INITIAL_PASSWORD;
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
}
});
test("root layout imports the read-only settings leaf", () => {
const layoutSource = fs.readFileSync(path.join(process.cwd(), "src/app/layout.tsx"), "utf8");
const leafSource = fs.readFileSync(
path.join(process.cwd(), "src/lib/db/rootLayoutSettings.ts"),
"utf8"
);
const coreSource = fs.readFileSync(path.join(process.cwd(), "src/lib/db/core.ts"), "utf8");
const singletonSource = fs.readFileSync(
path.join(process.cwd(), "src/lib/db/singleton.ts"),
"utf8"
);
assert.match(
layoutSource,
/import \{ getRootLayoutSettings \} from "@\/lib\/db\/rootLayoutSettings";/
);
assert.doesNotMatch(layoutSource, /@\/lib\/db\/settings/);
assert.match(leafSource, /from "\.\/singleton"/);
assert.doesNotMatch(
leafSource,
/db\/settings|\.\/core|runtimeSettings|tokenHealth|providerModels/
);
assert.doesNotMatch(leafSource, /\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i);
assert.match(coreSource, /from "\.\/singleton"/);
assert.doesNotMatch(singletonSource, /db\/settings|\.\/core|readCache|runtimeSettings/);
});
test("root layout settings reader does not initialize the database", async () => {
process.env.INITIAL_PASSWORD = "must-not-trigger-startup";
assert.deepEqual(await getRootLayoutSettings(), {
instanceName: "OmniRoute",
customFaviconUrl: "",
customFaviconBase64: "",
});
assert.equal(fs.existsSync(path.join(TEST_DATA_DIR, "storage.sqlite")), false);
});
test("root layout settings reader returns only the persisted metadata fields", async () => {
const db = core.getDbInstance();
const insert = db.prepare(
"INSERT INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)"
);
insert.run("instanceName", JSON.stringify("Route Lab"));
insert.run("customFaviconUrl", JSON.stringify("https://example.com/favicon.png"));
insert.run("customFaviconBase64", JSON.stringify("data:image/png;base64,AA=="));
insert.run("proxyEnabled", JSON.stringify(false));
assert.deepEqual(await getRootLayoutSettings(), {
instanceName: "Route Lab",
customFaviconUrl: "https://example.com/favicon.png",
customFaviconBase64: "data:image/png;base64,AA==",
});
});
test("root layout settings reader is read-only and falls back safely", async () => {
process.env.INITIAL_PASSWORD = "must-not-trigger-onboarding";
const db = core.getDbInstance();
db.prepare(
"INSERT INTO key_value (namespace, key, value) VALUES ('settings', 'instanceName', ?)"
).run("not-json");
assert.deepEqual(await getRootLayoutSettings(), {
instanceName: "OmniRoute",
customFaviconUrl: "",
customFaviconBase64: "",
});
const onboardingRows = db
.prepare(
"SELECT key FROM key_value WHERE namespace = 'settings' AND key IN ('setupComplete', 'requireLogin')"
)
.all();
assert.deepEqual(onboardingRows, []);
});

View File

@@ -15,7 +15,10 @@ import {
listStrategies,
type RoutingContext,
} from "../../open-sse/services/autoCombo/routerStrategy.ts";
import type { ProviderCandidate } from "../../open-sse/services/autoCombo/scoring.ts";
import {
DEFAULT_WEIGHTS,
type ProviderCandidate,
} from "../../open-sse/services/autoCombo/scoring.ts";
function cand(p: Partial<ProviderCandidate> & { provider: string }): ProviderCandidate {
return {
@@ -33,6 +36,32 @@ function cand(p: Partial<ProviderCandidate> & { provider: string }): ProviderCan
const ctx: RoutingContext = { taskType: "default" };
// ── score ────────────────────────────────────────────────────────────────────
test("score — exploits the configured winner and uses explorationRate", (t) => {
const pool = [
cand({ provider: "cheap", costPer1MTokens: 1 }),
cand({ provider: "expensive", costPer1MTokens: 9 }),
];
const weights = { ...DEFAULT_WEIGHTS, costInv: 1, quota: 0, health: 0, latencyInv: 0 };
t.mock.method(Math, "random", () => 0.99);
assert.equal(
getStrategy("score").select(pool, { ...ctx, weights, explorationRate: 0 }).provider,
"cheap"
);
assert.equal(
getStrategy("score").select(pool, { ...ctx, weights, explorationRate: 1 }).provider,
"expensive"
);
});
test("score — exact ties preserve configured candidate order", () => {
const pool = [cand({ provider: "first" }), cand({ provider: "second" })];
assert.equal(getStrategy("score").select(pool, { ...ctx, explorationRate: 0 }).provider, "first");
});
// ── cost ─────────────────────────────────────────────────────────────────────
test("cost — selects the cheapest healthy candidate", () => {
const pool = [
@@ -307,6 +336,7 @@ test("selectWithStrategy — unknown strategy silently falls back to rules", ()
test("listStrategies — exposes every registered strategy + aliases", () => {
const names = listStrategies().map((s) => s.name);
assert.ok(names.includes("score"), "listStrategies missing 'score'");
for (const n of ["rules", "cost", "eco", "latency", "fast", "sla-aware", "sla", "lkgp"]) {
assert.ok(names.includes(n), `listStrategies missing '${n}'`);
}

View File

@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
const runNextSource = readFileSync(join(process.cwd(), "scripts/dev/run-next.mjs"), "utf8");
test("the custom Next runner owns exit and awaits application cleanup before closing Next", () => {
const ownerRegistration = runNextSource.indexOf(
"globalThis.__omnirouteCustomServerOwnsShutdown = true"
);
const prepareCall = runNextSource.indexOf("await prepareWithHeal()");
assert.ok(ownerRegistration >= 0, "custom server shutdown ownership must be registered");
assert.ok(
ownerRegistration < prepareCall,
"shutdown ownership must exist before instrumentation"
);
const serverClose = runNextSource.indexOf("server.close(resolve)");
const applicationCleanup = runNextSource.indexOf(
"await globalThis.__omnirouteRequestShutdown?.(signal)"
);
const nextClose = runNextSource.indexOf("await nextApp.close()", applicationCleanup);
const processExit = runNextSource.indexOf("process.exit(0)", nextClose);
assert.ok(serverClose < applicationCleanup, "stop accepting requests before application cleanup");
assert.ok(applicationCleanup < nextClose, "application cleanup must finish before Next closes");
assert.ok(nextClose < processExit, "process exit must remain the final shutdown action");
});

View File

@@ -9,8 +9,8 @@ import { dirname, join } from "node:path";
// top-level IIFE at *require time* — merely importing the `playwright` package crashes,
// no browser needs to be launched. `claudeTurnstileSolver.ts` used to `import { chromium }
// from "playwright"` statically, and that module is unconditionally reachable from the
// Next.js instrumentation hook on every boot via open-sse/executors/index.ts, so any
// unsupported platform crashed the whole server at startup regardless of configured provider.
// Next.js instrumentation hook used to reach it on every boot via open-sse/executors/index.ts,
// so any unsupported platform crashed the whole server regardless of configured provider.
const HERE = dirname(fileURLToPath(import.meta.url));
const SOLVER = join(HERE, "../../open-sse/services/claudeTurnstileSolver.ts");
@@ -28,10 +28,9 @@ test("importing the real executor chain does not throw on an unsupported process
Object.defineProperty(process, "platform", { value: "android", configurable: true });
try {
// This is the exact reachability chain from the Next.js instrumentation hook:
// instrumentation-node.ts -> open-sse/index.ts -> executors/index.ts -> claude-web*.ts
// -> claudeTurnstileSolver.ts. Before the fix, this threw
// "Unsupported platform: android" purely from the static playwright import.
// Keep the executor-registry import safe for direct runtime consumers. Before the
// original fix, this threw "Unsupported platform: android" purely from the static
// playwright import; instrumentation no longer imports this graph at boot (#12074).
await import("../../open-sse/executors/index.ts");
} finally {
Object.defineProperty(process, "platform", originalDescriptor);

View File

@@ -162,7 +162,13 @@ test("Antigravity -> OpenAI keeps co-located function call and text but strips t
role: "model",
parts: [
{ text: "Let me look that up." },
{ functionResponse: { id: "call_9", name: "lookup", response: { result: { ok: true } } } },
{
functionResponse: {
id: "call_9",
name: "lookup",
response: { result: { ok: true } },
},
},
{ functionCall: { id: "call_10", name: "lookup", args: { q: "weather" } } },
],
},
@@ -249,7 +255,7 @@ test("Antigravity -> OpenAI lowers schema types recursively", () => {
false
);
assert.deepEqual((result.tools[0].function as any).parameters, {
assert.deepEqual((result.tools[0].function as Record<string, unknown>).parameters, {
type: "object",
properties: {
items: {
@@ -298,12 +304,17 @@ test("Antigravity -> OpenAI strips enumDescriptions from tool schema (top-level
false
);
const parameters = (result.tools[0].function as any).parameters;
const parameters = (result.tools[0].function as Record<string, unknown>).parameters as Record<
string,
unknown
>;
const props = parameters.properties as Record<string, unknown>;
const tags = props.tags as Record<string, unknown>;
// enumDescriptions must be removed at every level of the schema tree...
assert.equal("enumDescriptions" in parameters, false);
assert.equal("enumDescriptions" in parameters.properties.mode, false);
assert.equal("enumDescriptions" in parameters.properties.tags.items, false);
assert.equal("enumDescriptions" in (props.mode as Record<string, unknown>), false);
assert.equal("enumDescriptions" in (tags.items as Record<string, unknown>), false);
// ...while leaving the rest of the schema (incl. enum values) intact.
assert.deepEqual(parameters, {
@@ -349,7 +360,10 @@ test("Antigravity -> OpenAI preserves the required array on Draft 2020-12 tool s
false
);
const params = (result.tools[0].function as any).parameters;
const params = (result.tools[0].function as Record<string, unknown>).parameters as Record<
string,
unknown
>;
// The required array must survive so the model treats mandatory args as mandatory.
assert.deepEqual(params.required, ["path", "contents"]);
// Types are still lowered and Draft 2020-12 meta keywords are stripped.
@@ -385,6 +399,104 @@ test("Antigravity -> OpenAI drops required entries that no longer exist in prope
false
);
const params = (result.tools[0].function as any).parameters;
const params = (result.tools[0].function as Record<string, unknown>).parameters as Record<
string,
unknown
>;
assert.deepEqual(params.required, ["kept"]);
});
test("Antigravity -> OpenAI preserves falsy primitive results in function responses (false, 0, empty string, null)", () => {
const cases: Array<[unknown, string]> = [
[false, "false"],
[0, "0"],
["", '""'],
[null, "null"],
[true, "true"],
[42, "42"],
["done", '"done"'],
];
for (const [inputVal, expected] of cases) {
const result = antigravityToOpenAIRequest(
"gpt-4o",
{
request: {
contents: [
{
role: "model",
parts: [
{
functionCall: {
id: "call_test",
name: "check_condition",
args: {},
},
},
],
},
{
role: "user",
parts: [
{
functionResponse: {
id: "call_test",
name: "check_condition",
response: { result: inputVal },
},
},
],
},
],
},
},
false
);
const toolMsg = result.messages.find((m) => m.role === "tool");
assert.ok(toolMsg, "expected role:tool message");
assert.equal(toolMsg.tool_call_id, "call_test");
assert.equal(toolMsg.content, expected);
}
});
test("Antigravity -> OpenAI preserves custom response objects without result key", () => {
const result = antigravityToOpenAIRequest(
"gpt-4o",
{
request: {
contents: [
{
role: "model",
parts: [
{
functionCall: {
id: "call_custom",
name: "custom_op",
args: {},
},
},
],
},
{
role: "user",
parts: [
{
functionResponse: {
id: "call_custom",
name: "custom_op",
response: { output: "value", success: false },
},
},
],
},
],
},
},
false
);
const toolMsg = result.messages.find((m) => m.role === "tool");
assert.ok(toolMsg, "expected role:tool message");
assert.equal(toolMsg.content, '{"output":"value","success":false}');
});

View File

@@ -237,3 +237,70 @@ test("Gemini -> OpenAI maintains matching IDs across multi-turn tool call and re
assert.equal(toolResponseCallId, "call_calc_456");
assert.equal(assistantCallId, toolResponseCallId);
});
test("Gemini -> OpenAI preserves falsy primitive results in function responses (false, 0, empty string, null)", () => {
const cases: Array<[unknown, string]> = [
[false, "false"],
[0, "0"],
["", '""'],
[null, "null"],
[true, "true"],
[42, "42"],
["done", '"done"'],
];
for (const [inputVal, expected] of cases) {
const result = geminiToOpenAIRequest(
"gpt-4o",
{
contents: [
{
role: "user",
parts: [
{
functionResponse: {
id: "call_test",
name: "check_condition",
response: { result: inputVal },
},
},
],
},
],
},
false
);
assert.equal(result.messages.length, 1);
assert.equal(result.messages[0].role, "tool");
assert.equal(result.messages[0].tool_call_id, "call_test");
assert.equal(result.messages[0].content, expected);
}
});
test("Gemini -> OpenAI preserves custom response objects without result key", () => {
const result = geminiToOpenAIRequest(
"gpt-4o",
{
contents: [
{
role: "user",
parts: [
{
functionResponse: {
id: "call_custom",
name: "custom_op",
response: { output: "value", success: false },
},
},
],
},
],
},
false
);
assert.equal(result.messages.length, 1);
assert.equal(result.messages[0].role, "tool");
assert.equal(result.messages[0].content, '{"output":"value","success":false}');
});

View File

@@ -0,0 +1,372 @@
/**
* tests/unit/ui/orchestrationModel.test.ts
* Run: node --import tsx/esm --test tests/unit/ui/orchestrationModel.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
ORCH_STATES,
orchStateColor,
} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
import { STATUS_HEX } from "../../../src/shared/constants/statusColors.ts";
import { fromCloudAgent } from "../../../src/app/(dashboard)/dashboard/orchestration/model/fromCloudAgent.ts";
import type { CloudAgentTask } from "../../../src/lib/cloudAgent/types.ts";
import { fromA2A } from "../../../src/app/(dashboard)/dashboard/orchestration/model/fromA2A.ts";
import type { A2ATask } from "../../../src/lib/a2a/taskManager.ts";
import { fromConductor } from "../../../src/app/(dashboard)/dashboard/orchestration/model/fromConductor.ts";
import type { FleetSnapshot } from "../../../src/lib/conductor/hubProxy.ts";
import { mergeSnapshot } from "../../../src/app/(dashboard)/dashboard/orchestration/model/mergeSnapshot.ts";
import {
STALE_COMPLETED_MS,
MAX_WORK_NODES,
} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
describe("orchestrationTypes", () => {
it("covers all six states with a color each", () => {
assert.equal(ORCH_STATES.length, 6);
for (const s of ORCH_STATES) {
assert.match(orchStateColor(s), /^#[0-9a-f]{6}$/i, s);
}
});
it("waiting_approval maps to the new STATUS_HEX.approval violet", () => {
assert.equal(orchStateColor("waiting_approval"), STATUS_HEX.approval);
assert.equal(STATUS_HEX.approval, "#8b5cf6");
});
it("running maps to warning, succeeded to success, failed to error", () => {
assert.equal(orchStateColor("running"), STATUS_HEX.warning);
assert.equal(orchStateColor("succeeded"), STATUS_HEX.success);
assert.equal(orchStateColor("failed"), STATUS_HEX.error);
});
});
function caTask(over: Partial<CloudAgentTask>): CloudAgentTask {
return {
id: "t1",
providerId: "devin",
status: "running",
prompt: "Fix the flaky test in CI",
source: { repoName: "acme/app", repoUrl: "https://github.com/acme/app" },
options: {},
activities: [],
createdAt: "2026-08-30T10:00:00Z",
updatedAt: "2026-08-30T10:05:00Z",
...over,
} as CloudAgentTask;
}
describe("fromCloudAgent", () => {
it("maps every status to the unified OrchState", () => {
const cases: Array<[CloudAgentTask["status"], string]> = [
["queued", "queued"],
["running", "running"],
["awaiting_approval", "waiting_approval"],
["completed", "succeeded"],
["failed", "failed"],
["cancelled", "cancelled"],
];
for (const [input, expected] of cases) {
const { nodes } = fromCloudAgent([caTask({ status: input })]);
const work = nodes.find((n) => n.kind === "work");
assert.equal(work?.state, expected, input);
}
});
it("unknown status becomes failed with the raw value in sublabel", () => {
const { nodes } = fromCloudAgent([caTask({ status: "exploded" as CloudAgentTask["status"] })]);
const work = nodes.find((n) => n.kind === "work");
assert.equal(work?.state, "failed");
assert.match(work?.sublabel ?? "", /exploded/);
});
it("running task with activities gets one ActivityNode; completed does not", () => {
const running = caTask({
activities: [
{ id: "a1", type: "command", content: "npm test", timestamp: "2026-08-30T10:04:00Z" },
],
});
const done = caTask({ id: "t2", status: "completed", activities: running.activities });
const { nodes, edges } = fromCloudAgent([running, done]);
const acts = nodes.filter((n) => n.kind === "activity");
assert.equal(acts.length, 1);
assert.equal(acts[0].id, "cloud-agent:t1:activity");
assert.ok(
edges.some(
(e) => e.from === "cloud-agent:t1" && e.to === "cloud-agent:t1:activity" && e.active
)
);
});
it("emits a SourceNode with per-state counts and owns-edges from it", () => {
const { nodes, edges } = fromCloudAgent([caTask({}), caTask({ id: "t2", status: "failed" })]);
const src = nodes.find((n) => n.id === "source:cloud-agent");
assert.equal(src?.counts?.running, 1);
assert.equal(src?.counts?.failed, 1);
assert.ok(
edges.some(
(e) => e.from === "source:cloud-agent" && e.to === "cloud-agent:t1" && e.kind === "owns"
)
);
});
it("empty input emits nothing", () => {
const out = fromCloudAgent([]);
assert.equal(out.nodes.length, 0);
assert.equal(out.edges.length, 0);
});
});
function a2aTask(over: Partial<A2ATask>): A2ATask {
return {
id: "a1",
skill: "smart-routing",
state: "working",
input: { skill: "smart-routing", messages: [{ role: "user", content: "route this well" }] },
artifacts: [],
events: [{ timestamp: "2026-08-30T10:00:00Z", state: "submitted" }],
metadata: {},
createdAt: "2026-08-30T10:00:00Z",
updatedAt: "2026-08-30T10:01:00Z",
expiresAt: "2026-08-30T10:05:00Z",
...over,
} as A2ATask;
}
describe("fromA2A", () => {
it("maps the five A2A states", () => {
const cases: Array<[A2ATask["state"], string]> = [
["submitted", "queued"],
["working", "running"],
["completed", "succeeded"],
["failed", "failed"],
["cancelled", "cancelled"],
];
for (const [input, expected] of cases) {
const { nodes } = fromA2A([a2aTask({ state: input })]);
assert.equal(nodes.find((n) => n.kind === "work")?.state, expected, input);
}
});
it("work node id is a2a:<id>, label is the skill", () => {
const { nodes } = fromA2A([a2aTask({})]);
const w = nodes.find((n) => n.kind === "work");
assert.equal(w?.id, "a2a:a1");
assert.equal(w?.label, "smart-routing");
});
it("empty input emits nothing", () => {
assert.equal(fromA2A([]).nodes.length, 0);
});
});
const baseSnap: FleetSnapshot = {
offline: false,
runners: [{ id: "r1", name: "runner-one", clis: ["claude"], online: true, draining: false }],
tasks: [
{
id: "ct1",
status: "running",
mode: "auto",
repo: "acme/app",
runner: "r1",
summary: "Refactor auth",
branch: null,
error: null,
updated_at: "2026-08-30T10:00:00Z",
},
],
};
describe("fromConductor", () => {
it("online runner with a running task → running WorkNode + ActivityNode for the task", () => {
const { nodes, edges } = fromConductor(baseSnap);
const runner = nodes.find((n) => n.id === "conductor:runner:r1");
assert.equal(runner?.state, "running");
const act = nodes.find((n) => n.id === "conductor:task:ct1");
assert.equal(act?.kind, "activity");
assert.ok(edges.some((e) => e.from === "conductor:runner:r1" && e.to === "conductor:task:ct1"));
});
it("queued task without runner hangs directly under the source as a work node", () => {
const snap: FleetSnapshot = {
...baseSnap,
runners: [],
tasks: [{ ...baseSnap.tasks[0], id: "ct2", status: "queued", runner: null }],
};
const { nodes, edges } = fromConductor(snap);
const w = nodes.find((n) => n.id === "conductor:task:ct2");
assert.equal(w?.kind, "work");
assert.equal(w?.state, "queued");
assert.ok(edges.some((e) => e.from === "source:conductor" && e.to === "conductor:task:ct2"));
});
it("offline snapshot emits only nothing (hook marks the source offline separately)", () => {
const out = fromConductor({ offline: true, runners: [], tasks: [] });
assert.equal(out.nodes.length, 0);
});
it("unknown hub status maps to failed with the raw value in sublabel", () => {
const snap: FleetSnapshot = {
...baseSnap,
runners: [],
tasks: [{ ...baseSnap.tasks[0], id: "ct3", status: "vaporized", runner: null }],
};
const w = fromConductor(snap).nodes.find((n) => n.id === "conductor:task:ct3");
assert.equal(w?.state, "failed");
assert.match(w?.sublabel ?? "", /vaporized/);
});
it("running task whose runner is not present in snap.runners is emitted as a work node, not swallowed", () => {
const snap: FleetSnapshot = {
offline: false,
runners: [],
tasks: [
{
...baseSnap.tasks[0],
id: "ct-orphan",
status: "running",
runner: "ghost-runner",
},
],
};
const { nodes, edges } = fromConductor(snap);
const w = nodes.find((n) => n.id === "conductor:task:ct-orphan");
assert.equal(w?.kind, "work");
assert.equal(w?.state, "running");
assert.ok(
edges.some((e) => e.from === "source:conductor" && e.to === "conductor:task:ct-orphan")
);
});
});
const OK_SOURCES = [
{ source: "cloud-agent" as const, ok: true },
{ source: "a2a" as const, ok: true },
{ source: "conductor" as const, ok: true },
];
const NOW = Date.parse("2026-08-30T12:00:00Z");
const empty = { nodes: [], edges: [] };
describe("mergeSnapshot", () => {
it("adds the orchestrator root and root→source edges", () => {
const snap = mergeSnapshot(
{ cloudAgent: fromCloudAgent([caTask({})]), a2a: empty, conductor: empty },
OK_SOURCES,
{ now: NOW }
);
assert.ok(snap.nodes.some((n) => n.id === "orchestrator"));
assert.ok(snap.edges.some((e) => e.from === "orchestrator" && e.to === "source:cloud-agent"));
});
it("dedupes a Conductor-mirrored A2A task into one node with a mirror edge", () => {
const a2a = fromA2A([
a2aTask({ id: "am1", skill: "conductor", metadata: { conductor: { task_id: "ct1" } } }),
]);
const conductor = fromConductor(baseSnap); // contains conductor:task:ct1 as activity of r1
const snap = mergeSnapshot({ cloudAgent: empty, a2a, conductor }, OK_SOURCES, { now: NOW });
assert.ok(
!snap.nodes.some((n) => n.id === "a2a:am1"),
"mirrored A2A work node must be dropped"
);
const mirrorEdge = snap.edges.find((e) => e.kind === "mirror");
assert.equal(mirrorEdge?.to, "source:a2a");
});
it("drops terminal work older than STALE_COMPLETED_MS unless showCompleted", () => {
const old = caTask({
id: "old",
status: "completed",
completedAt: new Date(NOW - STALE_COMPLETED_MS - 1000).toISOString(),
});
const parts = { cloudAgent: fromCloudAgent([old]), a2a: empty, conductor: empty };
assert.ok(
!mergeSnapshot(parts, OK_SOURCES, { now: NOW }).nodes.some((n) => n.id === "cloud-agent:old")
);
assert.ok(
mergeSnapshot(parts, OK_SOURCES, { now: NOW, showCompleted: true }).nodes.some(
(n) => n.id === "cloud-agent:old"
)
);
});
it("caps work nodes at MAX_WORK_NODES with a per-source overflow node", () => {
const many = Array.from({ length: MAX_WORK_NODES + 10 }, (_, i) =>
caTask({ id: `m${i}`, updatedAt: new Date(NOW - i * 1000).toISOString() })
);
const snap = mergeSnapshot(
{ cloudAgent: fromCloudAgent(many), a2a: empty, conductor: empty },
OK_SOURCES,
{ now: NOW }
);
const works = snap.nodes.filter((n) => n.kind === "work");
assert.ok(works.length <= MAX_WORK_NODES, `got ${works.length}`);
const overflow = snap.nodes.find((n) => n.id === "overflow:cloud-agent");
assert.ok(overflow, "overflow node expected");
});
it("keeps SourceStatus[] verbatim on the snapshot", () => {
const src = [{ source: "conductor" as const, ok: false, offline: true }];
const snap = mergeSnapshot({ cloudAgent: empty, a2a: empty, conductor: empty }, src, {
now: NOW,
});
assert.deepEqual(snap.sources, src);
});
it("does not mutate input node objects when deduping (honors the Pure contract)", () => {
const a2a = fromA2A([
a2aTask({ id: "am2", skill: "conductor", metadata: { conductor: { task_id: "ct1" } } }),
]);
const conductor = fromConductor(baseSnap); // contains conductor:task:ct1 as activity of r1
const originalNode = conductor.nodes.find((n) => n.id === "conductor:task:ct1");
assert.ok(originalNode, "conductor:task:ct1 must exist in the source part");
const snap = mergeSnapshot({ cloudAgent: empty, a2a, conductor }, OK_SOURCES, { now: NOW });
assert.equal(
"mirrorOf" in (originalNode as object),
false,
"the original conductor input node must not be mutated"
);
const mergedNode = snap.nodes.find((n) => n.id === "conductor:task:ct1");
assert.equal(mergedNode?.mirrorOf, "a2a:am2");
});
it("materializes an offline placeholder source node for a source reporting offline:true even when ok:true", () => {
const src = [{ source: "conductor" as const, ok: true, offline: true }];
const snap = mergeSnapshot({ cloudAgent: empty, a2a: empty, conductor: empty }, src, {
now: NOW,
});
const node = snap.nodes.find((n) => n.id === "source:conductor");
assert.ok(node, "offline placeholder source:conductor node expected");
assert.equal(node?.sublabel, "offline");
assert.ok(
snap.edges.some(
(e) => e.from === "orchestrator" && e.to === "source:conductor" && e.kind === "owns"
)
);
});
it("overflow node carries droppedByState with the per-state counts of dropped work nodes", () => {
const many = Array.from({ length: MAX_WORK_NODES + 5 }, (_, i) =>
caTask({
id: `ov${i}`,
status: i < MAX_WORK_NODES ? "running" : "failed",
updatedAt: new Date(NOW - i * 1000).toISOString(),
})
);
const snap = mergeSnapshot(
{ cloudAgent: fromCloudAgent(many), a2a: empty, conductor: empty },
OK_SOURCES,
{ now: NOW }
);
const overflow = snap.nodes.find((n) => n.id === "overflow:cloud-agent");
assert.ok(overflow, "overflow node expected");
assert.deepEqual(overflow?.droppedByState, { failed: 5 });
});
it("drops a stale terminal Conductor task older than STALE_COMPLETED_MS unless showCompleted", () => {
const staleSnap: FleetSnapshot = {
...baseSnap,
runners: [],
tasks: [
{
...baseSnap.tasks[0],
id: "ctOld",
status: "completed",
runner: null,
updated_at: new Date(NOW - STALE_COMPLETED_MS - 1000).toISOString(),
},
],
};
const parts = { cloudAgent: empty, a2a: empty, conductor: fromConductor(staleSnap) };
assert.ok(
!mergeSnapshot(parts, OK_SOURCES, { now: NOW }).nodes.some(
(n) => n.id === "conductor:task:ctOld"
)
);
assert.ok(
mergeSnapshot(parts, OK_SOURCES, { now: NOW, showCompleted: true }).nodes.some(
(n) => n.id === "conductor:task:ctOld"
)
);
});
});

View File

@@ -0,0 +1,56 @@
/** Run: node --import tsx/esm --test tests/unit/ui/orchestrationToFlow.test.ts */
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { orchestrationToFlow } from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationToFlow.ts";
import type { OrchSnapshot } from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
const snap: OrchSnapshot = {
nodes: [
{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
{ id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
{ id: "a2a:t1", kind: "work", source: "a2a", state: "running", label: "smart-routing" },
{ id: "a2a:t2", kind: "work", source: "a2a", state: "failed", label: "cost-analysis" },
],
edges: [
{ id: "e1", from: "orchestrator", to: "source:a2a", kind: "owns", active: false },
{ id: "e2", from: "source:a2a", to: "a2a:t1", kind: "owns", active: true },
{ id: "e3", from: "source:a2a", to: "a2a:t2", kind: "owns", active: false },
],
sources: [],
generatedAt: "2026-08-30T12:00:00Z",
};
describe("orchestrationToFlow", () => {
it("puts each kind on its own Y layer and is deterministic", () => {
const a = orchestrationToFlow(snap);
const b = orchestrationToFlow(snap);
assert.deepEqual(
a.nodes.map((n) => n.position),
b.nodes.map((n) => n.position)
);
const ys = new Map(a.nodes.map((n) => [n.id, n.position.y]));
assert.equal(ys.get("orchestrator"), 0);
assert.equal(ys.get("source:a2a"), 150);
assert.equal(ys.get("a2a:t1"), 320);
});
it("active edge is animated; edge to failed work is red", () => {
const { edges } = orchestrationToFlow(snap);
assert.equal(edges.find((e) => e.id === "e2")?.animated, true);
const failedEdge = edges.find((e) => e.id === "e3");
assert.equal((failedEdge?.style as { stroke?: string })?.stroke, "#ef4444");
});
it("fitKey only tracks the set of work ids", () => {
const k1 = orchestrationToFlow(snap).fitKey;
const stateChanged = {
...snap,
nodes: snap.nodes.map((n) => (n.id === "a2a:t1" ? { ...n, state: "succeeded" as const } : n)),
};
assert.equal(orchestrationToFlow(stateChanged).fitKey, k1);
const nodeRemoved = {
...snap,
nodes: snap.nodes.filter((n) => n.id !== "a2a:t2"),
edges: snap.edges.filter((e) => e.to !== "a2a:t2"),
};
assert.notEqual(orchestrationToFlow(nodeRemoved).fitKey, k1);
});
});

View File

@@ -0,0 +1,67 @@
/** Run: node --import tsx/esm --test tests/unit/ui/overviewProjection.test.ts */
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { overviewProjection } from "../../../src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts";
import type { OrchSnapshot } from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
const snap: OrchSnapshot = {
nodes: [
{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
{ id: "cloud-agent:1", kind: "work", source: "cloud-agent", state: "running", label: "a" },
{
id: "cloud-agent:2",
kind: "work",
source: "cloud-agent",
state: "waiting_approval",
label: "b",
},
{
id: "a2a:3",
kind: "work",
source: "a2a",
state: "failed",
label: "c",
updatedAt: "2026-08-30T11:00:00Z",
},
{ id: "a2a:3:activity", kind: "activity", source: "a2a", state: "running", label: "noise" },
],
edges: [],
sources: [],
generatedAt: "2026-08-30T12:00:00Z",
};
describe("overviewProjection", () => {
it("counts only work nodes and adds comboActive to running", () => {
const { counts } = overviewProjection(snap, 4);
assert.equal(counts.running, 1 + 4);
assert.equal(counts.waiting_approval, 1);
assert.equal(counts.failed, 1);
assert.equal(counts.queued, 0);
});
it("folds terminals into the done column", () => {
const { columns } = overviewProjection(snap, 0);
assert.equal(columns.done.length, 1);
assert.equal(columns.done[0].id, "a2a:3");
assert.equal(columns.running.length, 1);
});
it("folds an overflow node's droppedByState into counts but not into columns", () => {
const snapWithOverflow: OrchSnapshot = {
...snap,
nodes: [
...snap.nodes,
{
id: "overflow:cloud-agent",
kind: "overflow",
source: "cloud-agent",
label: "+7 more",
droppedByState: { running: 5, failed: 2 },
},
],
};
const { counts, columns } = overviewProjection(snapWithOverflow, 0);
assert.equal(counts.running, 1 + 5);
assert.equal(counts.failed, 1 + 2);
assert.equal(columns.running.length, 1);
assert.equal(columns.done.length, 1);
});
});

View File

@@ -0,0 +1,121 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// Capture the onEvent handler the hook registers on the requests channel.
let capturedOnEvent: ((p: { channel: string }) => void) | null = null;
vi.mock("@/hooks/useLiveDashboard", () => ({
useLiveDashboard: (opts: { onEvent?: (p: { channel: string }) => void }) => {
capturedOnEvent = opts.onEvent ?? null;
return { connection: { isConnected: true }, events: [] };
},
}));
import { useOrchestrationSnapshot } from "@/app/(dashboard)/dashboard/orchestration/hooks/useOrchestrationSnapshot";
function HookProbe({
onRender,
}: {
onRender: (v: ReturnType<typeof useOrchestrationSnapshot>) => void;
}) {
onRender(useOrchestrationSnapshot());
return null;
}
const okJson = (body: unknown) =>
Promise.resolve({ ok: true, json: () => Promise.resolve(body) } as Response);
describe("useOrchestrationSnapshot", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
vi.useFakeTimers();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.useRealTimers();
vi.restoreAllMocks();
});
it("polls the three endpoints and builds a snapshot; a failed source keeps the last good data", async () => {
let latest: ReturnType<typeof useOrchestrationSnapshot> | null = null;
const task = {
id: "t1",
providerId: "devin",
status: "running",
prompt: "p",
source: { repoName: "r", repoUrl: "https://x" },
options: {},
activities: [],
createdAt: "2026-08-30T10:00:00Z",
updatedAt: "2026-08-30T10:00:00Z",
};
const fetchMock = vi.fn((url: string) => {
if (url.startsWith("/api/v1/agents/tasks")) return okJson({ data: [task] });
if (url.startsWith("/api/a2a/tasks"))
return okJson({ tasks: [], total: 0, limit: 200, offset: 0 });
return okJson({ offline: false, runners: [], tasks: [] });
});
vi.stubGlobal("fetch", fetchMock);
await act(async () => {
root.render(
<HookProbe
onRender={(v) => {
latest = v;
}}
/>
);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(10);
});
expect(latest!.snapshot.nodes.some((n) => n.id === "cloud-agent:t1")).toBe(true);
// Second tick: cloud agent fails — node must survive from the last good photo, source marked stale.
fetchMock.mockImplementation((url: string) => {
if (url.startsWith("/api/v1/agents/tasks")) return Promise.reject(new Error("boom"));
if (url.startsWith("/api/a2a/tasks"))
return okJson({ tasks: [], total: 0, limit: 200, offset: 0 });
return okJson({ offline: false, runners: [], tasks: [] });
});
await act(async () => {
await vi.advanceTimersByTimeAsync(5_100);
});
expect(latest!.snapshot.nodes.some((n) => n.id === "cloud-agent:t1")).toBe(true);
const st = latest!.snapshot.sources.find((s) => s.source === "cloud-agent");
expect(st?.ok).toBe(false);
});
it("a requests-channel WS event triggers a debounced immediate refetch", async () => {
const fetchMock = vi.fn((url: string) => {
if (url.startsWith("/api/v1/agents/tasks")) return okJson({ data: [] });
if (url.startsWith("/api/a2a/tasks"))
return okJson({ tasks: [], total: 0, limit: 200, offset: 0 });
return okJson({ offline: false, runners: [], tasks: [] });
});
vi.stubGlobal("fetch", fetchMock);
await act(async () => {
root.render(<HookProbe onRender={() => {}} />);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(10);
});
const callsAfterMount = fetchMock.mock.calls.length;
act(() => {
capturedOnEvent?.({ channel: "requests" });
capturedOnEvent?.({ channel: "requests" });
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1_100);
});
// Two burst events → exactly ONE extra round of 3 fetches (debounce), not two.
expect(fetchMock.mock.calls.length).toBe(callsAfterMount + 3);
});
});