mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
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:
9
.github/workflows/dast-smoke.yml
vendored
9
.github/workflows/dast-smoke.yml
vendored
@@ -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
89
Dockerfile.bun
Normal 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"]
|
||||
13
README.md
13
README.md
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}).`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
type CliAgentInfo,
|
||||
detectInstalledAgents,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { cookies } from "next/headers";
|
||||
import { jwtVerify } from "jose";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
1
src/app/api/cache/entries/route.ts
vendored
1
src/app/api/cache/entries/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
listSemanticCacheEntries,
|
||||
|
||||
1
src/app/api/cache/reasoning/route.ts
vendored
1
src/app/api/cache/reasoning/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
clearReasoningCacheAll,
|
||||
|
||||
1
src/app/api/cache/route.ts
vendored
1
src/app/api/cache/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
getCacheStats,
|
||||
clearCache,
|
||||
|
||||
1
src/app/api/cache/stats/route.ts
vendored
1
src/app/api/cache/stats/route.ts
vendored
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
|
||||
import {
|
||||
getProviderAuditTarget,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
105
tests/unit/bun-support.test.ts
Normal file
105
tests/unit/bun-support.test.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user