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
9 changes: 7 additions & 2 deletions .github/workflows/dast-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
env:
PORT: "20128"
INJECTION_GUARD_MODE: block
REQUIRE_API_KEY: "false"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
Expand All @@ -64,16 +65,20 @@ jobs:
# those 302s as "the API accepted a schema-violating request" and the configured-off
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
# still right (operators need it); fuzzing it is not what this smoke is for.
# /api/auth/login has brute-force rate limiting: repeated failed logins return 429,
# which Schemathesis flags as rejection of schema-compliant requests.
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
--exclude-path-regex '^/api/auth/oidc/' \
--exclude-path-regex '^/api/auth/(oidc/|login)' \
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
--request-timeout 20 --suppress-health-check all --no-color
- name: Install promptfoo
run: npm install -g promptfoo@0.122.0
- name: promptfoo injection-guard (blocking)
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
run: promptfoo eval -c promptfooconfig.yaml --no-cache
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
Expand Down
89 changes: 89 additions & 0 deletions Dockerfile.bun
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
FROM oven/bun:1.3.14-slim AS base
WORKDIR /app

RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
build-essential \
python3 \
python-is-python3 \
make \
g++ \
libsecret-1-0 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*

# ── Builder stage (100% Bun Native Install & Build) ─────────────────────────
FROM base AS builder
WORKDIR /app

COPY . .

# Fast Bun native package install
RUN bun install --include=optional --quiet

# Compile native better-sqlite3 Node-API addon under Bun
RUN if [ -d "node_modules/better-sqlite3" ]; then \
(cd node_modules/better-sqlite3 && bunx node-gyp rebuild); \
fi

# Fetch tls-client-node native binary if script exists
RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ]; then \
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
fi

# Disable Turbopack for Bun builder stage (Turbopack V8 internal worker bindings require Node)
ENV OMNIROUTE_USE_TURBOPACK=0

ARG OMNIROUTE_BASE_PATH=""
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH

ARG DASHBOARD_ALLOW_EMBED=""
ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED

ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production

# Bun native Next.js build execution
RUN bun run --quiet build

# ── Runner stage (100% Bun Native Production Runtime) ──────────────────────
FROM oven/bun:1.3.14-slim AS runner

LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \
org.opencontainers.image.url="https://omniroute.online" \
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
org.opencontainers.image.licenses="MIT"

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libsecret-1-0 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*

ENV NODE_ENV=production
ENV PORT=20128
ENV HOSTNAME=0.0.0.0
ENV OMNIROUTE_MEMORY_MB=1024

ENV DATA_DIR=/app/data
RUN mkdir -p /app/data

COPY --from=builder /app/.build/next/standalone ./
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations

COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs

EXPOSE 20128

HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD bun healthcheck.mjs || exit 1

ENTRYPOINT ["bun", "bin/omniroute.mjs", "serve", "--no-open"]
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,19 @@ Full table: [Docker Guide — runtime RAM](docs/guides/DOCKER_GUIDE.md#runtime-r
> are **not supported for production**. See
> [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).

**🥟 Bun**

Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).

```bash
# Install and run with Bun
bun install
bun run dev
```

**🛠️ From source**

```bash
Expand Down
7 changes: 5 additions & 2 deletions bin/cli/commands/plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import { discoverPlugins } from "../plugins.mjs";
// (instead of string-interpolating into `execSync`) prevents a malicious plugin
// name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell.
function runNpm(args) {
const res = spawnSync("npm", args, { stdio: "inherit", shell: false });
const isBun = Boolean(process.versions.bun);
const pm = isBun ? "bun" : "npm";
const cmdArgs = isBun && args[0] === "install" ? ["add", ...args.slice(1)] : args;
const res = spawnSync(pm, cmdArgs, { stdio: "inherit", shell: false });
if (res.error) throw res.error;
if (typeof res.status === "number" && res.status !== 0) {
throw new Error(`npm exited with code ${res.status}`);
throw new Error(`${pm} exited with code ${res.status}`);
}
}

Expand Down
44 changes: 22 additions & 22 deletions bin/cli/runtime/nativeDeps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,30 +114,30 @@ export function isBetterSqliteBinaryValid() {

export function npmInstallRuntime(pkgs, opts = {}) {
const cwd = ensureRuntimeDir();
// Persist to the runtime package.json (exact version) instead of --no-save so a later
// install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the
// same runtime dir) does not prune this package as "extraneous" — that pruning otherwise
// reproduces "No SQLite driver available" after a tray install removes better-sqlite3.
// npm 12+ defaults `allowScripts` to off, silently skipping lifecycle/install
// scripts (e.g. better-sqlite3's node-gyp/prebuild-install rebuild) unless the
// package has a matching `allowScripts` entry — and still exits 0, masking the
// failure (#10713). The runtime dir is a CLI-owned, non-user package.json, so
// explicitly allowing scripts for the packages we are installing here is safe.
const npmArgs = [
"install",
...pkgs,
"--no-audit",
"--no-fund",
"--prefer-online",
"--save-exact",
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
];
// On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly
// so we never set shell:true (which would propagate env and enable injection).
const isWin = platform() === "win32";
const [exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
const isBun = Boolean(process.versions.bun);

let exe, args, displayCmd;
if (isBun) {
const bunArgs = ["add", ...pkgs, "--trust"];
[exe, args] = isWin ? ["cmd.exe", ["/c", "bun", ...bunArgs]] : ["bun", bunArgs];
displayCmd = `bun ${bunArgs.join(" ")}`;
} else {
const npmArgs = [
"install",
...pkgs,
"--no-audit",
"--no-fund",
"--prefer-online",
"--save-exact",
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
];
[exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
displayCmd = `npm ${npmArgs.join(" ")}`;
}

if (!opts.silent) {
process.stdout.write(`[omniroute][runtime] npm ${npmArgs.join(" ")}\n`);
process.stdout.write(`[omniroute][runtime] ${displayCmd}\n`);
}
const res = spawnSync(exe, args, {
cwd,
Expand Down
27 changes: 16 additions & 11 deletions bin/cli/sqlite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./

async function loadSqlite() {
if (process.versions.bun) {
return { Database: (await import("bun:sqlite")).Database };
try {
return { Database: (await import("bun:sqlite")).Database, driver: "bun:sqlite" };
} catch (bunError) {
// fall through to better-sqlite3 if bun:sqlite fails
}
}
try {
return { Database: (await import("better-sqlite3")).default };
return { Database: (await import("better-sqlite3")).default, driver: "better-sqlite3" };
} catch (error) {
return { error };
}
Expand Down Expand Up @@ -86,12 +90,14 @@ export function normalizeBunSqliteParams(params) {

export function createSqliteNativeError(error) {
const message = error instanceof Error ? error.message : String(error);
const isBun = Boolean(process.versions.bun);
const rebuildCmd = isBun ? "bun add better-sqlite3 --trust" : "npm rebuild better-sqlite3";
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. " +
"Or run: omniroute runtime repair " +
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
`better-sqlite3 native binding is incompatible with this runtime. ` +
`Run \`${rebuildCmd}\` in the OmniRoute project and try again. ` +
`Or run: omniroute runtime repair ` +
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
);
}
if (
Expand All @@ -100,18 +106,17 @@ export function createSqliteNativeError(error) {
message.includes("Cannot find module 'better-sqlite3'")
) {
return new Error(
"better-sqlite3 native binding could not be found (no prebuilt addon for this platform). " +
"This is common under `npx`, which runs a fresh, ephemeral install that never built the addon. " +
"Run: omniroute runtime repair " +
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
`better-sqlite3 native binding could not be found (no prebuilt addon for this platform). ` +
`Run: omniroute runtime repair ` +
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
);
}
return error;
}

async function openSqliteDatabase(dbPath, options = {}) {
const loaded = await loadSqlite();
if (process.versions.bun) {
if (loaded.driver === "bun:sqlite" || (process.versions.bun && !loaded.Database)) {
if (options.fileMustExist && !fs.existsSync(dbPath)) {
throw new Error(`SQLite file does not exist: ${dbPath}`);
}
Expand Down
12 changes: 12 additions & 0 deletions bin/nodeRuntimeSupport.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ export function getSecureFloorForMajor(major) {
}

export function getNodeRuntimeSupport(version = process.versions.node) {
if (process.versions.bun) {
return {
nodeVersion: `bun-${process.versions.bun} (Node.js API ${version})`,
nodeCompatible: true,
reason: "supported-bun",
supportedRange: SUPPORTED_NODE_RANGE + " || Bun >=1.1.0",
supportedDisplay: SUPPORTED_NODE_DISPLAY + ", or Bun 1.1+",
recommendedVersion: `v${RECOMMENDED_NODE_VERSION}`,
minimumSecureVersion: null,
};
}

const parsed = parseNodeVersion(version);
const secureFloor = getSecureFloorForMajor(parsed.major);
const nodeCompatible = secureFloor ? compareNodeVersions(parsed, secureFloor) >= 0 : false;
Expand Down
10 changes: 8 additions & 2 deletions bin/omniroute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import updateNotifier from "update-notifier";
let updateNotifier = null;
try {
updateNotifier = (await import("update-notifier")).default;
} catch {
// update-notifier is optional in pruned standalone environments
}
import { isNativeBinaryCompatible } from "../scripts/build/native-binary-compat.mjs";
import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs";
import { getDefaultDataDir } from "./cli/data-dir.mjs";
Expand Down Expand Up @@ -251,8 +256,9 @@ if (shouldProvisionStorageKey(process.argv)) {

// Register update notifier — checks npm once per 24h, notifies on exit via stderr.
const _pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const _notifier = updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
const _notifier = updateNotifier ? updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }) : null;
process.on("exit", () => {
if (!_notifier || !_notifier.update) return;
if (process.env.OMNIROUTE_NO_UPDATE_NOTIFIER) return;
if (process.env.CI) return;
if (process.argv.includes("--quiet") || process.argv.includes("-q")) return;
Expand Down
7 changes: 0 additions & 7 deletions config/quality/open-sse-typecheck-baseline.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
{
"open-sse/handlers/chatCore/clientUsageBuffer.ts": {
"TS2345": 2
},
"open-sse/utils/stream.ts": {
"TS2345": 2,
"TS2322": 2
},
"src/lib/guardrails/videoBridgeHelpers.ts": {
"TS2488": 1,
"TS2365": 2,
Expand Down
2 changes: 1 addition & 1 deletion open-sse/services/autoCombo/virtualFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ export async function prepareVirtualAutoComboInputs(
// remaining allowance as a percentage, and a raw ">0" comparison would
// let a reading of e.g. 0.3% (rounding noise, not real headroom) pass.
minRemainingAllowance: 1,
maxStateAgeMs: (settings.autoRefreshProviderQuotaInterval ?? 180) * 1000,
maxStateAgeMs: (Number(settings.autoRefreshProviderQuotaInterval) || 180) * 1000,
});
if (strictFilteredPool !== pool) pool = strictFilteredPool;

Expand Down
12 changes: 6 additions & 6 deletions scripts/build/build-next-isolated.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ function runNextBuild() {
}

export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
// Turbopack is the default production bundler (Next 16 stable). Benchmarked on
// this codebase: 2-3x faster than the single-threaded webpack pass (17min -> 9min
// on a 32-core box; ~20min -> 7min on ubuntu-latest), artifact validated
// end-to-end (standalone smoke + e2e/package/electron CI jobs). Webpack stays as
// the explicit escape hatch (=0) for bundler-compat regressions.
return baseEnv.OMNIROUTE_USE_TURBOPACK === "0" ? "--webpack" : "--turbopack";
// Turbopack is the default on Node.js; on Bun or when explicitly disabled (=0),
// use Webpack (--webpack) to avoid Turbopack V8 internal worker API mismatches.
if (process.versions.bun || baseEnv.OMNIROUTE_USE_TURBOPACK === "0") {
return "--webpack";
}
return "--turbopack";
}

/**
Expand Down
12 changes: 9 additions & 3 deletions scripts/check/check-supported-node-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ if (!support.nodeCompatible) {
process.exit(1);
}

console.log(
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
);
if (process.versions.bun) {
console.log(
`Bun ${process.versions.bun} (${support.nodeVersion}) satisfies OmniRoute secure runtime policy.`
);
} else {
console.log(
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
);
}
6 changes: 4 additions & 2 deletions scripts/dev/run-next.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ const { dashboardPort } = runtimePorts;
const hostname = process.env.HOST || "0.0.0.0";
// Turbopack by default in dev (matches the Next 16 CLI default and the production
// build default in build-next-isolated.mjs); OMNIROUTE_USE_TURBOPACK=0 is the
// webpack escape hatch.
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0";
// webpack escape hatch. Under Bun, Turbopack native V8 bindings are unavailable,
// so Bun automatically disables Turbopack and uses Webpack.
const isBun = Boolean(process.versions.bun);
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0" && !isBun;
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
// Per-process secret used to prove the trusted peer-IP stamp came from this
// server (read by the authz middleware in the same process). See peer-stamp.mjs.
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/acp/agents/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";

export const dynamic = "force-dynamic";
import {
type CliAgentInfo,
detectInstalledAgents,
Expand Down
1 change: 1 addition & 0 deletions src/app/api/admin/concurrency/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getAllRateLimitStatus } from "@omniroute/open-sse/services/rateLimitManager.ts";
import {
getStats as getSemaphoreStats,
Expand Down
Loading
Loading