Compare commits

..

2 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
6afa6846c8 docs(changelog): link catalog responsiveness PR 2026-08-24 05:24:26 -03:00
Diego Rodrigues de Sa e Souza
acb15fefba fix(catalog): keep large builds event-loop responsive 2026-08-24 04:41:52 -03:00
21 changed files with 196 additions and 2135 deletions

View File

@@ -1 +0,0 @@
- **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(catalog):** keep large `/v1/models` builds responsive by reusing the build-local capability snapshot throughout enrichment and Auto-Combo preparation, yielding cooperatively while constructing virtual candidate pools, and avoiding unrelated synchronous database diagnostics on the cache-TTL read path ([#11367](https://github.com/diegosouzapw/OmniRoute/pull/11367))

View File

@@ -1 +0,0 @@
- **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-24
lastUpdated: 2026-08-14
---
# Guardrails
> **Source of truth:** `src/lib/guardrails/`
> **Last updated:** 2026-08-24 — v3.8.50 (Video Bridge visual dedup hardening)
> **Last updated:** 2026-08-15 — v3.8.50 (Video Bridge broker confinement)
Guardrails enforce safety, policy, and content transformations at the boundary
between OmniRoute and upstream providers. Each guardrail can inspect (and
@@ -340,20 +340,11 @@ 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. 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.
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.
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
@@ -410,10 +401,7 @@ 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. 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.
dimensions is a cache miss, never a stale reuse.
The guardrail extracts every supported video part but describes no more than
`modalityBridgeVideoMaxVideos`. For a target proven to have

View File

@@ -1,3 +1,5 @@
import type { ModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilities";
import type { AutoVariant } from "./autoPrefix";
import { VALID_VARIANTS } from "./autoPrefix";
import type { PreparedVirtualAutoComboInputs } from "./virtualFactory";
@@ -119,8 +121,7 @@ export function isPaidTierAutoId(autoId: string): boolean {
* a candidate filter so the virtual combo only scores vision-capable models.
*/
export type BuiltinAutoSpec =
| { variant: AutoVariant | undefined }
| { category: AutoCategory; tier?: AutoTier };
{ variant: AutoVariant | undefined } | { category: AutoCategory; tier?: AutoTier };
/**
* Vision-flavored flat ids that MUST resolve to the `vision` category (candidate
@@ -159,9 +160,14 @@ export function resolveBuiltinAutoSpec(modelStr: string, suffix: string): Builti
return { variant: undefined };
}
export async function prepareBuiltinAutoComboInputs(): Promise<PreparedVirtualAutoComboInputs> {
export async function prepareBuiltinAutoComboInputs(
resolutionSnapshot?: ModelCapabilityResolutionSnapshot
): Promise<PreparedVirtualAutoComboInputs> {
const { prepareVirtualAutoComboInputs } = await import("./virtualFactory.ts");
return prepareVirtualAutoComboInputs({ includeResolvedCapabilities: true });
return prepareVirtualAutoComboInputs({
includeResolvedCapabilities: true,
resolutionSnapshot,
});
}
export async function createBuiltinAutoCombo(

View File

@@ -404,7 +404,9 @@ export function computeAdvertisedLimits(candidates: AdvertisedLimitCandidate[]):
return { contextLength, maxOutputTokens };
}
const PREPARED_CAPABILITY_YIELD_INTERVAL = 16;
// Catalog-scale pools can contain hundreds of models. Keep both candidate construction
// and capability preparation cooperative instead of monopolising one event-loop turn.
const VIRTUAL_AUTO_PREPARATION_YIELD_INTERVAL = 4;
type PreparedCapabilityValues = {
resolvedContextLength: number | null;
@@ -468,7 +470,7 @@ async function attachPreparedCapabilityValues(
};
byModel.set(candidate.model, values);
state.resolvedSinceYield++;
if (state.resolvedSinceYield >= PREPARED_CAPABILITY_YIELD_INTERVAL) {
if (state.resolvedSinceYield >= VIRTUAL_AUTO_PREPARATION_YIELD_INTERVAL) {
state.resolvedSinceYield = 0;
await yieldVirtualAutoPreparationTurn();
}
@@ -479,7 +481,10 @@ async function attachPreparedCapabilityValues(
}
export async function prepareVirtualAutoComboInputs(
options: { includeResolvedCapabilities?: boolean } = {}
options: {
includeResolvedCapabilities?: boolean;
resolutionSnapshot?: ModelCapabilityResolutionSnapshot;
} = {}
): Promise<PreparedVirtualAutoComboInputs> {
const [connections, disabledNoAuthConnections, settings] = await Promise.all([
getCachedProviderConnections({ isActive: true }) as Promise<VirtualFactoryConn[]>,
@@ -524,6 +529,7 @@ export async function prepareVirtualAutoComboInputs(
// Build one logical candidate per provider/model and keep account fallback as an
// allowlist on that candidate. This avoids both the old "first registry model per
// connection" blind spot and a connections × models Cartesian candidate pool.
let candidateModelsSinceYield = 0;
for (const [providerId, providerConnections] of connectionsByProvider) {
const providerInfo = registry[providerId];
const registryModelIds = Array.isArray(providerInfo?.models)
@@ -557,6 +563,11 @@ export async function prepareVirtualAutoComboInputs(
: Array.from(new Set([...registryModelIds, ...defaultModelIds]));
for (const modelId of modelIds) {
candidateModelsSinceYield++;
if (candidateModelsSinceYield >= VIRTUAL_AUTO_PREPARATION_YIELD_INTERVAL) {
candidateModelsSinceYield = 0;
await yieldVirtualAutoPreparationTurn();
}
if (hiddenModels?.has(modelId)) continue;
const allowedConnectionIds = providerConnections
@@ -655,7 +666,7 @@ export async function prepareVirtualAutoComboInputs(
const capabilityState: PreparedCapabilityState = {
byTarget: new Map(),
resolvedSinceYield: 0,
resolutionSnapshot: createModelCapabilityResolutionSnapshot(),
resolutionSnapshot: options.resolutionSnapshot ?? createModelCapabilityResolutionSnapshot(),
};
return {
regularCandidates: await attachPreparedCapabilityValues(regularCandidates, capabilityState),

View File

@@ -1,78 +1,24 @@
/**
* Video Bridge benchmarks (VB-FU-03 dedup comparator, VB-FU-07 sampler overhead,
* and VB-FU-09 contact sheet A/B).
* Video Bridge benchmarks (VB-FU-07 sampler overhead + VB-FU-09 contact sheet A/B).
*
* Run: node --import tsx/esm scripts/perf/video-bridge-bench.ts
*
* 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
* 1. 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.
* 3. Contact sheet: composes synthetic JPEG frames into the timestamped grid
* 2. 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) ==");
@@ -101,12 +47,12 @@ function benchSampler(): void {
}
}
async function syntheticJpegFrame(index: number, width = 512, height = 288): Promise<string> {
async function syntheticJpegFrame(index: number): Promise<string> {
const { default: sharp } = await import("sharp");
const buffer = await sharp({
create: {
width,
height,
width: 512,
height: 288,
channels: 3,
background: { r: (index * 37) % 255, g: (index * 91) % 255, b: (index * 53) % 255 },
},
@@ -140,7 +86,5 @@ async function benchContactSheet(): Promise<void> {
}
}
await benchDedupComparator();
console.log("");
benchSampler();
await benchContactSheet();

View File

@@ -7,9 +7,9 @@ import {
getSettings,
getCachedProviderNodes,
getModelAliases,
getDatabaseSettings,
getHiddenModelsByProvider,
} from "@/lib/localDb";
import { getUserDatabaseSettings } from "@/lib/db/databaseSettings";
import { createLazyConnectionView } from "@/lib/db/providers/lazyConnectionView";
import { extractAliasBackedModels } from "./aliasBackedModels";
import {
@@ -229,7 +229,10 @@ async function buildCatalogPayload(
// Falls back to the hardcoded default if not set or on error.
let cacheTTL = CATALOG_CACHE_TTL_MS_DEFAULT;
try {
const dbSettings = await getDatabaseSettings();
// Only the persisted cache section is needed here. The full database-settings
// view also calculates dbstat, WAL, schema and integrity diagnostics, which are
// synchronous and can pin the event loop after an otherwise cooperative build.
const dbSettings = getUserDatabaseSettings();
cacheTTL = dbSettings.cache?.modelCatalogCacheTtlMs ?? CATALOG_CACHE_TTL_MS_DEFAULT;
} catch {
// Swallow — use default TTL on DB error
@@ -249,7 +252,7 @@ async function buildUnifiedModelsResponseCore(
// event-loop yield, so a large deployment pins the single Node.js thread for the
// whole build (reporter: 183 connections / 2000+ models → 10.1s stall that blocks the
// dashboard WS heartbeat). Yield every `catYIELD_EVERY` items across the hot loops.
const catYIELD_EVERY = 20;
const catYIELD_EVERY = 5;
let catYieldCount = 0;
const maybeYieldCatalogBuild = async (): Promise<void> => {
catYieldCount++;
@@ -393,11 +396,10 @@ async function buildUnifiedModelsResponseCore(
): boolean => {
if (!providerKey || !modelId) return false;
const canonical = canonicalProviderId || resolveCanonicalProviderId(providerKey);
const alias =
providerIdToAlias[canonical] || providerIdToAlias[providerKey] || undefined;
const alias = providerIdToAlias[canonical] || providerIdToAlias[providerKey] || undefined;
const nodePrefix = providerIdToPrefix[providerKey] || providerIdToPrefix[canonical];
const keysToCheck = [providerKey, canonical, alias, nodePrefix].filter(
(k): k is string => Boolean(k)
const keysToCheck = [providerKey, canonical, alias, nodePrefix].filter((k): k is string =>
Boolean(k)
);
for (const key of keysToCheck) {
const hiddenSet = hiddenModelsByProvider.get(key);
@@ -830,7 +832,7 @@ async function buildUnifiedModelsResponseCore(
try {
const suffix = autoId.replace(/^auto\/?/, "");
if (!preparedAutoInputs) {
preparedAutoInputs = await prepareBuiltinAutoComboInputs();
preparedAutoInputs = await prepareBuiltinAutoComboInputs(capabilityResolutionSnapshot);
await yieldCatalogBuildTurn();
}
const virtualCombo = await createBuiltinAutoCombo(autoId, suffix, preparedAutoInputs);
@@ -1053,11 +1055,7 @@ async function buildUnifiedModelsResponseCore(
// `openai` provider page (codex runs on the openai-compatible connection)
// or via the `cx` alias — check all three so a hide from any of them
// suppresses the bare model id here.
if (
isModelHiddenBulk("codex", modelId) ||
isModelHiddenBulk("openai", modelId)
)
continue;
if (isModelHiddenBulk("codex", modelId) || isModelHiddenBulk("openai", modelId)) continue;
const alias = providerIdToAlias.codex || "cx";
const aliasId = `${alias}/${modelId}`;
@@ -1892,7 +1890,9 @@ async function buildUnifiedModelsResponseCore(
const modelId =
model.root || (typeof model.id === "string" ? model.id.split("/").pop() : undefined);
return modelId ? getTokenLimit(canonicalId, modelId) : getTokenLimit(canonicalId);
return modelId
? getTokenLimit(canonicalId, modelId, capabilityResolutionSnapshot)
: getTokenLimit(canonicalId, null, capabilityResolutionSnapshot);
};
let enrichmentSnapshot: CatalogEnrichmentSnapshot | undefined;
@@ -1905,7 +1905,7 @@ async function buildUnifiedModelsResponseCore(
}
enrichmentSnapshot = {
modelsDevPricing,
capabilityResolution: capabilityResolutionSnapshot,
capabilityResolutionSnapshot,
providerNodeIdsByPrefix: providerNodeIdByPrefix,
};
// The production profile identified pricing snapshot construction as the last

View File

@@ -227,7 +227,8 @@ export async function finalizeCatalogResponse(
// per-entry work is interleaved with other callers / the dashboard WS.
const yieldTurn = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));
await yieldTurn();
const capabilityResolutionSnapshot = createModelCapabilityResolutionSnapshot();
const capabilityResolutionSnapshot =
enrichmentSnapshot?.capabilityResolutionSnapshot ?? createModelCapabilityResolutionSnapshot();
const enriched: Array<Record<string, unknown>> = [];
const catYIELD_EVERY = 5;
let catEnrichCount = 0;

View File

@@ -11,9 +11,6 @@ import type { VisionBridgeRuntimeSettings } from "@/shared/constants/modalityBri
export interface BridgeCacheKeyOptions {
kind?: string;
dedupCandidateFrameCount?: number;
dedupPolicyVersion?: string;
dedupThreshold?: number;
extractorVersion?: string;
policyVersion?: string;
strategy?: string;
@@ -41,9 +38,6 @@ 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,
@@ -61,8 +55,6 @@ 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;
@@ -75,37 +67,8 @@ export interface BridgeCacheEntry {
metadata?: Record<string, unknown>;
}
/** 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;
export class BridgeCache {
private readonly entries = new Map<string, { entry: BridgeCacheEntry; expiresAt: number }>();
constructor(private readonly opts: BridgeCacheOptions) {}
@@ -118,7 +81,7 @@ export class BridgeCache implements BridgeCacheStore {
if (!hit) return undefined;
const now = (this.opts.now ?? Date.now)();
if (hit.expiresAt <= now) {
this.delete(key);
this.entries.delete(key);
return undefined;
}
// Map preserves insertion order — re-insert to mark as most-recently-used.
@@ -133,17 +96,12 @@ export class BridgeCache implements BridgeCacheStore {
setEntry(key: string, entry: BridgeCacheEntry): void {
const now = (this.opts.now ?? Date.now)();
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) {
this.entries.delete(key);
this.entries.set(key, { entry, expiresAt: now + this.opts.ttlMs });
while (this.entries.size > this.opts.maxEntries) {
const oldest = this.entries.keys().next().value;
if (oldest === undefined) break;
this.delete(oldest);
this.entries.delete(oldest);
}
}
@@ -151,52 +109,21 @@ export class BridgeCache implements BridgeCacheStore {
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; maxBytes: number; maxEntries: number } | null =
null;
let shared: { cache: BridgeCache; ttlMs: number; maxEntries: number } | null = null;
/**
* 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,
};
export function getSharedBridgeCache(ttlMs: number, maxEntries: number): BridgeCache {
if (!shared || shared.ttlMs !== ttlMs || shared.maxEntries !== maxEntries) {
shared = { cache: new BridgeCache({ maxEntries, ttlMs }), ttlMs, maxEntries };
}
return shared.cache;
}

View File

@@ -19,8 +19,6 @@ 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;
@@ -49,7 +47,6 @@ function emptyStats(): BridgeModalityStats {
resultCacheBytes: 0,
resultCacheHits: 0,
resultCacheLatencyMs: 0,
resultSingleflightCoalesced: 0,
failures: 0,
fusionRuns: 0,
fusionPartials: 0,
@@ -72,8 +69,6 @@ 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];
@@ -109,7 +104,6 @@ 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,5 +1,3 @@
import { createHash } from "node:crypto";
import { fetch as undiciFetch } from "undici";
import { getSettings as defaultGetSettings } from "@/lib/db/settings";
@@ -10,38 +8,18 @@ import {
} from "@/shared/constants/modalityBridgeDefaults";
import { BaseGuardrail, type GuardrailContext, type GuardrailResult } from "./base";
import {
bridgeCacheKey,
getSharedBridgeCacheFor,
type BridgeCacheEntry,
type BridgeCacheStore,
} from "./modalityBridge/bridgeCache";
import { bridgeCacheKey, getSharedBridgeCacheFor } 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,
@@ -70,62 +48,9 @@ function safeTranscriptFingerprint(value: unknown): string {
}
}
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])}`;
}
const VIDEO_BRIDGE_RESULT_CACHE_VERSION = "v2";
const VIDEO_BRIDGE_RESULT_CACHE_POLICY = "default";
const VIDEO_BRIDGE_RESULT_CACHE_KEY_KIND = "video-result-v2";
interface VideoResultCacheMetadata {
cacheVersion: string;
@@ -136,9 +61,6 @@ interface VideoResultCacheMetadata {
prompt: string;
frameCount: number;
maxVideos: number;
dedupCandidateFrameCount: number;
dedupPolicyVersion: string;
dedupThreshold: number;
durationSeconds: number;
framesRequested: number;
framesExtracted: number;
@@ -156,86 +78,6 @@ 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>;
@@ -260,8 +102,6 @@ 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,
@@ -270,70 +110,28 @@ export interface VideoBridgeDependencies {
) => Promise<string>;
}
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 {
function isVideoResultCacheMetadata(value: unknown): 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" &&
isFiniteNonNegativeInteger(record.frameCount) &&
isFiniteNonNegativeInteger(record.maxVideos) &&
isFiniteNonNegativeNumber(record.durationSeconds) &&
isFiniteNonNegativeInteger(record.cacheBytes) &&
record.cacheBytes === expectedCacheBytes &&
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" &&
typeof record.modelUsed === "string" &&
(record.samplingCandidateCount === undefined ||
isFiniteNonNegativeInteger(record.samplingCandidateCount)) &&
(typeof record.samplingCandidateCount === "number" && record.samplingCandidateCount >= 0)) &&
(record.samplingPolicyEffective === undefined ||
record.samplingPolicyEffective === "uniform" ||
record.samplingPolicyEffective === "scene_aware" ||
@@ -343,22 +141,12 @@ function isVideoResultCacheMetadata(
record.samplingPolicyRequested === "scene_aware" ||
record.samplingPolicyRequested === "segment_aware") &&
(record.transcriptCuesApplied === undefined ||
isFiniteNonNegativeInteger(record.transcriptCuesApplied)) &&
(typeof record.transcriptCuesApplied === "number" && record.transcriptCuesApplied >= 0)) &&
(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;
@@ -400,9 +188,7 @@ 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
? (this.deps.resultCache ?? getSharedVideoResultCacheFor(runtime))
: null;
const cache = runtime.cacheEnabled ? getSharedBridgeCacheFor(runtime) : null;
const successfulModels = new Set<string>();
let selectedModelPromise: Promise<string | null> | null = null;
const selectVideoModel = (): Promise<string | null> => {
@@ -445,62 +231,37 @@ 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 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 =
const selectedModel = await selectVideoModel();
const resultCacheKey =
cache && selectedModel
? createVideoResultCacheIdentity(runtime, visionRuntime, 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,
})
: null;
const resultCacheKey = resultCacheIdentity
? buildVideoResultCacheKey(contentFingerprint, resultCacheIdentity, part)
: null;
const cachedResult = resultCacheKey
? safeGetCacheEntry(cache, resultCacheKey, context.log)
: null;
if (cachedResult && isVideoResultCacheEntry(cachedResult)) {
const cachedResult = resultCacheKey ? cache.getEntry(resultCacheKey) : null;
if (cachedResult && isVideoResultCacheMetadata(cachedResult.metadata)) {
const meta = cachedResult.metadata;
const matchPolicy =
resultCacheIdentity && matchesVideoResultCacheIdentity(meta, resultCacheIdentity);
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;
if (matchPolicy) {
const elapsed = Date.now() - attemptStartedAt;
descriptions.push(cachedResult.value);
@@ -538,60 +299,21 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
});
continue;
}
safeDeleteCacheEntry(cache, resultCacheKey, context.log);
cache.delete(resultCacheKey);
} else if (cachedResult) {
safeDeleteCacheEntry(cache, resultCacheKey, context.log);
cache.delete(resultCacheKey);
}
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
const cacheStartAt = Date.now();
const described = this.deps.describePart
? await this.deps.describePart(part)
: await this.describeWithVisionModel(
part,
runtime,
visionRuntime,
selectedModel,
context.signal
);
}
return described;
};
const resolved =
resultCacheKey && selectedModel
? await runVideoResultSingleflight(resultCacheKey, attemptSignal, describeAndCache)
: { coalesced: false, value: await describeAndCache(attemptSignal) };
const described = resolved.value;
if (context.signal?.aborted) throw new Error("Video Bridge processing was aborted");
if (described.modelUsed) successfulModels.add(described.modelUsed);
const videoCacheHits = described.cacheHits ?? 0;
const processingLatencyMs = Date.now() - attemptStartedAt;
@@ -614,12 +336,46 @@ 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,
resultSingleflightCoalesced: resolved.coalesced,
resultCacheBytes,
resultCacheHit: false,
resultCacheLatencyMs: cacheLatencyMs,
});
} else {
recordBridgeUse("video", {
@@ -652,8 +408,6 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
? `[Video ${index + 1}]: (unavailable — video could not be described)`
: null
);
} finally {
clearTimeout(attemptTimeout);
}
}
@@ -703,8 +457,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
runtime: ReturnType<typeof resolveVideoBridgeRuntimeSettings>,
visionRuntime: ReturnType<typeof resolveVisionBridgeRuntimeSettings>,
selectedModel: string | null,
signal?: AbortSignal,
preloadedBytes?: Uint8Array
signal?: AbortSignal
): Promise<DescribedVideo> {
if (!selectedModel) {
throw new Error("No vision-capable provider connected for Video Bridge");
@@ -750,11 +503,7 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
if (key && cache) cache.setEntry(key, { value: caption, producerModel });
return caption;
},
{
extractFrames: this.deps.extractFrames,
fetchRemote: this.deps.fetchRemote,
},
preloadedBytes
{ extractFrames: this.deps.extractFrames }
);
return {
...described,

View File

@@ -274,86 +274,40 @@ export interface VideoFrameDeduplicationResult {
type VideoFrameComparator = (
previous: VideoCaptionFrame,
current: VideoCaptionFrame,
signal?: AbortSignal
current: VideoCaptionFrame
) => Promise<number>;
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;
const VIDEO_DEDUP_THRESHOLD = 0.04;
/**
* 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(
async function compareVideoFramesByGrayscale(
previous: VideoCaptionFrame,
current: VideoCaptionFrame,
signal?: AbortSignal
current: VideoCaptionFrame
): 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++) {
const cellDifference = Math.abs(left[index] - right[index]) / 255;
difference += cellDifference;
if (cellDifference >= VIDEO_DEDUP_CELL_DELTA_THRESHOLD) changedCells += 1;
difference += Math.abs(left[index] - right[index]) / 255;
}
return Math.max(difference / left.length, changedCells / left.length);
return difference / left.length;
}
export async function deduplicateVideoFrames(
frames: readonly VideoCaptionFrame[],
options: {
compare?: VideoFrameComparator;
maxFrames?: number;
signal?: AbortSignal;
threshold?: number;
} = {}
options: { compare?: VideoFrameComparator; threshold?: number } = {}
): Promise<VideoFrameDeduplicationResult> {
throwIfVideoDedupAborted(options.signal);
if (frames.length < 2) return { dropped: 0, frames: [...frames] };
const compare = options.compare ?? compareVideoFramesByGrayscale;
const threshold =
@@ -363,37 +317,23 @@ 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, options.signal);
throwIfVideoDedupAborted(options.signal);
const distance = await compare(kept[kept.length - 1], current);
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);
}
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 };
return { dropped, frames: kept };
}
function normalizeBase64(base64: string): string {
@@ -432,18 +372,7 @@ export function decodeVideoDataUri(
return decode(normalized);
}
/**
* 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(
async function loadVideoBytes(
part: VideoPart,
maxBytes: number,
timeoutMs: number,
@@ -498,8 +427,7 @@ export async function describeVideoPart(
timestampSeconds: number,
signal: AbortSignal
) => Promise<string>,
deps: DescribeVideoDependencies = {},
preloadedBytes?: Uint8Array
deps: DescribeVideoDependencies = {}
): Promise<DescribedVideo> {
const timeoutController = new AbortController();
const timeout = setTimeout(() => timeoutController.abort(), options.timeoutMs);
@@ -507,28 +435,23 @@ export async function describeVideoPart(
? AbortSignal.any([options.signal, timeoutController.signal])
: timeoutController.signal;
try {
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 bytes = await loadVideoBytes(
part,
options.maxBytes ?? VIDEO_BRIDGE_MAX_BYTES,
options.timeoutMs,
signal,
deps
);
const extractFrames = deps.extractFrames ?? extractVideoFramesViaBroker;
const candidateFrameCount = resolveVideoDedupCandidateFrameCount(options.frameCount);
const extracted = await extractFrames(bytes, {
focusWindow: options.focusWindow,
frameCount: candidateFrameCount,
frameCount: options.frameCount,
samplingPolicy: options.samplingPolicy,
signal,
timeoutMs: options.timeoutMs,
});
const deduplicated = await deduplicateVideoFrames(extracted.frames, {
maxFrames: options.frameCount,
signal,
});
const deduplicated = await deduplicateVideoFrames(extracted.frames);
const contactSheet = part.contactSheet
? await buildVideoContactSheet(deduplicated.frames, {
signal,

View File

@@ -1,232 +0,0 @@
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

@@ -40,7 +40,6 @@ type JsonRecord = Record<string, unknown>;
export interface CatalogEnrichmentSnapshot {
modelsDevPricing: PricingByProvider | null;
capabilityResolution?: ModelCapabilityResolutionSnapshot;
providerNodeIdsByPrefix?: Readonly<Record<string, string>>;
/** #9147: build-local bulk load of synced capabilities + token/context overrides
* so per-entry enrichment never hits SQLite again (see catalogResponse.ts). */

View File

@@ -1,53 +0,0 @@
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

@@ -58,7 +58,7 @@ test.after(async () => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("#9147 — catalog build at catalog-scale must not pin the event loop for a long stretch", async () => {
test("#9147 — catalog build at catalog-scale must not pin the event loop for a long stretch", async (t) => {
await seedCatalogScaleDataset();
const req = new Request("http://localhost/v1/models");
let settled = false;
@@ -79,6 +79,9 @@ test("#9147 — catalog build at catalog-scale must not pin the event loop for a
}
const res = await buildPromise;
assert.equal(res.status, 200);
t.diagnostic(
`maximum event-loop gap: ${maxGapMs.toFixed(1)}ms across ${ticks} interleaved ticks`
);
// 150ms is tight on GitHub-hosted unit shards (`--test-concurrency=4`):
// sibling tests share the event loop, so a healthy yielding builder still
// records 200260ms gaps. 400ms still fails a true pin (seconds) while
@@ -89,4 +92,9 @@ test("#9147 — catalog build at catalog-scale must not pin the event loop for a
`catalog for ${CONNECTION_COUNT} connections / ${CONNECTION_COUNT * MODELS_PER_CONNECTION} models ` +
`(${ticks} interleaved ticks observed) — the builder is not yielding to the event loop`
);
const body = (await res.json()) as { data?: Array<{ root?: string }> };
assert.ok(
body.data?.some((model) => model.root === "probe-model-59-11"),
"the responsiveness probe must still traverse and return the last seeded catalog model"
);
});

View File

@@ -3,40 +3,8 @@ 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,
@@ -69,80 +37,13 @@ test("deduplication keeps visually distinct frames", async () => {
assert.equal(result.dropped, 0);
});
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,
}
);
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");
},
});
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.frames.length, 2);
assert.equal(result.dropped, 0);
});

View File

@@ -366,44 +366,6 @@ 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,37 +23,6 @@ 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 });