Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c78020a
feat(perf): lazy-boot quota auto-ping scheduler + build heap floor guard
linhdmn Sep 1, 2026
38c6970
fix(build): keep the historical 8 GB default when no heap flag is inh…
linhdmn Sep 1, 2026
8eac254
fix(build): raise heap floor 4096 to 6144 after live OOM at 4 GB
linhdmn Sep 1, 2026
3e5461a
fix(build): heap floor raised to 8192 — 6 GB ceiling also OOMed live
linhdmn Sep 1, 2026
33e251f
fix(autoping): broaden deps.getProviderConnections signature to match…
linhdmn Sep 1, 2026
96b1337
fix(autoping): cast db rows to the dep contract at the single boundary
linhdmn Sep 1, 2026
0a4bba3
fix(autoping): keep a single getProviderConnections call site
linhdmn Sep 2, 2026
0cd4acd
Merge branch 'release/v3.8.51' into feat/perf-lazy-boot
linhdmn Sep 3, 2026
90c0e06
Merge branch 'release/v3.8.51' into feat/perf-lazy-boot
linhdmn Sep 3, 2026
dbc0aab
Merge branch 'release/v3.8.51' into feat/perf-lazy-boot
linhdmn Sep 4, 2026
36ac408
fix(perf): extract quota auto-ping boot helpers from god functions
linhdmn Sep 4, 2026
23780bf
Merge remote-tracking branch 'origin/release/v3.8.51' into fix/pr-123…
linhdmn Sep 4, 2026
4e7fd2b
chore(changelog): prefix reset-aware-model-family fragment with a bul…
linhdmn Sep 4, 2026
0ed96b0
fix(ci): drain inherited base-reds — glm.ts TS2554 stale callsite + c…
linhdmn Sep 4, 2026
9d3340f
fix(ci): register reset-aware-request-scope-12600 test in stryker tap…
linhdmn Sep 4, 2026
dabdad7
fix(base-reds): drain the 2026-09-04 unit-shard failures inherited fr…
linhdmn Sep 4, 2026
6066de9
fix(base-reds): revert keepReadable stream change — it breaks the pas…
linhdmn Sep 4, 2026
c2551e8
fix(base-reds): tighten credential-label boundary to redacted remaind…
linhdmn Sep 4, 2026
04999ad
fix(base-reds): reconcile the two stream-termination contracts + ReDo…
linhdmn Sep 4, 2026
79fa094
fix(base-reds): ratchet stream-utils test baseline 2517→2519 for the …
linhdmn Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/features/12333-lazy-boot-quota-heap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **perf(boot):** lazy-arm the quota auto-ping scheduler only when a connection has opted in, re-arm it from settings PATCH, and replace inherited `NODE_OPTIONS --max-old-space-size` below the 8 GB build heap floor ([#12333](https://github.com/diegosouzapw/OmniRoute/pull/12333)) — thanks @linhdmn
2 changes: 1 addition & 1 deletion changelog.d/fixes/reset-aware-model-family.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
- Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
6 changes: 3 additions & 3 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@
"tests/unit/reasoning-cache.test.ts": 1291,
"tests/unit/route-edge-coverage.test.ts": 1244,
"tests/unit/sse-auth.test.ts": 1729,
"tests/unit/stream-utils.test.ts": 2517,
"tests/unit/stream-utils.test.ts": 2520,
"tests/unit/token-refresh-service.test.ts": 1407,
"tests/unit/translator-openai-responses-req.test.ts": 1470,
"tests/unit/translator-openai-to-gemini.test.ts": 1625,
Expand Down Expand Up @@ -419,7 +419,7 @@
"open-sse/executors/cursor.ts": 1759,
"open-sse/executors/muse-spark-web.ts": 1405,
"open-sse/handlers/chatCore.ts": 5984,
"open-sse/handlers/imageGeneration.ts": 3259,
"open-sse/handlers/imageGeneration.ts": 3260,
"open-sse/handlers/search.ts": 1789,
"open-sse/mcp-server/schemas/tools.ts": 1621,
"open-sse/mcp-server/server.ts": 1572,
Expand Down Expand Up @@ -457,7 +457,7 @@
"src/shared/components/RequestLoggerV2.tsx": 1718,
"src/shared/constants/providers/apikey/gateways.ts": 1462,
"src/shared/services/cliRuntime.ts": 1296,
"src/sse/handlers/chat.ts": 2454,
"src/sse/handlers/chat.ts": 2457,
"src/sse/services/auth.ts": 3450,
"tests/unit/account-fallback-service.test.ts": 2453,
"tests/unit/provider-validation-specialty.test.ts": 4656
Expand Down
5 changes: 1 addition & 4 deletions open-sse/executors/glm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,7 @@ export function translateSseResponse(
null,
null,
false,
suppressThinkClose,
undefined,
undefined,
65536
suppressThinkClose
);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "text/event-stream");
Expand Down
3 changes: 2 additions & 1 deletion open-sse/handlers/imageGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2810,7 +2810,8 @@ export function saveImageErrorResult({
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: typeof error === "string" ? error.slice(0, 500) : String(error).slice(0, 500),
error:
typeof error === "string" ? error.slice(0, 500) : JSON.stringify(error ?? null).slice(0, 500),
requestBody,
}).catch(() => {});

Expand Down
2 changes: 1 addition & 1 deletion open-sse/utils/credentialPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const CREDENTIAL_PATTERNS: CredentialPattern[] = [
regex: /sk-ant-[A-Za-z0-9_-]{20,}/g,
replacement: "[REDACTED:anthropic]",
},
{ name: "google", regex: /AIza[0-9A-Za-z_-]{35}/g, replacement: "[REDACTED:google]" },
{ name: "google", regex: /AIza[0-9A-Za-z_-]{20,}/g, replacement: "[REDACTED:google]" },
{ name: "huggingface", regex: /hf_[A-Za-z0-9]{34}/g, replacement: "[REDACTED:hf]" },
{ name: "replicate", regex: /r8_[A-Za-z0-9]{37}/g, replacement: "[REDACTED:replicate]" },
{ name: "github", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g, replacement: "[REDACTED:github]" },
Expand Down
4 changes: 3 additions & 1 deletion open-sse/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ const SAFE_PUBLIC_ERROR_IDENTIFIERS = new Set([
"executor_error",
"feature_disabled",
"gateway_timeout",
"gemini_tpm_exhausted",
"gcp_project_required",
"gemini_tpm_exhausted",
"grok_error",
"huggingchat_generation_error",
"insufficient_quota",
"incompatible_reasoning_effort",
"internal_server_error",
Expand Down Expand Up @@ -282,6 +283,7 @@ const SAFE_PUBLIC_ERROR_IDENTIFIERS = new Set([
"vision",
"claude_web_protocol_error",
"wreq_unavailable",
"zai_stream_error",
]);

function isSafePublicErrorIdentifier(value: string): boolean {
Expand Down
43 changes: 33 additions & 10 deletions open-sse/utils/errorPathRedaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ const CLEAR_PROSE_BOUNDARIES = [
"retry",
"then",
"when",
"while",
"redacted",
"with",
] as const;
// Credential labels whose `label:`/`label=`-introducer marks prose (the value is
// redacted by the sanitizer before path spans are resolved). Mirrors the
// BLOCKED_KEYS vocabulary in errorSanitization.ts.
const CREDENTIAL_LABEL_BOUNDARY =
/stack|trace|path|file|cwd|dir|password|secret|token|key|authorization|cookie|credential|session/i;
const POSIX_FILESYSTEM_ROOTS = [
"/Users",
"/app",
Expand Down Expand Up @@ -435,19 +441,36 @@ function remainderContainsFilesystemSeparator(value: string, start: number): boo
return false;
}

function isClearProseBoundaryToken(
value: string,
start: number,
end: number,
remainderStart: number
): boolean {
while (start < end && LEADING_PATH_PUNCTUATION.includes(value[start])) start++;
end = trimPathSpanEnd(value, start, end);
const token = value.slice(start, end).toLowerCase();
if ((CLEAR_PROSE_BOUNDARIES as readonly string[]).includes(token)) return true;
// A redacted credential assignment (`label=[REDACTED]`) is sanitizer output,
// never a path continuation — treat it as a prose boundary so the span that
// swallowed a preceding ambiguous path cannot also swallow the redaction.
// trimPathSpanEnd may already have stripped the closing bracket.
if (/(?:^|[^a-z0-9_])[a-z0-9_-]+=\[redacted\]?[)\]},'"`.:;!?]?$/.test(token)) return true;
// Same for the label half of a split assignment: `Authorization:` followed by
// `[REDACTED]` (the value was scrubbed by an earlier pass). Without this the
// label token poisons `hasUnresolvedFragments` and the fail-closed span eats
// the redaction that follows it. The boundary ONLY fires when the remainder
// actually is redacted output — a bare credential-shaped word in ordinary
// prose (e.g. "…/internal secret directory") must stay fail-closed.
const remainder = value.slice(remainderStart).replace(/^[)\]},'"`.:;!?]?\s+/, "");
if (!/^\[redacted\b/i.test(remainder)) return false;
return /^[a-z0-9_-]+:?$/.test(token) && CREDENTIAL_LABEL_BOUNDARY.test(token);
}
function trimPathSpanEnd(value: string, start: number, end: number): number {
while (end > start && PATH_SPAN_END_PUNCTUATION.includes(value[end - 1])) end--;
return end;
}

function isClearProseBoundaryToken(value: string, start: number, end: number): boolean {
while (start < end && LEADING_PATH_PUNCTUATION.includes(value[start])) start++;
end = trimPathSpanEnd(value, start, end);
return (CLEAR_PROSE_BOUNDARIES as readonly string[]).includes(
value.slice(start, end).toLowerCase()
);
}

function findUnquotedPathEnd(
value: string,
start: number,
Expand Down Expand Up @@ -493,7 +516,7 @@ function findUnquotedPathEnd(
// boundary only when no later token carries path-separator evidence;
// otherwise keep scanning so a filesystem suffix cannot survive.
} else if (
isClearProseBoundaryToken(value, tokenStart, tokenEnd) &&
isClearProseBoundaryToken(value, tokenStart, tokenEnd, tokenEnd) &&
(!remainderContainsFilesystemSeparator(value, tokenEnd) ||
(!failClosedAmbiguity && !hasFilesystemEvidence))
) {
Expand Down
11 changes: 9 additions & 2 deletions open-sse/utils/errorSanitization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ function findUnquotedCredentialEnd(value: string, start: number): number {
return end;
}

function redactLabeledCredentialAssignments(value: string): string {
export function redactLabeledCredentialAssignments(value: string): string {
const parts: string[] = [];
let copyStart = 0;
let index = 0;
Expand Down Expand Up @@ -689,7 +689,14 @@ function sanitizeErrorMessageWithStackPolicy(
// Raw URI credentials must be projected before the path tokenizer consumes
// the URI tail; Windows path evidence still stays intact until after this
// credential-only pass and is redacted before escape normalization.
str = redactKnownCredentialPatterns(redactSensitiveUrlCredentials(stripStackTail(str)));
// Credential kv-assignments (`access_token=…`, `api_key=…`, …) must be
// scrubbed BEFORE path-span redaction: an unquoted path with line:col
// ambiguity fails closed over the rest of the line, which would otherwise
// swallow a following `label=value` credential wholesale and destroy it
// instead of redacting it (the HuggingChat transport regression).
str = redactLabeledCredentialAssignments(
redactKnownCredentialPatterns(redactSensitiveUrlCredentials(stripStackTail(str)))
);
str = redactErrorPaths(str);
str = redactSensitiveErrorText(str);
str = truncateSanitizedErrorText(str);
Expand Down
12 changes: 9 additions & 3 deletions open-sse/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,7 +1080,8 @@ export function createSSEStream(options: StreamOptions = {}) {
cacheHit: false,
latencyMs: Date.now() - streamStartedAt,
usage: timing.withTps(finalUsage),
costUsd, ttftMs: timing.ttftMs(),
costUsd,
ttftMs: timing.ttftMs(),
});
if (!comment) return;
reqLogger?.appendConvertedChunk?.(comment);
Expand Down Expand Up @@ -1205,6 +1206,7 @@ export function createSSEStream(options: StreamOptions = {}) {
doneSent = true;
abortStreamFailure(controller, failure.internalFailure, failure.publicMessage, {
notifyComplete: true,
keepReadable: true,
});
return true;
};
Expand Down Expand Up @@ -2046,7 +2048,9 @@ export function createSSEStream(options: StreamOptions = {}) {
// estimate is now emitted in flush(), only when the upstream stayed silent.
if (isFinishChunk && hasValidUsage(usage) && !passthroughForwardedUsage) {
const buffered = addBufferToUsage(usage);
parsed.usage = timing.withTps(filterUsageForFormat(buffered, sourceFormat || FORMATS.OPENAI));
parsed.usage = timing.withTps(
filterUsageForFormat(buffered, sourceFormat || FORMATS.OPENAI)
);
output = `data: ${JSON.stringify(parsed)}\n\n`;
passthroughForwardedUsage = true;
injectedUsage = true;
Expand Down Expand Up @@ -2571,7 +2575,9 @@ export function createSSEStream(options: StreamOptions = {}) {
created: Math.floor(Date.now() / 1000),
model,
choices: [],
usage: timing.withTps(filterUsageForFormat(usage, sourceFormat || FORMATS.OPENAI)),
usage: timing.withTps(
filterUsageForFormat(usage, sourceFormat || FORMATS.OPENAI)
),
};
const usageOutput = `data: ${JSON.stringify(usageOnlyChunk)}\n\n`;
reqLogger?.appendConvertedChunk?.(usageOutput);
Expand Down
12 changes: 10 additions & 2 deletions open-sse/utils/streamFailureBoundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function createStreamFailureAborter(context: AborterContext) {
controller: TransformStreamDefaultController<Uint8Array>,
failure: StreamFailurePayload,
publicMessage: string,
options: { notifyComplete?: boolean } = {}
options: { notifyComplete?: boolean; keepReadable?: boolean } = {}
): void => {
let handled = false;
context.timing.markInterrupted();
Expand Down Expand Up @@ -71,6 +71,14 @@ export function createStreamFailureAborter(context: AborterContext) {
}
context.clearIdleTimer();
if (!handled) context.clearPendingRequest();
controller.error(context.markPendingRequestCleared(new Error(safeMessage)));
// `keepReadable` is for paths that already forwarded a terminal failure event to the
// client: the translated failure frame IS the end of the public protocol, and erroring
// the readable discards the queued frame from a `.text()`/pipe consumer (Kiro
// response.failed contract). Paths that forward nothing still hard-error.
if (options.keepReadable) {
context.markPendingRequestCleared(new Error(safeMessage));
} else {
controller.error(context.markPendingRequestCleared(new Error(safeMessage)));
}
};
}
32 changes: 25 additions & 7 deletions scripts/build/build-next-isolated.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,36 @@ export function resolveNextBuildEnv(baseEnv = process.env, platform = process.pl
// this only in the Docker builder stage (ENV NODE_OPTIONS); the local/native path
// was left unprotected. Respect an existing --max-old-space-size (Docker already
// sets one — don't clobber/duplicate) and let OMNIROUTE_BUILD_MEMORY_MB override.
// NOTE (#6409): --max-old-space-size only bounds V8's JS heap — it does NOT bound
// Turbopack's native (Rust, off-V8-heap) memory, which is the default bundler as of
// #6283. On memory-constrained machines, set OMNIROUTE_USE_TURBOPACK=0 (webpack
// fallback) instead of raising this heap value; see docs/reference/ENVIRONMENT.md.
if (!/--max-old-space-size/.test(env.NODE_OPTIONS || "")) {
//
// Guard against INHERITED low ceilings (#perf-lazy-boot): an operator shell that
// exports NODE_OPTIONS=--max-old-space-size=1024 (a common dotfile leftover) made
// the check above "respect" a ceiling far below what the compile actually needs —
// measured 2026-09-01 on a 16 GB macOS host: the webpack production pass OOMs at
// 2 GB, 4 GB and 6 GB ceilings (live builds, GC logs show full consumption at
// each). The historical 8 GB default is the only validated-good ceiling on this
// machine. If the inherited ceiling is below 8 GB, raise it to the default
// instead of failing minutes into the compile with an opaque SIGABRT.
const MEASURED_HEAP_FLOOR_MB = 8192;
const inheritedMatch = (env.NODE_OPTIONS || "").match(/--max-old-space-size=(\d+)/);
const inheritedMb = inheritedMatch ? Number(inheritedMatch[1]) : 0;
if (!inheritedMatch || inheritedMb < MEASURED_HEAP_FLOOR_MB) {
// Default 8 GB (was 4 GB): the clean module graph peaks ~3.9 GB during the webpack
// production pass, which brushed the old 4 GB ceiling on a borderline OOM. 8 GB gives
// headroom without risk. NOTE: heap size does NOT fix a poisoned scope — if the build
// OOMs/livelocks far above this, check for worktrees/cruft leaking into the tsconfig
// scope (run `npm run check:build-scope`), not for "more heap". See incident 2026-06-25.
const heapMb = Number(baseEnv.OMNIROUTE_BUILD_MEMORY_MB) || 8192;
env.NODE_OPTIONS = `${env.NODE_OPTIONS || ""} --max-old-space-size=${heapMb}`.trim();
const heapMb = Number(baseEnv.OMNIROUTE_BUILD_MEMORY_MB) || MEASURED_HEAP_FLOOR_MB;
// Replace any inherited ceiling in place (instead of appending a second flag) so
// NODE_OPTIONS stays single-valued and self-documenting. Node honors the LAST
// repeated flag, but a duplicated value reads like a bug and confuses CI logs.
if (inheritedMatch) {
env.NODE_OPTIONS = (env.NODE_OPTIONS || "").replace(
/--max-old-space-size=\d+/,
`--max-old-space-size=${heapMb}`
);
} else {
env.NODE_OPTIONS = `${env.NODE_OPTIONS || ""} --max-old-space-size=${heapMb}`.trim();
}
}

return env;
Expand Down
4 changes: 2 additions & 2 deletions skills/cli-tunnel/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ omniroute tunnel
omniroute tunnel list
```

### `tunnel create [type]`
### `tunnel create`

**Example:**

```bash
omniroute tunnel create [type]
omniroute tunnel create
```

### `tunnel stop <type>`
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/providers/[id]/sync-models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function readJsonResponse(response: Response): Promise<{
if (!body.trim()) {
return {
data: {},
parseError: "Empty response body from /models",
parseError: "Empty response body from GET /models",
};
}

Expand All @@ -105,7 +105,7 @@ async function readJsonResponse(response: Response): Promise<{
} catch {
return {
data: {},
parseError: "Invalid JSON response from /models",
parseError: "Invalid JSON response from GET /models",
};
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getUpstreamProxyConfig,
} from "@/lib/db/upstreamProxy";
import { getProviderConnections } from "@/lib/db/providers";
import { rearmQuotaAutoPingAfterSettingsPatch } from "@/lib/services/quotaAutoPing";
import { clearCliproxyapiUrlCache } from "@omniroute/open-sse/executors/cliproxyapi.ts";
import {
ensurePersistentManagementPasswordHash,
Expand Down Expand Up @@ -513,6 +514,10 @@ export async function PATCH(request: Request) {
});
}

// Boot-lazy parity with instrumentation-node (#perf-lazy-boot): re-arm the
// scheduler when this PATCH touches quota auto-ping opt-ins.
rearmQuotaAutoPingAfterSettingsPatch(rawBody, settings as Record<string, unknown>);

// Audit success — diff of changed keys only. Idempotent PATCH (no diff)
// intentionally writes NO row (spec §Observability + AC-9/AC-11).
try {
Expand Down
8 changes: 5 additions & 3 deletions src/instrumentation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,11 @@ export async function registerNodejs(): Promise<void> {
console.log("[STARTUP] Quota cache background refresh started");
startProviderLimitsSyncScheduler();
console.log("[STARTUP] Provider limits sync scheduler started");
const { startQuotaAutoPing } = await import("@/lib/services/quotaAutoPing");
startQuotaAutoPing();
console.log("[STARTUP] Quota auto-ping scheduler started (opt-in, no-op until enabled)");
// Boot-lazy (#perf-lazy-boot): only arm the auto-ping scheduler when at least
// one connection opted in (9router #27b37705 parity). Dashboard opt-in
// changes re-arm it via the settings PATCH path.
const { bootQuotaAutoPingIfOptedIn } = await import("@/lib/services/quotaAutoPing");
await bootQuotaAutoPingIfOptedIn();
const cloudSyncInitialized = await ensureCloudSyncInitialized();
console.log(
`[STARTUP] Cloud/model sync background bootstrap ${cloudSyncInitialized ? "initialized" : "skipped"}`
Expand Down
Loading
Loading