Compare commits

..

5 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
db482069e7 fix(video): harden visual frame deduplication (#11382)
Merged via consolidated batch validation (worktree `.claude/worktrees/batch-0824d`), stacked on the just-merged #11362 as documented. Moves the Video Bridge frame cap to post-dedup, bounds the perceptual candidate pool to at most 2x budget (max 16), includes the dedup policy/version in result-cache identity, adds cooperative abort checks to the comparator loop. Static gates green; own dedup/cache-version regression suite passed in the combined-batch run (grayscale-16x16-mean-cells-v2 policy, real fixtures). Thanks!
2026-08-24 09:26:33 -03:00
Diego Rodrigues de Sa e Souza
d4ade9d1d3 fix(video): separate tenant scope from download hash 2026-08-24 05:55:00 -03:00
Diego Rodrigues de Sa e Souza
f54c93c879 fix(video): key download flights with process HMAC 2026-08-24 05:16:13 -03:00
Diego Rodrigues de Sa e Souza
e2e48fdab8 docs(changelog): link Video Bridge cache fix PR 2026-08-24 04:37:54 -03:00
Diego Rodrigues de Sa e Souza
d2cea0811a fix(video): harden result cache identity and bounds 2026-08-24 04:31:27 -03:00
20 changed files with 2120 additions and 288 deletions

View File

@@ -180,7 +180,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
### 🐛 Bug Fixes
- **fix(build):** every route no longer answers HTTP 500 on artifacts built from the release tip ([#11343](https://github.com/diegosouzapw/OmniRoute/issues/11343)) — `next.config.mjs` aliased `better-sqlite3` to its build-time stub **unconditionally**, on the premise that `serverExternalPackages` still won at runtime. It does not: a Turbopack `resolveAlias` rewrites the request *before* the externals check, so the request stopped matching the `better-sqlite3` external entry and the stub was baked into the shipped bundle. The sync driver then failed with `r(...) is not a constructor`, fell through `node:sqlite` and sql.js, and the instrumentation hook aborted at boot. Same failure shape as [#6344](https://github.com/diegosouzapw/OmniRoute/issues/6344), so it gets the same treatment: the alias is opt-in via `OMNIROUTE_BETTER_SQLITE3_STUB=1` through the shared `scripts/build/better-sqlite3-stub-flag.mjs` helper — set it only on a build host that actually hits the SIGABRT build-worker teardown ([#10060](https://github.com/diegosouzapw/OmniRoute/issues/10060)); default builds externalize the real native addon. Regression guards: `tests/unit/better-sqlite3-stub-alias-11343.test.mjs` (5) and the env matrix in `tests/unit/next-config.test.ts`.
- **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963
- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366)
- **cli**: route provider test commands through configured connection test endpoints (#10570)

View File

@@ -0,0 +1 @@
- **fix(video):** fingerprint protected Video Bridge bytes, coalesce concurrent work, and fail open when the bounded TTL/LRU result cache is unavailable or corrupt ([#11362](https://github.com/diegosouzapw/OmniRoute/pull/11362))

View File

@@ -0,0 +1 @@
- **fix(video):** apply the caption-frame cap after bounded visual deduplication, preserve first/final candidates plus small high-contrast motion and text changes, and version the dedup policy in result-cache identity ([#11382](https://github.com/diegosouzapw/OmniRoute/pull/11382)).

View File

@@ -1,13 +1,13 @@
---
title: "Guardrails"
version: 3.8.50
lastUpdated: 2026-08-14
lastUpdated: 2026-08-24
---
# Guardrails
> **Source of truth:** `src/lib/guardrails/`
> **Last updated:** 2026-08-15 — v3.8.50 (Video Bridge broker confinement)
> **Last updated:** 2026-08-24 — v3.8.50 (Video Bridge visual dedup hardening)
Guardrails enforce safety, policy, and content transformations at the boundary
between OmniRoute and upstream providers. Each guardrail can inspect (and
@@ -340,11 +340,20 @@ serialized broker response to 32 MiB. A private temporary directory is removed
in `finally`. OmniRoute does not bundle FFmpeg and does not accept a custom
executable path. Before captioning, the bridge applies a conservative visual
deduplication pass: each JPEG is reduced to a 16×16 grayscale buffer and is
compared only with the last frame retained, using a fixed similarity threshold
of 0.04 — a deliberate constant chosen for predictability, not a runtime
setting. The first and final timeline frames
are always retained; comparator or decoder errors fail open and keep coverage.
The output metadata reports how many frames were dropped.
compared only with the last frame retained. For a requested caption budget
above one frame, extraction supplies a
bounded candidate pool of up to twice that budget and never more than 16 frames.
The requested cap is applied only after deduplication, with the first and final
selected candidates preserved during final thinning when the budget is at least
two. The versioned
`grayscale-16x16-mean-cells-v2` policy uses the larger of mean luma delta and
the ratio of thumbnail cells whose normalized delta is at least 0.05. The
duplicate threshold is the constant 0.04, chosen for predictability rather than
exposed as a runtime setting. This secondary
high-contrast signal preserves small motion and visible-text changes that a
mean-only comparison can hide. Comparator or decoder errors fail open and keep
coverage. Output metadata separates extracted candidates, successfully used
frames, and visual duplicates dropped.
An explicitly marked video part may request a timestamped contact sheet. The
bridge builds at most a 4-column, 16-frame JPEG grid and labels the resulting
@@ -401,7 +410,10 @@ instead of relabeling it as the requested routing plan. The whole-video result
cache is keyed on every input that changes the output — prompt, effective
model, sampling policy, frame count, focus window, `transcript`,
`audioTranscript`, and the contact-sheet flag — so changing any of those
dimensions is a cache miss, never a stale reuse.
dimensions is a cache miss, never a stale reuse. The visual dedup policy
version, threshold, and bounded candidate-frame count are also explicit in the
result-cache key and metadata; a policy change therefore cannot reuse a stale
whole-video description.
The guardrail extracts every supported video part but describes no more than
`modalityBridgeVideoMaxVideos`. For a target proven to have

View File

@@ -2,7 +2,6 @@ import createNextIntlPlugin from "next-intl/plugin";
import { createMDX } from "fumadocs-mdx/next";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { betterSqlite3AliasFor } from "./scripts/build/better-sqlite3-stub-flag.mjs";
import { mitmManagerAliasFor } from "./scripts/build/mitm-stub-flag.mjs";
import { normalizeBasePath } from "./scripts/build/normalizeBasePath.mjs";
import {
@@ -139,14 +138,10 @@ const nextConfig = {
// the stub to every npm/Electron/VPS artifact and broke Agent Bridge
// start for all non-Docker users (#6344). See scripts/build/mitm-stub-flag.mjs.
...mitmManagerAliasFor(process.env),
// better-sqlite3 → build-time stub ONLY where the build worker actually
// aborts while tracing the native addon (SIGABRT at worker teardown,
// #10060); opt in with OMNIROUTE_BETTER_SQLITE3_STUB=1. The alias used to
// be unconditional on the premise that serverExternalPackages still won
// at runtime — it does not: resolveAlias rewrites the request before the
// externals check, so the stub was bundled and EVERY route answered 500
// (#11343). See scripts/build/better-sqlite3-stub-flag.mjs.
...betterSqlite3AliasFor(process.env),
// Build-time stub so the bundler never traces the native better-sqlite3
// addon into a build worker (SIGABRT at worker teardown). Runtime still
// uses the real package via serverExternalPackages. (#10060)
"better-sqlite3": "./src/lib/db/better-sqlite3.stub.js",
...minimalBuildAliases,
},
// src/lib/agentSkills/generator.ts builds its fs base path from a runtime

View File

@@ -1,36 +0,0 @@
/**
* Decide whether the Next.js build should alias `better-sqlite3` to the
* build-time stub (src/lib/db/better-sqlite3.stub.js).
*
* History (#11343): the alias was UNCONDITIONAL, added to keep the bundler from
* tracing the native addon into a Next.js build worker, whose thread teardown
* can abort with SIGABRT (assertion in node::RemoveEnvironmentCleanupHook) and
* leave the build without standalone output (#10060).
*
* The premise recorded next to that alias — "runtime still uses the real
* package via serverExternalPackages" — does not hold. A Turbopack
* `resolveAlias` rewrites the request BEFORE the externals check runs, so
* `better-sqlite3` becomes a relative path, no longer matches the
* `serverExternalPackages` entry, and the stub is baked into the bundle. Every
* artifact built from that config answered HTTP 500 on every route: the stub's
* default export is not a constructor, the sync driver chain fell through to
* `node:sqlite` and then sql.js, and the instrumentation hook aborted at boot.
*
* This is the same failure shape as #6344 (the @/mitm/manager stub shipping to
* every npm/Electron/VPS artifact), so it gets the same treatment: the alias is
* opt-in, and a default build gets the real, externalized native package.
*
* Set OMNIROUTE_BETTER_SQLITE3_STUB=1 ONLY on a build host that actually hits
* the SIGABRT worker teardown, and never for an artifact that will be run —
* the resulting bundle cannot open a database.
*/
export function shouldStubBetterSqlite3(env = process.env) {
return env.OMNIROUTE_BETTER_SQLITE3_STUB === "1";
}
/** Turbopack resolveAlias fragment for `better-sqlite3`, derived from the env. */
export function betterSqlite3AliasFor(env = process.env) {
return shouldStubBetterSqlite3(env)
? { "better-sqlite3": "./src/lib/db/better-sqlite3.stub.js" }
: {};
}

View File

@@ -1,24 +1,78 @@
/**
* Video Bridge benchmarks (VB-FU-07 sampler overhead + VB-FU-09 contact sheet A/B).
* Video Bridge benchmarks (VB-FU-03 dedup comparator, VB-FU-07 sampler overhead,
* and VB-FU-09 contact sheet A/B).
*
* Run: node --import tsx/esm scripts/perf/video-bridge-bench.ts
*
* 1. Sampler: measures the pure timestamp-selection cost of uniform vs
* 1. Dedup: measures bounded CPU and process-memory observations for the
* production 16x16 grayscale comparator over the hard 16-frame candidate cap.
* 2. Sampler: measures the pure timestamp-selection cost of uniform vs
* scene_aware vs segment_aware for growing scene-candidate counts. The
* ffmpeg scene-detection pass is shared by both aware policies and is
* I/O-bound, so the incremental policy cost is exactly this selection step.
* 2. Contact sheet: composes synthetic JPEG frames into the timestamped grid
* 3. Contact sheet: composes synthetic JPEG frames into the timestamped grid
* and compares payload bytes + model calls against individual frames.
*/
import { performance } from "node:perf_hooks";
import { buildVideoContactSheet } from "../../src/lib/guardrails/videoBridgeContactSheet";
import {
compareVideoFramesByGrayscale,
VIDEO_DEDUP_POLICY_VERSION,
VIDEO_DEDUP_THRESHOLD,
} from "../../src/lib/guardrails/videoBridgeHelpers";
import {
calculateSamplingDecision,
type VideoSamplingPolicy,
} from "../../src/lib/guardrails/videoBridgeRuntime";
const SAMPLER_ITERATIONS = 2_000;
const DEDUP_FRAME_CAP = 16;
const DEDUP_ITERATIONS = 10;
function mebibytes(bytes: number): string {
return (bytes / (1024 * 1024)).toFixed(2);
}
async function benchDedupComparator(): Promise<void> {
const frames = await Promise.all(
Array.from({ length: DEDUP_FRAME_CAP }, async (_unused, index) => ({
dataUri: await syntheticJpegFrame(index, 1024, 576),
timestampSeconds: index,
}))
);
await compareVideoFramesByGrayscale(frames[0], frames[1]);
const memoryBefore = process.memoryUsage();
const maxRssBefore = process.resourceUsage().maxRSS * 1024;
const cpuBefore = process.cpuUsage();
const wallBefore = performance.now();
let comparisons = 0;
for (let iteration = 0; iteration < DEDUP_ITERATIONS; iteration++) {
for (let index = 1; index < frames.length; index++) {
await compareVideoFramesByGrayscale(frames[index - 1], frames[index]);
comparisons += 1;
}
}
const wallMs = performance.now() - wallBefore;
const cpu = process.cpuUsage(cpuBefore);
const memoryAfter = process.memoryUsage();
const maxRssAfter = process.resourceUsage().maxRSS * 1024;
const cpuMs = (cpu.user + cpu.system) / 1000;
console.log("== Visual dedup comparator (synthetic 1024x576 JPEG, bounded) ==");
console.log(
`policy=${VIDEO_DEDUP_POLICY_VERSION} threshold=${VIDEO_DEDUP_THRESHOLD} frames=${DEDUP_FRAME_CAP} iterations=${DEDUP_ITERATIONS} comparisons=${comparisons}`
);
console.log(
`wall_ms=${wallMs.toFixed(1)} cpu_ms=${cpuMs.toFixed(1)} cpu_ms/comparison=${(cpuMs / comparisons).toFixed(3)}`
);
console.log(
`rss_delta_MiB=${mebibytes(memoryAfter.rss - memoryBefore.rss)} heap_delta_MiB=${mebibytes(memoryAfter.heapUsed - memoryBefore.heapUsed)} max_rss_delta_MiB=${mebibytes(Math.max(0, maxRssAfter - maxRssBefore))}`
);
console.log(
"Scope: comparator decode/resize/delta cost only; this does not measure caption-model quality."
);
}
function benchSampler(): void {
console.log("== Sampler timestamp-selection cost (pure, per call) ==");
@@ -47,12 +101,12 @@ function benchSampler(): void {
}
}
async function syntheticJpegFrame(index: number): Promise<string> {
async function syntheticJpegFrame(index: number, width = 512, height = 288): Promise<string> {
const { default: sharp } = await import("sharp");
const buffer = await sharp({
create: {
width: 512,
height: 288,
width,
height,
channels: 3,
background: { r: (index * 37) % 255, g: (index * 91) % 255, b: (index * 53) % 255 },
},
@@ -86,5 +140,7 @@ async function benchContactSheet(): Promise<void> {
}
}
await benchDedupComparator();
console.log("");
benchSampler();
await benchContactSheet();

View File

@@ -1,19 +1,13 @@
// Build-time stub for better-sqlite3 (#10060).
//
// OPT-IN ONLY — set OMNIROUTE_BETTER_SQLITE3_STUB=1 to alias it in, and only on
// a build host that actually hits the SIGABRT worker teardown: the native
// Statement destructor aborts when a Next.js build worker thread exits
// Aliased in for the Next.js production build (turbopack + webpack) so the
// bundler never pulls the real native addon into a build worker. The native
// Statement destructor aborts with SIGABRT when a build worker thread exits
// (assertion in node::RemoveEnvironmentCleanupHook, env == nullptr), which can
// leave the build with no standalone output.
//
// It is NOT a build-only stand-in. A Turbopack resolveAlias rewrites the
// request before the externals check, so aliasing `better-sqlite3` here also
// removes it from serverExternalPackages' reach and bakes THIS FILE into the
// shipped bundle. An artifact built with the flag on cannot open a database:
// the sync driver chain fails with "r(...) is not a constructor", falls through
// node:sqlite and sql.js, and the instrumentation hook aborts at boot, so every
// route answers HTTP 500. That is exactly what an unconditional alias shipped
// in #11343. See scripts/build/better-sqlite3-stub-flag.mjs.
// leave the build with no standalone output. At runtime the real package is
// used (it is listed in serverExternalPackages, so it is require()'d natively,
// not bundled); this stub only stands in during the build, where the DB is
// never actually queried.
class Database {
constructor() {}
prepare() {

View File

@@ -11,6 +11,9 @@ import type { VisionBridgeRuntimeSettings } from "@/shared/constants/modalityBri
export interface BridgeCacheKeyOptions {
kind?: string;
dedupCandidateFrameCount?: number;
dedupPolicyVersion?: string;
dedupThreshold?: number;
extractorVersion?: string;
policyVersion?: string;
strategy?: string;
@@ -38,6 +41,9 @@ export function bridgeCacheKey(
kind: options.kind ?? "media-frame",
model,
prompt,
dedupCandidateFrameCount: options.dedupCandidateFrameCount,
dedupPolicyVersion: options.dedupPolicyVersion,
dedupThreshold: options.dedupThreshold,
policyVersion: options.policyVersion,
extractorVersion: options.extractorVersion,
strategy: options.strategy,
@@ -55,6 +61,8 @@ export function bridgeCacheKey(
export interface BridgeCacheOptions {
maxEntries: number;
/** Aggregate UTF-8 key/value/metadata budget; unlimited when omitted. */
maxBytes?: number;
ttlMs: number;
/** Injectable clock for tests. */
now?: () => number;
@@ -67,8 +75,37 @@ export interface BridgeCacheEntry {
metadata?: Record<string, unknown>;
}
export class BridgeCache {
private readonly entries = new Map<string, { entry: BridgeCacheEntry; expiresAt: number }>();
/** Minimal fail-open store contract accepted by complete-result bridge caches. */
export interface BridgeCacheStore {
delete(key: string): void;
getEntry(key: string): BridgeCacheEntry | undefined;
setEntry(key: string, entry: BridgeCacheEntry): void;
}
type StoredBridgeCacheEntry = {
bytes: number;
entry: BridgeCacheEntry;
expiresAt: number;
};
function cacheEntryBytes(entry: BridgeCacheEntry): number {
try {
const metadata = JSON.stringify({
metadata: entry.metadata,
producerModel: entry.producerModel,
});
return Buffer.byteLength(entry.value, "utf8") + Buffer.byteLength(metadata, "utf8");
} catch (error) {
console.debug("[MODALITY_BRIDGE_CACHE] Entry size calculation failed open", {
errorType: error instanceof Error ? error.name : typeof error,
});
return Number.POSITIVE_INFINITY;
}
}
export class BridgeCache implements BridgeCacheStore {
private readonly entries = new Map<string, StoredBridgeCacheEntry>();
private totalBytes = 0;
constructor(private readonly opts: BridgeCacheOptions) {}
@@ -81,7 +118,7 @@ export class BridgeCache {
if (!hit) return undefined;
const now = (this.opts.now ?? Date.now)();
if (hit.expiresAt <= now) {
this.entries.delete(key);
this.delete(key);
return undefined;
}
// Map preserves insertion order — re-insert to mark as most-recently-used.
@@ -96,12 +133,17 @@ export class BridgeCache {
setEntry(key: string, entry: BridgeCacheEntry): void {
const now = (this.opts.now ?? Date.now)();
this.entries.delete(key);
this.entries.set(key, { entry, expiresAt: now + this.opts.ttlMs });
while (this.entries.size > this.opts.maxEntries) {
const bytes = cacheEntryBytes(entry) + Buffer.byteLength(key, "utf8");
const maxBytes = Math.max(0, this.opts.maxBytes ?? Number.POSITIVE_INFINITY);
const maxEntries = Math.max(0, Math.floor(this.opts.maxEntries));
this.delete(key);
if (!Number.isFinite(bytes) || bytes > maxBytes || maxEntries === 0) return;
this.entries.set(key, { bytes, entry, expiresAt: now + this.opts.ttlMs });
this.totalBytes += bytes;
while (this.entries.size > maxEntries || this.totalBytes > maxBytes) {
const oldest = this.entries.keys().next().value;
if (oldest === undefined) break;
this.entries.delete(oldest);
this.delete(oldest);
}
}
@@ -109,21 +151,52 @@ export class BridgeCache {
return this.entries.size;
}
/** Current aggregate UTF-8 bytes retained by this cache. */
get bytes(): number {
return this.totalBytes;
}
delete(key: string): void {
const existing = this.entries.get(key);
if (existing) this.totalBytes = Math.max(0, this.totalBytes - existing.bytes);
this.entries.delete(key);
}
clear(): void {
this.entries.clear();
this.totalBytes = 0;
}
}
/** Process-wide singleton used by the bridges; recreated when config changes. */
let shared: { cache: BridgeCache; ttlMs: number; maxEntries: number } | null = null;
let shared: { cache: BridgeCache; ttlMs: number; maxBytes: number; maxEntries: number } | null =
null;
export function getSharedBridgeCache(ttlMs: number, maxEntries: number): BridgeCache {
if (!shared || shared.ttlMs !== ttlMs || shared.maxEntries !== maxEntries) {
shared = { cache: new BridgeCache({ maxEntries, ttlMs }), ttlMs, maxEntries };
/**
* Resolve the process-wide bridge cache, recreating it when any bound changes.
*
* @param ttlMs - Entry lifetime in milliseconds.
* @param maxEntries - Maximum retained entry count.
* @param maxBytes - Aggregate UTF-8 storage budget.
* @returns The process-wide cache for these exact bounds.
*/
export function getSharedBridgeCache(
ttlMs: number,
maxEntries: number,
maxBytes = Number.POSITIVE_INFINITY
): BridgeCache {
if (
!shared ||
shared.ttlMs !== ttlMs ||
shared.maxEntries !== maxEntries ||
shared.maxBytes !== maxBytes
) {
shared = {
cache: new BridgeCache({ maxBytes, maxEntries, ttlMs }),
ttlMs,
maxBytes,
maxEntries,
};
}
return shared.cache;
}

View File

@@ -19,6 +19,8 @@ export interface BridgeModalityStats {
resultCacheBytes: number;
resultCacheHits: number;
resultCacheLatencyMs: number;
/** Requests that joined an in-flight complete result instead of hitting the persistent cache. */
resultSingleflightCoalesced: number;
failures: number;
/** Audio/video fusion runs (video bridge only; 0 for other modalities). */
fusionRuns: number;
@@ -47,6 +49,7 @@ function emptyStats(): BridgeModalityStats {
resultCacheBytes: 0,
resultCacheHits: 0,
resultCacheLatencyMs: 0,
resultSingleflightCoalesced: 0,
failures: 0,
fusionRuns: 0,
fusionPartials: 0,
@@ -69,6 +72,8 @@ export function recordBridgeUse(
resultCacheBytes?: number;
resultCacheHit?: boolean;
resultCacheLatencyMs?: number;
/** True only when this request joined existing in-flight result work. */
resultSingleflightCoalesced?: boolean;
} = {}
): void {
const s = stats[kind];
@@ -104,6 +109,7 @@ export function recordBridgeUse(
s.resultCacheLatencyMs += Math.max(0, opts.resultCacheLatencyMs);
}
}
if (opts.resultSingleflightCoalesced) s.resultSingleflightCoalesced += 1;
if (typeof opts.latencyMs === "number" && Number.isFinite(opts.latencyMs)) {
s.totalLatencyMs += Math.max(0, opts.latencyMs);
s.latencySamples += 1;

View File

@@ -1,3 +1,5 @@
import { createHash } from "node:crypto";
import { fetch as undiciFetch } from "undici";
import { getSettings as defaultGetSettings } from "@/lib/db/settings";
@@ -8,18 +10,38 @@ import {
} from "@/shared/constants/modalityBridgeDefaults";
import { BaseGuardrail, type GuardrailContext, type GuardrailResult } from "./base";
import { bridgeCacheKey, getSharedBridgeCacheFor } from "./modalityBridge/bridgeCache";
import {
bridgeCacheKey,
getSharedBridgeCacheFor,
type BridgeCacheEntry,
type BridgeCacheStore,
} from "./modalityBridge/bridgeCache";
import { recordBridgeUse } from "./modalityBridge/bridgeStats";
import {
describeVideoPart as defaultDescribeVideoPart,
extractVideoParts,
formatVideoTimestamp,
loadVideoPartBytes,
replaceVideoParts,
resolveVideoDedupCandidateFrameCount,
VIDEO_BRIDGE_MAX_BYTES,
VIDEO_DEDUP_MAX_CANDIDATE_FRAMES,
VIDEO_DEDUP_POLICY_VERSION,
VIDEO_DEDUP_THRESHOLD,
type DescribeVideoDependencies,
type DescribedVideo,
type VideoFusionTelemetry,
type VideoPart,
} from "./videoBridgeHelpers";
import {
getSharedVideoResultCacheFor,
runVideoDownloadSingleflight,
runVideoResultSingleflight,
safeDeleteCacheEntry,
safeGetCacheEntry,
safeSetCacheEntry,
videoBridgeAbortError,
} from "./videoBridgeResultCache";
import {
callVisionModel as defaultCallVisionModel,
type VisionModelConfig,
@@ -48,9 +70,62 @@ function safeTranscriptFingerprint(value: unknown): string {
}
}
const VIDEO_BRIDGE_RESULT_CACHE_VERSION = "v2";
const VIDEO_BRIDGE_RESULT_CACHE_POLICY = "default";
const VIDEO_BRIDGE_RESULT_CACHE_KEY_KIND = "video-result-v2";
function waitForVideoBridgePromise<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
if (signal.aborted) return Promise.reject(videoBridgeAbortError());
return new Promise<T>((resolve, reject) => {
let completed = false;
const finish = (callback: () => void): void => {
if (completed) return;
completed = true;
signal.removeEventListener("abort", onAbort);
callback();
};
const onAbort = (): void => finish(() => reject(videoBridgeAbortError()));
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
promise.then(
(value) => finish(() => resolve(value)),
(error: unknown) => finish(() => reject(error))
);
});
}
const VIDEO_BRIDGE_RESULT_CACHE_VERSION = "v4";
const VIDEO_BRIDGE_RESULT_CACHE_POLICY = "sampling-then-dedup-v2";
const VIDEO_BRIDGE_RESULT_CACHE_KEY_KIND = "video-result-v4";
const VIDEO_BRIDGE_DOWNLOAD_FLIGHT_VERSION = "v1";
function buildVideoDownloadFlightKey(
part: VideoPart,
context: GuardrailContext,
maxBytes: number,
timeoutMs: number
): string {
const rawPrincipalId = context.apiKeyInfo?.id;
const principalId =
typeof rawPrincipalId === "string" || typeof rawPrincipalId === "number"
? String(rawPrincipalId)
: "local";
const canonicalIdentity = JSON.stringify({
container: part.container,
endpoint: context.endpoint ?? null,
maxBytes,
method: context.method ?? null,
model: context.model ?? null,
provider: context.provider ?? null,
ref: part.ref,
shape: part.shape,
sourceFormat: context.sourceFormat ?? null,
targetFormat: context.targetFormat ?? null,
timeoutMs,
version: VIDEO_BRIDGE_DOWNLOAD_FLIGHT_VERSION,
});
const requestFingerprint = createHash("sha256").update(canonicalIdentity).digest("hex");
// The authenticated database id is an ephemeral in-memory scope, not a
// password or persisted credential. Keep it out of cryptographic hashes so
// password-hash analysis cannot conflate tenant partitioning with storage.
return `video-download:${JSON.stringify([principalId, requestFingerprint])}`;
}
interface VideoResultCacheMetadata {
cacheVersion: string;
@@ -61,6 +136,9 @@ interface VideoResultCacheMetadata {
prompt: string;
frameCount: number;
maxVideos: number;
dedupCandidateFrameCount: number;
dedupPolicyVersion: string;
dedupThreshold: number;
durationSeconds: number;
framesRequested: number;
framesExtracted: number;
@@ -78,6 +156,86 @@ interface VideoResultCacheMetadata {
modelUsed: string;
}
type VideoResultCacheIdentity = Pick<
VideoResultCacheMetadata,
| "cacheVersion"
| "dedupCandidateFrameCount"
| "dedupPolicyVersion"
| "dedupThreshold"
| "extractorVersion"
| "frameCount"
| "maxVideos"
| "model"
| "policyVersion"
| "prompt"
| "strategy"
>;
const VIDEO_RESULT_CACHE_IDENTITY_KEYS: readonly (keyof VideoResultCacheIdentity)[] = [
"cacheVersion",
"dedupCandidateFrameCount",
"dedupPolicyVersion",
"dedupThreshold",
"extractorVersion",
"frameCount",
"maxVideos",
"model",
"policyVersion",
"prompt",
"strategy",
];
function createVideoResultCacheIdentity(
runtime: ReturnType<typeof resolveVideoBridgeRuntimeSettings>,
visionRuntime: ReturnType<typeof resolveVisionBridgeRuntimeSettings>,
model: string
): VideoResultCacheIdentity {
return {
cacheVersion: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
dedupCandidateFrameCount: resolveVideoDedupCandidateFrameCount(runtime.frameCount),
dedupPolicyVersion: VIDEO_DEDUP_POLICY_VERSION,
dedupThreshold: VIDEO_DEDUP_THRESHOLD,
extractorVersion: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
frameCount: runtime.frameCount,
maxVideos: runtime.maxVideos,
model,
policyVersion: VIDEO_BRIDGE_RESULT_CACHE_POLICY,
prompt: visionRuntime.prompt,
strategy: runtime.samplingPolicy,
};
}
function buildVideoResultCacheKey(
contentFingerprint: string,
identity: VideoResultCacheIdentity,
part: VideoPart
): string {
return bridgeCacheKey(contentFingerprint, identity.prompt, identity.model, {
kind: VIDEO_BRIDGE_RESULT_CACHE_KEY_KIND,
dedupCandidateFrameCount: identity.dedupCandidateFrameCount,
dedupPolicyVersion: identity.dedupPolicyVersion,
dedupThreshold: identity.dedupThreshold,
extractorVersion: identity.extractorVersion,
policyVersion: identity.policyVersion,
strategy: identity.strategy,
frameCount: identity.frameCount,
maxVideos: identity.maxVideos,
focusEndSeconds: part.focusWindow?.endSeconds ?? null,
focusStartSeconds: part.focusWindow?.startSeconds ?? null,
transcript: safeTranscriptFingerprint(part.transcript),
audioTranscript: safeTranscriptFingerprint(part.audioTranscript),
contactSheet: part.contactSheet ?? false,
version: identity.cacheVersion,
});
}
function matchesVideoResultCacheIdentity(
metadata: VideoResultCacheMetadata,
identity: VideoResultCacheIdentity
): boolean {
return VIDEO_RESULT_CACHE_IDENTITY_KEYS.every((key) => metadata[key] === identity[key]);
}
function isFusionTelemetry(value: unknown): value is VideoFusionTelemetry {
if (!value || typeof value !== "object") return false;
const record = value as Record<string, unknown>;
@@ -102,6 +260,8 @@ export interface VideoBridgeDependencies {
getCapabilities?: (model: string) => { supportsVideo: boolean | null };
describePart?: (part: VideoPart) => Promise<DescribedVideo>;
extractFrames?: DescribeVideoDependencies["extractFrames"];
fetchRemote?: DescribeVideoDependencies["fetchRemote"];
resultCache?: BridgeCacheStore;
selectVisionModel?: (fixedModel?: string) => Promise<string | null>;
callVisionModel?: (
imageDataUri: string,
@@ -110,28 +270,70 @@ export interface VideoBridgeDependencies {
) => Promise<string>;
}
function isVideoResultCacheMetadata(value: unknown): value is VideoResultCacheMetadata {
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function isFiniteNonNegativeInteger(value: unknown): value is number {
return isFiniteNonNegativeNumber(value) && Number.isInteger(value);
}
function isVideoResultCacheMetadata(
value: unknown,
expectedCacheBytes: number
): value is VideoResultCacheMetadata {
if (!value || typeof value !== "object") return false;
const record = value as Record<string, unknown>;
if (
!isFiniteNonNegativeInteger(record.framesRequested) ||
!isFiniteNonNegativeInteger(record.framesExtracted) ||
!isFiniteNonNegativeInteger(record.framesUsed) ||
!isFiniteNonNegativeInteger(record.dedupCandidateFrameCount) ||
record.dedupCandidateFrameCount < 1 ||
record.dedupCandidateFrameCount > VIDEO_DEDUP_MAX_CANDIDATE_FRAMES ||
record.framesExtracted > record.dedupCandidateFrameCount ||
record.framesUsed > record.framesRequested ||
record.framesUsed > record.framesExtracted
) {
return false;
}
const dedupDropped = record.dedupDropped ?? 0;
if (
!isFiniteNonNegativeInteger(dedupDropped) ||
record.framesUsed + dedupDropped > record.framesExtracted
) {
return false;
}
if (
(record.focusStartSeconds !== undefined &&
!isFiniteNonNegativeNumber(record.focusStartSeconds)) ||
(record.focusEndSeconds !== undefined && !isFiniteNonNegativeNumber(record.focusEndSeconds)) ||
(typeof record.focusStartSeconds === "number" &&
typeof record.focusEndSeconds === "number" &&
record.focusStartSeconds > record.focusEndSeconds)
) {
return false;
}
return (
typeof record.cacheVersion === "string" &&
typeof record.dedupPolicyVersion === "string" &&
typeof record.dedupThreshold === "number" &&
Number.isFinite(record.dedupThreshold) &&
record.dedupThreshold >= 0 &&
record.dedupThreshold <= 1 &&
typeof record.policyVersion === "string" &&
typeof record.extractorVersion === "string" &&
typeof record.strategy === "string" &&
typeof record.model === "string" &&
typeof record.prompt === "string" &&
typeof record.frameCount === "number" &&
typeof record.maxVideos === "number" &&
typeof record.durationSeconds === "number" &&
typeof record.framesRequested === "number" &&
typeof record.framesExtracted === "number" &&
typeof record.framesUsed === "number" &&
(record.dedupDropped === undefined ||
(typeof record.dedupDropped === "number" && record.dedupDropped >= 0)) &&
typeof record.cacheBytes === "number" &&
isFiniteNonNegativeInteger(record.frameCount) &&
isFiniteNonNegativeInteger(record.maxVideos) &&
isFiniteNonNegativeNumber(record.durationSeconds) &&
isFiniteNonNegativeInteger(record.cacheBytes) &&
record.cacheBytes === expectedCacheBytes &&
typeof record.modelUsed === "string" &&
(record.samplingCandidateCount === undefined ||
(typeof record.samplingCandidateCount === "number" && record.samplingCandidateCount >= 0)) &&
isFiniteNonNegativeInteger(record.samplingCandidateCount)) &&
(record.samplingPolicyEffective === undefined ||
record.samplingPolicyEffective === "uniform" ||
record.samplingPolicyEffective === "scene_aware" ||
@@ -141,12 +343,22 @@ function isVideoResultCacheMetadata(value: unknown): value is VideoResultCacheMe
record.samplingPolicyRequested === "scene_aware" ||
record.samplingPolicyRequested === "segment_aware") &&
(record.transcriptCuesApplied === undefined ||
(typeof record.transcriptCuesApplied === "number" && record.transcriptCuesApplied >= 0)) &&
isFiniteNonNegativeInteger(record.transcriptCuesApplied)) &&
(record.contactSheetUsed === undefined || typeof record.contactSheetUsed === "boolean") &&
(record.fusion === undefined || isFusionTelemetry(record.fusion))
);
}
function isVideoResultCacheEntry(
entry: BridgeCacheEntry
): entry is BridgeCacheEntry & { metadata: VideoResultCacheMetadata; value: string } {
if (typeof entry.value !== "string") return false;
return (
(entry.producerModel === undefined || typeof entry.producerModel === "string") &&
isVideoResultCacheMetadata(entry.metadata, Buffer.byteLength(entry.value, "utf8"))
);
}
export class VideoBridgeGuardrail extends BaseGuardrail {
name = "video-bridge";
priority = 7;
@@ -188,7 +400,9 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
const visionRuntime = resolveVisionBridgeRuntimeSettings(persisted);
const configuredModel = runtime.model.trim() || visionRuntime.model.trim();
const routingPlanModel = configuredModel || "auto";
const cache = runtime.cacheEnabled ? getSharedBridgeCacheFor(runtime) : null;
const cache = runtime.cacheEnabled
? (this.deps.resultCache ?? getSharedVideoResultCacheFor(runtime))
: null;
const successfulModels = new Set<string>();
let selectedModelPromise: Promise<string | null> | null = null;
const selectVideoModel = (): Promise<string | null> => {
@@ -231,37 +445,62 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
if (context.signal?.aborted) throw new Error("Video Bridge processing was aborted");
const part = attemptedParts[index];
const attemptStartedAt = Date.now();
const timeoutController = new AbortController();
const attemptTimeout = setTimeout(() => timeoutController.abort(), runtime.timeoutMs);
const attemptSignal = context.signal
? AbortSignal.any([context.signal, timeoutController.signal])
: timeoutController.signal;
try {
const selectedModel = await selectVideoModel();
const resultCacheKey =
const selectedModel = await waitForVideoBridgePromise(selectVideoModel(), attemptSignal);
if (attemptSignal.aborted) throw videoBridgeAbortError();
const shouldLoadVideoBytes =
Boolean(selectedModel) &&
(Boolean(cache) || (part.ref.startsWith("https://") && !this.deps.describePart));
const videoBytes = shouldLoadVideoBytes
? part.ref.startsWith("https://")
? await runVideoDownloadSingleflight(
buildVideoDownloadFlightKey(
part,
context,
VIDEO_BRIDGE_MAX_BYTES,
runtime.timeoutMs
),
attemptSignal,
(downloadSignal) =>
loadVideoPartBytes(
part,
VIDEO_BRIDGE_MAX_BYTES,
runtime.timeoutMs,
downloadSignal,
{ fetchRemote: this.deps.fetchRemote }
)
)
: await loadVideoPartBytes(
part,
VIDEO_BRIDGE_MAX_BYTES,
runtime.timeoutMs,
attemptSignal,
{ fetchRemote: this.deps.fetchRemote }
)
: null;
const contentFingerprint =
cache && videoBytes
? `sha256:${createHash("sha256").update(videoBytes).digest("hex")}`
: part.ref;
const resultCacheIdentity =
cache && selectedModel
? bridgeCacheKey(part.ref, visionRuntime.prompt, selectedModel, {
kind: VIDEO_BRIDGE_RESULT_CACHE_KEY_KIND,
extractorVersion: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
policyVersion: VIDEO_BRIDGE_RESULT_CACHE_POLICY,
strategy: runtime.samplingPolicy,
frameCount: runtime.frameCount,
maxVideos: runtime.maxVideos,
focusEndSeconds: part.focusWindow?.endSeconds ?? null,
focusStartSeconds: part.focusWindow?.startSeconds ?? null,
transcript: safeTranscriptFingerprint(part.transcript),
audioTranscript: safeTranscriptFingerprint(part.audioTranscript),
contactSheet: part.contactSheet ?? false,
version: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
})
? createVideoResultCacheIdentity(runtime, visionRuntime, selectedModel)
: null;
const cachedResult = resultCacheKey ? cache.getEntry(resultCacheKey) : null;
if (cachedResult && isVideoResultCacheMetadata(cachedResult.metadata)) {
const resultCacheKey = resultCacheIdentity
? buildVideoResultCacheKey(contentFingerprint, resultCacheIdentity, part)
: null;
const cachedResult = resultCacheKey
? safeGetCacheEntry(cache, resultCacheKey, context.log)
: null;
if (cachedResult && isVideoResultCacheEntry(cachedResult)) {
const meta = cachedResult.metadata;
const matchPolicy =
meta.cacheVersion === VIDEO_BRIDGE_RESULT_CACHE_VERSION &&
meta.policyVersion === VIDEO_BRIDGE_RESULT_CACHE_POLICY &&
meta.extractorVersion === VIDEO_BRIDGE_RESULT_CACHE_VERSION &&
meta.strategy === runtime.samplingPolicy &&
meta.frameCount === runtime.frameCount &&
meta.maxVideos === runtime.maxVideos &&
meta.model === selectedModel &&
meta.prompt === visionRuntime.prompt;
resultCacheIdentity && matchesVideoResultCacheIdentity(meta, resultCacheIdentity);
if (matchPolicy) {
const elapsed = Date.now() - attemptStartedAt;
descriptions.push(cachedResult.value);
@@ -299,21 +538,60 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
});
continue;
}
cache.delete(resultCacheKey);
safeDeleteCacheEntry(cache, resultCacheKey, context.log);
} else if (cachedResult) {
cache.delete(resultCacheKey);
safeDeleteCacheEntry(cache, resultCacheKey, context.log);
}
const cacheStartAt = Date.now();
const described = this.deps.describePart
? await this.deps.describePart(part)
: await this.describeWithVisionModel(
part,
runtime,
visionRuntime,
selectedModel,
context.signal
const describeAndCache = async (processingSignal: AbortSignal) => {
const described = this.deps.describePart
? await this.deps.describePart(part)
: await this.describeWithVisionModel(
part,
runtime,
visionRuntime,
selectedModel,
processingSignal,
videoBytes ?? undefined
);
if (processingSignal.aborted) throw videoBridgeAbortError();
const resultCacheBytes = Buffer.byteLength(described.description, "utf8");
if (resultCacheKey && resultCacheIdentity) {
safeSetCacheEntry(
cache,
resultCacheKey,
{
value: described.description,
producerModel: described.modelUsed ?? resultCacheIdentity.model,
metadata: {
...resultCacheIdentity,
durationSeconds: described.durationSeconds,
framesRequested: described.framesRequested,
framesExtracted: described.framesExtracted ?? described.framesUsed,
framesUsed: described.framesUsed,
dedupDropped: described.dedupDropped ?? 0,
focusEndSeconds: described.focusWindow?.endSeconds,
focusStartSeconds: described.focusWindow?.startSeconds,
cacheBytes: resultCacheBytes,
modelUsed: described.modelUsed ?? resultCacheIdentity.model,
samplingCandidateCount: described.sampling?.candidateCount ?? 0,
samplingPolicyEffective: described.sampling?.policyEffective ?? "uniform",
samplingPolicyRequested:
described.sampling?.policyRequested ?? runtime.samplingPolicy,
transcriptCuesApplied: described.transcriptCues?.length ?? 0,
contactSheetUsed: described.contactSheetUsed ?? false,
...(described.fusion ? { fusion: described.fusion } : {}),
},
},
context.log
);
if (context.signal?.aborted) throw new Error("Video Bridge processing was aborted");
}
return described;
};
const resolved =
resultCacheKey && selectedModel
? await runVideoResultSingleflight(resultCacheKey, attemptSignal, describeAndCache)
: { coalesced: false, value: await describeAndCache(attemptSignal) };
const described = resolved.value;
if (described.modelUsed) successfulModels.add(described.modelUsed);
const videoCacheHits = described.cacheHits ?? 0;
const processingLatencyMs = Date.now() - attemptStartedAt;
@@ -336,46 +614,12 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
}
totalCacheHits += videoCacheHits;
if (resultCacheKey && selectedModel) {
const resultCacheBytes = Buffer.byteLength(described.description, "utf8");
const cacheLatencyMs = Date.now() - cacheStartAt;
cache.setEntry(resultCacheKey, {
value: described.description,
producerModel: described.modelUsed ?? selectedModel,
metadata: {
cacheVersion: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
policyVersion: VIDEO_BRIDGE_RESULT_CACHE_POLICY,
extractorVersion: VIDEO_BRIDGE_RESULT_CACHE_VERSION,
strategy: runtime.samplingPolicy,
model: selectedModel,
prompt: visionRuntime.prompt,
frameCount: runtime.frameCount,
maxVideos: runtime.maxVideos,
durationSeconds: described.durationSeconds,
framesRequested: described.framesRequested,
framesExtracted: described.framesExtracted ?? described.framesUsed,
framesUsed: described.framesUsed,
dedupDropped: described.dedupDropped ?? 0,
focusEndSeconds: described.focusWindow?.endSeconds,
focusStartSeconds: described.focusWindow?.startSeconds,
cacheBytes: resultCacheBytes,
modelUsed: described.modelUsed ?? selectedModel,
samplingCandidateCount: described.sampling?.candidateCount ?? 0,
samplingPolicyEffective: described.sampling?.policyEffective ?? "uniform",
samplingPolicyRequested:
described.sampling?.policyRequested ?? runtime.samplingPolicy,
transcriptCuesApplied: described.transcriptCues?.length ?? 0,
contactSheetUsed: described.contactSheetUsed ?? false,
...(described.fusion ? { fusion: described.fusion } : {}),
},
});
recordBridgeUse("video", {
cacheHits: videoCacheHits,
fusionRun: Boolean(described.fusion),
fusionPartial: described.fusion?.partial ?? false,
latencyMs: processingLatencyMs,
resultCacheBytes,
resultCacheHit: false,
resultCacheLatencyMs: cacheLatencyMs,
resultSingleflightCoalesced: resolved.coalesced,
});
} else {
recordBridgeUse("video", {
@@ -408,6 +652,8 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
? `[Video ${index + 1}]: (unavailable — video could not be described)`
: null
);
} finally {
clearTimeout(attemptTimeout);
}
}
@@ -457,7 +703,8 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
runtime: ReturnType<typeof resolveVideoBridgeRuntimeSettings>,
visionRuntime: ReturnType<typeof resolveVisionBridgeRuntimeSettings>,
selectedModel: string | null,
signal?: AbortSignal
signal?: AbortSignal,
preloadedBytes?: Uint8Array
): Promise<DescribedVideo> {
if (!selectedModel) {
throw new Error("No vision-capable provider connected for Video Bridge");
@@ -503,7 +750,11 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
if (key && cache) cache.setEntry(key, { value: caption, producerModel });
return caption;
},
{ extractFrames: this.deps.extractFrames }
{
extractFrames: this.deps.extractFrames,
fetchRemote: this.deps.fetchRemote,
},
preloadedBytes
);
return {
...described,

View File

@@ -274,40 +274,86 @@ export interface VideoFrameDeduplicationResult {
type VideoFrameComparator = (
previous: VideoCaptionFrame,
current: VideoCaptionFrame
current: VideoCaptionFrame,
signal?: AbortSignal
) => Promise<number>;
const VIDEO_DEDUP_THRESHOLD = 0.04;
export const VIDEO_DEDUP_POLICY_VERSION = "grayscale-16x16-mean-cells-v2";
export const VIDEO_DEDUP_THRESHOLD = 0.04;
const VIDEO_DEDUP_CELL_DELTA_THRESHOLD = 0.05;
export const VIDEO_DEDUP_MAX_CANDIDATE_FRAMES = 16;
async function compareVideoFramesByGrayscale(
/**
* Expand a final caption budget into the bounded pool evaluated by visual deduplication.
*
* @param frameCount - Requested number of frames that may reach captioning.
* @returns One candidate for a one-frame budget, otherwise twice the budget capped at 16.
*/
export function resolveVideoDedupCandidateFrameCount(frameCount: number): number {
const normalizedFrameCount = Number.isFinite(frameCount) ? Math.floor(frameCount) : 1;
const finalFrameCount = Math.max(
1,
Math.min(VIDEO_DEDUP_MAX_CANDIDATE_FRAMES, normalizedFrameCount)
);
if (finalFrameCount === 1) return 1;
return Math.min(VIDEO_DEDUP_MAX_CANDIDATE_FRAMES, finalFrameCount * 2);
}
function throwIfVideoDedupAborted(signal?: AbortSignal): void {
if (signal?.aborted) throw new Error("Video Bridge processing timed out or was aborted");
}
/**
* Compare JPEG frames using the versioned 16x16 grayscale visual policy.
*
* @param previous - Last frame retained by deduplication.
* @param current - Candidate frame being evaluated.
* @param signal - Optional request cancellation signal checked around asynchronous image work.
* @returns The larger of mean luma delta and the ratio of materially changed cells.
* @throws When cancelled or when either frame cannot be decoded as a JPEG data URI.
*/
export async function compareVideoFramesByGrayscale(
previous: VideoCaptionFrame,
current: VideoCaptionFrame
current: VideoCaptionFrame,
signal?: AbortSignal
): Promise<number> {
throwIfVideoDedupAborted(signal);
const decode = (dataUri: string): Buffer => {
const match = /^data:image\/jpeg;base64,([A-Za-z0-9+/=]+)$/i.exec(dataUri);
if (!match) throw new Error("Video frame is not a JPEG data URI");
return Buffer.from(match[1], "base64");
};
const { default: sharp } = await import("sharp");
throwIfVideoDedupAborted(signal);
const [left, right] = await Promise.all(
[previous, current].map((frame) =>
sharp(decode(frame.dataUri)).resize(16, 16, { fit: "fill" }).greyscale().raw().toBuffer()
)
);
throwIfVideoDedupAborted(signal);
if (left.length !== right.length || left.length === 0) {
throw new Error("Video frame comparison returned invalid dimensions");
}
let difference = 0;
let changedCells = 0;
for (let index = 0; index < left.length; index++) {
difference += Math.abs(left[index] - right[index]) / 255;
const cellDifference = Math.abs(left[index] - right[index]) / 255;
difference += cellDifference;
if (cellDifference >= VIDEO_DEDUP_CELL_DELTA_THRESHOLD) changedCells += 1;
}
return difference / left.length;
return Math.max(difference / left.length, changedCells / left.length);
}
export async function deduplicateVideoFrames(
frames: readonly VideoCaptionFrame[],
options: { compare?: VideoFrameComparator; threshold?: number } = {}
options: {
compare?: VideoFrameComparator;
maxFrames?: number;
signal?: AbortSignal;
threshold?: number;
} = {}
): Promise<VideoFrameDeduplicationResult> {
throwIfVideoDedupAborted(options.signal);
if (frames.length < 2) return { dropped: 0, frames: [...frames] };
const compare = options.compare ?? compareVideoFramesByGrayscale;
const threshold =
@@ -317,23 +363,37 @@ export async function deduplicateVideoFrames(
const kept: VideoCaptionFrame[] = [frames[0]];
let dropped = 0;
for (let index = 1; index < frames.length; index++) {
throwIfVideoDedupAborted(options.signal);
const current = frames[index];
if (index === frames.length - 1) {
kept.push(current);
continue;
}
try {
const distance = await compare(kept[kept.length - 1], current);
const distance = await compare(kept[kept.length - 1], current, options.signal);
throwIfVideoDedupAborted(options.signal);
if (Number.isFinite(distance) && distance <= threshold) {
dropped += 1;
continue;
}
} catch {
throwIfVideoDedupAborted(options.signal);
// A malformed or unsupported frame must never reduce visual coverage.
}
kept.push(current);
}
return { dropped, frames: kept };
throwIfVideoDedupAborted(options.signal);
const maxFrames =
typeof options.maxFrames === "number" && Number.isFinite(options.maxFrames)
? Math.max(1, Math.floor(options.maxFrames))
: kept.length;
if (kept.length <= maxFrames) return { dropped, frames: kept };
if (maxFrames === 1) return { dropped, frames: [kept[0]] };
const capped = Array.from({ length: maxFrames }, (_unused, index) => {
const sourceIndex = Math.round((index * (kept.length - 1)) / (maxFrames - 1));
return kept[sourceIndex];
});
return { dropped, frames: capped };
}
function normalizeBase64(base64: string): string {
@@ -372,7 +432,18 @@ export function decodeVideoDataUri(
return decode(normalized);
}
async function loadVideoBytes(
/**
* Load protected video bytes from an inline data URI or SSRF-guarded HTTPS source.
*
* @param part - Extracted request video part.
* @param maxBytes - Maximum accepted decoded/downloaded size.
* @param timeoutMs - Download deadline passed to the protected fetch boundary.
* @param signal - Caller abort/deadline signal.
* @param deps - Injectable external download boundary.
* @returns Validated video bytes suitable for hashing and extraction.
* @throws When the source, size, deadline, or abort policy rejects the input.
*/
export async function loadVideoPartBytes(
part: VideoPart,
maxBytes: number,
timeoutMs: number,
@@ -427,7 +498,8 @@ export async function describeVideoPart(
timestampSeconds: number,
signal: AbortSignal
) => Promise<string>,
deps: DescribeVideoDependencies = {}
deps: DescribeVideoDependencies = {},
preloadedBytes?: Uint8Array
): Promise<DescribedVideo> {
const timeoutController = new AbortController();
const timeout = setTimeout(() => timeoutController.abort(), options.timeoutMs);
@@ -435,23 +507,28 @@ export async function describeVideoPart(
? AbortSignal.any([options.signal, timeoutController.signal])
: timeoutController.signal;
try {
const bytes = await loadVideoBytes(
part,
options.maxBytes ?? VIDEO_BRIDGE_MAX_BYTES,
options.timeoutMs,
signal,
deps
);
const maxBytes = options.maxBytes ?? VIDEO_BRIDGE_MAX_BYTES;
const bytes = preloadedBytes
? Buffer.isBuffer(preloadedBytes)
? preloadedBytes
: Buffer.from(preloadedBytes)
: await loadVideoPartBytes(part, maxBytes, options.timeoutMs, signal, deps);
if (signal.aborted) throw new Error("Video Bridge processing timed out or was aborted");
if (bytes.byteLength > maxBytes) throw new Error("Video exceeds the maximum size");
const extractFrames = deps.extractFrames ?? extractVideoFramesViaBroker;
const candidateFrameCount = resolveVideoDedupCandidateFrameCount(options.frameCount);
const extracted = await extractFrames(bytes, {
focusWindow: options.focusWindow,
frameCount: options.frameCount,
frameCount: candidateFrameCount,
samplingPolicy: options.samplingPolicy,
signal,
timeoutMs: options.timeoutMs,
});
const deduplicated = await deduplicateVideoFrames(extracted.frames);
const deduplicated = await deduplicateVideoFrames(extracted.frames, {
maxFrames: options.frameCount,
signal,
});
const contactSheet = part.contactSheet
? await buildVideoContactSheet(deduplicated.frames, {
signal,

View File

@@ -0,0 +1,232 @@
import type { VideoBridgeRuntimeSettings } from "@/shared/constants/modalityBridgeDefaults";
import {
BridgeCache,
type BridgeCacheEntry,
type BridgeCacheStore,
} from "./modalityBridge/bridgeCache";
import type { GuardrailContext } from "./base";
/** Aggregate in-memory budget for complete Video Bridge results. */
export const VIDEO_RESULT_CACHE_MAX_BYTES = 16 * 1024 * 1024;
let sharedResultCache: { cache: BridgeCache; maxEntries: number; ttlMs: number } | null = null;
/**
* Resolve the process-wide complete-result cache for Video Bridge settings.
*
* @param settings - Runtime TTL and entry-count bounds.
* @returns A cache isolated from the frame/caption bridge cache.
*/
export function getSharedVideoResultCacheFor(
settings: Pick<VideoBridgeRuntimeSettings, "cacheTtlMinutes" | "cacheMaxEntries">
): BridgeCache {
const ttlMs = settings.cacheTtlMinutes * 60_000;
if (
!sharedResultCache ||
sharedResultCache.ttlMs !== ttlMs ||
sharedResultCache.maxEntries !== settings.cacheMaxEntries
) {
sharedResultCache = {
cache: new BridgeCache({
maxBytes: VIDEO_RESULT_CACHE_MAX_BYTES,
maxEntries: settings.cacheMaxEntries,
ttlMs,
}),
maxEntries: settings.cacheMaxEntries,
ttlMs,
};
}
return sharedResultCache.cache;
}
interface VideoFlight {
controller: AbortController;
promise: Promise<unknown>;
settled: boolean;
waiters: number;
}
const videoDownloadFlights = new Map<string, VideoFlight>();
const videoResultFlights = new Map<string, VideoFlight>();
/**
* Build the canonical abort error used by Video Bridge waiters.
*
* @returns A sanitized abort error safe to propagate through the guardrail.
*/
export function videoBridgeAbortError(): Error {
return new Error("Video Bridge processing was aborted");
}
function waitForVideoFlight<T>(flight: VideoFlight, signal: AbortSignal): Promise<T> {
if (signal.aborted) return Promise.reject(videoBridgeAbortError());
return new Promise<T>((resolve, reject) => {
let completed = false;
const finish = (callback: () => void): void => {
if (completed) return;
completed = true;
signal.removeEventListener("abort", onAbort);
callback();
};
const onAbort = (): void => finish(() => reject(videoBridgeAbortError()));
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
(flight.promise as Promise<T>).then(
(value) => finish(() => resolve(value)),
(error: unknown) => finish(() => reject(error))
);
});
}
async function runVideoSingleflight<T>(
flights: Map<string, VideoFlight>,
key: string,
signal: AbortSignal,
operation: (signal: AbortSignal) => Promise<T>
): Promise<{ coalesced: boolean; value: T }> {
let flight = flights.get(key);
const coalesced = Boolean(flight);
if (!flight) {
const controller = new AbortController();
flight = {
controller,
promise: Promise.resolve().then(() => operation(controller.signal)),
settled: false,
waiters: 0,
};
const createdFlight = flight;
flights.set(key, createdFlight);
createdFlight.promise.then(
() => {
createdFlight.settled = true;
if (flights.get(key) === createdFlight) flights.delete(key);
},
() => {
createdFlight.settled = true;
if (flights.get(key) === createdFlight) flights.delete(key);
}
);
}
flight.waiters += 1;
try {
return { coalesced, value: await waitForVideoFlight<T>(flight, signal) };
} finally {
flight.waiters = Math.max(0, flight.waiters - 1);
if (flight.waiters === 0 && !flight.settled) {
flight.controller.abort();
if (flights.get(key) === flight) flights.delete(key);
}
}
}
/**
* Coalesce only concurrent protected downloads and release the Buffer after the flight settles.
*
* @param key - Hashed remote-part and request-isolation identity.
* @param signal - Abort signal for this waiter only.
* @param operation - Protected downloader invoked once with a shared producer signal.
* @returns The downloaded value shared by active waiters; it is never retained after settlement.
* @throws When this waiter aborts or the shared producer rejects.
*/
export async function runVideoDownloadSingleflight<T>(
key: string,
signal: AbortSignal,
operation: (signal: AbortSignal) => Promise<T>
): Promise<T> {
return (await runVideoSingleflight(videoDownloadFlights, key, signal, operation)).value;
}
/**
* Coalesce identical complete-result work while preserving each waiter's abort signal.
*
* @param key - Complete-result cache key.
* @param signal - Abort signal for this waiter only.
* @param operation - Producer invoked once with a shared signal.
* @returns The produced value and whether this waiter joined existing work.
* @throws When this waiter aborts or the shared producer rejects.
*/
export async function runVideoResultSingleflight<T>(
key: string,
signal: AbortSignal,
operation: (signal: AbortSignal) => Promise<T>
): Promise<{ coalesced: boolean; value: T }> {
return runVideoSingleflight(videoResultFlights, key, signal, operation);
}
type ResultCacheOperation = "delete" | "read" | "write";
function logCacheFailure(
log: GuardrailContext["log"],
operation: ResultCacheOperation,
error: unknown
): void {
const message = `Video result cache ${operation} failed open`;
const meta = { errorType: error instanceof Error ? error.name : typeof error };
if (log?.debug) {
log.debug("VIDEO_BRIDGE_CACHE", message, meta);
} else {
console.debug(`[VIDEO_BRIDGE_CACHE] ${message}`, meta);
}
}
/**
* Read a complete-result cache entry without allowing cache failure to break video processing.
*
* @param cache - Cache implementation, including caller-supplied adapters.
* @param key - Complete-result key.
* @param log - Optional request logger for fail-open diagnostics.
* @returns The entry, or `undefined` for misses and cache failures.
*/
export function safeGetCacheEntry(
cache: BridgeCacheStore,
key: string,
log?: GuardrailContext["log"]
): BridgeCacheEntry | undefined {
try {
return cache.getEntry(key);
} catch (error) {
logCacheFailure(log, "read", error);
return undefined;
}
}
/**
* Delete an invalid complete-result entry without breaking video processing.
*
* @param cache - Cache implementation, including caller-supplied adapters.
* @param key - Complete-result key.
* @param log - Optional request logger for fail-open diagnostics.
*/
export function safeDeleteCacheEntry(
cache: BridgeCacheStore,
key: string,
log?: GuardrailContext["log"]
): void {
try {
cache.delete(key);
} catch (error) {
logCacheFailure(log, "delete", error);
}
}
/**
* Store a computed complete result without allowing cache failure to discard valid output.
*
* @param cache - Cache implementation, including caller-supplied adapters.
* @param key - Complete-result key.
* @param entry - Valid computed description and metadata.
* @param log - Optional request logger for fail-open diagnostics.
*/
export function safeSetCacheEntry(
cache: BridgeCacheStore,
key: string,
entry: BridgeCacheEntry,
log?: GuardrailContext["log"]
): void {
try {
cache.setEntry(key, entry);
} catch (error) {
logCacheFailure(log, "write", error);
}
}

View File

@@ -0,0 +1,53 @@
import sharp from "sharp";
type Rectangle = {
height: number;
value: number;
width: number;
x: number;
y: number;
};
const FIXTURE_WIDTH = 256;
const FIXTURE_HEIGHT = 144;
async function renderJpeg(rectangles: readonly Rectangle[]): Promise<string> {
const pixels = Buffer.alloc(FIXTURE_WIDTH * FIXTURE_HEIGHT * 3, 255);
for (const rectangle of rectangles) {
for (let y = rectangle.y; y < rectangle.y + rectangle.height; y++) {
for (let x = rectangle.x; x < rectangle.x + rectangle.width; x++) {
const offset = (y * FIXTURE_WIDTH + x) * 3;
pixels[offset] = rectangle.value;
pixels[offset + 1] = rectangle.value;
pixels[offset + 2] = rectangle.value;
}
}
}
const jpeg = await sharp(pixels, {
raw: { channels: 3, height: FIXTURE_HEIGHT, width: FIXTURE_WIDTH },
})
.jpeg({ chromaSubsampling: "4:4:4", quality: 100 })
.toBuffer();
return `data:image/jpeg;base64,${jpeg.toString("base64")}`;
}
export async function createVideoDedupFixtures(): Promise<{
smallMotion: readonly [string, string];
staticFrame: string;
visibleText: readonly [string, string];
}> {
const staticFrame = await renderJpeg([{ height: 48, value: 0, width: 48, x: 64, y: 48 }]);
const movedFrame = await renderJpeg([{ height: 48, value: 0, width: 48, x: 68, y: 48 }]);
// Rectangular strokes stand in for glyphs without depending on platform fonts.
const textBefore = [
{ height: 64, value: 0, width: 8, x: 32, y: 32 },
{ height: 64, value: 0, width: 8, x: 48, y: 32 },
{ height: 64, value: 0, width: 8, x: 64, y: 32 },
] as const;
const textAfter = [...textBefore, { height: 64, value: 0, width: 8, x: 80, y: 32 }] as const;
return {
smallMotion: [staticFrame, movedFrame],
staticFrame,
visibleText: [await renderJpeg(textBefore), await renderJpeg(textAfter)],
};
}

View File

@@ -1,59 +0,0 @@
// Regression test for #11343 — an unconditional Turbopack `resolveAlias` for
// better-sqlite3 shipped the build-time stub into the runtime bundle, so every
// artifact built from the release tip answered HTTP 500 on every route (the
// stub export is not a constructor, the sync driver chain fell through to
// node:sqlite and sql.js, and the instrumentation hook aborted at boot).
//
// The alias defeats `serverExternalPackages` because resolveAlias rewrites the
// request BEFORE the externals check runs. It must therefore be opt-in, and a
// default production build must externalize the REAL native package.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const { shouldStubBetterSqlite3, betterSqlite3AliasFor } =
await import("../../scripts/build/better-sqlite3-stub-flag.mjs");
describe("better-sqlite3 stub alias (#11343)", () => {
it("default env does NOT stub better-sqlite3 (shipped artifacts get the real addon)", () => {
assert.equal(shouldStubBetterSqlite3({}), false);
assert.deepEqual(betterSqlite3AliasFor({}), {});
});
it("only the exact opt-in value enables the stub", () => {
for (const value of ["", "0", "true", "yes"]) {
assert.equal(
shouldStubBetterSqlite3({ OMNIROUTE_BETTER_SQLITE3_STUB: value }),
false,
`OMNIROUTE_BETTER_SQLITE3_STUB=${JSON.stringify(value)} must not enable the stub`
);
}
});
it("OMNIROUTE_BETTER_SQLITE3_STUB=1 opts into the stub (SIGABRT-prone build hosts, #10060)", () => {
assert.equal(shouldStubBetterSqlite3({ OMNIROUTE_BETTER_SQLITE3_STUB: "1" }), true);
assert.deepEqual(betterSqlite3AliasFor({ OMNIROUTE_BETTER_SQLITE3_STUB: "1" }), {
"better-sqlite3": "./src/lib/db/better-sqlite3.stub.js",
});
});
it("next.config.mjs derives the turbopack alias from the flag (no unconditional stub)", () => {
const config = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8");
assert.match(
config,
/betterSqlite3AliasFor/,
"next.config.mjs must use betterSqlite3AliasFor()"
);
assert.doesNotMatch(
config,
/^\s*"better-sqlite3":\s*"\.\/src\/lib\/db\/better-sqlite3\.stub\.js",?\s*$/m,
"next.config.mjs must not hardcode the better-sqlite3 stub alias"
);
});
it("better-sqlite3 stays in serverExternalPackages so the default build externalizes it", () => {
const config = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8");
const externals = config.slice(config.indexOf("serverExternalPackages:"));
assert.match(externals.slice(0, externals.indexOf("]")), /"better-sqlite3"/);
});
});

View File

@@ -3,8 +3,40 @@ import test from "node:test";
import {
deduplicateVideoFrames,
resolveVideoDedupCandidateFrameCount,
type VideoCaptionFrame,
} from "../../../src/lib/guardrails/videoBridgeHelpers.ts";
import { createVideoDedupFixtures } from "../../fixtures/videoBridgeDedupFixtures.ts";
const fixturesPromise = createVideoDedupFixtures();
test("dedup candidate count doubles the caption budget within the hard frame bound", () => {
assert.equal(resolveVideoDedupCandidateFrameCount(1), 1);
assert.equal(resolveVideoDedupCandidateFrameCount(3), 6);
assert.equal(resolveVideoDedupCandidateFrameCount(8), 16);
assert.equal(resolveVideoDedupCandidateFrameCount(9), 16);
assert.equal(resolveVideoDedupCandidateFrameCount(Number.NaN), 1);
});
test("deduplication stops scheduling comparator work after abort", async () => {
const controller = new AbortController();
let comparisons = 0;
const pending = deduplicateVideoFrames(
[frame(1), frame(2), frame(3), frame(4), frame(5), frame(6)],
{
compare: async () => {
comparisons += 1;
await new Promise<void>((resolve) => setTimeout(resolve, 30));
return 0.2;
},
signal: controller.signal,
}
);
setTimeout(() => controller.abort(), 5);
await assert.rejects(pending, /aborted/i);
assert.equal(comparisons, 1);
});
const frame = (
timestampSeconds: number,
@@ -37,13 +69,80 @@ test("deduplication keeps visually distinct frames", async () => {
assert.equal(result.dropped, 0);
});
test("deduplication fails open when the visual comparator errors", async () => {
const result = await deduplicateVideoFrames([frame(1), frame(2)], {
compare: async () => {
throw new Error("invalid JPEG");
},
});
test("deduplication applies the final cap after comparison while preserving both endpoints", async () => {
const result = await deduplicateVideoFrames(
[frame(1), frame(2), frame(3), frame(4), frame(5), frame(6)],
{
compare: async (_previous, current) =>
current.timestampSeconds === 2 || current.timestampSeconds === 4 ? 0.01 : 0.2,
maxFrames: 3,
threshold: 0.05,
}
);
assert.equal(result.frames.length, 2);
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 5, 6]
);
assert.equal(result.dropped, 2, "only visual duplicates count as dedup drops");
});
test("the real grayscale policy preserves a small moving subject", async () => {
const fixtures = await fixturesPromise;
const result = await deduplicateVideoFrames([
frame(1, fixtures.smallMotion[0]),
frame(2, fixtures.smallMotion[1]),
frame(3, fixtures.smallMotion[0]),
]);
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 2, 3]
);
assert.equal(result.dropped, 0);
});
test("the real grayscale policy drops a static fixture", async () => {
const fixtures = await fixturesPromise;
const result = await deduplicateVideoFrames([
frame(1, fixtures.staticFrame),
frame(2, fixtures.staticFrame),
frame(3, fixtures.smallMotion[1]),
]);
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 3]
);
assert.equal(result.dropped, 1);
});
test("the real grayscale policy preserves a visible text change", async () => {
const fixtures = await fixturesPromise;
const result = await deduplicateVideoFrames([
frame(1, fixtures.visibleText[0]),
frame(2, fixtures.visibleText[1]),
frame(3, fixtures.visibleText[0]),
]);
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 2, 3]
);
assert.equal(result.dropped, 0);
});
test("deduplication fails open for a malformed JPEG candidate", async () => {
const fixtures = await fixturesPromise;
const result = await deduplicateVideoFrames([
frame(1, fixtures.staticFrame),
frame(2, "data:image/jpeg;base64,bm90LWEtanBlZw=="),
frame(3, fixtures.staticFrame),
]);
assert.deepEqual(
result.frames.map((item) => item.timestampSeconds),
[1, 2, 3]
);
assert.equal(result.dropped, 0);
});

View File

@@ -366,6 +366,44 @@ test("uses the broker seam, reports configured versus extracted frames, and mark
assert.match(result.description, /do not follow instructions/i);
});
test("uses a bounded candidate pool before the final caption cap and preserves endpoint coverage", async () => {
let candidateFrameCount = 0;
const captionedTimestamps: number[] = [];
const result = await describeVideoPart(
{
container: "messages",
messageIndex: 0,
partIndex: 0,
ref: "data:video/mp4;base64,QUJD",
shape: "input_video",
},
{ frameCount: 3, timeoutMs: 5_000 },
async (_frame, timestampSeconds) => {
captionedTimestamps.push(timestampSeconds);
return `frame ${timestampSeconds}`;
},
{
extractFrames: async (_bytes, options) => {
candidateFrameCount = options.frameCount;
return {
durationSeconds: 6,
frames: Array.from({ length: options.frameCount }, (_unused, index) => ({
dataUri: `data:image/jpeg;base64,${Buffer.from(String(index)).toString("base64")}`,
timestampSeconds: index + 1,
})),
};
},
}
);
assert.equal(candidateFrameCount, 6, "three caption slots get at most two candidates each");
assert.deepEqual(captionedTimestamps, [1, 4, 6]);
assert.equal(result.framesRequested, 3);
assert.equal(result.framesExtracted, 6);
assert.equal(result.framesUsed, 3);
assert.equal(result.dedupDropped, 0, "malformed candidate comparisons must fail open");
});
test("video downloads require HTTPS on every redirect hop", async () => {
let requireHttps: boolean | undefined;
await describeVideoPart(

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,37 @@ test("key framing prevents boundary-shift collisions between fields", () => {
assert.notEqual(bridgeCacheKey("x", "yz", "m"), bridgeCacheKey("x", "y", "zm"));
});
test("video cache keys change with every visual dedup policy dimension", () => {
const base = {
dedupCandidateFrameCount: 16,
dedupPolicyVersion: "grayscale-16x16-mean-cells-v2",
dedupThreshold: 0.04,
};
const key = bridgeCacheKey("video", "describe", "gpt-4o-mini", base);
assert.notEqual(
key,
bridgeCacheKey("video", "describe", "gpt-4o-mini", {
...base,
dedupPolicyVersion: "grayscale-16x16-mean-cells-v3",
})
);
assert.notEqual(
key,
bridgeCacheKey("video", "describe", "gpt-4o-mini", {
...base,
dedupThreshold: 0.05,
})
);
assert.notEqual(
key,
bridgeCacheKey("video", "describe", "gpt-4o-mini", {
...base,
dedupCandidateFrameCount: 8,
})
);
});
test("get/set roundtrip and TTL expiry", () => {
let now = 1000;
const cache = new BridgeCache({ maxEntries: 10, ttlMs: 500, now: () => now });

View File

@@ -83,10 +83,6 @@ test("next config declares Turbopack aliases, runtime assets and server external
// A default production build must NOT alias it, or the stub ships to npm/Electron/VPS
// artifacts and breaks Agent Bridge start. See the dedicated env-matrix test below.
assert.equal(nextConfig.turbopack.resolveAlias["@/mitm/manager"], undefined);
// #11343: same story for the better-sqlite3 build stub. resolveAlias is applied
// BEFORE the serverExternalPackages check, so an unconditional alias bundles the
// stub and every route answers 500 at runtime ("r(...) is not a constructor").
assert.equal(nextConfig.turbopack.resolveAlias["better-sqlite3"], undefined);
assert.equal(nextConfig.outputFileTracingRoot, process.cwd());
assert.ok(tracingIncludes.includes("./src/lib/db/migrations/**/*"));
assert.ok(
@@ -122,28 +118,6 @@ test("next config declares Turbopack aliases, runtime assets and server external
}
});
test("Turbopack aliases better-sqlite3 to the stub ONLY when OMNIROUTE_BETTER_SQLITE3_STUB=1 (#11343)", async () => {
const original = process.env.OMNIROUTE_BETTER_SQLITE3_STUB;
try {
delete process.env.OMNIROUTE_BETTER_SQLITE3_STUB;
const { default: def } = await loadNextConfig("bettersqlite-default");
assert.equal(def.turbopack.resolveAlias["better-sqlite3"], undefined);
// The default build must keep the real package reachable as an external, which
// is exactly what the alias silently defeated.
assert.ok(new Set(def.serverExternalPackages).has("better-sqlite3"));
process.env.OMNIROUTE_BETTER_SQLITE3_STUB = "1";
const { default: stubbed } = await loadNextConfig("bettersqlite-optin");
assert.equal(
stubbed.turbopack.resolveAlias["better-sqlite3"],
"./src/lib/db/better-sqlite3.stub.js"
);
} finally {
if (original === undefined) delete process.env.OMNIROUTE_BETTER_SQLITE3_STUB;
else process.env.OMNIROUTE_BETTER_SQLITE3_STUB = original;
}
});
test("Turbopack aliases @/mitm/manager to the stub ONLY when OMNIROUTE_MITM_STUB=1 (#6344)", async () => {
const original = process.env.OMNIROUTE_MITM_STUB;
try {