Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### 🐛 Fixed

- **fix(intelligence): run pricing + models.dev sync from the live startup path** — like the Arena ELO sync (v3.8.24), the external **pricing sync** (`PRICING_SYNC_ENABLED`) and the **models.dev capability sync** (Settings → AI toggle) were only initialized from `server-init.ts`, which the Next standalone runtime never executes — and models.dev had no caller at all. Their toggles were inert in production. Both are now initialized from `instrumentation-node.ts` (self-gated, opt-in preserved, non-blocking, never fatal). (thanks @diegosouzapw)
- **fix(cli): surface `omniroute runtime repair` in the native-module error messages** — after a Node major upgrade, `better-sqlite3`'s prebuilt binary mismatches the ABI and the service can crash-loop; the error only mentioned `npm rebuild better-sqlite3` (which fails for global / no-toolchain installs). The startup + SQLite error hints now also point to the existing self-heal command `omniroute runtime repair` (rebuilds into a user-writable runtime), and a top-level `omniroute repair` alias was added. ([#3476](https://github.com/diegosouzapw/OmniRoute/issues/3476) — thanks @Rahulsharma0810)
- **fix(antigravity): per-request Pro-family upstream-id fallback chain (`gemini-3.1-pro-high` 400)** — Antigravity silently renamed the Gemini 3.1 Pro-high upstream id, so `gemini-3.1-pro-high` started returning HTTP 400 (while `-low` still worked) and the live id can't be determined statically (competitor proxies disagree). The executor now retries alternative ids on a 400 (`gemini-3.1-pro-high` → `gemini-pro-agent` → `gemini-3-pro-high`, analogous for pro-low), bounded and only on a 400, with zero extra cost on the happy path; the 1:1 tier-passthrough invariant is preserved (the chain is request-time, not a static alias remap). ([#3786](https://github.com/diegosouzapw/OmniRoute/issues/3786) — thanks @aliaksandrsen)
- **fix(sse): retry once on an early stream close (`STREAM_EARLY_EOF`) for single-model requests** — flaky OpenAI-compatible upstreams (e.g. NVIDIA NIM with minimax-m3 / qwen3.5 / glm-5.1) intermittently send HTTP 200 then close the SSE with zero useful frames, surfacing as a 502 "Stream ended before producing useful content". Only Antigravity got an early-close retry; every other provider returned the 502 immediately on the non-combo single-model path. A bounded one-retry (early-close only — not readiness-timeout — and without marking the account unavailable) now generalizes it. (The separate qwen-web validation SSRF part of the same report was already fixed in v3.8.24, [#3767](https://github.com/diegosouzapw/OmniRoute/pull/3767).) ([#3758](https://github.com/diegosouzapw/OmniRoute/issues/3758) — thanks @Svatosalav)
- **fix(models): preserve eye-hidden models across auto-sync / import** — hiding models via the visibility (eye) toggle to keep only a combo's models was undone on every model import or auto-sync, which re-showed all of them. The sync re-import treated "hidden" identically to "deleted" and dropped both; a distinct `isDeleted` marker now separates the trash/delete path (still dropped on re-import, #3199) from the eye toggle (preserved as listed-but-hidden), and eye-hidden models are no longer re-aliased into the routable catalog on sync. ([#3782](https://github.com/diegosouzapw/OmniRoute/issues/3782) — thanks @xenstar)
Expand Down
48 changes: 33 additions & 15 deletions bin/cli/commands/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ async function confirm(msg) {
return /^y(es)?$/i.test(answer);
}

/**
* Shared repair action used by both `omniroute runtime repair` and the
* top-level `omniroute repair` alias. Reinstalls better-sqlite3 into the
* user-writable runtime directory via the existing engine — no hand-rolled
* npm-rebuild spawn. Exits with code 1 on failure.
*/
async function runRepairAction(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
await withSpinner(
"Repairing native deps",
async () => ensureBetterSqliteRuntime({ silent: true, force: opts.force }),
globalOpts
);
const ok = hasModule("better-sqlite3") && isBetterSqliteBinaryValid();
if (ok) {
process.stdout.write("✓ better-sqlite3 repaired OK\n");
} else {
process.stderr.write("✗ Repair failed — check npm availability\n");
process.exit(1);
}
}

export function registerRuntime(program) {
const runtime = program
.command("runtime")
Expand All @@ -41,21 +63,17 @@ export function registerRuntime(program) {
.command("repair")
.description("Reinstall native deps in runtime directory")
.option("--force", "Force reinstall even if valid")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
await withSpinner(
"Repairing native deps",
async () => ensureBetterSqliteRuntime({ silent: true, force: opts.force }),
globalOpts
);
const ok = hasModule("better-sqlite3") && isBetterSqliteBinaryValid();
if (ok) {
process.stdout.write("✓ better-sqlite3 repaired OK\n");
} else {
process.stderr.write("✗ Repair failed — check npm availability\n");
process.exit(1);
}
});
.action(runRepairAction);

// Top-level discoverability alias: `omniroute repair` invokes the SAME action
// as `omniroute runtime repair` (no duplicated logic). Surfaced in the
// native-error / startup hints so users with a broken better-sqlite3 binding
// have a single self-heal command that works without a C++ toolchain.
program
.command("repair")
.description("Repair native deps (alias for `runtime repair`)")
.option("--force", "Force reinstall even if valid")
.action(runRepairAction);

runtime
.command("clean")
Expand Down
11 changes: 9 additions & 2 deletions bin/cli/commands/serve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const ROOT = join(__dirname, "..", "..", "..");
// refactor). Fall back to the legacy `app/` location so an upgrade over a
// partially-replaced install — or a package built before the rename — still
// boots. Backward-compatible by design: every deployed runtime keeps its path.
const APP_DIR = existsSync(join(ROOT, "dist", "server.js")) ? join(ROOT, "dist") : join(ROOT, "app");
const APP_DIR = existsSync(join(ROOT, "dist", "server.js"))
? join(ROOT, "dist")
: join(ROOT, "app");

function parsePort(value, fallback) {
const parsed = parseInt(String(value), 10);
Expand Down Expand Up @@ -66,7 +68,8 @@ export async function runServe(opts = {}) {

Supported secure runtimes: ${nodeSupport.supportedDisplay}
Recommended: use Node.js ${nodeSupport.recommendedVersion} or newer on the 22.x LTS line.
Workaround: npm rebuild better-sqlite3\x1b[0m
Workaround: npm rebuild better-sqlite3
Or run: omniroute runtime repair (rebuilds into a user-writable runtime; works without a C++ toolchain)\x1b[0m
`);
}

Expand Down Expand Up @@ -111,6 +114,10 @@ export async function runServe(opts = {}) {
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
);
console.error(` Run: cd ${APP_DIR} && npm rebuild better-sqlite3`);
console.error(
" Or run: \x1b[36momniroute runtime repair\x1b[0m" +
" (rebuilds into a user-writable runtime; works without a C++ toolchain)"
);
if (platform() === "darwin") {
console.error(" If build tools are missing: xcode-select --install");
}
Expand Down
6 changes: 4 additions & 2 deletions bin/cli/sqlite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ async function loadBetterSqlite() {
}
}

function createSqliteNativeError(error) {
export function createSqliteNativeError(error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) {
return new Error(
"better-sqlite3 native binding is incompatible with this Node.js runtime. " +
"Run `npm rebuild better-sqlite3` in the OmniRoute project and try again."
"Run `npm rebuild better-sqlite3` in the OmniRoute project and try again. " +
"Or run: omniroute runtime repair " +
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
);
}
return error;
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/sqlite-repair-discoverability-3476.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

import { createProgram } from "../../bin/cli/program.mjs";
import { createSqliteNativeError } from "../../bin/cli/sqlite.mjs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const BIN_CLI = join(__dirname, "..", "..", "bin", "cli");

// ─── #3476: discoverability of `omniroute runtime repair` ──────────────────────
//
// When better-sqlite3's native binding mismatches the Node ABI, OmniRoute prints
// an error/startup hint. Historically those hints only suggested
// `npm rebuild better-sqlite3`, which fails for global / no-toolchain installs.
// A self-heal command already exists (`omniroute runtime repair`); these tests
// guard that the hint text now points users to it.

test("createSqliteNativeError message mentions `runtime repair` on ABI mismatch", () => {
const abiError = new Error(
"The module was compiled against a different Node.js version using NODE_MODULE_VERSION 115."
);
const produced = createSqliteNativeError(abiError);
assert.ok(produced instanceof Error, "returns an Error");
assert.match(
produced.message,
/runtime repair/,
`expected the native-error message to mention \`runtime repair\`, got: ${produced.message}`
);
});

test("createSqliteNativeError message mentions `runtime repair` on ERR_DLOPEN_FAILED", () => {
const dlopenError = new Error("Error: ERR_DLOPEN_FAILED — cannot open shared object");
const produced = createSqliteNativeError(dlopenError);
assert.match(
produced.message,
/runtime repair/,
`expected the native-error message to mention \`runtime repair\`, got: ${produced.message}`
);
});

test("createSqliteNativeError passes through unrelated errors untouched", () => {
const unrelated = new Error("disk full");
const produced = createSqliteNativeError(unrelated);
assert.equal(produced, unrelated, "non-native errors are returned as-is");
});

test("serve.mjs ABI-mismatch hint source mentions `runtime repair`", () => {
const src = readFileSync(join(BIN_CLI, "commands", "serve.mjs"), "utf8");
assert.match(
src,
/runtime repair/,
"serve.mjs should surface `omniroute runtime repair` as a recovery hint"
);
});

test("registerRuntime registers a top-level `repair` alias command", () => {
const program = createProgram();
const repair = program.commands.find((c) => c.name() === "repair");
assert.ok(repair, "top-level `repair` command exists");
});

test("`runtime repair` subcommand still exists", () => {
const program = createProgram();
const runtime = program.commands.find((c) => c.name() === "runtime");
assert.ok(runtime, "runtime command exists");
const repairSub = runtime.commands.find((c) => c.name() === "repair");
assert.ok(repairSub, "`runtime repair` subcommand exists");
});

test("top-level `repair` alias and `runtime repair` share the same handler module", () => {
// The alias must not hand-roll its own npm-rebuild spawn — it must invoke the
// existing engine (`ensureBetterSqliteRuntime`). Guard against logic
// duplication by asserting both call into nativeDeps' export.
const src = readFileSync(join(BIN_CLI, "commands", "runtime.mjs"), "utf8");
assert.match(
src,
/ensureBetterSqliteRuntime/,
"runtime.mjs must use the existing ensureBetterSqliteRuntime engine"
);
// The repair action helper is invoked from both registrations.
const repairActionRefs = (src.match(/runRepairAction/g) || []).length;
assert.ok(
repairActionRefs >= 2,
`expected the shared repair action to be referenced by both \`runtime repair\` and the top-level alias, found ${repairActionRefs} reference(s)`
);
});