Compare commits

..

1 Commits

Author SHA1 Message Date
diegosouzapw
3113e571fc docs(api): document every implemented route in openapi.yaml (276 -> 692 paths)
Follow-up nº 3 of the 2026-08-31 docs audit: 416 implemented routes had no
OpenAPI entry (gamification, radar, skills, webhooks, mcp, a2a, tunnels,
version-manager and plugins were absent entirely). Adds a minimal, honest
entry for each — real methods parsed from every route.ts's exports, a group
tag and a neutral path-derived summary; no invented semantics. Rich schemas
remain hand-curated in the existing entries.

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,155 +0,0 @@
/**
* 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

@@ -1,69 +0,0 @@
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,13 +202,6 @@ 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)
@@ -271,7 +264,6 @@ test("Batch handles and counts failures correctly", async () => {
}
} finally {
stopBatchProcessor();
globalThis.fetch = originalFetch;
}
});
@@ -398,21 +390,6 @@ 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 = [
@@ -472,10 +449,8 @@ 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,12 +27,11 @@ import path from "node:path";
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-quota-"));
const { getCredentialRefreshExecutor } =
await import("../../open-sse/executors/credential.ts");
const { getExecutor } = await import("../../open-sse/executors/index.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 getCredentialRefreshExecutor("codex");
const exec = await getExecutor("codex");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;
@@ -69,7 +68,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 getCredentialRefreshExecutor("cursor");
const exec = await getExecutor("cursor");
const origNeeds = exec.needsRefresh;
const origRefresh = exec.refreshCredentials;
let refreshCalls = 0;

View File

@@ -15,7 +15,6 @@ 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 });
@@ -55,9 +54,3 @@ 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,12 +1,5 @@
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,
@@ -14,90 +7,17 @@ const shutdownSignals = ["SIGTERM", "SIGINT", "SIGHUP"] as const;
// 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("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)
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}).`
);
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;
}
}
// 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");
});

View File

@@ -3,8 +3,6 @@
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,
@@ -117,88 +115,5 @@ test("shouldSwallowUncaught preserves crash semantics for genuine errors", () =>
test("installProcessCrashGuard does not throw on import and is idempotent", () => {
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/);
assert.doesNotThrow(() => installProcessCrashGuard(() => {}));
});

View File

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

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

View File

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

View File

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

@@ -1,111 +0,0 @@
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,10 +15,7 @@ import {
listStrategies,
type RoutingContext,
} from "../../open-sse/services/autoCombo/routerStrategy.ts";
import {
DEFAULT_WEIGHTS,
type ProviderCandidate,
} from "../../open-sse/services/autoCombo/scoring.ts";
import type { ProviderCandidate } from "../../open-sse/services/autoCombo/scoring.ts";
function cand(p: Partial<ProviderCandidate> & { provider: string }): ProviderCandidate {
return {
@@ -36,32 +33,6 @@ 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 = [
@@ -336,7 +307,6 @@ 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

@@ -1,29 +0,0 @@
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 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.
// 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.
const HERE = dirname(fileURLToPath(import.meta.url));
const SOLVER = join(HERE, "../../open-sse/services/claudeTurnstileSolver.ts");
@@ -28,9 +28,10 @@ test("importing the real executor chain does not throw on an unsupported process
Object.defineProperty(process, "platform", { value: "android", configurable: true });
try {
// 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).
// 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.
await import("../../open-sse/executors/index.ts");
} finally {
Object.defineProperty(process, "platform", originalDescriptor);

View File

@@ -162,13 +162,7 @@ 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" } } },
],
},
@@ -255,7 +249,7 @@ test("Antigravity -> OpenAI lowers schema types recursively", () => {
false
);
assert.deepEqual((result.tools[0].function as Record<string, unknown>).parameters, {
assert.deepEqual((result.tools[0].function as any).parameters, {
type: "object",
properties: {
items: {
@@ -304,17 +298,12 @@ test("Antigravity -> OpenAI strips enumDescriptions from tool schema (top-level
false
);
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>;
const parameters = (result.tools[0].function as any).parameters;
// enumDescriptions must be removed at every level of the schema tree...
assert.equal("enumDescriptions" in parameters, false);
assert.equal("enumDescriptions" in (props.mode as Record<string, unknown>), false);
assert.equal("enumDescriptions" in (tags.items as Record<string, unknown>), false);
assert.equal("enumDescriptions" in parameters.properties.mode, false);
assert.equal("enumDescriptions" in parameters.properties.tags.items, false);
// ...while leaving the rest of the schema (incl. enum values) intact.
assert.deepEqual(parameters, {
@@ -360,10 +349,7 @@ test("Antigravity -> OpenAI preserves the required array on Draft 2020-12 tool s
false
);
const params = (result.tools[0].function as Record<string, unknown>).parameters as Record<
string,
unknown
>;
const params = (result.tools[0].function as any).parameters;
// 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.
@@ -399,104 +385,6 @@ test("Antigravity -> OpenAI drops required entries that no longer exist in prope
false
);
const params = (result.tools[0].function as Record<string, unknown>).parameters as Record<
string,
unknown
>;
const params = (result.tools[0].function as any).parameters;
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,70 +237,3 @@ 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

@@ -1,372 +0,0 @@
/**
* 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

@@ -1,56 +0,0 @@
/** 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

@@ -1,67 +0,0 @@
/** 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

@@ -1,121 +0,0 @@
// @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);
});
});