mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (#2939 unchanged). Also addresses #5160 (same OOM root); #5152 (docker) benefits via the same knob. Closes #5172
This commit is contained in:
committed by
GitHub
parent
a5cc116397
commit
c57aa04d41
@@ -20,6 +20,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **fix(sse): normalize array user-message content in the Command Code executor to prevent upstream 400** — when a client sends a user turn whose `content` is an array of content parts (e.g. `[{type:"text",text:"…"}, …]`), the raw array was forwarded verbatim to the Command Code upstream, which requires `messages[N].content` for the `user` role to be a plain string — resulting in `expected string, received array` / HTTP 400 on DeepSeek V4-Pro and other Command Code models. The user branch of `convertMessages` now calls `normalizeContentText()` (already used by system, assistant, and tool branches) so multi-part user content is joined to a string before dispatch. Partially addresses ([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166)); the 0-output-token symptom on reasoning-only models is tracked separately.
|
||||
- **fix(mcp): return HTTP 404 (not 400) for an unknown/expired Streamable HTTP session id** — when an MCP session is terminated or idles out and the client reuses the stale `Mcp-Session-Id` header, the Streamable HTTP transport replied with HTTP 400. The MCP spec (2025-03-26 and 2025-11-25, Session Management) mandates HTTP 404 Not Found in that case, and spec-compliant clients only re-initialize a session on 404 — so the 400 was non-recoverable. The handler now returns 404 for a present-but-unknown session id, while a *missing* session id on a non-initialize request correctly stays 400. ([#5169](https://github.com/diegosouzapw/OmniRoute/issues/5169) — thanks @czer323)
|
||||
- **fix(api): blocking "Auto (Zero-Config)" in Security settings now removes `auto/*` from `/v1/models`** — the built-in `auto/*` combo advertiser (#4164 / #4235) at the top of the models catalog ignored `settings.blockedProviders`, so checking **Auto (Zero-Config)** under Security → Blocked Providers had no effect and the model picker kept listing every `auto/*` entry. The injection loop now skips the entire `auto/*` block when the system provider `auto` (its id and alias are both `auto`) is blocked, consistent with how every other provider is filtered from the catalog. ([#5192](https://github.com/diegosouzapw/OmniRoute/issues/5192) — thanks @WslzGmzs)
|
||||
- **fix(cli): auto-calibrate the server V8 heap from physical RAM instead of a fixed 512MB default** — the server was spawned with a hard-coded `--max-old-space-size=512` (`omniroute serve`) or with no heap flag at all (Electron desktop, which then inherited the runtime's low ~512MB default), so RAM-rich machines still OOM-crashed under load (`FATAL ERROR: Ineffective mark-compacts near heap limit … ~500MB` at code=134) with many providers/accounts and large model catalogs (one report: 16GB RAM, 65 providers, ~100 accounts, ~2600 models). A new `calibrateHeapFallbackMb(os.totalmem())` helper derives the default heap as ~35% of physical RAM, clamped to `[512, 4096]`, and is wired into both `bin/cli/commands/serve.mjs` and `electron/main.js`. An explicit `OMNIROUTE_MEMORY_MB` (or a pre-set `--max-old-space-size`) still wins, so the #2939 override contract is unchanged. ([#5172](https://github.com/diegosouzapw/OmniRoute/issues/5172), [#5160](https://github.com/diegosouzapw/OmniRoute/issues/5160), [#5152](https://github.com/diegosouzapw/OmniRoute/issues/5152) — thanks @manchairwang, @Xyzjesus)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@ import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { platform } from "node:os";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
|
||||
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
|
||||
import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
} from "../../../scripts/build/runtime-env.mjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..", "..", "..");
|
||||
@@ -126,9 +130,13 @@ export async function runServe(opts = {}) {
|
||||
|
||||
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
|
||||
|
||||
const rawMemory = parseInt(process.env.OMNIROUTE_MEMORY_MB || "512", 10);
|
||||
const memoryLimit =
|
||||
Number.isFinite(rawMemory) && rawMemory >= 64 && rawMemory <= 16384 ? rawMemory : 512;
|
||||
// #5172/#5160/#5152: default the V8 heap to ~35% of physical RAM (clamped
|
||||
// [512, 4096]) instead of a fixed 512MB, which OOM-crashed boxes with plenty
|
||||
// of RAM under load. An explicit OMNIROUTE_MEMORY_MB still wins.
|
||||
const memoryLimit = resolveMaxOldSpaceMb(
|
||||
process.env.OMNIROUTE_MEMORY_MB,
|
||||
calibrateHeapFallbackMb(totalmem())
|
||||
);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
||||
@@ -614,8 +614,32 @@ function startNextServer() {
|
||||
|
||||
const nodeExecutable = resolveNodeExecutable(serverEnv);
|
||||
|
||||
// #5172/#5160/#5152: the Electron-spawned server inherited the runtime's low
|
||||
// default V8 heap (~512MB) and OOM-crashed on RAM-rich boxes under load
|
||||
// (65 providers / 2600 models → "Ineffective mark-compacts near heap limit").
|
||||
// Default the heap to ~35% of physical RAM (clamped [512, 4096]); an explicit
|
||||
// OMNIROUTE_MEMORY_MB or a pre-set --max-old-space-size still wins. Mirrors
|
||||
// scripts/build/runtime-env.mjs (CJS can't import the ESM helper).
|
||||
const serverNodeOptions = (() => {
|
||||
const existing = serverEnv.NODE_OPTIONS || "";
|
||||
if (existing.includes("--max-old-space-size")) return existing;
|
||||
const explicit = parseInt(serverEnv.OMNIROUTE_MEMORY_MB, 10);
|
||||
let heapMb;
|
||||
if (Number.isFinite(explicit) && explicit >= 64 && explicit <= 16384) {
|
||||
heapMb = explicit;
|
||||
} else {
|
||||
const totalMb = require("os").totalmem() / (1024 * 1024);
|
||||
heapMb =
|
||||
Number.isFinite(totalMb) && totalMb > 0
|
||||
? Math.min(4096, Math.max(512, Math.floor(totalMb * 0.35)))
|
||||
: 512;
|
||||
}
|
||||
return `${existing} --max-old-space-size=${heapMb}`.trim();
|
||||
})();
|
||||
|
||||
console.log("[Electron] Starting Next.js server on port", serverPort);
|
||||
console.log("[Electron] Using Node executable:", nodeExecutable);
|
||||
console.log("[Electron] Server NODE_OPTIONS:", serverNodeOptions);
|
||||
sendToRenderer("server-status", { status: "starting", port: serverPort });
|
||||
|
||||
// Fix #10: Use pipe instead of inherit for logging & readiness detection
|
||||
@@ -630,6 +654,7 @@ function startNextServer() {
|
||||
NODE_ENV: "production",
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
NODE_PATH: resolveServerNodePath(serverEnv),
|
||||
NODE_OPTIONS: serverNodeOptions,
|
||||
},
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
|
||||
@@ -19,6 +19,23 @@ export function resolveMaxOldSpaceMb(value, fallback = 512) {
|
||||
return Number.isFinite(parsed) && parsed >= 64 && parsed <= 16384 ? parsed : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a sane DEFAULT V8 heap ceiling (MB) from the host's physical RAM, used
|
||||
* when `OMNIROUTE_MEMORY_MB` is unset. A fixed 512MB default crashed boxes with
|
||||
* plenty of RAM under load (65 providers / 2600 models → "Ineffective
|
||||
* mark-compacts near heap limit ~500MB"); see #5172 / #5160 / #5152. Targets
|
||||
* ~35% of total RAM, clamped to [512, 4096]. Invalid/zero totalmem → 512.
|
||||
* Pass the result as the `fallback` of {@link resolveMaxOldSpaceMb} so an
|
||||
* explicit OMNIROUTE_MEMORY_MB override always wins.
|
||||
* @param {number | undefined | null} totalmemBytes — typically `os.totalmem()`
|
||||
*/
|
||||
export function calibrateHeapFallbackMb(totalmemBytes) {
|
||||
const totalMb = Number(totalmemBytes) / (1024 * 1024);
|
||||
if (!Number.isFinite(totalMb) || totalMb <= 0) return 512;
|
||||
const target = Math.floor(totalMb * 0.35);
|
||||
return Math.min(4096, Math.max(512, target));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
|
||||
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.
|
||||
|
||||
50
tests/unit/heap-autocalibrate-5172.test.ts
Normal file
50
tests/unit/heap-autocalibrate-5172.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Issue #5172 / #5160 / #5152 — server OOM ("Ineffective mark-compacts near heap
|
||||
* limit ... ~500MB") on machines with plenty of RAM. Root cause: the server was
|
||||
* spawned with a FIXED 512MB heap default (`omniroute serve`) or with no
|
||||
* `--max-old-space-size` at all (Electron), so a 16GB box with 65 providers /
|
||||
* 2600 models still crashed at ~512MB.
|
||||
*
|
||||
* Fix: `calibrateHeapFallbackMb(totalmemBytes)` derives a sane default heap from
|
||||
* the host's physical RAM (~35%, clamped to [512, 4096]) so the out-of-the-box
|
||||
* ceiling scales with the machine. An explicit `OMNIROUTE_MEMORY_MB` still wins
|
||||
* (resolveMaxOldSpaceMb), and the existing #2939 contract is unchanged.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { calibrateHeapFallbackMb, resolveMaxOldSpaceMb } =
|
||||
await import("../../scripts/build/runtime-env.mjs");
|
||||
|
||||
const GB = 1024 * 1024 * 1024;
|
||||
|
||||
test("#5172 calibrates the default heap to ~35% of physical RAM", () => {
|
||||
// 8GB → 8192 * 0.35 ≈ 2867
|
||||
assert.equal(calibrateHeapFallbackMb(8 * GB), 2867);
|
||||
// 4GB → floor(4096 * 0.35) = 1433
|
||||
assert.equal(calibrateHeapFallbackMb(4 * GB), 1433);
|
||||
});
|
||||
|
||||
test("#5172 clamps the calibrated default to [512, 4096]", () => {
|
||||
// 16GB → 5734 → clamped to the 4096 ceiling (the reporter's box)
|
||||
assert.equal(calibrateHeapFallbackMb(16 * GB), 4096);
|
||||
assert.equal(calibrateHeapFallbackMb(64 * GB), 4096);
|
||||
// 1GB → 358 → floored to 512
|
||||
assert.equal(calibrateHeapFallbackMb(1 * GB), 512);
|
||||
});
|
||||
|
||||
test("#5172 falls back to 512 for missing/invalid totalmem", () => {
|
||||
assert.equal(calibrateHeapFallbackMb(0), 512);
|
||||
assert.equal(calibrateHeapFallbackMb(undefined), 512);
|
||||
assert.equal(calibrateHeapFallbackMb(null), 512);
|
||||
assert.equal(calibrateHeapFallbackMb(NaN), 512);
|
||||
assert.equal(calibrateHeapFallbackMb(-1), 512);
|
||||
});
|
||||
|
||||
test("#5172 an explicit OMNIROUTE_MEMORY_MB still wins over the calibrated default", () => {
|
||||
const calibrated = calibrateHeapFallbackMb(16 * GB); // 4096
|
||||
// explicit override (in-range) is honored verbatim, not the calibrated default
|
||||
assert.equal(resolveMaxOldSpaceMb("1536", calibrated), 1536);
|
||||
// unset → the calibrated default is used (not the old fixed 512)
|
||||
assert.equal(resolveMaxOldSpaceMb(undefined, calibrated), 4096);
|
||||
});
|
||||
Reference in New Issue
Block a user