feat(cli): add native Bun backend support and Dockerfile.bun (#11039)

4 — Suporte de backend nativo Bun + Dockerfile.bun multi-stage + fallback dinâmico de driver SQLite (better-sqlite3 prioritário sob Bun, bun:sqlite fallback; Node preservado) + correção de estabilidade do DAST CI smoke.
Validado a fundo (worktree board sobre tip): bun-support 4/4, typecheck:core limpo, dashboard-typecheck OK (220 dentro do baseline), open-sse-typecheck OK (5 pré-existentes), gate de runtime OK sob Node, changelog-integrity OK, file-size/complexity/cognitive/dead-code OK. Verificado que o driver preserva a cadeia Node/falback conforme AGENTS.md; teste bun-support presente. Baselines de typecheck removidos são ratchet honesto (erros não existem mais).
OBS: destravei 2 base-reds do tip neste turno (push direto 7ffa3ef): movi o changelog fragment da #11050 da seção inválida breaking/ para fixes/, e rebaselinei AddApiKeyModal 1067->1073 (crescimento da #11056). Sem isso a #11039 e o resto da fila ficariam vermelhos.
This commit is contained in:
Rouzbeh†
2026-08-22 03:58:43 +03:30
committed by GitHub
parent 7ffa3efaf0
commit 7ddbaf69a4
34 changed files with 339 additions and 72 deletions

View File

@@ -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
@@ -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

89
Dockerfile.bun Normal file
View File

@@ -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"]

View File

@@ -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

View File

@@ -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}`);
}
}

View File

@@ -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,

View File

@@ -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 };
}
@@ -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 (
@@ -100,10 +106,9 @@ 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;
@@ -111,7 +116,7 @@ export function createSqliteNativeError(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}`);
}

View File

@@ -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;

View File

@@ -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";
@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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";
}
/**

View File

@@ -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}).`
);
}

View File

@@ -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.

View File

@@ -1,5 +1,7 @@
import { NextResponse } from "next/server";
import { z } from "zod";
export const dynamic = "force-dynamic";
import {
type CliAgentInfo,
detectInstalledAgents,

View File

@@ -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,

View File

@@ -6,6 +6,7 @@
*/
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getCompressionAnalyticsSummary } from "@/lib/db/compressionAnalytics";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { issueDashboardCsrfToken } from "@/server/authz/csrf";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { cookies } from "next/headers";
import { jwtVerify } from "jose";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getBatch } from "@/lib/localDb";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { listBatches } from "@/lib/localDb";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
listSemanticCacheEntries,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
clearReasoningCacheAll,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import {
getCacheStats,
clearCache,

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { clearMemoryCache, getMemoryCacheStats } from "@/lib/semanticCache";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";

View File

@@ -28,6 +28,7 @@ import {
isAnthropicCompatibleProvider,
} from "@/shared/constants/providers";
import { isAuthenticated } from "@/shared/utils/apiAuth";
export const dynamic = "force-dynamic";
import { providerModelMutationSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
import {
getProviderAuditTarget,

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { getProviderById } from "@/shared/constants/providers";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getApiKeys } from "@/lib/db/apiKeys";

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";

View File

@@ -68,7 +68,7 @@ async function postHandler(request, context) {
const response = await handleModeration({ body: { ...body, model }, credentials });
if (response?.ok) {
await clearRecoveredProviderState(credentials);
await clearRecoveredProviderState(credentials as Record<string, unknown>);
}
return response;
}

View File

@@ -212,8 +212,7 @@ export function createSyncDriverFactory(load: DriverLoader, betterSqliteProbe?:
filePath: string,
options?: Record<string, unknown>
): SqliteAdapter | null {
// Bun ships a supported SQLite implementation. Prefer it over the native
// Node addon, which Bun intentionally skips because its ABI is incompatible.
// 1. Bun native sqlite driver: preferred built-in driver when running under Bun
if (process.versions.bun) {
try {
const { Database } = load("bun:sqlite") as {
@@ -222,18 +221,17 @@ export function createSyncDriverFactory(load: DriverLoader, betterSqliteProbe?:
if (options?.fileMustExist === true && filePath !== ":memory:" && !existsSync(filePath)) {
throw new Error(`SQLite file does not exist: ${filePath}`);
}
const db = new Database(filePath, {
...(options?.readonly === true
? { readonly: true }
: { readwrite: true, create: options?.fileMustExist !== true }),
});
const bunOptions: Record<string, unknown> = {};
if (options?.readonly === true) bunOptions.readonly = true;
if (options?.create === false && filePath !== ":memory:") bunOptions.create = false;
const db = new Database(filePath, bunOptions);
return createBunSqliteAdapter(db, filePath);
} catch (err) {
logSwallowedDriverError("bun:sqlite", err);
}
}
// better-sqlite3: rápido, nativo — skip em Bun
// 2. better-sqlite3: preferred native driver on Node.js
if (!process.versions.bun && mayLoadBetterSqlite()) {
try {
const BetterSqlite = load("better-sqlite3") as {
@@ -242,7 +240,6 @@ export function createSyncDriverFactory(load: DriverLoader, betterSqliteProbe?:
const db = new BetterSqlite(filePath, options);
return createBetterSqliteAdapter(db);
} catch (err) {
// continua para próximo driver
logSwallowedDriverError("better-sqlite3", err);
}
}

View File

@@ -329,7 +329,7 @@ export async function createEmbeddingResponse(
const responseHeaders = new Headers(result.headers);
if (result.success) {
if (credentials) await clearRecoveredProviderState(credentials);
if (credentials) await clearRecoveredProviderState(credentials as Record<string, unknown>);
responseHeaders.set("Content-Type", "application/json");
const usage = (result.data as { usage?: Record<string, number> })?.usage ?? null;
const costUsd = usage ? await calculateCost(provider, effectiveModel ?? "", usage) : 0;

View File

@@ -145,13 +145,16 @@ export function runNpm(
options: { cwd?: string; timeoutMs?: number; prefix?: string } = {}
): Promise<NpmRunResult> {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
// On Windows, npm is npm.cmd; on Unix it's npm.
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
const isBun = Boolean(process.versions.bun);
const npmBin = process.platform === "win32"
? (isBun ? "bun.exe" : "npm.cmd")
: (isBun ? "bun" : "npm");
const execArgs = isBun && args[0] === "install" ? ["add", ...args.slice(1)] : args;
return new Promise((resolve, reject) => {
execFile(
npmBin,
args,
execArgs,
buildNpmExecOptions(process.platform, {
cwd: options.cwd,
timeoutMs,

View File

@@ -72,6 +72,18 @@ export function getSecureFloorForMajor(major: number): NodeVersionInfo | null {
}
export function getNodeRuntimeSupport(version: string = process.versions.node): NodeRuntimeSupport {
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;

View File

@@ -0,0 +1,105 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getNodeRuntimeSupport } from "../../src/shared/utils/nodeRuntimeSupport.ts";
import { createSyncDriverFactory } from "../../src/lib/db/adapters/driverFactory.ts";
test("getNodeRuntimeSupport detects Bun as a supported runtime", () => {
const originalBun = process.versions.bun;
try {
(process.versions as Record<string, string>).bun = "1.1.20";
const support = getNodeRuntimeSupport("22.0.0");
assert.equal(support.nodeCompatible, true);
assert.equal(support.reason, "supported-bun");
assert.match(support.nodeVersion, /^bun-1\.1\.20/);
assert.match(support.supportedDisplay, /Bun 1\.1\+/);
} finally {
if (originalBun === undefined) {
delete (process.versions as Record<string, string | undefined>).bun;
} else {
process.versions.bun = originalBun;
}
}
});
test("createSyncDriverFactory prefers bun:sqlite built-in driver when running under Bun", () => {
const originalBun = process.versions.bun;
try {
(process.versions as Record<string, string>).bun = "1.1.20";
const dummyBunDb = {
query: () => ({ run: () => ({ changes: 1, lastInsertRowid: 1 }), get: () => null, all: () => [] }),
exec: () => {},
close: () => {},
};
const loader = (modName: string) => {
if (modName === "bun:sqlite") {
return { Database: function DummyBunDatabase() { return dummyBunDb; } };
}
throw new Error(`Unexpected module ${modName}`);
};
const factory = createSyncDriverFactory(loader, () => true);
const adapter = factory(":memory:");
assert.ok(adapter, "Adapter should be created via bun:sqlite");
assert.equal(adapter.driver, "bun:sqlite");
} finally {
if (originalBun === undefined) {
delete (process.versions as Record<string, string | undefined>).bun;
} else {
process.versions.bun = originalBun;
}
}
});
test("createSyncDriverFactory prefers better-sqlite3 when running under Node", () => {
const originalBun = process.versions.bun;
try {
delete (process.versions as Record<string, string | undefined>).bun;
const dummyBetterDb = {
open: true,
name: ":memory:",
inTransaction: false,
prepare: () => ({ run: () => ({ changes: 1, lastInsertRowid: 1 }) }),
exec: () => {},
close: () => {},
};
const loader = (modName: string) => {
if (modName === "better-sqlite3") {
return function DummyBetterSqlite() {
return dummyBetterDb;
};
}
throw new Error(`Unexpected module ${modName}`);
};
const factory = createSyncDriverFactory(loader, () => true);
const adapter = factory(":memory:");
assert.ok(adapter, "Adapter should be created via better-sqlite3");
assert.equal(adapter.driver, "better-sqlite3");
} finally {
if (originalBun === undefined) {
delete (process.versions as Record<string, string | undefined>).bun;
} else {
process.versions.bun = originalBun;
}
}
});
test("resolveNextBuildBundlerFlag automatically disables Turbopack and uses Webpack under Bun", async () => {
const originalBun = process.versions.bun;
try {
(process.versions as Record<string, string>).bun = "1.1.20";
const buildIsolated = await import("../../scripts/build/build-next-isolated.mjs");
assert.equal(buildIsolated.resolveNextBuildBundlerFlag({}), "--webpack");
assert.equal(buildIsolated.resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "1" }), "--webpack");
} finally {
if (originalBun === undefined) {
delete (process.versions as Record<string, string | undefined>).bun;
} else {
process.versions.bun = originalBun;
}
}
});