Compare commits

...

40 Commits

Author SHA1 Message Date
diegosouzapw
45dbe13620 test(video): reproduce Kiro malformed payload retention 2026-08-27 04:23:23 -03:00
diegosouzapw
52670c1bf6 docs(video): align transcript retention contract 2026-08-27 04:09:28 -03:00
diegosouzapw
60dd135d62 security(video): close executor and lockout retention gaps 2026-08-27 04:06:28 -03:00
diegosouzapw
0b18b2dc27 test(video): close retained executor and lockout gaps 2026-08-27 03:55:15 -03:00
diegosouzapw
4fee959ad2 fix(video): preserve safe continuation and handoffs 2026-08-27 03:29:50 -03:00
diegosouzapw
d62ecb5d98 test(video): prove safe continuation and handoff gaps 2026-08-27 03:18:39 -03:00
diegosouzapw
ab60b95e31 fix(video): redact executor diagnostics 2026-08-27 03:07:06 -03:00
diegosouzapw
6d209a9128 test(video): cover GLM fallback diagnostic retention 2026-08-27 03:02:28 -03:00
diegosouzapw
62b1835bb5 test(video): prove executor diagnostics redaction gaps 2026-08-27 02:58:06 -03:00
diegosouzapw
23832ba1e9 security(video): bypass response-derived caches 2026-08-27 02:44:46 -03:00
diegosouzapw
11e501f099 Merge remote cache-retention RED commit 2026-08-27 02:38:37 -03:00
diegosouzapw
747ae10e62 test(video): expose response-derived cache retention 2026-08-27 02:36:42 -03:00
diegosouzapw
64b78df100 test(video): expose response-derived cache retention 2026-08-27 02:35:27 -03:00
diegosouzapw
ff70ea7633 security(video): propagate stream retention context 2026-08-27 02:27:14 -03:00
diegosouzapw
6b36696d95 test(video): expose streaming finalization leak 2026-08-27 02:24:25 -03:00
diegosouzapw
d62c0f2688 fix(video): preserve operational model lockouts 2026-08-27 02:20:04 -03:00
diegosouzapw
b945e3c80e test(video): preserve daily quota lockout semantics 2026-08-27 02:17:21 -03:00
diegosouzapw
132ad5f16d security(video): fail closed on malformed transcript cues 2026-08-27 02:13:51 -03:00
diegosouzapw
6475be13c7 test(video): expose malformed serialized cue leak 2026-08-27 02:09:24 -03:00
diegosouzapw
f47f63df83 Merge release/v3.8.51 into security/video-transcript-redaction 2026-08-27 02:05:33 -03:00
diegosouzapw
c467c3232b Merge remote-tracking branch 'origin/release/v3.8.51' into security/video-transcript-redaction 2026-08-27 01:20:11 -03:00
diegosouzapw
4d519c406a docs(video): define transcript retention trust boundary 2026-08-26 23:01:16 -03:00
diegosouzapw
6fee356aa2 test(video): retain structured fusion log details 2026-08-26 22:10:21 -03:00
diegosouzapw
e77b386619 security(video): close transcript retention boundaries 2026-08-26 22:02:13 -03:00
diegosouzapw
8893816d0c test(video): defend server-owned plugin sensitivity 2026-08-26 21:58:53 -03:00
diegosouzapw
71765435fb test(video): exercise proxy fast-fail boundary 2026-08-26 21:52:31 -03:00
diegosouzapw
e385778bcf test(video): expose proxy fast-fail log retention 2026-08-26 21:49:04 -03:00
diegosouzapw
fb8897e926 test(video): expose quota-model transcript retention 2026-08-26 21:34:41 -03:00
diegosouzapw
a9f98fcb48 test(video): expose guardrail log retention paths 2026-08-26 21:30:08 -03:00
diegosouzapw
6dab0df038 test(video): cover terminal and plugin-context retention 2026-08-26 21:24:41 -03:00
diegosouzapw
2ac715aa9c test(video): expose retained combo and plugin error paths 2026-08-26 21:22:23 -03:00
diegosouzapw
86e48e3c9a test(video): cover nested carrier chains 2026-08-26 21:11:10 -03:00
diegosouzapw
5fbf218139 test(video): cover round-robin transcript error logs 2026-08-26 21:09:00 -03:00
diegosouzapw
0095f3b38f test(video): prove application logs omit transcript echoes 2026-08-26 21:07:55 -03:00
diegosouzapw
dabf8dd580 test(video): expose residual transcript retention paths 2026-08-26 20:47:09 -03:00
Xiangzhe
d92088c5dc docs(video): document transcript log-protection contract 2026-08-26 19:05:50 -03:00
Xiangzhe
20bd2c067c security(video): fail closed on hostile log payloads 2026-08-26 19:01:30 -03:00
Xiangzhe
1ed8f4ad4a test(video): prove fail-closed transcript inspection 2026-08-26 18:59:04 -03:00
Xiangzhe
f2f2fab636 security(video): redact transcript content across persistence and streams 2026-08-26 18:54:18 -03:00
Xiangzhe
b76aa02875 test(video): prove transcript retention across log sinks 2026-08-26 18:29:29 -03:00
81 changed files with 4725 additions and 392 deletions

View File

@@ -0,0 +1 @@
- **fix(security):** Video Bridge transcript text is now omitted from OmniRoute-owned retained request, response, usage, error, stream, continuation, handoff, and Memory surfaces while the live bridge request remains intact. Server-generated descriptions are matched by bounded SHA-256/length identities emitted only by a successful Video Bridge rewrite; ordinary fields named `transcript` and caller-forged description prose remain untouched. Traversal is cycle-safe and bounded, and hostile getters, proxies, or budget overflow fail closed to a constant omission marker instead of leaking content or breaking the request. Custom plugins and guardrails remain privileged processors of the live payload and must secure any sinks they create themselves. ([#11658](https://github.com/diegosouzapw/OmniRoute/issues/11658))

View File

@@ -1,13 +1,13 @@
--- ---
title: "Guardrails" title: "Guardrails"
version: 3.8.50 version: 3.8.51
lastUpdated: 2026-08-24 lastUpdated: 2026-08-27
--- ---
# Guardrails # Guardrails
> **Source of truth:** `src/lib/guardrails/` > **Source of truth:** `src/lib/guardrails/`
> **Last updated:** 2026-08-24 — v3.8.50 (Video Bridge visual dedup hardening + focused captions) > **Last updated:** 2026-08-27 — v3.8.51 (Video Bridge transcript-retention boundary)
Guardrails enforce safety, policy, and content transformations at the boundary Guardrails enforce safety, policy, and content transformations at the boundary
between OmniRoute and upstream providers. Each guardrail can inspect (and between OmniRoute and upstream providers. Each guardrail can inspect (and
@@ -439,6 +439,69 @@ are rendered as untrusted observations alongside the frame captions. Invalid,
out-of-range, or provenance-free text is rejected rather than mixed into the out-of-range, or provenance-free text is rejected rather than mixed into the
caption stream. caption stream.
#### Transcript retention boundary
Transcript text remains available to the live bridge and upstream request, but it is not retained
by OmniRoute's logging and memory surfaces. `videoTranscriptLogRedaction.ts` recognizes
`transcript` and `audioTranscript` only inside supported video carriers. A field with either name
in an ordinary tool argument or application object is preserved, and delimiter-shaped caller
prose is not trusted as a bridge result.
When the Video Bridge produces a description containing validated cues, it emits only a
SHA-256/length identity in guardrail metadata. The request pipeline accepts those identities only
from a successful, payload-modifying `video-bridge` result and uses them to omit the exact generated
description segment after translation or concatenation. Raw transcript text is never placed in the
metadata.
The retention policy covers raw, converted, and provider request logs; provider and client response
bodies; detailed pipeline artifacts; call logs, active/pending usage, rejected requests, proxy/error
diagnostics, SSE chunks, and Memory extraction. Response bodies and stream chunks for a sensitive
request are omitted because an upstream may echo request text. Safe operational metadata such as
status, timing, provider, model, headers-presence, token counts, and the fact that redaction occurred
remains available. The constant retained marker is `[omitted: video transcript]`.
Sensitivity is resolved before the guardrail chain from the original client body and resolved again
after the chain from the original body, processed body, and trusted description identities. A
guardrail that removes or replaces the raw carrier therefore cannot clear the request-scoped bit.
Explicit video parts remain sensitive when malformed, and nested carrier objects are inspected under
the same bounds. Rejected-request persistence also inspects the body itself, so forgetting to pass the
bit at one caller does not expose a raw carrier.
The request-scoped bit protects OmniRoute-owned application paths in addition to structured payload
clones. Native priority, round-robin, pinned, and fusion diagnostics; quality-rejection and live-event
logs; terminal combo errors; proxy fast-fail logs; guardrail-registry logs; plugin-dispatcher failure
logs; and stream lifecycle callbacks retain only the constant marker when their detail could echo the
request. A request-scoped executor logger keeps the diagnostic tag but replaces arbitrary executor
messages with the marker and drops attached metadata before those values reach the application
logger. Context Relay and Universal Handoff summaries are still generated from a sanitized clone:
safe adjacent conversation context remains available while recognized carriers and trusted generated
descriptions are omitted. Memory may likewise extract safe request-derived text from the sanitized
copy, but response-derived Memory extraction is skipped because an upstream response may echo the
transcript. Daily-quota classification still inspects the original upstream error, while sensitive
requests use the server-resolved model as the retained lockout key instead of promoting a model token
parsed from that diagnostic. These rules affect retained copies, not the live provider request or the
functional response returned to the same client.
Inspection is bounded by depth, aggregate entries, trusted identities, description-prefix
occurrences, candidate hashes, and total hashed text. Cycles and binary views are handled without
retaining their contents. If a bound is exceeded, or an enumerable getter/proxy throws during
inspection, the retained copy fails closed to the omission marker; the live request is not changed.
Privacy-redacted Responses artifacts cannot reconstruct the omitted transcript portion of
`previous_response_id` history. `responsesContinuationStore.ts` therefore reuses only valid,
non-truncated retained `input` and `output` arrays, including the explicit omission marker, and never
attempts to restore raw transcript text from the pipeline marker. This preserves continuation for the
safe portions of the turn while making the missing private segment visible; a client that needs that
context again must submit it as part of a new live request.
Custom guardrails and plugins are privileged, in-process processors: they intentionally receive the
live payload so they can inspect, transform, or block it. OmniRoute supplies the server-owned
`videoTranscriptSensitive` bit, wraps `GuardrailContext.log`, and protects errors emitted by its native
plugin dispatcher. It cannot control a third-party processor that independently writes the live body
to `console`, a file, a database, or the network. Operators must audit such code and treat its own
sinks as outside the OmniRoute-owned retention boundary; clearing or ignoring the bit does not make
the transcript safe to persist.
An advanced caller may provide an already-authorized `audioTranscript` track An advanced caller may provide an already-authorized `audioTranscript` track
for the same video. The fusion seam runs visual and audio observations under for the same video. The fusion seam runs visual and audio observations under
one deadline and abort signal, orders them on a common timeline, collapses one deadline and abort signal, orders them on a common timeline, collapses

View File

@@ -179,10 +179,10 @@ export type ProviderCredentials = {
}; };
export type ExecutorLog = { export type ExecutorLog = {
debug?: (tag: string, message: string) => void; debug?: (tag: string, message: string, data?: Record<string, unknown> | null) => void;
info?: (tag: string, message: string) => void; info?: (tag: string, message: string, data?: Record<string, unknown> | null) => void;
warn?: (tag: string, message: string) => void; warn?: (tag: string, message: string, data?: Record<string, unknown> | null) => void;
error?: (tag: string, message: string) => void; error?: (tag: string, message: string, data?: Record<string, unknown> | null) => void;
}; };
export type ExecuteInput = { export type ExecuteInput = {
@@ -202,6 +202,10 @@ export type ExecuteInput = {
* this to apply client-format-aware policies such as `</think>` close-marker * this to apply client-format-aware policies such as `</think>` close-marker
* suppression. */ * suppression. */
clientResponseFormat?: string | null; clientResponseFormat?: string | null;
/** True when upstream diagnostics may echo a video transcript. Executors must
* preserve operational responses/errors while omitting those echoes from
* retained logs. */
videoTranscriptSensitive?: boolean;
/** Callback to persist tokens that are proactively refreshed during execution. /** Callback to persist tokens that are proactively refreshed during execution.
* Accepts a partial credentials patch (e.g. `{ accessToken, refreshToken }` or * Accepts a partial credentials patch (e.g. `{ accessToken, refreshToken }` or
* `{ testStatus: "expired", isActive: false }`); the caller merges into the * `{ testStatus: "expired", isActive: false }`); the caller merges into the

View File

@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { redactVideoTranscriptSensitiveText } from "../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
import type { KeyHealth } from "../services/apiKeyRotator.ts"; import type { KeyHealth } from "../services/apiKeyRotator.ts";
import { DefaultExecutor } from "./default.ts"; import { DefaultExecutor } from "./default.ts";
@@ -45,6 +46,11 @@ function asRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null; return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
} }
function retainGlmDiagnostic(error: unknown, input: ExecuteInput): string {
const message = error instanceof Error ? error.message : String(error);
return redactVideoTranscriptSensitiveText(message, input.videoTranscriptSensitive === true);
}
function getEffectiveKey(credentials: ProviderCredentials): string { function getEffectiveKey(credentials: ProviderCredentials): string {
const extraKeys = (credentials.providerSpecificData?.extraApiKeys as string[] | undefined) ?? []; const extraKeys = (credentials.providerSpecificData?.extraApiKeys as string[] | undefined) ?? [];
if (credentials.apiKey && credentials.connectionId && extraKeys.length > 0) { if (credentials.apiKey && credentials.connectionId && extraKeys.length > 0) {
@@ -465,6 +471,7 @@ export class GlmExecutor extends DefaultExecutor {
timeoutMs: STREAM_READINESS_TIMEOUT_MS, timeoutMs: STREAM_READINESS_TIMEOUT_MS,
provider: this.provider, provider: this.provider,
model: input.model, model: input.model,
redactUpstreamDiagnosticForLog: input.videoTranscriptSensitive === true,
log: input.log, log: input.log,
}); });
response = readiness.response; response = readiness.response;
@@ -559,7 +566,7 @@ export class GlmExecutor extends DefaultExecutor {
if (!isRetryableGlmFallbackError(error)) throw error; if (!isRetryableGlmFallbackError(error)) throw error;
input.log?.debug?.( input.log?.debug?.(
"GLM_FALLBACK", "GLM_FALLBACK",
`${primaryTransport} error (${error instanceof Error ? error.message : String(error)}); trying ${fallbackTransport}` `${primaryTransport} error (${retainGlmDiagnostic(error, input)}); trying ${fallbackTransport}`
); );
} }
@@ -572,7 +579,7 @@ export class GlmExecutor extends DefaultExecutor {
if (!primaryResult) throw error; if (!primaryResult) throw error;
input.log?.debug?.( input.log?.debug?.(
"GLM_FALLBACK", "GLM_FALLBACK",
`${fallbackTransport} fallback failed (${error instanceof Error ? error.message : String(error)}); returning primary response` `${fallbackTransport} fallback failed (${retainGlmDiagnostic(error, input)}); returning primary response`
); );
} }

View File

@@ -166,6 +166,11 @@ import {
runWithCasGuard, runWithCasGuard,
} from "../services/tokenRefresh.ts"; } from "../services/tokenRefresh.ts";
import { createRequestLogger } from "../utils/requestLogger.ts"; import { createRequestLogger } from "../utils/requestLogger.ts";
import {
redactVideoTranscriptSensitiveText,
resolveVideoTranscriptLogSensitivity,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { createExecutorRetentionLog } from "./chatCore/executorRetentionLog.ts";
import { createPreparedRequestLogger, runWithCapture } from "../utils/providerRequestLogging.ts"; import { createPreparedRequestLogger, runWithCapture } from "../utils/providerRequestLogging.ts";
import { summarizeToolSources } from "../utils/toolSources.ts"; import { summarizeToolSources } from "../utils/toolSources.ts";
import { applyResponsesPreviousResponseIdPolicy } from "../utils/responsesStatePolicy.ts"; import { applyResponsesPreviousResponseIdPolicy } from "../utils/responsesStatePolicy.ts";
@@ -524,7 +529,22 @@ export async function handleChatCore({
skipResourcePressureGuard = false, skipResourcePressureGuard = false,
reasoningTransportFallback = "drop", reasoningTransportFallback = "drop",
managedLease = null, managedLease = null,
videoTranscriptSensitive = false,
videoTranscriptDescriptionFingerprints = [],
}) { }) {
videoTranscriptSensitive =
videoTranscriptSensitive ||
resolveVideoTranscriptLogSensitivity({
rawRequestBody: clientRawRequest?.body,
processedBody: body,
trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints,
});
const retainedErrorTextForLog = (error: unknown): string =>
redactVideoTranscriptSensitiveText(
error instanceof Error ? error.message : String(error),
videoTranscriptSensitive
);
const retainedExecutorLog = createExecutorRetentionLog(log, videoTranscriptSensitive);
let { provider, model, extendedContext } = modelInfo; let { provider, model, extendedContext } = modelInfo;
const resilienceSettings = resolveResilienceSettings(cachedSettings); const resilienceSettings = resolveResilienceSettings(cachedSettings);
if (!skipResourcePressureGuard) { if (!skipResourcePressureGuard) {
@@ -637,6 +657,7 @@ export async function handleChatCore({
apiKeyInfo, apiKeyInfo,
headers: clientRawRequest?.headers, headers: clientRawRequest?.headers,
log, log,
videoTranscriptSensitive,
}); });
if (pluginGate.blocked === true) { if (pluginGate.blocked === true) {
return { return {
@@ -721,6 +742,7 @@ export async function handleChatCore({
effectiveServiceTier, effectiveServiceTier,
startTime, startTime,
log, log,
videoTranscriptSensitive,
}); });
if (idempotencyHit) { if (idempotencyHit) {
return idempotencyHit; return idempotencyHit;
@@ -902,6 +924,8 @@ export async function handleChatCore({
stage: "registered", stage: "registered",
correlationId, correlationId,
sessionTag: conversationId || null, sessionTag: conversationId || null,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
}) || generateRequestId(); }) || generateRequestId();
// Initialize rate limit settings from persisted DB (once, lazy) // Initialize rate limit settings from persisted DB (once, lazy)
@@ -1034,6 +1058,8 @@ export async function handleChatCore({
noLogEnabled, noLogEnabled,
correlationId, correlationId,
modelPinned, modelPinned,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
// Resolved conversationId (open-sse/services/conversationTracker.ts) wins when // Resolved conversationId (open-sse/services/conversationTracker.ts) wins when
// present — it's populated for every request now, not just ones where the // present — it's populated for every request now, not just ones where the
// client explicitly sent x-omniroute-session-id. The raw header remains a // client explicitly sent x-omniroute-session-id. The raw header remains a
@@ -1156,8 +1182,17 @@ export async function handleChatCore({
model, model,
provider: provider || undefined, provider: provider || undefined,
connectionId: connectionId || credentials?.connectionId || undefined, connectionId: connectionId || credentials?.connectionId || undefined,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
}); });
const pendingScope = { id: pendingRequestId, model, provider, connectionId: pendingConnId }; const pendingScope = {
id: pendingRequestId,
model,
provider,
connectionId: pendingConnId,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
};
const providerRequestCapture = createPreparedRequestLogger(reqLogger, pendingScope); const providerRequestCapture = createPreparedRequestLogger(reqLogger, pendingScope);
// 0. Log client raw request (before format conversion) // 0. Log client raw request (before format conversion)
if (clientRawRequest) { if (clientRawRequest) {
@@ -1203,6 +1238,7 @@ export async function handleChatCore({
apiKeyId: apiKeyInfo?.id ?? undefined, apiKeyId: apiKeyInfo?.id ?? undefined,
cacheDefaultMode: (apiKeyInfo as { cacheDefaultMode?: "legacy" | "bypass" } | null) cacheDefaultMode: (apiKeyInfo as { cacheDefaultMode?: "legacy" | "bypass" } | null)
?.cacheDefaultMode, ?.cacheDefaultMode,
videoTranscriptSensitive,
}); });
if (cacheHit) { if (cacheHit) {
return cacheHit; return cacheHit;
@@ -1482,8 +1518,7 @@ export async function handleChatCore({
} catch (err) { } catch (err) {
log?.debug?.( log?.debug?.(
"COMPRESSION", "COMPRESSION",
"Combo compression override lookup skipped: " + "Combo compression override lookup skipped: " + retainedErrorTextForLog(err)
(err instanceof Error ? err.message : String(err))
); );
} }
} }
@@ -1492,10 +1527,7 @@ export async function handleChatCore({
const { listCompressionCombos } = await import("../../src/lib/db/compressionCombos.ts"); const { listCompressionCombos } = await import("../../src/lib/db/compressionCombos.ts");
namedCombos = buildNamedComboLookup(listCompressionCombos()); namedCombos = buildNamedComboLookup(listCompressionCombos());
} catch (err) { } catch (err) {
log?.debug?.( log?.debug?.("COMPRESSION", "Named combos load skipped: " + retainedErrorTextForLog(err));
"COMPRESSION",
"Named combos load skipped: " + (err instanceof Error ? err.message : String(err))
);
} }
// Phase 3: per-request override. Unknown values fall through in the resolver (never error). // Phase 3: per-request override. Unknown values fall through in the resolver (never error).
const compressionHeader = resolveCompressionHeader(clientRawRequest?.headers ?? null); const compressionHeader = resolveCompressionHeader(clientRawRequest?.headers ?? null);
@@ -1543,8 +1575,7 @@ export async function handleChatCore({
} catch (err) { } catch (err) {
log?.debug?.( log?.debug?.(
"COMPRESSION", "COMPRESSION",
"Default compression combo lookup skipped: " + "Default compression combo lookup skipped: " + retainedErrorTextForLog(err)
(err instanceof Error ? err.message : String(err))
); );
} }
} }
@@ -1586,10 +1617,7 @@ export async function handleChatCore({
} }
} }
} catch (err) { } catch (err) {
log?.debug?.( log?.debug?.("COMPRESSION", "Output styles skipped: " + retainedErrorTextForLog(err));
"COMPRESSION",
"Output styles skipped: " + (err instanceof Error ? err.message : String(err))
);
} }
} }
const compressionInputBody = body as Record<string, unknown>; const compressionInputBody = body as Record<string, unknown>;
@@ -1899,8 +1927,7 @@ export async function handleChatCore({
} catch (err) { } catch (err) {
log?.warn?.( log?.warn?.(
"COMPRESSION", "COMPRESSION",
"Compression pipeline error (non-fatal): " + "Compression pipeline error (non-fatal): " + retainedErrorTextForLog(err)
(err instanceof Error ? err.message : String(err))
); );
} }
// --- End Modular Compression Pipeline --- // --- End Modular Compression Pipeline ---
@@ -1954,7 +1981,10 @@ export async function handleChatCore({
`Combo context limit: ${resolved.limit} (source=${resolved.source})` `Combo context limit: ${resolved.limit} (source=${resolved.source})`
); );
} catch (err) { } catch (err) {
log?.warn?.("CONTEXT", "Failed to resolve combo limits for compression: " + err); log?.warn?.(
"CONTEXT",
"Failed to resolve combo limits for compression: " + retainedErrorTextForLog(err)
);
} }
} }
@@ -2432,13 +2462,21 @@ export async function handleChatCore({
try { try {
const { runOnError } = await import("@/lib/plugins/hooks"); const { runOnError } = await import("@/lib/plugins/hooks");
await runOnError( await runOnError(
{ requestId: traceId, body, model, provider, apiKeyInfo, metadata: {} }, {
requestId: traceId,
body,
model,
provider,
apiKeyInfo,
metadata: {},
videoTranscriptSensitive,
},
error instanceof Error ? error : new Error(String(error)) error instanceof Error ? error : new Error(String(error))
); );
} catch (pluginErr) { } catch (pluginErr) {
log?.debug?.( log?.debug?.(
"PLUGIN", "PLUGIN",
`onError hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}` `onError hook error (non-fatal): ${retainedErrorTextForLog(pluginErr)}`
); );
} }
@@ -2450,7 +2488,13 @@ export async function handleChatCore({
const message = error?.message || "Invalid request"; const message = error?.message || "Invalid request";
const errorType = typeof error?.errorType === "string" ? error.errorType : null; const errorType = typeof error?.errorType === "string" ? error.errorType : null;
log?.warn?.("TRANSLATE", `Request translation failed: ${message}`); log?.warn?.(
"TRANSLATE",
`Request translation failed: ${redactVideoTranscriptSensitiveText(
message,
videoTranscriptSensitive
)}`
);
if (errorType) { if (errorType) {
trackPendingRequest(model, provider, connectionId, false); trackPendingRequest(model, provider, connectionId, false);
@@ -2544,8 +2588,7 @@ export async function handleChatCore({
// must never turn an otherwise valid translated request into a 500. // must never turn an otherwise valid translated request into a 500.
log?.warn?.( log?.warn?.(
"COMPRESSION", "COMPRESSION",
"Post-translation OmniGlyph skipped: " + "Post-translation OmniGlyph skipped: " + retainedErrorTextForLog(error)
(error instanceof Error ? error.message : String(error))
); );
} }
} }
@@ -2839,7 +2882,7 @@ export async function handleChatCore({
const resolveExecutorWithProxy = (prov: string) => const resolveExecutorWithProxy = (prov: string) =>
resolveExecutorWithProxyFor( resolveExecutorWithProxyFor(
prov, prov,
log, retainedExecutorLog,
(credentials?.providerSpecificData as Record<string, unknown> | null | undefined) ?? null (credentials?.providerSpecificData as Record<string, unknown> | null | undefined) ?? null
); );
@@ -2861,7 +2904,7 @@ export async function handleChatCore({
}).catch((err: unknown): EnforceDecision => { }).catch((err: unknown): EnforceDecision => {
log?.warn?.( log?.warn?.(
"QUOTA_SHARE", "QUOTA_SHARE",
`enforceQuotaShare failed; fail-open: ${err instanceof Error ? err.message : String(err)}` `enforceQuotaShare failed; fail-open: ${retainedErrorTextForLog(err)}`
); );
return { kind: "allow" as const }; return { kind: "allow" as const };
}); });
@@ -2903,7 +2946,7 @@ export async function handleChatCore({
// Outer fail-open guard — should not be reached (inner .catch covers it) // Outer fail-open guard — should not be reached (inner .catch covers it)
log?.warn?.( log?.warn?.(
"QUOTA_SHARE", "QUOTA_SHARE",
`[quotaShare] enforceQuotaShare unexpected error; fail-open: ${err instanceof Error ? err.message : String(err)}` `[quotaShare] enforceQuotaShare unexpected error; fail-open: ${retainedErrorTextForLog(err)}`
); );
} }
} }
@@ -2915,7 +2958,7 @@ export async function handleChatCore({
} catch (err) { } catch (err) {
log?.warn?.( log?.warn?.(
"QUOTA_SHARE", "QUOTA_SHARE",
`[quotaShare] could not set soft penalty on candidate: ${err instanceof Error ? err.message : String(err)}` `[quotaShare] could not set soft penalty on candidate: ${retainedErrorTextForLog(err)}`
); );
} }
} }
@@ -2949,6 +2992,8 @@ export async function handleChatCore({
let onPipelineStreamError: streamFailure.PipelineStreamErrorHandler | null = null; let onPipelineStreamError: streamFailure.PipelineStreamErrorHandler | null = null;
let onClientDisconnectFinalize: let onClientDisconnectFinalize:
((event: { reason: string; duration: number }) => boolean) | null = null; ((event: { reason: string; duration: number }) => boolean) | null = null;
const redactStreamDiagnosticsForLog =
videoTranscriptSensitive || reqLogger.isVideoTranscriptSensitive();
// Create stream controller for disconnect detection // Create stream controller for disconnect detection
const streamController = createStreamController({ const streamController = createStreamController({
@@ -2980,6 +3025,7 @@ export async function handleChatCore({
clientAbortSignal: clientRawRequest?.signal, clientAbortSignal: clientRawRequest?.signal,
allowCompletedToolHandoffGrace: isCodexResponsesEcho, allowCompletedToolHandoffGrace: isCodexResponsesEcho,
clientDisconnectGracePeriodMs: STREAM_DISCONNECT_GRACE_PERIOD_MS, clientDisconnectGracePeriodMs: STREAM_DISCONNECT_GRACE_PERIOD_MS,
redactStreamDiagnosticsForLog,
}); });
const dedupRequestBody = { ...translatedBody, model: `${provider}/${model}`, stream }; const dedupRequestBody = { ...translatedBody, model: `${provider}/${model}`, stream };
@@ -3112,7 +3158,7 @@ export async function handleChatCore({
execCreds?.providerSpecificData execCreds?.providerSpecificData
), ),
signal: streamController.signal, signal: streamController.signal,
log, log: retainedExecutorLog,
execute: (signal) => execute: (signal) =>
runWithCapture(providerRequestCapture, () => runWithCapture(providerRequestCapture, () =>
executor.execute({ executor.execute({
@@ -3121,7 +3167,7 @@ export async function handleChatCore({
stream: upstreamStream, stream: upstreamStream,
credentials: execCreds, credentials: execCreds,
signal, signal,
log, log: retainedExecutorLog,
extendedContext, extendedContext,
upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall), upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall),
clientHeaders: buildExecutorClientHeaders( clientHeaders: buildExecutorClientHeaders(
@@ -3129,6 +3175,7 @@ export async function handleChatCore({
userAgent userAgent
), ),
clientResponseFormat, clientResponseFormat,
videoTranscriptSensitive,
onCredentialsRefreshed, onCredentialsRefreshed,
skipUpstreamRetry, skipUpstreamRetry,
contextEditing: { enabled: contextEditingEnabled }, contextEditing: { enabled: contextEditingEnabled },
@@ -3163,7 +3210,7 @@ export async function handleChatCore({
invalidateCodexQuotaCache(String(attemptConnectionId)); invalidateCodexQuotaCache(String(attemptConnectionId));
} }
} catch (err) { } catch (err) {
const errMessage = err instanceof Error ? err.message : String(err); const errMessage = retainedErrorTextForLog(err);
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`); log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
} }
} }
@@ -3428,7 +3475,7 @@ export async function handleChatCore({
execCreds?.providerSpecificData execCreds?.providerSpecificData
), ),
signal: streamController.signal, signal: streamController.signal,
log, log: retainedExecutorLog,
execute: (signal) => execute: (signal) =>
runWithCapture(providerRequestCapture, () => runWithCapture(providerRequestCapture, () =>
executor.execute({ executor.execute({
@@ -3437,7 +3484,7 @@ export async function handleChatCore({
stream: upstreamStream, stream: upstreamStream,
credentials: execCreds, credentials: execCreds,
signal, signal,
log, log: retainedExecutorLog,
extendedContext, extendedContext,
upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall), upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall),
clientHeaders: buildExecutorClientHeaders( clientHeaders: buildExecutorClientHeaders(
@@ -3445,6 +3492,7 @@ export async function handleChatCore({
userAgent userAgent
), ),
clientResponseFormat, clientResponseFormat,
videoTranscriptSensitive,
onCredentialsRefreshed, onCredentialsRefreshed,
skipUpstreamRetry, skipUpstreamRetry,
contextEditing: { enabled: contextEditingEnabled }, contextEditing: { enabled: contextEditingEnabled },
@@ -3677,7 +3725,9 @@ export async function handleChatCore({
} catch (err) { } catch (err) {
// Fail-open at Tier 2: Tier 1 already enforced the model/global limit pre-dispatch. // Fail-open at Tier 2: Tier 1 already enforced the model/global limit pre-dispatch.
// A transient counter read error here must not break an otherwise-valid request. // A transient counter read error here must not break an otherwise-valid request.
log?.warn?.("TOKEN_LIMIT", "Tier 2 token-limit check failed; allowing request", { err }); log?.warn?.("TOKEN_LIMIT", "Tier 2 token-limit check failed; allowing request", {
error: retainedErrorTextForLog(err),
});
} }
} }
@@ -3696,7 +3746,9 @@ export async function handleChatCore({
); );
} }
} catch (err) { } catch (err) {
log?.warn?.("GEMINI_RATE_LIMIT", "Pre-dispatch TPM check failed; allowing request", { err }); log?.warn?.("GEMINI_RATE_LIMIT", "Pre-dispatch TPM check failed; allowing request", {
error: retainedErrorTextForLog(err),
});
} }
} }
@@ -3846,7 +3898,12 @@ export async function handleChatCore({
failureStatus, failureStatus,
upstreamErrorCode || (error instanceof Error && error.name ? error.name : "upstream_error") upstreamErrorCode || (error instanceof Error && error.name ? error.name : "upstream_error")
); );
console.log(`${COLORS.red}[ERROR] ${failureMessage}${COLORS.reset}`); console.log(
`${COLORS.red}[ERROR] ${redactVideoTranscriptSensitiveText(
failureMessage,
videoTranscriptSensitive
)}${COLORS.reset}`
);
if (stream && upstreamErrorCode) { if (stream && upstreamErrorCode) {
const result = createStreamingErrorResult( const result = createStreamingErrorResult(
failureStatus, failureStatus,
@@ -3978,11 +4035,12 @@ export async function handleChatCore({
stream: upstreamStream, stream: upstreamStream,
credentials: getExecutionCredentials(), credentials: getExecutionCredentials(),
signal: streamController.signal, signal: streamController.signal,
log, log: retainedExecutorLog,
extendedContext, extendedContext,
upstreamExtraHeaders: buildUpstreamHeadersForExecute(retryModelId), upstreamExtraHeaders: buildUpstreamHeadersForExecute(retryModelId),
clientHeaders: buildExecutorClientHeaders(clientRawRequest?.headers, userAgent), clientHeaders: buildExecutorClientHeaders(clientRawRequest?.headers, userAgent),
clientResponseFormat, clientResponseFormat,
videoTranscriptSensitive,
onCredentialsRefreshed, onCredentialsRefreshed,
skipUpstreamRetry: isCombo, skipUpstreamRetry: isCombo,
contextEditing: { enabled: contextEditingEnabled }, contextEditing: { enabled: contextEditingEnabled },
@@ -4012,9 +4070,13 @@ export async function handleChatCore({
// executor throw). Don't swallow — the operator-visible signal "the user // executor throw). Don't swallow — the operator-visible signal "the user
// saw 401 even though auth was actually fixed" is much more confusing // saw 401 even though auth was actually fixed" is much more confusing
// than the original 401 alone. Surface at error level with sanitization. // than the original 401 alone. Surface at error level with sanitization.
const retainedRetryError = redactVideoTranscriptSensitiveText(
sanitizeErrorMessage(retryErr),
videoTranscriptSensitive
);
log?.error?.( log?.error?.(
"TOKEN", "TOKEN",
`${provider?.toUpperCase()} | retry after refresh failed: ${sanitizeErrorMessage(retryErr)}` `${provider?.toUpperCase()} | retry after refresh failed: ${retainedRetryError}`
); );
} }
} else { } else {
@@ -4131,6 +4193,11 @@ export async function handleChatCore({
if (signatureRecovery.succeeded) break providerFailure; if (signatureRecovery.succeeded) break providerFailure;
const retainedProviderMessage = redactVideoTranscriptSensitiveText(
message,
videoTranscriptSensitive
);
// #10281 — tiny-budget reasoning probes (e.g. Claude Code's `/model` check // #10281 — tiny-budget reasoning probes (e.g. Claude Code's `/model` check
// sends `max_tokens: 1`): the model burns the whole budget on thinking, and // sends `max_tokens: 1`): the model burns the whole budget on thinking, and
// some upstreams (e.g. api.cline.bot for deepseek-v4-flash) answer the empty // some upstreams (e.g. api.cline.bot for deepseek-v4-flash) answer the empty
@@ -4153,7 +4220,7 @@ export async function handleChatCore({
}); });
log?.warn?.( log?.warn?.(
"PROBE", "PROBE",
`Reasoning probe (max_tokens < ${REASONING_BUFFER_MIN_TRIGGER}) answered with truncated 200 — upstream reported "${message}"` `Reasoning probe (max_tokens < ${REASONING_BUFFER_MIN_TRIGGER}) answered with truncated 200 — upstream reported "${retainedProviderMessage}"`
); );
break providerFailure; break providerFailure;
} }
@@ -4182,7 +4249,7 @@ export async function handleChatCore({
{ {
testStatus: "banned", testStatus: "banned",
isActive: false, isActive: false,
lastError: message, lastError: retainedProviderMessage,
lastErrorType: errorType, lastErrorType: errorType,
errorCode: String(statusCode), errorCode: String(statusCode),
}, },
@@ -4213,7 +4280,7 @@ export async function handleChatCore({
) { ) {
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
console.warn( console.warn(
@@ -4226,7 +4293,7 @@ export async function handleChatCore({
{ {
testStatus: "deactivated", testStatus: "deactivated",
isActive: false, isActive: false,
lastError: message, lastError: retainedProviderMessage,
lastErrorType: errorType, lastErrorType: errorType,
errorCode: String(statusCode), errorCode: String(statusCode),
}, },
@@ -4250,7 +4317,7 @@ export async function handleChatCore({
errorConnectionId, errorConnectionId,
{ {
testStatus: "credits_exhausted", testStatus: "credits_exhausted",
lastError: message, lastError: retainedProviderMessage,
lastErrorType: errorType, lastErrorType: errorType,
errorCode: String(statusCode), errorCode: String(statusCode),
}, },
@@ -4298,7 +4365,7 @@ export async function handleChatCore({
rateLimitedUntil: kimiRateLimitResetAt, rateLimitedUntil: kimiRateLimitResetAt,
backoffLevel: 0, backoffLevel: 0,
lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED, lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
console.warn( console.warn(
@@ -4328,7 +4395,7 @@ export async function handleChatCore({
errorConnectionId, errorConnectionId,
{ {
testStatus: "credits_exhausted", testStatus: "credits_exhausted",
lastError: message, lastError: retainedProviderMessage,
lastErrorType: errorType, lastErrorType: errorType,
errorCode: String(statusCode), errorCode: String(statusCode),
}, },
@@ -4344,14 +4411,14 @@ export async function handleChatCore({
// Normal 401 (token/session auth issue): keep account active for refresh/re-auth. // Normal 401 (token/session auth issue): keep account active for refresh/re-auth.
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
} else if (errorType === PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN) { } else if (errorType === PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN) {
// OAuth 401 with invalid credentials - token refresh can recover // OAuth 401 with invalid credentials - token refresh can recover
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
console.warn( console.warn(
@@ -4361,7 +4428,7 @@ export async function handleChatCore({
// Cloud Code 403 with stale project: not a ban, keep account active. // Cloud Code 403 with stale project: not a ban, keep account active.
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
console.warn( console.warn(
@@ -4377,7 +4444,7 @@ export async function handleChatCore({
const geoCooldownMs = COOLDOWN_MS.geoBlocked ?? 24 * 60 * 60 * 1000; const geoCooldownMs = COOLDOWN_MS.geoBlocked ?? 24 * 60 * 60 * 1000;
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
// T-PROBE: the 24h exclusion is a routing mutation — a probe must // T-PROBE: the 24h exclusion is a routing mutation — a probe must
@@ -4402,7 +4469,7 @@ export async function handleChatCore({
const byopCooldownMs = COOLDOWN_MS.gcpProjectRequired ?? 24 * 60 * 60 * 1000; const byopCooldownMs = COOLDOWN_MS.gcpProjectRequired ?? 24 * 60 * 60 * 1000;
await updateProviderConnection(errorConnectionId, { await updateProviderConnection(errorConnectionId, {
lastErrorType: errorType, lastErrorType: errorType,
lastError: message, lastError: retainedProviderMessage,
errorCode: statusCode, errorCode: statusCode,
}); });
try { try {
@@ -4448,7 +4515,13 @@ export async function handleChatCore({
}).catch(() => {}); }).catch(() => {});
const errMsg = formatProviderError(new Error(message), provider, model, statusCode); const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); const retainedErrMsg = formatProviderError(
new Error(retainedProviderMessage),
provider,
model,
statusCode
);
console.log(`${COLORS.red}[ERROR] ${retainedErrMsg}${COLORS.reset}`);
// Log Antigravity retry time if available // Log Antigravity retry time if available
if (retryAfterMs && provider === "antigravity") { if (retryAfterMs && provider === "antigravity") {
@@ -4775,12 +4848,11 @@ export async function handleChatCore({
} }
} }
} catch (retryErr) { } catch (retryErr) {
log?.warn?.( const retainedRetryError = redactVideoTranscriptSensitiveText(
"RETRY", retryErr instanceof Error ? retryErr.message : String(retryErr),
`clinepass retry failed: ${ videoTranscriptSensitive
retryErr instanceof Error ? retryErr.message : String(retryErr)
}`
); );
log?.warn?.("RETRY", `clinepass retry failed: ${retainedRetryError}`);
} }
} }
if (envError) { if (envError) {
@@ -4999,6 +5071,7 @@ export async function handleChatCore({
cacheReasoningFromAssistantMessage(msg, provider, model, { cacheReasoningFromAssistantMessage(msg, provider, model, {
scope: reasoningCacheScope, scope: reasoningCacheScope,
historyMessages: Array.isArray(historyMessages) ? historyMessages : [], historyMessages: Array.isArray(historyMessages) ? historyMessages : [],
videoTranscriptSensitive,
}); });
} }
} catch { } catch {
@@ -5050,12 +5123,18 @@ export async function handleChatCore({
); );
if (memoryOwnerId && memorySettings?.enabled && memorySettings.maxTokens > 0) { if (memoryOwnerId && memorySettings?.enabled && memorySettings.maxTokens > 0) {
const requestMemoryText = extractMemoryTextFromRequestBody(body as Record<string, unknown>); const requestMemoryText = extractMemoryTextFromRequestBody(
body as Record<string, unknown>,
videoTranscriptSensitive,
{ trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints }
);
if (requestMemoryText) { if (requestMemoryText) {
extractFacts(requestMemoryText, memoryOwnerId, pipelineSessionId); extractFacts(requestMemoryText, memoryOwnerId, pipelineSessionId);
} }
const memoryText = extractMemoryTextFromResponse(memoryExtractionResponse); const memoryText = videoTranscriptSensitive
? ""
: extractMemoryTextFromResponse(memoryExtractionResponse);
if (memoryText) { if (memoryText) {
extractFacts(memoryText, memoryOwnerId, pipelineSessionId); extractFacts(memoryText, memoryOwnerId, pipelineSessionId);
} }
@@ -5095,6 +5174,7 @@ export async function handleChatCore({
provider, provider,
responsePayloadFormat, responsePayloadFormat,
clientResponseFormat, clientResponseFormat,
videoTranscriptSensitive,
}); });
const postCallGuardrails = await guardrailRegistry.runPostCallHooks( const postCallGuardrails = await guardrailRegistry.runPostCallHooks(
translatedResponse, translatedResponse,
@@ -5136,7 +5216,10 @@ export async function handleChatCore({
} }
log?.warn?.( log?.warn?.(
"GUARDRAIL", "GUARDRAIL",
`Response blocked by ${postCallGuardrails.guardrail || "guardrail"}: ${guardrailMessage}` `Response blocked by ${postCallGuardrails.guardrail || "guardrail"}: ${redactVideoTranscriptSensitiveText(
guardrailMessage,
videoTranscriptSensitive
)}`
); );
finalizePendingScope(pendingScope, { finalizePendingScope(pendingScope, {
providerResponse: responseBody, providerResponse: responseBody,
@@ -5239,6 +5322,7 @@ export async function handleChatCore({
apiKeyId: apiKeyInfo?.id ?? undefined, apiKeyId: apiKeyInfo?.id ?? undefined,
usage, usage,
log, log,
videoTranscriptSensitive,
}); });
// ── Phase 9.2: Save for idempotency ── // ── Phase 9.2: Save for idempotency ──
@@ -5318,6 +5402,7 @@ export async function handleChatCore({
apiKeyInfo, apiKeyInfo,
headers: clientRawRequest?.headers, headers: clientRawRequest?.headers,
response: { status: 200, data: translatedResponse }, response: { status: 200, data: translatedResponse },
videoTranscriptSensitive,
}); });
// Routing event (feedback foundation) — fire-and-forget, cheap. // Routing event (feedback foundation) — fire-and-forget, cheap.
@@ -5393,6 +5478,7 @@ export async function handleChatCore({
provider, provider,
model, model,
log, log,
redactUpstreamDiagnosticForLog: videoTranscriptSensitive,
}); });
if (streamReadiness.ok === false) { if (streamReadiness.ok === false) {
const { response: failureResponse, reason } = streamReadiness; const { response: failureResponse, reason } = streamReadiness;
@@ -5524,6 +5610,7 @@ export async function handleChatCore({
cacheReasoningFromAssistantMessage(msg, provider, model, { cacheReasoningFromAssistantMessage(msg, provider, model, {
scope: reasoningCacheScope, scope: reasoningCacheScope,
historyMessages: Array.isArray(historyMessages) ? historyMessages : [], historyMessages: Array.isArray(historyMessages) ? historyMessages : [],
videoTranscriptSensitive,
}); });
} }
} catch { } catch {
@@ -5556,6 +5643,7 @@ export async function handleChatCore({
status: normalizedStreamStatus, status: normalizedStreamStatus,
error: streamError, error: streamError,
errorCode: streamErrorCode, errorCode: streamErrorCode,
videoTranscriptSensitive,
}); });
// Track cache token metrics for streaming responses // Track cache token metrics for streaming responses
@@ -5678,14 +5766,20 @@ export async function handleChatCore({
memorySettings.maxTokens > 0 && memorySettings.maxTokens > 0 &&
streamStatus === 200 streamStatus === 200
) { ) {
const requestMemoryText = extractMemoryTextFromRequestBody(body as Record<string, unknown>); const requestMemoryText = extractMemoryTextFromRequestBody(
body as Record<string, unknown>,
videoTranscriptSensitive,
{ trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints }
);
if (requestMemoryText) { if (requestMemoryText) {
extractFacts(requestMemoryText, memoryOwnerId, pipelineSessionId); extractFacts(requestMemoryText, memoryOwnerId, pipelineSessionId);
} }
const streamedMemoryText = extractMemoryTextFromResponse( const streamedMemoryText = videoTranscriptSensitive
(streamResponseBody ?? null) as Record<string, unknown> | null ? ""
); : extractMemoryTextFromResponse(
(streamResponseBody ?? null) as Record<string, unknown> | null
);
if (streamedMemoryText) { if (streamedMemoryText) {
extractFacts(streamedMemoryText, memoryOwnerId, pipelineSessionId); extractFacts(streamedMemoryText, memoryOwnerId, pipelineSessionId);
} }
@@ -5702,6 +5796,7 @@ export async function handleChatCore({
apiKeyId: apiKeyInfo?.id ?? undefined, apiKeyId: apiKeyInfo?.id ?? undefined,
streamUsage, streamUsage,
log, log,
videoTranscriptSensitive,
}); });
// Plugin onStreamComplete hook — fire-and-forget, fail-open (#9571) // Plugin onStreamComplete hook — fire-and-forget, fail-open (#9571)
@@ -5770,7 +5865,8 @@ export async function handleChatCore({
// openai-responses → openai translation still wants the namespace identity // openai-responses → openai translation still wants the namespace identity
// map for #7936-style round-trip closure when the client also speaks // map for #7936-style round-trip closure when the client also speaks
// Responses (Codex CLI). // Responses (Codex CLI).
requestToolIdentityMap requestToolIdentityMap,
redactStreamDiagnosticsForLog
); );
} else if (needsTranslation(targetFormat, clientResponseFormat)) { } else if (needsTranslation(targetFormat, clientResponseFormat)) {
// Standard translation for other providers // Standard translation for other providers
@@ -5800,7 +5896,8 @@ export async function handleChatCore({
clientResponseFormat, clientResponseFormat,
}), }),
customToolNames, customToolNames,
requestToolIdentityMap requestToolIdentityMap,
redactStreamDiagnosticsForLog
); );
} else { } else {
log?.debug?.("STREAM", `Standard passthrough mode`); log?.debug?.("STREAM", `Standard passthrough mode`);
@@ -5815,7 +5912,8 @@ export async function handleChatCore({
apiKeyInfo, apiKeyInfo,
handleStreamFailure, handleStreamFailure,
clientResponseFormat, clientResponseFormat,
requestToolIdentityMap requestToolIdentityMap,
redactStreamDiagnosticsForLog
); );
} }
@@ -5827,6 +5925,7 @@ export async function handleChatCore({
clientRawRequestHeaders: clientRawRequest?.headers, clientRawRequestHeaders: clientRawRequest?.headers,
clientResponseFormat, clientResponseFormat,
echoModel, echoModel,
redactStreamDiagnosticsForLog,
responseHeaders, responseHeaders,
}); });
@@ -5842,6 +5941,7 @@ export async function handleChatCore({
apiKeyInfo, apiKeyInfo,
headers: clientRawRequest?.headers, headers: clientRawRequest?.headers,
response: { status: 200, streamed: true }, response: { status: 200, streamed: true },
videoTranscriptSensitive,
}); });
return { return {

View File

@@ -15,6 +15,10 @@ import { logAuditEvent } from "@/lib/compliance";
import { emit } from "@/lib/events/eventBus"; import { emit } from "@/lib/events/eventBus";
import type { RequestCompletedPayload, RequestFailedPayload } from "@/lib/events/types"; import type { RequestCompletedPayload, RequestFailedPayload } from "@/lib/events/types";
import { saveCallLog } from "@/lib/usageDb"; import { saveCallLog } from "@/lib/usageDb";
import {
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
VIDEO_TRANSCRIPT_REDACTION_PIPELINE_KEY,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { FORMATS } from "../../translator/formats.ts"; import { FORMATS } from "../../translator/formats.ts";
import { takeEarlyKeepaliveBytes } from "../../utils/earlyKeepaliveByteBuffer.ts"; import { takeEarlyKeepaliveBytes } from "../../utils/earlyKeepaliveByteBuffer.ts";
import { cloneBoundedChatLogPayload, truncateForLog } from "./logTruncation.ts"; import { cloneBoundedChatLogPayload, truncateForLog } from "./logTruncation.ts";
@@ -68,7 +72,13 @@ export type PersistAttemptLogsContext = {
model: string | null | undefined; model: string | null | undefined;
skillRequestId: string; skillRequestId: string;
detailedLoggingEnabled: boolean; detailedLoggingEnabled: boolean;
reqLogger: { getPipelinePayloads?: () => Record<string, unknown> | undefined } | null | undefined; reqLogger:
| {
getPipelinePayloads?: () => Record<string, unknown> | undefined;
isVideoTranscriptSensitive?: () => boolean;
}
| null
| undefined;
pendingRequestId: unknown; pendingRequestId: unknown;
clientRawRequest: { endpoint?: string } | null | undefined; clientRawRequest: { endpoint?: string } | null | undefined;
requestedModel: unknown; requestedModel: unknown;
@@ -89,12 +99,34 @@ export type PersistAttemptLogsContext = {
* explicitly present (never synthesized from skillRequestId) — persisted as call_logs.session_tag * explicitly present (never synthesized from skillRequestId) — persisted as call_logs.session_tag
* for per-session cost attribution. */ * for per-session cost attribution. */
sessionTag?: string | null; sessionTag?: string | null;
/** Trusted request state derived from the Video Bridge guardrail result. */
videoTranscriptSensitive?: boolean;
/** Exact SHA-256 identities of transcript descriptions generated by the guardrail. */
videoTranscriptDescriptionFingerprints?: readonly string[];
}; };
function toConnectionId(value: unknown): string | null { function toConnectionId(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
} }
function omitSensitivePipelineResponse(value: unknown): Record<string, unknown> {
const record =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
...(typeof record.timestamp === "string" ? { timestamp: record.timestamp } : {}),
...(typeof record.status === "number" && Number.isFinite(record.status)
? { status: record.status }
: {}),
...(record.headers !== undefined ? { headers: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER } : {}),
...(record.statusText !== undefined
? { statusText: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER }
: {}),
body: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
};
}
function buildAccountRotationMeta( function buildAccountRotationMeta(
provider: string | null | undefined, provider: string | null | undefined,
initialConnectionId: string | null, initialConnectionId: string | null,
@@ -204,6 +236,8 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
correlationId, correlationId,
modelPinned, modelPinned,
sessionTag, sessionTag,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
} = ctx; } = ctx;
const initialConnectionId = toConnectionId(connectionId); const initialConnectionId = toConnectionId(connectionId);
const finalConnectionId = toConnectionId(credentials?.connectionId) || initialConnectionId; const finalConnectionId = toConnectionId(credentials?.connectionId) || initialConnectionId;
@@ -212,8 +246,21 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
initialConnectionId, initialConnectionId,
finalConnectionId finalConnectionId
); );
const transcriptSensitive =
videoTranscriptSensitive === true || reqLogger?.isVideoTranscriptSensitive?.() === true;
const descriptionLogContext = {
trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints ?? [],
};
const providerWarnings = extractProviderWarnings(providerResponse, clientResponse, responseBody); const detectedProviderWarnings = extractProviderWarnings(
providerResponse,
clientResponse,
responseBody
);
const providerWarnings =
transcriptSensitive && detectedProviderWarnings.length > 0
? [VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER]
: detectedProviderWarnings;
if (providerWarnings.length > 0) { if (providerWarnings.length > 0) {
logAuditEvent({ logAuditEvent({
action: "provider.warning", action: "provider.warning",
@@ -227,6 +274,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
model, model,
connectionId: finalConnectionId, connectionId: finalConnectionId,
httpStatus: status, httpStatus: status,
warningCount: detectedProviderWarnings.length,
warnings: providerWarnings, warnings: providerWarnings,
}, },
}); });
@@ -273,8 +321,51 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
}; };
} }
} }
if (transcriptSensitive) {
pipelinePayloads[VIDEO_TRANSCRIPT_REDACTION_PIPELINE_KEY] = true;
for (const requestStage of [
"clientRawRequest",
"openaiRequest",
"providerRequest",
] as const) {
if (pipelinePayloads[requestStage] !== undefined) {
pipelinePayloads[requestStage] = cloneBoundedChatLogPayload(
pipelinePayloads[requestStage],
0,
requestStage !== "clientRawRequest" ? descriptionLogContext : {}
) as Record<string, unknown>;
}
}
if (pipelinePayloads.providerResponse !== undefined || providerResponse !== undefined) {
pipelinePayloads.providerResponse = omitSensitivePipelineResponse(
pipelinePayloads.providerResponse ?? providerResponse
);
}
if (pipelinePayloads.clientResponse !== undefined || clientResponse !== undefined) {
pipelinePayloads.clientResponse = omitSensitivePipelineResponse(
pipelinePayloads.clientResponse ?? clientResponse
);
}
if (pipelinePayloads.error !== undefined || error) {
const errorRecord =
pipelinePayloads.error && typeof pipelinePayloads.error === "object"
? pipelinePayloads.error
: {};
pipelinePayloads.error = {
...(typeof errorRecord.timestamp === "string"
? { timestamp: errorRecord.timestamp }
: {}),
message: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
};
}
delete pipelinePayloads.streamChunks;
}
} }
const responseBodyForLog = transcriptSensitive
? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER
: responseBody;
saveCallLog({ saveCallLog({
id: pendingRequestId, id: pendingRequestId,
method: "POST", method: "POST",
@@ -290,10 +381,12 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
attachLogMeta(truncateForLog(body as Record<string, unknown>), { attachLogMeta(truncateForLog(body as Record<string, unknown>), {
...accountRotationMeta, ...accountRotationMeta,
claudePromptCache: claudeCacheMeta, claudePromptCache: claudeCacheMeta,
}) }),
0,
descriptionLogContext
), ),
responseBody: cloneBoundedChatLogPayload( responseBody: cloneBoundedChatLogPayload(
attachLogMeta(truncateForLog(responseBody as Record<string, unknown>), { attachLogMeta(truncateForLog(responseBodyForLog as Record<string, unknown>), {
...accountRotationMeta, ...accountRotationMeta,
claudePromptCache: claudeCacheMeta claudePromptCache: claudeCacheMeta
? { ? {
@@ -305,7 +398,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
claudePromptCacheUsage: claudeCacheUsageMeta, claudePromptCacheUsage: claudeCacheUsageMeta,
}) })
), ),
error: error || null, error: transcriptSensitive && error ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : error || null,
sourceFormat, sourceFormat,
targetFormat, targetFormat,
comboName, comboName,
@@ -332,7 +425,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
const lifecycle = resolveRequestLifecycleEvent({ const lifecycle = resolveRequestLifecycleEvent({
traceId, traceId,
status, status,
error, error: transcriptSensitive && error ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : error,
model, model,
provider, provider,
comboName, comboName,

View File

@@ -17,8 +17,10 @@
*/ */
import { getExecutor } from "../../executors/index.ts"; import { getExecutor } from "../../executors/index.ts";
import type { ExecuteInput } from "../../executors/base.ts";
import { isCliproxyapiDeepModeEnabled } from "../../executors/cliproxyapi.ts"; import { isCliproxyapiDeepModeEnabled } from "../../executors/cliproxyapi.ts";
import { isDarioDeepModeEnabled } from "../../executors/dario.ts"; import { isDarioDeepModeEnabled } from "../../executors/dario.ts";
import { redactVideoTranscriptSensitiveText } from "../../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
import { getCachedSettings } from "@/lib/db/readCache"; import { getCachedSettings } from "@/lib/db/readCache";
import { getUpstreamProxyConfigCached } from "./comboContextCache.ts"; import { getUpstreamProxyConfigCached } from "./comboContextCache.ts";
import type { FallbackBackend } from "@/lib/db/upstreamProxy"; import type { FallbackBackend } from "@/lib/db/upstreamProxy";
@@ -48,6 +50,11 @@ function parseFallbackCodes(raw: unknown): number[] | null {
return parsed.length > 0 ? parsed : null; return parsed.length > 0 ? parsed : null;
} }
function retainExecutorDiagnostic(error: unknown, input: ExecuteInput): string {
const message = error instanceof Error ? error.message : String(error);
return redactVideoTranscriptSensitiveText(message, input.videoTranscriptSensitive === true);
}
/** /**
* Reads the CLIProxyAPI-related settings shared by both the direct * Reads the CLIProxyAPI-related settings shared by both the direct
* `mode: "cliproxyapi"` passthrough leg and the `mode: "fallback"` retry leg: * `mode: "cliproxyapi"` passthrough leg and the `mode: "fallback"` retry leg:
@@ -155,25 +162,20 @@ export async function resolveExecutorWithProxy(
const isRetryableStatus = (s: number) => fallbackCodes.includes(s) || s === 0; const isRetryableStatus = (s: number) => fallbackCodes.includes(s) || s === 0;
const wrapper = Object.create(nativeExec); const wrapper = Object.create(nativeExec);
wrapper.execute = async (input: { wrapper.execute = async (input: ExecuteInput) => {
model: string;
body: unknown;
stream: boolean;
credentials: unknown;
signal?: AbortSignal | null;
log?: unknown;
upstreamExtraHeaders?: Record<string, string> | null;
}) => {
let result; let result;
try { try {
result = await nativeExec.execute(input); result = await nativeExec.execute(input);
} catch (err) { } catch (err) {
const errMsg = err instanceof Error ? err.message : String(err); const errMsg = retainExecutorDiagnostic(err, input);
log?.info?.("UPSTREAM_PROXY", `${prov} native error (${errMsg}), retrying via ${backendLabel}`); log?.info?.(
"UPSTREAM_PROXY",
`${prov} native error (${errMsg}), retrying via ${backendLabel}`
);
try { try {
return await proxyExec.execute(input); return await proxyExec.execute(input);
} catch (proxyErr) { } catch (proxyErr) {
const proxyMsg = proxyErr instanceof Error ? proxyErr.message : String(proxyErr); const proxyMsg = retainExecutorDiagnostic(proxyErr, input);
log?.error?.("UPSTREAM_PROXY", `${prov} ${backendLabel} fallback also failed: ${proxyMsg}`); log?.error?.("UPSTREAM_PROXY", `${prov} ${backendLabel} fallback also failed: ${proxyMsg}`);
throw proxyErr; throw proxyErr;
} }
@@ -189,7 +191,7 @@ export async function resolveExecutorWithProxy(
try { try {
return await proxyExec.execute(input); return await proxyExec.execute(input);
} catch (proxyErr) { } catch (proxyErr) {
const proxyMsg = proxyErr instanceof Error ? proxyErr.message : String(proxyErr); const proxyMsg = retainExecutorDiagnostic(proxyErr, input);
log?.error?.("UPSTREAM_PROXY", `${prov} ${backendLabel} fallback also failed: ${proxyMsg}`); log?.error?.("UPSTREAM_PROXY", `${prov} ${backendLabel} fallback also failed: ${proxyMsg}`);
throw proxyErr; throw proxyErr;
} }

View File

@@ -0,0 +1,36 @@
import { VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER } from "@/lib/guardrails/videoTranscriptLogRedaction";
import type { ExecutorLog } from "../../executors/base.ts";
type ExecutorLogMethod = NonNullable<ExecutorLog["debug"]>;
function retainMethod(
owner: ExecutorLog,
method: ExecutorLogMethod | undefined
): ExecutorLogMethod | undefined {
if (!method) return undefined;
return (tag) => {
method.call(owner, tag, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
};
}
/**
* Executor diagnostics can contain arbitrary provider text, including request echoes.
* Keep the original logger for ordinary requests, but give every executor a request-scoped
* fail-closed view when the Video Bridge marked the request sensitive. Tags remain useful;
* messages and optional structured metadata are replaced/dropped only at the retention seam.
*/
export function createExecutorRetentionLog(
log: ExecutorLog | null | undefined,
videoTranscriptSensitive: boolean
): ExecutorLog | null | undefined {
if (!log || !videoTranscriptSensitive) return log;
return {
debug: retainMethod(log, log.debug),
info: retainMethod(log, log.info),
warn: retainMethod(log, log.warn),
error: retainMethod(log, log.error),
};
}

View File

@@ -124,6 +124,7 @@ export async function checkIdempotencyCache({
effectiveServiceTier, effectiveServiceTier,
startTime, startTime,
log, log,
videoTranscriptSensitive,
}: { }: {
clientRawRequest: IdempotencyRequest; clientRawRequest: IdempotencyRequest;
provider: string; provider: string;
@@ -132,7 +133,9 @@ export async function checkIdempotencyCache({
effectiveServiceTier: EffectiveServiceTier | null | undefined; effectiveServiceTier: EffectiveServiceTier | null | undefined;
startTime: number; startTime: number;
log: LoggerLike; log: LoggerLike;
videoTranscriptSensitive: boolean;
}): Promise<{ hit: { success: true; response: Response } | null; idempotencyKey: string | null }> { }): Promise<{ hit: { success: true; response: Response } | null; idempotencyKey: string | null }> {
if (videoTranscriptSensitive) return { hit: null, idempotencyKey: null };
// NEXA fusion-idempotency fix: namespace the raw header key (see composeIdempotencyKey). // NEXA fusion-idempotency fix: namespace the raw header key (see composeIdempotencyKey).
const rawIdempotencyKey = getIdempotencyKey(clientRawRequest?.headers); const rawIdempotencyKey = getIdempotencyKey(clientRawRequest?.headers);
const idempotencyKey = composeIdempotencyKey({ const idempotencyKey = composeIdempotencyKey({

View File

@@ -5,6 +5,10 @@ import {
getChatLogMaxObjectKeys, getChatLogMaxObjectKeys,
getChatLogMaxBodyBytes, getChatLogMaxBodyBytes,
} from "@/lib/logEnv"; } from "@/lib/logEnv";
import {
omitVideoTranscriptForLog,
type VideoTranscriptLogContext,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { estimateSizeFast } from "../../utils/estimateSize.ts"; import { estimateSizeFast } from "../../utils/estimateSize.ts";
export const MEMORY_EXTRACTION_TEXT_LIMIT = 64 * 1024; export const MEMORY_EXTRACTION_TEXT_LIMIT = 64 * 1024;
@@ -22,7 +26,7 @@ export function truncateChatLogText(value: string): string {
return `${head}\n[...truncated ${value.length - limit} chars...]\n${tail}`; return `${head}\n[...truncated ${value.length - limit} chars...]\n${tail}`;
} }
export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown { function cloneBoundedChatLogPayloadValue(value: unknown, depth = 0): unknown {
if (value === null || value === undefined) return value; if (value === null || value === undefined) return value;
if (typeof value === "string") return truncateChatLogText(value); if (typeof value === "string") return truncateChatLogText(value);
if (typeof value !== "object") return value; if (typeof value !== "object") return value;
@@ -32,7 +36,7 @@ export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
if (Array.isArray(value)) { if (Array.isArray(value)) {
const retained = value.length > maxTailItems ? value.slice(-maxTailItems) : value; const retained = value.length > maxTailItems ? value.slice(-maxTailItems) : value;
const cloned = retained.map((item) => cloneBoundedChatLogPayload(item, depth + 1)); const cloned = retained.map((item) => cloneBoundedChatLogPayloadValue(item, depth + 1));
if (value.length > maxTailItems) { if (value.length > maxTailItems) {
return [ return [
{ {
@@ -47,10 +51,11 @@ export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
} }
const result: Record<string, unknown> = {}; const result: Record<string, unknown> = {};
const entries = Object.entries(value as Record<string, unknown>); const record = value as Record<string, unknown>;
const entries = Object.entries(record);
const maxKeys = getChatLogMaxObjectKeys(); const maxKeys = getChatLogMaxObjectKeys();
for (const [key, item] of maxKeys > 0 ? entries.slice(0, maxKeys) : entries) { for (const [key, item] of maxKeys > 0 ? entries.slice(0, maxKeys) : entries) {
result[key] = cloneBoundedChatLogPayload(item, depth + 1); result[key] = cloneBoundedChatLogPayloadValue(item, depth + 1);
} }
if (maxKeys > 0 && entries.length > maxKeys) { if (maxKeys > 0 && entries.length > maxKeys) {
result._omniroute_truncated_keys = entries.length - maxKeys; result._omniroute_truncated_keys = entries.length - maxKeys;
@@ -58,6 +63,16 @@ export function cloneBoundedChatLogPayload(value: unknown, depth = 0): unknown {
return result; return result;
} }
export function cloneBoundedChatLogPayload(
value: unknown,
depth = 0,
descriptionContext: VideoTranscriptLogContext = {}
): unknown {
const transcriptSafeValue =
depth === 0 ? omitVideoTranscriptForLog(value, descriptionContext) : value;
return cloneBoundedChatLogPayloadValue(transcriptSafeValue, depth);
}
/** /**
* Truncate a large object for logging. If its JSON representation exceeds * Truncate a large object for logging. If its JSON representation exceeds
* getChatLogMaxBodyBytes() (default 1MB; CHAT_LOG_MAX_BODY_KB env override), * getChatLogMaxBodyBytes() (default 1MB; CHAT_LOG_MAX_BODY_KB env override),

View File

@@ -1,5 +1,39 @@
import {
containsVideoTranscriptForLog,
omitVideoTranscriptDerivedTextForMemory,
type VideoTranscriptLogContext,
} from "../../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
import { capMemoryExtractionText, MEMORY_EXTRACTION_TEXT_LIMIT } from "./logTruncation.ts"; import { capMemoryExtractionText, MEMORY_EXTRACTION_TEXT_LIMIT } from "./logTruncation.ts";
function normalizeMemoryInputText(value: unknown, context: VideoTranscriptLogContext = {}): string {
if (typeof value !== "string") return "";
return omitVideoTranscriptDerivedTextForMemory(value, context).trim();
}
function extractMemoryTextPart(
part: Record<string, unknown>,
transcriptSensitive: boolean,
context: VideoTranscriptLogContext
): string {
try {
const rawText = typeof part?.text === "string" ? part.text : "";
if (!rawText) return "";
const retainedText = normalizeMemoryInputText(rawText, context);
if (!retainedText) return "";
if (
transcriptSensitive &&
containsVideoTranscriptForLog(part, context) &&
retainedText === rawText.trim()
) {
return "";
}
return retainedText;
} catch {
return "";
}
}
export function extractMemoryTextFromResponse( export function extractMemoryTextFromResponse(
response: Record<string, unknown> | null | undefined response: Record<string, unknown> | null | undefined
): string { ): string {
@@ -29,9 +63,16 @@ export function extractMemoryTextFromResponse(
} }
export function extractMemoryTextFromRequestBody( export function extractMemoryTextFromRequestBody(
body: Record<string, unknown> | null | undefined body: Record<string, unknown> | null | undefined,
videoTranscriptSensitive = false,
context: VideoTranscriptLogContext = {}
): string { ): string {
if (!body || typeof body !== "object") return ""; if (!body || typeof body !== "object") return "";
// Re-check the structured body at the sink boundary. The explicit bit covers
// processed requests whose raw carrier was already replaced; trusted hashes
// identify only descriptions emitted by a modified Video Bridge guardrail.
const transcriptSensitive =
videoTranscriptSensitive || containsVideoTranscriptForLog(body, context);
const messages = Array.isArray(body.messages) ? body.messages : null; const messages = Array.isArray(body.messages) ? body.messages : null;
if (messages && messages.length > 0) { if (messages && messages.length > 0) {
@@ -39,18 +80,16 @@ export function extractMemoryTextFromRequestBody(
const msg = messages[i] as Record<string, unknown>; const msg = messages[i] as Record<string, unknown>;
if (msg?.role !== "user") continue; if (msg?.role !== "user") continue;
if (typeof msg.content === "string" && msg.content.trim().length > 0) { const messageText = normalizeMemoryInputText(msg.content, context);
return capMemoryExtractionText(msg.content.trim()); if (messageText) {
return capMemoryExtractionText(messageText);
} }
if (Array.isArray(msg.content)) { if (Array.isArray(msg.content)) {
const text = msg.content const text = msg.content
.map((part: Record<string, unknown>) => { .map((part: Record<string, unknown>) =>
if (typeof part?.text === "string") return part.text.trim(); extractMemoryTextPart(part, transcriptSensitive, context)
if (part?.type === "input_text" && typeof part?.text === "string") )
return part.text.trim();
return "";
})
.filter(Boolean) .filter(Boolean)
.join("\n") .join("\n")
.trim(); .trim();
@@ -68,17 +107,15 @@ export function extractMemoryTextFromRequestBody(
if (role && role !== "user") continue; if (role && role !== "user") continue;
if (itemType && itemType !== "message") continue; if (itemType && itemType !== "message") continue;
if (typeof item?.content === "string" && item.content.trim()) { const itemText = normalizeMemoryInputText(item?.content, context);
return capMemoryExtractionText(item.content.trim()); if (itemText) {
return capMemoryExtractionText(itemText);
} }
if (Array.isArray(item?.content)) { if (Array.isArray(item?.content)) {
const text = item.content const text = item.content
.map((part: Record<string, unknown>) => { .map((part: Record<string, unknown>) =>
if (typeof part?.text === "string") return part.text.trim(); extractMemoryTextPart(part, transcriptSensitive, context)
if (part?.type === "input_text" && typeof part?.text === "string") )
return part.text.trim();
return "";
})
.filter(Boolean) .filter(Boolean)
.join("\n") .join("\n")
.trim(); .trim();
@@ -96,15 +133,14 @@ export function extractMemoryTextFromRequestBody(
if (role && role !== "user") return ""; if (role && role !== "user") return "";
if (itemType && itemType !== "message") return ""; if (itemType && itemType !== "message") return "";
if (typeof item?.content === "string") return item.content.trim(); if (typeof item?.content === "string") {
return normalizeMemoryInputText(item.content, context);
}
if (Array.isArray(item?.content)) { if (Array.isArray(item?.content)) {
return item.content return item.content
.map((part: Record<string, unknown>) => { .map((part: Record<string, unknown>) =>
if (typeof part?.text === "string") return part.text.trim(); extractMemoryTextPart(part, transcriptSensitive, context)
if (part?.type === "input_text" && typeof part?.text === "string") )
return part.text.trim();
return "";
})
.filter(Boolean) .filter(Boolean)
.join("\n") .join("\n")
.trim(); .trim();

View File

@@ -9,6 +9,8 @@
* byte-identical to the previous inline block. * byte-identical to the previous inline block.
*/ */
import { redactVideoTranscriptSensitiveText } from "@/lib/guardrails/videoTranscriptLogRedaction";
type LoggerLike = type LoggerLike =
{ info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void } | null | undefined; { info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void } | null | undefined;
@@ -25,6 +27,7 @@ export async function runPluginOnRequestHook(args: {
apiKeyInfo: unknown; apiKeyInfo: unknown;
headers?: Record<string, string | string[] | undefined>; headers?: Record<string, string | string[] | undefined>;
log?: LoggerLike; log?: LoggerLike;
videoTranscriptSensitive?: boolean;
}): Promise<PluginOnRequestGate> { }): Promise<PluginOnRequestGate> {
try { try {
const { runOnRequest } = await import("@/lib/plugins/hooks"); const { runOnRequest } = await import("@/lib/plugins/hooks");
@@ -36,6 +39,7 @@ export async function runPluginOnRequestHook(args: {
apiKeyInfo: args.apiKeyInfo, apiKeyInfo: args.apiKeyInfo,
headers: args.headers, headers: args.headers,
metadata: {}, metadata: {},
videoTranscriptSensitive: args.videoTranscriptSensitive === true,
}; };
const pluginResult = await runOnRequest(pluginCtx); const pluginResult = await runOnRequest(pluginCtx);
if (pluginResult?.blocked) { if (pluginResult?.blocked) {
@@ -55,10 +59,11 @@ export async function runPluginOnRequestHook(args: {
} }
return { blocked: false, body: pluginResult?.body }; return { blocked: false, body: pluginResult?.body };
} catch (pluginErr) { } catch (pluginErr) {
args.log?.debug?.( const retainedPluginError = redactVideoTranscriptSensitiveText(
"PLUGIN", pluginErr instanceof Error ? pluginErr.message : String(pluginErr),
`onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}` args.videoTranscriptSensitive === true
); );
args.log?.debug?.("PLUGIN", `onRequest hook error (non-fatal): ${retainedPluginError}`);
return { blocked: false }; return { blocked: false };
} }
} }

View File

@@ -26,6 +26,7 @@ export async function runPluginOnResponseHook(args: {
apiKeyInfo: unknown; apiKeyInfo: unknown;
headers?: Record<string, string | string[] | undefined>; headers?: Record<string, string | string[] | undefined>;
response: PluginOnResponsePayload; response: PluginOnResponsePayload;
videoTranscriptSensitive?: boolean;
}): Promise<void> { }): Promise<void> {
try { try {
const { runOnResponse } = await import("@/lib/plugins/hooks"); const { runOnResponse } = await import("@/lib/plugins/hooks");
@@ -38,6 +39,7 @@ export async function runPluginOnResponseHook(args: {
apiKeyInfo: args.apiKeyInfo, apiKeyInfo: args.apiKeyInfo,
headers: args.headers, headers: args.headers,
metadata: {}, metadata: {},
videoTranscriptSensitive: args.videoTranscriptSensitive === true,
}, },
args.response args.response
).catch(() => {}); ).catch(() => {});

View File

@@ -36,6 +36,7 @@ export function buildPostCallGuardrailContext(
provider: string | null | undefined; provider: string | null | undefined;
responsePayloadFormat: unknown; responsePayloadFormat: unknown;
clientResponseFormat: unknown; clientResponseFormat: unknown;
videoTranscriptSensitive?: boolean;
}, },
resolveDisabledGuardrails: typeof defaultResolveDisabled = defaultResolveDisabled resolveDisabledGuardrails: typeof defaultResolveDisabled = defaultResolveDisabled
): GuardrailContext { ): GuardrailContext {
@@ -57,5 +58,6 @@ export function buildPostCallGuardrailContext(
sourceFormat: optionalString(args.responsePayloadFormat), sourceFormat: optionalString(args.responsePayloadFormat),
stream: false, stream: false,
targetFormat: optionalString(args.clientResponseFormat), targetFormat: optionalString(args.clientResponseFormat),
videoTranscriptSensitive: args.videoTranscriptSensitive === true,
}; };
} }

View File

@@ -1,8 +1,4 @@
import { import { generateSignature, getCachedResponse, isCacheableForRead } from "@/lib/semanticCache";
generateSignature,
getCachedResponse,
isCacheableForRead,
} from "@/lib/semanticCache";
import { calculateCost } from "@/lib/usage/costCalculator"; import { calculateCost } from "@/lib/usage/costCalculator";
import { trackPendingRequest } from "@/lib/usageDb"; import { trackPendingRequest } from "@/lib/usageDb";
import { synthesizeOpenAiSseFromJson } from "../../utils/jsonToSse.ts"; import { synthesizeOpenAiSseFromJson } from "../../utils/jsonToSse.ts";
@@ -25,6 +21,7 @@ export async function checkSemanticCache({
persistAttemptLogs, persistAttemptLogs,
apiKeyId, apiKeyId,
cacheDefaultMode, cacheDefaultMode,
videoTranscriptSensitive,
}: { }: {
semanticCacheEnabled: boolean; semanticCacheEnabled: boolean;
// Only the fields this read path actually touches are named; everything else // Only the fields this read path actually touches are named; everything else
@@ -42,7 +39,9 @@ export async function checkSemanticCache({
persistAttemptLogs: (args: unknown) => void; persistAttemptLogs: (args: unknown) => void;
apiKeyId?: string | null; apiKeyId?: string | null;
cacheDefaultMode?: "legacy" | "bypass" | null; cacheDefaultMode?: "legacy" | "bypass" | null;
videoTranscriptSensitive: boolean;
}) { }) {
if (videoTranscriptSensitive) return null;
// Per-key bypass: skip cache lookup entirely when the API key opts out. // Per-key bypass: skip cache lookup entirely when the API key opts out.
if (cacheDefaultMode === "bypass") return null; if (cacheDefaultMode === "bypass") return null;
if (semanticCacheEnabled && isCacheableForRead(body, clientRawRequest?.headers)) { if (semanticCacheEnabled && isCacheableForRead(body, clientRawRequest?.headers)) {

View File

@@ -50,10 +50,12 @@ export function storeSemanticCacheResponse(
apiKeyId?: string; apiKeyId?: string;
usage?: UsageLike; usage?: UsageLike;
log?: LoggerLike; log?: LoggerLike;
videoTranscriptSensitive: boolean;
}, },
deps: SemanticCacheStoreDeps = DEFAULT_DEPS deps: SemanticCacheStoreDeps = DEFAULT_DEPS
): void { ): void {
if ( if (
args.videoTranscriptSensitive ||
!args.enabled || !args.enabled ||
!deps.isCacheableForWrite(args.body, args.headers) || !deps.isCacheableForWrite(args.body, args.headers) ||
!deps.isSmallEnoughForSemanticCache(args.translatedResponse) !deps.isSmallEnoughForSemanticCache(args.translatedResponse)

View File

@@ -68,6 +68,7 @@ export function assembleStreamingPipeline(
clientRawRequestHeaders: HeadersLike; clientRawRequestHeaders: HeadersLike;
clientResponseFormat: Parameters<typeof defaultShape>[0]; clientResponseFormat: Parameters<typeof defaultShape>[0];
echoModel: string | null | undefined; echoModel: string | null | undefined;
redactStreamDiagnosticsForLog?: boolean;
responseHeaders: Record<string, string>; responseHeaders: Record<string, string>;
}, },
deps: StreamingPipelineDeps = DEFAULT_DEPS deps: StreamingPipelineDeps = DEFAULT_DEPS
@@ -83,7 +84,8 @@ export function assembleStreamingPipeline(
let piiStream = deps.pipeWithDisconnect( let piiStream = deps.pipeWithDisconnect(
args.providerResponse, args.providerResponse,
args.transformStream, args.transformStream,
args.streamController args.streamController,
{ redactStreamDiagnosticsForLog: args.redactStreamDiagnosticsForLog }
); );
if (typeof args.createPiiTransform === "function") { if (typeof args.createPiiTransform === "function") {
piiStream = piiStream.pipeThrough((args.createPiiTransform as () => TransformStream)()); piiStream = piiStream.pipeThrough((args.createPiiTransform as () => TransformStream)());

View File

@@ -49,6 +49,7 @@ interface StreamingCacheArgs {
apiKeyId?: string; apiKeyId?: string;
streamUsage?: Record<string, unknown> | null; streamUsage?: Record<string, unknown> | null;
log?: LoggerLike; log?: LoggerLike;
videoTranscriptSensitive: boolean;
} }
function streamTokensSaved(streamUsage: Record<string, unknown> | null | undefined): number { function streamTokensSaved(streamUsage: Record<string, unknown> | null | undefined): number {
@@ -87,6 +88,7 @@ export function storeStreamingSemanticCacheResponse(
deps: StreamingSemanticCacheStoreDeps = DEFAULT_DEPS deps: StreamingSemanticCacheStoreDeps = DEFAULT_DEPS
): void { ): void {
if ( if (
args.videoTranscriptSensitive ||
!args.enabled || !args.enabled ||
args.streamStatus !== 200 || args.streamStatus !== 200 ||
!args.streamResponseBody || !args.streamResponseBody ||

View File

@@ -63,6 +63,10 @@ import {
} from "../../src/lib/db/contextHandoffs.ts"; } from "../../src/lib/db/contextHandoffs.ts";
import { extractSessionAffinityKey } from "@/sse/services/auth"; import { extractSessionAffinityKey } from "@/sse/services/auth";
import { getHiddenModelsByProvider } from "@/models"; import { getHiddenModelsByProvider } from "@/models";
import {
containsVideoTranscriptForLog,
redactVideoTranscriptSensitiveText,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings"; import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings";
import { fetchCodexQuota } from "./codexQuotaFetcher.ts"; import { fetchCodexQuota } from "./codexQuotaFetcher.ts";
import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts"; import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
@@ -702,7 +706,15 @@ export async function resolveTargetTimeoutMsForTarget(
*/ */
export async function handleComboChat(options: HandleComboChatOptions): Promise<Response> { export async function handleComboChat(options: HandleComboChatOptions): Promise<Response> {
const traceInvocationId = options.invocationId ?? createInvocationId(); const traceInvocationId = options.invocationId ?? createInvocationId();
const response = await handleComboChatInner({ ...options, invocationId: traceInvocationId }); // Re-check a still-structured body at the service boundary. The explicit bit remains
// authoritative when the caller already ran Video Bridge and replaced the raw carrier.
const videoTranscriptSensitive =
options.videoTranscriptSensitive === true || containsVideoTranscriptForLog(options.body);
const response = await handleComboChatInner({
...options,
invocationId: traceInvocationId,
videoTranscriptSensitive,
});
response.headers.set("X-OmniRoute-Combo-Trace", traceInvocationId); response.headers.set("X-OmniRoute-Combo-Trace", traceInvocationId);
const trace = getComboTrace(traceInvocationId); const trace = getComboTrace(traceInvocationId);
options.log.info( options.log.info(
@@ -732,6 +744,8 @@ async function handleComboChatInner({
sourceFormat = null, sourceFormat = null,
endpointPath = null, endpointPath = null,
requestHeaders = null, requestHeaders = null,
videoTranscriptSensitive = false,
videoTranscriptDescriptionFingerprints = [],
invocationId, invocationId,
}: HandleComboChatOptions): Promise<Response> { }: HandleComboChatOptions): Promise<Response> {
const comboCtx = createComboContext({ body, combo, settings, relayOptions, log }); const comboCtx = createComboContext({ body, combo, settings, relayOptions, log });
@@ -783,6 +797,7 @@ async function handleComboChatInner({
handleSingleModelWithTimeout, handleSingleModelWithTimeout,
log, log,
hiddenModelsByProvider, hiddenModelsByProvider,
videoTranscriptSensitive,
}); });
if (pinnedDispatch) return pinnedDispatch; if (pinnedDispatch) return pinnedDispatch;
} }
@@ -805,6 +820,8 @@ async function handleComboChatInner({
signal, signal,
apiKeyAllowedConnections, apiKeyAllowedConnections,
hiddenModelsByProvider, hiddenModelsByProvider,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
perTargetAdmission, perTargetAdmission,
deferContextOverflowWhenCompressible, deferContextOverflowWhenCompressible,
compressionExclusions, compressionExclusions,
@@ -861,6 +878,8 @@ async function handleComboChatInner({
signal, signal,
apiKeyAllowedConnections, apiKeyAllowedConnections,
hiddenModelsByProvider, hiddenModelsByProvider,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
perTargetAdmission, perTargetAdmission,
deferContextOverflowWhenCompressible, deferContextOverflowWhenCompressible,
compressionExclusions, compressionExclusions,
@@ -889,6 +908,7 @@ async function handleComboChatInner({
allCombos, allCombos,
signal, signal,
hiddenModelsByProvider, hiddenModelsByProvider,
videoTranscriptSensitive,
clientManagedResponsesContext, clientManagedResponsesContext,
deferContextOverflowWhenCompressible, deferContextOverflowWhenCompressible,
compressionExclusions, compressionExclusions,
@@ -1681,9 +1701,13 @@ async function handleComboChatInner({
releaseQualityClone(qualityClone, result, quality); releaseQualityClone(qualityClone, result, quality);
if (!quality.valid) { if (!quality.valid) {
releaseRejectedQualityResponse(qualityClone, result); releaseRejectedQualityResponse(qualityClone, result);
const retainedQualityReason = redactVideoTranscriptSensitiveText(
quality.reason || "upstream response failed quality validation",
videoTranscriptSensitive
);
log.warn( log.warn(
"COMBO", "COMBO",
`Model ${modelStr} returned 200 but failed quality check: ${quality.reason}` `Model ${modelStr} returned 200 but failed quality check: ${retainedQualityReason}`
); );
// #6692: a quality-rejected 200 never marks the connection row // #6692: a quality-rejected 200 never marks the connection row
// unhealthy, so the sticky pin's lazy headroom recheck would never // unhealthy, so the sticky pin's lazy headroom recheck would never
@@ -1736,7 +1760,7 @@ async function handleComboChatInner({
targetIndex: i, targetIndex: i,
provider, provider,
model: modelStr, model: modelStr,
error: `Quality: ${quality.reason}`, error: `Quality: ${retainedQualityReason}`,
latencyMs: Date.now() - startTime, latencyMs: Date.now() - startTime,
}); });
observeFailure(false, target.executionKey); observeFailure(false, target.executionKey);
@@ -1869,6 +1893,8 @@ async function handleComboChatInner({
prevModel, prevModel,
currModel: modelStr, currModel: modelStr,
universalConfig: universalHandoffConfig, universalConfig: universalHandoffConfig,
videoTranscriptSensitive,
trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints,
handleSingleModel: handleSingleModelWithTimeout, handleSingleModel: handleSingleModelWithTimeout,
}); });
} }
@@ -1920,6 +1946,8 @@ async function handleComboChatInner({
model: modelStr, model: modelStr,
expiresAt: resetCandidates[0] || null, expiresAt: resetCandidates[0] || null,
config: relayConfig, config: relayConfig,
videoTranscriptSensitive,
trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints,
handleSingleModel: handleSingleModelWithTimeout, handleSingleModel: handleSingleModelWithTimeout,
}); });
} }
@@ -2443,7 +2471,9 @@ async function handleComboChatInner({
} }
log.warn("COMBO", `Model ${modelStr} failed, trying next`, { log.warn("COMBO", `Model ${modelStr} failed, trying next`, {
status: result.status, status: result.status,
errorBody: redactConnectionLabel(errorText), errorBody: redactConnectionLabel(
redactVideoTranscriptSensitiveText(errorText, videoTranscriptSensitive)
),
}); });
// #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models // #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models
@@ -2524,7 +2554,11 @@ async function handleComboChatInner({
} }
})().catch((err) => { })().catch((err) => {
const logError = log.error ?? log.warn; const logError = log.error ?? log.warn;
logError("COMBO", `Speculative task error for target ${i}`, err); logError(
"COMBO",
`Speculative task error for target ${i}`,
videoTranscriptSensitive ? redactVideoTranscriptSensitiveText(String(err), true) : err
);
// G2 (silent-stop fix): never leave the speculative loop waiting on an // G2 (silent-stop fix): never leave the speculative loop waiting on an
// unresolved globalPromise. If a task throws unexpectedly (outside // unresolved globalPromise. If a task throws unexpectedly (outside
// executeTarget's error handling) and no other task succeeds, the post-loop // executeTarget's error handling) and no other task succeeds, the post-loop
@@ -2745,9 +2779,13 @@ async function handleComboChatInner({
}); });
if (decision.wait) { if (decision.wait) {
const retainedComboFailure = redactVideoTranscriptSensitiveText(
msg,
videoTranscriptSensitive
);
log.info( log.info(
"COMBO", "COMBO",
`${strategy} cooldown wait: ${msg} — waiting ${Math.ceil( `${strategy} cooldown wait: ${retainedComboFailure} — waiting ${Math.ceil(
decision.waitMs / 1000 decision.waitMs / 1000
)}s (reason=${decision.reason ?? "?"}) then retrying (attempt ${ )}s (reason=${decision.reason ?? "?"}) then retrying (attempt ${
comboCooldownAttempt + 1 comboCooldownAttempt + 1
@@ -2773,7 +2811,13 @@ async function handleComboChatInner({
// a config-class status like 403/422). // a config-class status like 403/422).
if (earliestRetryAfter && isRetryAfterEligibleStatus(status)) { if (earliestRetryAfter && isRetryAfterEligibleStatus(status)) {
const retryHuman = formatRetryAfter(toRetryAfterDisplayValue(earliestRetryAfter)); const retryHuman = formatRetryAfter(toRetryAfterDisplayValue(earliestRetryAfter));
log.warn("COMBO", `All models failed | ${msg} (${retryHuman})`); log.warn(
"COMBO",
`All models failed | ${redactVideoTranscriptSensitiveText(
msg,
videoTranscriptSensitive
)} (${retryHuman})`
);
return withQuotaExhaustionClassification( return withQuotaExhaustionClassification(
unavailableResponse(status, msg, earliestRetryAfter, retryHuman), unavailableResponse(status, msg, earliestRetryAfter, retryHuman),
observedFailure ? allObservedFailuresQuota : null observedFailure ? allObservedFailuresQuota : null
@@ -2784,7 +2828,10 @@ async function handleComboChatInner({
// `try-auto` recovery action via buildRecoveryHint so the OC plugin can show "→ Try // `try-auto` recovery action via buildRecoveryHint so the OC plugin can show "→ Try
// model: auto" instead of an opaque 5xx. We pass the upstream retry-after seconds to // model: auto" instead of an opaque 5xx. We pass the upstream retry-after seconds to
// the hint so the client can render a precise "wait Ns and retry" message. // the hint so the client can render a precise "wait Ns and retry" message.
log.warn("COMBO", `All models failed | ${msg}`); log.warn(
"COMBO",
`All models failed | ${redactVideoTranscriptSensitiveText(msg, videoTranscriptSensitive)}`
);
const { pinClearedNow } = recordComboFailure(effectiveSessionId, combo.name); const { pinClearedNow } = recordComboFailure(effectiveSessionId, combo.name);
if (pinClearedNow) { if (pinClearedNow) {
log.info( log.info(
@@ -2888,6 +2935,7 @@ async function handleRoundRobinCombo({
requestHeaders = null, requestHeaders = null,
relayOptions, relayOptions,
perTargetAdmission = null, perTargetAdmission = null,
videoTranscriptSensitive = false,
}: HandleRoundRobinOptions): Promise<Response> { }: HandleRoundRobinOptions): Promise<Response> {
const config = settings const config = settings
? resolveComboConfig(combo, settings) ? resolveComboConfig(combo, settings)
@@ -3286,24 +3334,20 @@ async function handleRoundRobinCombo({
"COMBO-RR", "COMBO-RR",
`Maximum combo attempts (${maxGlobalAttempts}) exceeded. Terminating loop to prevent runaway requests.` `Maximum combo attempts (${maxGlobalAttempts}) exceeded. Terminating loop to prevent runaway requests.`
); );
return errorResponseWithComboDiagnostics( return errorResponseWithComboDiagnostics(503, "Maximum combo retry limit reached", {
503, poolSize: modelCount,
"Maximum combo retry limit reached", attempted: globalAttempts,
{ excluded: [
poolSize: modelCount, ...[...exhaustedProviders].map((p) => ({ provider: p, reason: "exhausted" })),
attempted: globalAttempts, ...[...exhaustedConnections].map((c) => formatExhaustedConnectionKey(String(c))),
excluded: [ ],
...[...exhaustedProviders].map((p) => ({ provider: p, reason: "exhausted" })), attemptOrder: rrOutcomes.map((o) => ({
...[...exhaustedConnections].map((c) => formatExhaustedConnectionKey(String(c))), provider: o.model.split("/")[0] || "unknown",
], model: o.model,
attemptOrder: rrOutcomes.map((o) => ({ })),
provider: o.model.split("/")[0] || "unknown", terminalReason: "max_attempts_exceeded",
model: o.model, recovery: buildRecoveryHint("max_attempts_exceeded"),
})), });
terminalReason: "max_attempts_exceeded",
recovery: buildRecoveryHint("max_attempts_exceeded"),
}
);
} }
if (retry > 0) { if (retry > 0) {
log.info( log.info(
@@ -3404,9 +3448,13 @@ async function handleRoundRobinCombo({
releaseQualityClone(rrClone, result, quality); releaseQualityClone(rrClone, result, quality);
if (!quality.valid) { if (!quality.valid) {
releaseRejectedQualityResponse(rrClone, result); releaseRejectedQualityResponse(rrClone, result);
const retainedQualityReason = redactVideoTranscriptSensitiveText(
quality.reason || "upstream response failed quality validation",
videoTranscriptSensitive
);
log.warn( log.warn(
"COMBO-RR", "COMBO-RR",
`${modelStr} returned 200 but failed quality check: ${quality.reason}` `${modelStr} returned 200 but failed quality check: ${retainedQualityReason}`
); );
// #6692: same rationale as handleComboChat's quality-fail branch — // #6692: same rationale as handleComboChat's quality-fail branch —
// a quality-rejected 200 never marks the connection row unhealthy, // a quality-rejected 200 never marks the connection row unhealthy,
@@ -3758,7 +3806,9 @@ async function handleRoundRobinCombo({
if (offset > 0) fallbackCount++; if (offset > 0) fallbackCount++;
log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { log.warn("COMBO-RR", `${modelStr} failed, trying next model`, {
status: result.status, status: result.status,
errorBody: redactConnectionLabel(errorText), errorBody: redactConnectionLabel(
redactVideoTranscriptSensitiveText(errorText, videoTranscriptSensitive)
),
}); });
if ( if (
@@ -3811,7 +3861,11 @@ async function handleRoundRobinCombo({
} catch (err) { } catch (err) {
// G4: unexpected exception in the round-robin loop must never crash the // G4: unexpected exception in the round-robin loop must never crash the
// request silently — surface a 500 instead of hanging the client. // request silently — surface a 500 instead of hanging the client.
log.error?.("COMBO-RR", "Unexpected error in round-robin loop", err); log.error?.(
"COMBO-RR",
"Unexpected error in round-robin loop",
videoTranscriptSensitive ? redactVideoTranscriptSensitiveText(String(err), true) : err
);
return errorResponse(500, "Unexpected error in round-robin combo"); return errorResponse(500, "Unexpected error in round-robin combo");
} finally { } finally {
if (rrLoopSafetyTimer) { if (rrLoopSafetyTimer) {
@@ -3907,11 +3961,20 @@ async function handleRoundRobinCombo({
if (earliestRetryAfter && isRetryAfterEligibleStatus(status)) { if (earliestRetryAfter && isRetryAfterEligibleStatus(status)) {
const retryHuman = formatRetryAfter(toRetryAfterDisplayValue(earliestRetryAfter)); const retryHuman = formatRetryAfter(toRetryAfterDisplayValue(earliestRetryAfter));
log.warn("COMBO-RR", `All models failed | ${msg} (${retryHuman})`); log.warn(
"COMBO-RR",
`All models failed | ${redactVideoTranscriptSensitiveText(
msg,
videoTranscriptSensitive
)} (${retryHuman})`
);
return unavailableResponse(status, msg, earliestRetryAfter, retryHuman); return unavailableResponse(status, msg, earliestRetryAfter, retryHuman);
} }
log.warn("COMBO-RR", `All models failed | ${msg}`); log.warn(
"COMBO-RR",
`All models failed | ${redactVideoTranscriptSensitiveText(msg, videoTranscriptSensitive)}`
);
return new Response(JSON.stringify({ error: { message: msg } }), { return new Response(JSON.stringify({ error: { message: msg } }), {
status, status,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@@ -13,6 +13,7 @@
* Extracted from combo.ts as a pure move (#3501). No behaviour change. * Extracted from combo.ts as a pure move (#3501). No behaviour change.
*/ */
import { getCachedProviderConnections } from "../../../src/lib/db/readCache"; import { getCachedProviderConnections } from "../../../src/lib/db/readCache";
import { redactVideoTranscriptSensitiveText } from "../../../src/lib/guardrails/videoTranscriptLogRedaction";
import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker"; import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker";
import { fisherYatesShuffle, getNextFromDeck } from "../../../src/shared/utils/shuffleDeck"; import { fisherYatesShuffle, getNextFromDeck } from "../../../src/shared/utils/shuffleDeck";
import { handleFusionChat, type FusionTuning } from "../fusion.ts"; import { handleFusionChat, type FusionTuning } from "../fusion.ts";
@@ -84,6 +85,8 @@ type PreludeBaseOptionArgs = {
signal?: AbortSignal | null; signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null; apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider; hiddenModelsByProvider?: HiddenModelsByProvider;
videoTranscriptSensitive?: boolean;
videoTranscriptDescriptionFingerprints?: readonly string[];
clientManagedResponsesContext?: boolean; clientManagedResponsesContext?: boolean;
/** #9654 Wave 2: per-target lane-aware admission probe (see HandleComboChatOptions). */ /** #9654 Wave 2: per-target lane-aware admission probe (see HandleComboChatOptions). */
perTargetAdmission?: PerTargetAdmissionHook | null; perTargetAdmission?: PerTargetAdmissionHook | null;
@@ -111,6 +114,8 @@ function buildBaseOptions(a: PreludeBaseOptionArgs): HandleComboChatOptions {
signal: a.signal, signal: a.signal,
apiKeyAllowedConnections: a.apiKeyAllowedConnections, apiKeyAllowedConnections: a.apiKeyAllowedConnections,
hiddenModelsByProvider: a.hiddenModelsByProvider, hiddenModelsByProvider: a.hiddenModelsByProvider,
videoTranscriptSensitive: a.videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints: a.videoTranscriptDescriptionFingerprints,
invocationId: a.invocationId, invocationId: a.invocationId,
clientManagedResponsesContext: a.clientManagedResponsesContext, clientManagedResponsesContext: a.clientManagedResponsesContext,
perTargetAdmission: a.perTargetAdmission, perTargetAdmission: a.perTargetAdmission,
@@ -221,8 +226,16 @@ async function evaluatePinnedResponse(args: {
clientRequestedStream: boolean; clientRequestedStream: boolean;
config: ComboSetupConfig; config: ComboSetupConfig;
log: ComboLogger; log: ComboLogger;
videoTranscriptSensitive?: boolean;
}): Promise<Response | null> { }): Promise<Response | null> {
const { pinnedResult, pinnedModel, clientRequestedStream, config, log } = args; const {
pinnedResult,
pinnedModel,
clientRequestedStream,
config,
log,
videoTranscriptSensitive = false,
} = args;
if (pinnedResult.ok) { if (pinnedResult.ok) {
let pinnedClone: Response; let pinnedClone: Response;
try { try {
@@ -239,9 +252,13 @@ async function evaluatePinnedResponse(args: {
releaseQualityClone(pinnedClone, pinnedResult, pinnedQuality); releaseQualityClone(pinnedClone, pinnedResult, pinnedQuality);
if (pinnedQuality.valid) return pinnedResult; if (pinnedQuality.valid) return pinnedResult;
releaseRejectedQualityResponse(pinnedClone, pinnedResult); releaseRejectedQualityResponse(pinnedClone, pinnedResult);
const retainedReason = redactVideoTranscriptSensitiveText(
pinnedQuality.reason || "upstream response failed quality validation",
videoTranscriptSensitive
);
log.warn( log.warn(
"COMBO", "COMBO",
`Pinned model ${pinnedModel} returned 200 but failed quality check: ${pinnedQuality.reason}, falling through to combo retry/fallback` `Pinned model ${pinnedModel} returned 200 but failed quality check: ${retainedReason}, falling through to combo retry/fallback`
); );
return null; return null;
} }
@@ -271,6 +288,7 @@ export async function tryPinnedModelDispatch(args: {
handleSingleModelWithTimeout: HandleSingleModel; handleSingleModelWithTimeout: HandleSingleModel;
log: ComboLogger; log: ComboLogger;
hiddenModelsByProvider?: HiddenModelsByProvider; hiddenModelsByProvider?: HiddenModelsByProvider;
videoTranscriptSensitive?: boolean;
}): Promise<Response | null> { }): Promise<Response | null> {
const { const {
body, body,
@@ -282,6 +300,7 @@ export async function tryPinnedModelDispatch(args: {
handleSingleModelWithTimeout, handleSingleModelWithTimeout,
log, log,
hiddenModelsByProvider, hiddenModelsByProvider,
videoTranscriptSensitive = false,
} = args; } = args;
// The pin is read from session_model_history (a PRIOR turn) and may name a // The pin is read from session_model_history (a PRIOR turn) and may name a
// model that has since been removed from this combo, or a provider whose // model that has since been removed from this combo, or a provider whose
@@ -341,9 +360,13 @@ export async function tryPinnedModelDispatch(args: {
modelPinned: true, modelPinned: true,
} as SingleModelTarget); } as SingleModelTarget);
} catch (pinErr) { } catch (pinErr) {
const retainedPinError = redactVideoTranscriptSensitiveText(
pinErr instanceof Error ? pinErr.message : String(pinErr),
videoTranscriptSensitive
);
log.warn( log.warn(
"COMBO", "COMBO",
`Pinned model ${pinnedModel} threw error: ${pinErr instanceof Error ? pinErr.message : String(pinErr)}, falling through to combo retry/fallback` `Pinned model ${pinnedModel} threw error: ${retainedPinError}, falling through to combo retry/fallback`
); );
} }
if (pinnedResult) { if (pinnedResult) {
@@ -353,6 +376,7 @@ export async function tryPinnedModelDispatch(args: {
clientRequestedStream, clientRequestedStream,
config, config,
log, log,
videoTranscriptSensitive,
}); });
if (accepted) return accepted; if (accepted) return accepted;
} }
@@ -395,6 +419,8 @@ export async function tryFusionDispatch(args: {
signal?: AbortSignal | null; signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null; apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider; hiddenModelsByProvider?: HiddenModelsByProvider;
videoTranscriptSensitive?: boolean;
videoTranscriptDescriptionFingerprints?: readonly string[];
perTargetAdmission?: PerTargetAdmissionHook | null; perTargetAdmission?: PerTargetAdmissionHook | null;
deferContextOverflowWhenCompressible?: boolean; deferContextOverflowWhenCompressible?: boolean;
compressionExclusions?: import("../compression/exclusions.ts").CompressionExclusions; compressionExclusions?: import("../compression/exclusions.ts").CompressionExclusions;
@@ -524,6 +550,7 @@ export async function tryFusionDispatch(args: {
perTargetAdmission: args.perTargetAdmission, perTargetAdmission: args.perTargetAdmission,
judgeModel, judgeModel,
tuning: fusionTuning, tuning: fusionTuning,
videoTranscriptSensitive: args.videoTranscriptSensitive,
}); });
} }
@@ -676,6 +703,8 @@ export async function tryRuntimeUnitDispatch(args: {
signal?: AbortSignal | null; signal?: AbortSignal | null;
apiKeyAllowedConnections?: string[] | null; apiKeyAllowedConnections?: string[] | null;
hiddenModelsByProvider?: HiddenModelsByProvider; hiddenModelsByProvider?: HiddenModelsByProvider;
videoTranscriptSensitive?: boolean;
videoTranscriptDescriptionFingerprints?: readonly string[];
perTargetAdmission?: PerTargetAdmissionHook | null; perTargetAdmission?: PerTargetAdmissionHook | null;
deferContextOverflowWhenCompressible?: boolean; deferContextOverflowWhenCompressible?: boolean;
compressionExclusions?: import("../compression/exclusions.ts").CompressionExclusions; compressionExclusions?: import("../compression/exclusions.ts").CompressionExclusions;

View File

@@ -114,6 +114,10 @@ export type HandleComboChatOptions = {
apiKeyAllowedConnections?: string[] | null; apiKeyAllowedConnections?: string[] | null;
nesting?: ComboNestingContext | null; nesting?: ComboNestingContext | null;
hiddenModelsByProvider?: HiddenModelsByProvider; hiddenModelsByProvider?: HiddenModelsByProvider;
/** Request-scoped retention bit derived before guardrails can replace a video carrier. */
videoTranscriptSensitive?: boolean;
/** Exact bounded identities emitted by a modified Video Bridge guardrail. */
videoTranscriptDescriptionFingerprints?: readonly string[];
/** Native Responses clients (for example Codex CLI/Desktop) manage compaction themselves. */ /** Native Responses clients (for example Codex CLI/Desktop) manage compaction themselves. */
clientManagedResponsesContext?: boolean; clientManagedResponsesContext?: boolean;
/** /**

View File

@@ -5,6 +5,11 @@ import {
type HandoffPayload, type HandoffPayload,
upsertHandoff, upsertHandoff,
} from "../../src/lib/db/contextHandoffs.ts"; } from "../../src/lib/db/contextHandoffs.ts";
import {
containsVideoTranscriptForLog,
omitVideoTranscriptForLog,
type VideoTranscriptLogContext,
} from "../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
import { estimateTokens } from "./contextManager.ts"; import { estimateTokens } from "./contextManager.ts";
import { stripMarkdownCodeFence } from "../utils/aiSdkCompat.ts"; import { stripMarkdownCodeFence } from "../utils/aiSdkCompat.ts";
@@ -43,6 +48,23 @@ export type MessageLike = {
content?: unknown; content?: unknown;
}; };
type HandoffTranscriptContext = VideoTranscriptLogContext & {
videoTranscriptSensitive?: boolean;
};
function retainHandoffMessages(
messages: MessageLike[],
context: HandoffTranscriptContext
): MessageLike[] | null {
const source = Array.isArray(messages) ? messages : [];
const transcriptSensitive =
context.videoTranscriptSensitive === true || containsVideoTranscriptForLog(source, context);
if (!transcriptSensitive) return source;
const retained = omitVideoTranscriptForLog(source, context);
return Array.isArray(retained) ? (retained as MessageLike[]) : null;
}
export interface ContextRelayConfig { export interface ContextRelayConfig {
handoffModel?: string; handoffModel?: string;
handoffThreshold?: number; handoffThreshold?: number;
@@ -445,6 +467,10 @@ export function maybeGenerateHandoff(options: {
model: string; model: string;
expiresAt: string | null; expiresAt: string | null;
config?: ContextRelayConfig | null; config?: ContextRelayConfig | null;
/** Trusted request bit for carriers already replaced by the Video Bridge guardrail. */
videoTranscriptSensitive?: boolean;
/** Exact bounded identities emitted by a modified Video Bridge guardrail. */
trustedDescriptionFingerprints?: readonly string[];
handleSingleModel: (body: Record<string, unknown>, modelStr: string) => Promise<Response>; handleSingleModel: (body: Record<string, unknown>, modelStr: string) => Promise<Response>;
}): void { }): void {
if (!options.sessionId || !options.connectionId) return; if (!options.sessionId || !options.connectionId) return;
@@ -454,6 +480,12 @@ export function maybeGenerateHandoff(options: {
if (options.percentUsed < relayConfig.handoffThreshold) return; if (options.percentUsed < relayConfig.handoffThreshold) return;
if (options.percentUsed >= HANDOFF_EXHAUSTION_THRESHOLD) return; if (options.percentUsed >= HANDOFF_EXHAUSTION_THRESHOLD) return;
const retainedMessages = retainHandoffMessages(options.messages, {
videoTranscriptSensitive: options.videoTranscriptSensitive,
trustedDescriptionFingerprints: options.trustedDescriptionFingerprints,
});
if (!retainedMessages) return;
cleanupExpiredHandoffs(); cleanupExpiredHandoffs();
if (hasActiveHandoff(options.sessionId, options.comboName)) return; if (hasActiveHandoff(options.sessionId, options.comboName)) return;
const inflightKey = getInflightKey(options.sessionId, options.comboName); const inflightKey = getInflightKey(options.sessionId, options.comboName);
@@ -463,6 +495,7 @@ export function maybeGenerateHandoff(options: {
setImmediate(() => { setImmediate(() => {
generateHandoffAsync({ generateHandoffAsync({
...options, ...options,
messages: retainedMessages,
sessionId: options.sessionId as string, sessionId: options.sessionId as string,
connectionId: options.connectionId as string, connectionId: options.connectionId as string,
config: relayConfig, config: relayConfig,
@@ -690,6 +723,10 @@ export function maybeGenerateUniversalHandoff(options: {
prevModel: string | null; prevModel: string | null;
currModel: string; currModel: string;
universalConfig: UniversalHandoffConfig; universalConfig: UniversalHandoffConfig;
/** Trusted request bit for carriers already replaced by the Video Bridge guardrail. */
videoTranscriptSensitive?: boolean;
/** Exact bounded identities emitted by a modified Video Bridge guardrail. */
trustedDescriptionFingerprints?: readonly string[];
handleSingleModel: (body: Record<string, unknown>, modelStr: string) => Promise<Response>; handleSingleModel: (body: Record<string, unknown>, modelStr: string) => Promise<Response>;
}): void { }): void {
const decision = shouldGenerateUniversalHandoff({ const decision = shouldGenerateUniversalHandoff({
@@ -703,6 +740,12 @@ export function maybeGenerateUniversalHandoff(options: {
if (decision !== "generate") return; if (decision !== "generate") return;
if (!options.sessionId) return; if (!options.sessionId) return;
const retainedMessages = retainHandoffMessages(options.messages, {
videoTranscriptSensitive: options.videoTranscriptSensitive,
trustedDescriptionFingerprints: options.trustedDescriptionFingerprints,
});
if (!retainedMessages) return;
const inflightKey = getInflightKey(options.sessionId, options.comboName); const inflightKey = getInflightKey(options.sessionId, options.comboName);
if (inflightHandoffGenerations.has(inflightKey)) return; if (inflightHandoffGenerations.has(inflightKey)) return;
inflightHandoffGenerations.add(inflightKey); inflightHandoffGenerations.add(inflightKey);
@@ -713,7 +756,7 @@ export function maybeGenerateUniversalHandoff(options: {
generateUniversalHandoffAsync({ generateUniversalHandoffAsync({
sessionId: options.sessionId as string, sessionId: options.sessionId as string,
comboName: options.comboName, comboName: options.comboName,
messages: options.messages, messages: retainedMessages,
prevModel: options.prevModel || "unknown", prevModel: options.prevModel || "unknown",
currModel: options.currModel, currModel: options.currModel,
handoffModel: options.universalConfig.handoffModel || options.currModel, handoffModel: options.universalConfig.handoffModel || options.currModel,

View File

@@ -20,6 +20,7 @@
*/ */
import { errorResponse, sanitizeErrorMessage } from "../utils/error.ts"; import { errorResponse, sanitizeErrorMessage } from "../utils/error.ts";
import { extractTextContent } from "../translator/helpers/geminiHelper.ts"; import { extractTextContent } from "../translator/helpers/geminiHelper.ts";
import { redactVideoTranscriptSensitiveText } from "../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
import type { PerTargetAdmissionHook } from "./admission/types.ts"; import type { PerTargetAdmissionHook } from "./admission/types.ts";
import type { ComboLogger, HandleSingleModel, ResolvedComboTarget } from "./combo/types.ts"; import type { ComboLogger, HandleSingleModel, ResolvedComboTarget } from "./combo/types.ts";
@@ -231,6 +232,8 @@ export type HandleFusionChatOptions = {
tuning?: FusionTuning | null; tuning?: FusionTuning | null;
/** #9654 Wave 2: per-target lane-aware admission probe (see HandleComboChatOptions). */ /** #9654 Wave 2: per-target lane-aware admission probe (see HandleComboChatOptions). */
perTargetAdmission?: PerTargetAdmissionHook | null; perTargetAdmission?: PerTargetAdmissionHook | null;
/** Request-scoped bit set before Video Bridge can replace the raw carrier. */
videoTranscriptSensitive?: boolean;
}; };
function getFusionModelString(model: FusionModel): string { function getFusionModelString(model: FusionModel): string {
@@ -276,6 +279,7 @@ export async function handleFusionChat({
judgeTarget, judgeTarget,
tuning, tuning,
perTargetAdmission, perTargetAdmission,
videoTranscriptSensitive = false,
}: HandleFusionChatOptions): Promise<Response> { }: HandleFusionChatOptions): Promise<Response> {
const panel = Array.isArray(models) ? models.filter(Boolean) : []; const panel = Array.isArray(models) ? models.filter(Boolean) : [];
if (panel.length === 0) { if (panel.length === 0) {
@@ -400,7 +404,10 @@ export async function handleFusionChat({
} }
if (sentinel.__error) { if (sentinel.__error) {
log.warn("FUSION", `Panel ${model} threw`, { log.warn("FUSION", `Panel ${model} threw`, {
error: sanitizeErrorMessage(sentinel.__error as Error), error: redactVideoTranscriptSensitiveText(
sanitizeErrorMessage(sentinel.__error as Error),
videoTranscriptSensitive
),
}); });
failures.push({ model, reason: "threw" }); failures.push({ model, reason: "threw" });
continue; continue;
@@ -428,7 +435,10 @@ export async function handleFusionChat({
} }
} catch (e) { } catch (e) {
log.warn("FUSION", `Panel ${model} unparseable`, { log.warn("FUSION", `Panel ${model} unparseable`, {
error: sanitizeErrorMessage(e as Error), error: redactVideoTranscriptSensitiveText(
sanitizeErrorMessage(e as Error),
videoTranscriptSensitive
),
}); });
failures.push({ model, reason: "unparseable" }); failures.push({ model, reason: "unparseable" });
} }

View File

@@ -140,6 +140,7 @@ type AssistantMessageLike = {
type AssistantMessageCacheContext = { type AssistantMessageCacheContext = {
scope?: string; scope?: string;
historyMessages?: AssistantMessageLike[]; historyMessages?: AssistantMessageLike[];
videoTranscriptSensitive?: boolean;
}; };
type ToolCallLike = { type ToolCallLike = {
@@ -334,6 +335,7 @@ export function cacheReasoningFromAssistantMessage(
model: string, model: string,
context?: AssistantMessageCacheContext context?: AssistantMessageCacheContext
): number { ): number {
if (context?.videoTranscriptSensitive === true) return 0;
if (!message || message.role !== "assistant") { if (!message || message.role !== "assistant") {
return 0; return 0;
} }

View File

@@ -1,5 +1,11 @@
import { getPendingById } from "@/lib/usage/usageHistory"; import { getPendingById } from "@/lib/usage/usageHistory";
import { getChatLogMaxDepth, getChatLogArrayTailItems } from "@/lib/logEnv"; import { getChatLogMaxDepth, getChatLogArrayTailItems } from "@/lib/logEnv";
import {
containsVideoTranscriptForLog,
omitVideoTranscriptForLog,
type VideoTranscriptLogContext,
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { sanitizeErrorMessage } from "./error.ts"; import { sanitizeErrorMessage } from "./error.ts";
type JsonRecord = Record<string, unknown>; type JsonRecord = Record<string, unknown>;
@@ -12,6 +18,7 @@ type HeaderInput =
| undefined; | undefined;
export type RequestPipelinePayloads = { export type RequestPipelinePayloads = {
_omnirouteVideoTranscriptRedacted?: true;
routeDecision?: JsonRecord; routeDecision?: JsonRecord;
clientRawRequest?: JsonRecord; clientRawRequest?: JsonRecord;
openaiRequest?: JsonRecord; openaiRequest?: JsonRecord;
@@ -44,6 +51,7 @@ type RequestLogger = {
appendConvertedChunk: (chunk: string) => void; appendConvertedChunk: (chunk: string) => void;
logError: (error: unknown, requestBody?: unknown) => void; logError: (error: unknown, requestBody?: unknown) => void;
getPipelinePayloads: () => RequestPipelinePayloads | null; getPipelinePayloads: () => RequestPipelinePayloads | null;
isVideoTranscriptSensitive: () => boolean;
}; };
type RequestLoggerOptions = { type RequestLoggerOptions = {
@@ -55,6 +63,13 @@ type RequestLoggerOptions = {
model?: string; model?: string;
provider?: string; provider?: string;
connectionId?: string | null; connectionId?: string | null;
/**
* Server-derived from a recognized video carrier, successful bridge result, or bounded
* unknown-sensitive detector overflow; never from delimiter-shaped prose alone.
*/
videoTranscriptSensitive?: boolean;
/** Exact SHA-256 identities of transcript descriptions generated by the guardrail. */
videoTranscriptDescriptionFingerprints?: readonly string[];
}; };
const DEFAULT_MAX_STREAM_CHUNK_BYTES = 128 * 1024; const DEFAULT_MAX_STREAM_CHUNK_BYTES = 128 * 1024;
@@ -159,7 +174,7 @@ function truncateLogString(value: string, maxLength = MAX_LOG_STRING_LENGTH): st
* recursing into an object's values, enabling the per-field exemption above. * recursing into an object's values, enabling the per-field exemption above.
* Top-level arrays (no key context) remain subject to truncation. * Top-level arrays (no key context) remain subject to truncation.
*/ */
export function cloneBoundedForLog(value: unknown, depth = 0, key: string | null = null): unknown { function cloneBoundedForLogValue(value: unknown, depth = 0, key: string | null = null): unknown {
if (value === null || value === undefined) return value; if (value === null || value === undefined) return value;
if (typeof value === "string") return truncateLogString(value); if (typeof value === "string") return truncateLogString(value);
if (typeof value !== "object") return value; if (typeof value !== "object") return value;
@@ -178,12 +193,15 @@ export function cloneBoundedForLog(value: unknown, depth = 0, key: string | null
// item and rewrite originalLength with the truncated length (25 instead of the true 800), so // item and rewrite originalLength with the truncated length (25 instead of the true 800), so
// the log would misreport how much was cut. Keep the original marker, re-bound only the tail. // the log would misreport how much was cut. Keep the original marker, re-bound only the tail.
if (isTruncatedArrayMarker(value[0])) { if (isTruncatedArrayMarker(value[0])) {
return [value[0], ...value.slice(1).map((item) => cloneBoundedForLog(item, depth + 1))]; return [
value[0],
...value.slice(1).map((item) => cloneBoundedForLogValue(item, depth + 1, null)),
];
} }
const exempt = key === "tools"; const exempt = key === "tools";
const shouldTruncate = !exempt && value.length > MAX_LOG_ARRAY_ITEMS; const shouldTruncate = !exempt && value.length > MAX_LOG_ARRAY_ITEMS;
const source = shouldTruncate ? value.slice(-MAX_LOG_ARRAY_ITEMS) : value; const source = shouldTruncate ? value.slice(-MAX_LOG_ARRAY_ITEMS) : value;
const mapped = source.map((item) => cloneBoundedForLog(item, depth + 1)); const mapped = source.map((item) => cloneBoundedForLogValue(item, depth + 1, null));
if (shouldTruncate) { if (shouldTruncate) {
return [ return [
{ {
@@ -206,7 +224,7 @@ export function cloneBoundedForLog(value: unknown, depth = 0, key: string | null
([k]) => !(carried > 0 && k === TRUNCATED_KEYS_MARKER) ([k]) => !(carried > 0 && k === TRUNCATED_KEYS_MARKER)
); );
for (const [k, item] of entries.slice(0, MAX_LOG_OBJECT_KEYS)) { for (const [k, item] of entries.slice(0, MAX_LOG_OBJECT_KEYS)) {
result[k] = cloneBoundedForLog(item, depth + 1, k); result[k] = cloneBoundedForLogValue(item, depth + 1, k);
} }
const dropped = Math.max(0, entries.length - MAX_LOG_OBJECT_KEYS) + carried; const dropped = Math.max(0, entries.length - MAX_LOG_OBJECT_KEYS) + carried;
if (dropped > 0) { if (dropped > 0) {
@@ -215,6 +233,17 @@ export function cloneBoundedForLog(value: unknown, depth = 0, key: string | null
return result; return result;
} }
export function cloneBoundedForLog(
value: unknown,
depth = 0,
key: string | null = null,
descriptionContext: VideoTranscriptLogContext = {}
): unknown {
const transcriptSafeValue =
depth === 0 ? omitVideoTranscriptForLog(value, descriptionContext) : value;
return cloneBoundedForLogValue(transcriptSafeValue, depth, key);
}
function appendBoundedChunk( function appendBoundedChunk(
chunks: string[], chunks: string[],
bytes: { value: number; truncated: boolean }, bytes: { value: number; truncated: boolean },
@@ -277,7 +306,7 @@ function compactPipelinePayloads(
continue; continue;
} }
result[key as keyof RequestPipelinePayloads] = value; (result as Record<string, unknown>)[key] = value;
} }
return hasOwnValues(result) ? result : null; return hasOwnValues(result) ? result : null;
@@ -298,6 +327,7 @@ function makeStreamChunkMethods(options: RequestLoggerOptions, captureChunks: bo
? Number(options.maxStreamChunkItems) ? Number(options.maxStreamChunkItems)
: DEFAULT_MAX_STREAM_CHUNK_ITEMS; : DEFAULT_MAX_STREAM_CHUNK_ITEMS;
let pendingPushed = false; let pendingPushed = false;
let videoTranscriptSensitive = options.videoTranscriptSensitive === true;
const push = () => { const push = () => {
if (pendingPushed) return; if (pendingPushed) return;
@@ -330,7 +360,7 @@ function makeStreamChunkMethods(options: RequestLoggerOptions, captureChunks: bo
}; };
const append = (arr: string[], bytes: { value: number; truncated: boolean }, chunk: string) => { const append = (arr: string[], bytes: { value: number; truncated: boolean }, chunk: string) => {
if (!captureChunks) return; if (!captureChunks || videoTranscriptSensitive) return;
push(); push();
const ts = new Date().toISOString().slice(11, 23); const ts = new Date().toISOString().slice(11, 23);
appendBoundedChunk(arr, bytes, `[${ts}] ${chunk}`, maxBytes, maxItems); appendBoundedChunk(arr, bytes, `[${ts}] ${chunk}`, maxBytes, maxItems);
@@ -348,6 +378,17 @@ function makeStreamChunkMethods(options: RequestLoggerOptions, captureChunks: bo
appendConvertedChunk(chunk: string) { appendConvertedChunk(chunk: string) {
append(streamChunks.client, streamChunkBytes.client, chunk); append(streamChunks.client, streamChunkBytes.client, chunk);
}, },
suppressVideoTranscriptChunks() {
videoTranscriptSensitive = true;
for (const chunks of Object.values(streamChunks)) chunks.splice(0, chunks.length);
for (const state of Object.values(streamChunkBytes)) {
state.value = 0;
state.truncated = false;
}
},
isVideoTranscriptSensitive() {
return videoTranscriptSensitive;
},
}; };
} }
@@ -362,26 +403,58 @@ export async function createRequestLogger(
// so that active requests always have real-time stream data available via // so that active requests always have real-time stream data available via
// the /api/logs/active endpoint. // the /api/logs/active endpoint.
const chunkMethods = makeStreamChunkMethods(options, captureStreamChunks); const chunkMethods = makeStreamChunkMethods(options, captureStreamChunks);
const descriptionLogContext: VideoTranscriptLogContext = {
trustedDescriptionFingerprints: options.videoTranscriptDescriptionFingerprints ?? [],
};
const suppressTranscriptChunksIfNeeded = (
descriptionContext: VideoTranscriptLogContext,
...values: unknown[]
): void => {
if (values.some((value) => containsVideoTranscriptForLog(value, descriptionContext))) {
chunkMethods.suppressVideoTranscriptChunks();
}
};
const cloneRequestBody = (
value: unknown,
descriptionContext: VideoTranscriptLogContext
): unknown => cloneBoundedForLog(value, 0, null, descriptionContext);
const cloneResponseBody = (value: unknown): unknown =>
chunkMethods.isVideoTranscriptSensitive()
? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER
: cloneBoundedForLog(value);
if (options.enabled === false) { if (options.enabled === false) {
let routeDecision: JsonRecord | null = null; let routeDecision: JsonRecord | null = null;
return { return {
sessionPath: null, sessionPath: null,
logClientRawRequest() {}, logClientRawRequest(_endpoint, body) {
suppressTranscriptChunksIfNeeded({}, body);
},
logRouteDecision(decision) { logRouteDecision(decision) {
routeDecision = cloneBoundedForLog(decision) as JsonRecord; routeDecision = cloneBoundedForLog(decision) as JsonRecord;
}, },
logOpenAIRequest() {}, logOpenAIRequest(body) {
logTargetRequest() {}, suppressTranscriptChunksIfNeeded(descriptionLogContext, body);
logProviderResponse() {}, },
logTargetRequest(_url, _headers, body) {
suppressTranscriptChunksIfNeeded(descriptionLogContext, body);
},
logProviderResponse(_status, _statusText, _headers, body) {
void body;
},
appendProviderChunk: chunkMethods.appendProviderChunk, appendProviderChunk: chunkMethods.appendProviderChunk,
appendOpenAIChunk: chunkMethods.appendOpenAIChunk, appendOpenAIChunk: chunkMethods.appendOpenAIChunk,
logConvertedResponse() {}, logConvertedResponse(body) {
void body;
},
appendConvertedChunk: chunkMethods.appendConvertedChunk, appendConvertedChunk: chunkMethods.appendConvertedChunk,
logError() {}, logError(_error, requestBody) {
suppressTranscriptChunksIfNeeded(descriptionLogContext, requestBody);
},
getPipelinePayloads() { getPipelinePayloads() {
return routeDecision ? { routeDecision } : null; return routeDecision ? { routeDecision } : null;
}, },
isVideoTranscriptSensitive: chunkMethods.isVideoTranscriptSensitive,
}; };
} }
@@ -393,11 +466,12 @@ export async function createRequestLogger(
sessionPath: null, sessionPath: null,
logClientRawRequest(endpoint, body, headers = {}) { logClientRawRequest(endpoint, body, headers = {}) {
suppressTranscriptChunksIfNeeded({}, body);
payloads.clientRawRequest = { payloads.clientRawRequest = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
endpoint, endpoint,
headers: maskSensitiveHeaders(headers), headers: maskSensitiveHeaders(headers),
body: cloneBoundedForLog(body), body: cloneRequestBody(body, {}),
}; };
}, },
@@ -406,18 +480,20 @@ export async function createRequestLogger(
}, },
logOpenAIRequest(body) { logOpenAIRequest(body) {
suppressTranscriptChunksIfNeeded(descriptionLogContext, body);
payloads.openaiRequest = { payloads.openaiRequest = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
body: cloneBoundedForLog(body), body: cloneRequestBody(body, descriptionLogContext),
}; };
}, },
logTargetRequest(url, headers, body) { logTargetRequest(url, headers, body) {
suppressTranscriptChunksIfNeeded(descriptionLogContext, body);
payloads.providerRequest = { payloads.providerRequest = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
url, url,
headers: maskSensitiveHeaders(headers), headers: maskSensitiveHeaders(headers),
body: cloneBoundedForLog(body), body: cloneRequestBody(body, descriptionLogContext),
}; };
}, },
@@ -427,7 +503,7 @@ export async function createRequestLogger(
status, status,
statusText, statusText,
headers: maskSensitiveHeaders(headers), headers: maskSensitiveHeaders(headers),
body: cloneBoundedForLog(body), body: cloneResponseBody(body),
}; };
}, },
@@ -436,21 +512,25 @@ export async function createRequestLogger(
logConvertedResponse(body) { logConvertedResponse(body) {
payloads.clientResponse = { payloads.clientResponse = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
body: cloneBoundedForLog(body), body: cloneResponseBody(body),
}; };
}, },
appendConvertedChunk: chunkMethods.appendConvertedChunk, appendConvertedChunk: chunkMethods.appendConvertedChunk,
logError(error, requestBody = null) { logError(error, requestBody = null) {
suppressTranscriptChunksIfNeeded(descriptionLogContext, requestBody);
payloads.error = { payloads.error = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)), error: chunkMethods.isVideoTranscriptSensitive()
requestBody: cloneBoundedForLog(requestBody), ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER
: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
requestBody: cloneRequestBody(requestBody, descriptionLogContext),
}; };
}, },
getPipelinePayloads() { getPipelinePayloads() {
return compactPipelinePayloads(payloads); return compactPipelinePayloads(payloads);
}, },
isVideoTranscriptSensitive: chunkMethods.isVideoTranscriptSensitive,
}; };
} }

View File

@@ -1,6 +1,7 @@
import { VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER } from "@/lib/guardrails/videoTranscriptLogRedaction";
import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb";
import { translateResponse, initState } from "../translator/index.ts"; import { translateResponse, initState } from "../translator/index.ts";
import { FORMATS } from "../translator/formats.ts"; import { FORMATS } from "../translator/formats.ts";
import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb";
import { import {
extractUsage, extractUsage,
hasValidUsage, hasValidUsage,
@@ -177,6 +178,8 @@ type StreamOptions = {
* codex-compatible `namespace` + `name` fields. * codex-compatible `namespace` + `name` fields.
*/ */
requestToolIdentityMap?: Map<string, { namespace: string; name: string }> | null; requestToolIdentityMap?: Map<string, { namespace: string; name: string }> | null;
/** Omit request-sensitive transcript text from retained stream diagnostics only. */
redactStreamDiagnosticsForLog?: boolean;
}; };
type TranslateState = ReturnType<typeof initState> & { type TranslateState = ReturnType<typeof initState> & {
@@ -654,7 +657,10 @@ export function createSSEStream(options: StreamOptions = {}) {
dropResponsesCommentary, dropResponsesCommentary,
customToolNames = new Set<string>(), customToolNames = new Set<string>(),
requestToolIdentityMap = null, requestToolIdentityMap = null,
redactStreamDiagnosticsForLog = false,
} = options; } = options;
const retainDiagnosticForLog = (value: unknown): unknown =>
redactStreamDiagnosticsForLog ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : value;
const signatureNamespace = connectionId; const signatureNamespace = connectionId;
// Request-body-size metric (for monitoring payload size distribution & correlation with TTFT). // Request-body-size metric (for monitoring payload size distribution & correlation with TTFT).
// The size is JSON-serialised byte count; stored as a performance mark detail so monitoring // The size is JSON-serialised byte count; stored as a performance mark detail so monitoring
@@ -1005,7 +1011,10 @@ export function createSSEStream(options: StreamOptions = {}) {
try { try {
failureHandled = onFailure({ status: 502, message: msg, code: "empty_response" }) === true; failureHandled = onFailure({ status: 502, message: msg, code: "empty_response" }) === true;
} catch (e) { } catch (e) {
console.debug(`[STREAM] onFailure callback error (empty_response):`, e); console.debug(
`[STREAM] onFailure callback error (empty_response):`,
retainDiagnosticForLog(e)
);
} }
} }
if (decrementPendingRequest && !failureHandled) { if (decrementPendingRequest && !failureHandled) {
@@ -1199,7 +1208,10 @@ export function createSSEStream(options: StreamOptions = {}) {
type: "timeout_error", type: "timeout_error",
}) === true; }) === true;
} catch (e) { } catch (e) {
console.debug(`[STREAM] onFailure callback error (idle_timeout):`, e); console.debug(
`[STREAM] onFailure callback error (idle_timeout):`,
retainDiagnosticForLog(e)
);
} }
} }
if (!failureHandled) { if (!failureHandled) {
@@ -1641,10 +1653,7 @@ export function createSSEStream(options: StreamOptions = {}) {
isResponsesCommentaryMessageItem isResponsesCommentaryMessageItem
).items ).items
: passthroughResponsesOutputItems; : passthroughResponsesOutputItems;
const backfilled = backfillResponsesCompletedOutput( const backfilled = backfillResponsesCompletedOutput(parsed, backfillCandidates);
parsed,
backfillCandidates
);
const usageNormalized = normalizeUsage(parsed); const usageNormalized = normalizeUsage(parsed);
if ( if (
stripped || stripped ||
@@ -2040,7 +2049,7 @@ export function createSSEStream(options: StreamOptions = {}) {
try { try {
failureHandled = onFailure(failurePayload) === true; failureHandled = onFailure(failurePayload) === true;
} catch (e) { } catch (e) {
console.debug(`[STREAM] onFailure callback error:`, e); console.debug(`[STREAM] onFailure callback error:`, retainDiagnosticForLog(e));
} }
} }
clearIdleTimer(); clearIdleTimer();
@@ -2624,7 +2633,10 @@ export function createSSEStream(options: StreamOptions = {}) {
), ),
}); });
} catch (e) { } catch (e) {
console.debug(`[STREAM] onComplete callback error (${model || "unknown"}):`, e); console.debug(
`[STREAM] onComplete callback error (${model || "unknown"}):`,
retainDiagnosticForLog(e)
);
} }
} else { } else {
clearPendingRequestFromStream(); clearPendingRequestFromStream();
@@ -2712,7 +2724,10 @@ export function createSSEStream(options: StreamOptions = {}) {
type: err.type, type: err.type,
}) === true; }) === true;
} catch (e) { } catch (e) {
console.debug(`[STREAM] onFailure callback error (${model || "unknown"}):`, e); console.debug(
`[STREAM] onFailure callback error (${model || "unknown"}):`,
retainDiagnosticForLog(e)
);
} }
} }
@@ -2740,7 +2755,7 @@ export function createSSEStream(options: StreamOptions = {}) {
} catch (e) { } catch (e) {
console.debug( console.debug(
`[STREAM] onComplete callback error in error path (${model || "unknown"}):`, `[STREAM] onComplete callback error in error path (${model || "unknown"}):`,
e retainDiagnosticForLog(e)
); );
} }
} }
@@ -2945,14 +2960,18 @@ export function createSSEStream(options: StreamOptions = {}) {
} catch (e) { } catch (e) {
console.debug( console.debug(
`[STREAM] onComplete callback error in flush (${model || "unknown"}):`, `[STREAM] onComplete callback error in flush (${model || "unknown"}):`,
e retainDiagnosticForLog(e)
); );
} }
} else { } else {
clearPendingRequestFromStream(); clearPendingRequestFromStream();
} }
} catch (error) { } catch (error) {
console.log(`[STREAM] Error in flush (${model || "unknown"}):`, error.message || error); const diagnostic = error instanceof Error ? error.message : error;
console.log(
`[STREAM] Error in flush (${model || "unknown"}):`,
retainDiagnosticForLog(diagnostic)
);
} }
}, },
cancel(reason) { cancel(reason) {
@@ -2982,7 +3001,8 @@ export function createSSETransformStreamWithLogger(
copilotCompatibleReasoning = false, copilotCompatibleReasoning = false,
suppressThinkClose = false, suppressThinkClose = false,
customToolNames: ReadonlySet<string> = new Set(), customToolNames: ReadonlySet<string> = new Set(),
requestToolIdentityMap: Map<string, { namespace: string; name: string }> | null = null requestToolIdentityMap: Map<string, { namespace: string; name: string }> | null = null,
redactStreamDiagnosticsForLog = false
) { ) {
return createSSEStream({ return createSSEStream({
mode: STREAM_MODE.TRANSLATE, mode: STREAM_MODE.TRANSLATE,
@@ -3001,6 +3021,7 @@ export function createSSETransformStreamWithLogger(
suppressThinkClose, suppressThinkClose,
customToolNames, customToolNames,
requestToolIdentityMap, requestToolIdentityMap,
redactStreamDiagnosticsForLog,
}); });
} }
@@ -3015,7 +3036,8 @@ export function createPassthroughStreamWithLogger(
apiKeyInfo: unknown = null, apiKeyInfo: unknown = null,
onFailure: ((payload: StreamFailurePayload) => boolean | void | Promise<void>) | null = null, onFailure: ((payload: StreamFailurePayload) => boolean | void | Promise<void>) | null = null,
clientResponseFormat: string | null = null, clientResponseFormat: string | null = null,
requestToolIdentityMap: Map<string, { namespace: string; name: string }> | null = null requestToolIdentityMap: Map<string, { namespace: string; name: string }> | null = null,
redactStreamDiagnosticsForLog = false
) { ) {
return createSSEStream({ return createSSEStream({
mode: STREAM_MODE.PASSTHROUGH, mode: STREAM_MODE.PASSTHROUGH,
@@ -3030,6 +3052,7 @@ export function createPassthroughStreamWithLogger(
onFailure, onFailure,
clientResponseFormat, clientResponseFormat,
requestToolIdentityMap, requestToolIdentityMap,
redactStreamDiagnosticsForLog,
}); });
} }

View File

@@ -92,6 +92,7 @@ export function finalizeStreamRequestLog({
status, status,
error, error,
errorCode, errorCode,
videoTranscriptSensitive,
onWarn, onWarn,
}: { }: {
pendingRequestId: string; pendingRequestId: string;
@@ -103,6 +104,7 @@ export function finalizeStreamRequestLog({
status: number; status: number;
error?: string | null; error?: string | null;
errorCode?: string | null; errorCode?: string | null;
videoTranscriptSensitive: boolean;
onWarn?: (error: unknown) => void; onWarn?: (error: unknown) => void;
}) { }) {
try { try {
@@ -112,6 +114,7 @@ export function finalizeStreamRequestLog({
status, status,
error: error || null, error: error || null,
errorCode: errorCode || null, errorCode: errorCode || null,
videoTranscriptSensitive,
}); });
if (!completedById) { if (!completedById) {
finalizeMostRecentPendingRequest(model, provider, connectionId, { finalizeMostRecentPendingRequest(model, provider, connectionId, {
@@ -120,6 +123,7 @@ export function finalizeStreamRequestLog({
status, status,
error: error || null, error: error || null,
errorCode: errorCode || null, errorCode: errorCode || null,
videoTranscriptSensitive,
}); });
} }
} catch (error) { } catch (error) {

View File

@@ -1,3 +1,4 @@
import { VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER } from "@/lib/guardrails/videoTranscriptLogRedaction";
import { trackPendingRequest } from "@/lib/usageDb"; import { trackPendingRequest } from "@/lib/usageDb";
import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts"; import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts";
import { FORMATS } from "../translator/formats.ts"; import { FORMATS } from "../translator/formats.ts";
@@ -39,6 +40,7 @@ type StreamControllerOptions = {
clientAbortSignal?: AbortSignal | null; clientAbortSignal?: AbortSignal | null;
allowCompletedToolHandoffGrace?: boolean; allowCompletedToolHandoffGrace?: boolean;
clientDisconnectGracePeriodMs?: number; clientDisconnectGracePeriodMs?: number;
redactStreamDiagnosticsForLog?: boolean;
}; };
type StreamController = ReturnType<typeof createStreamController>; type StreamController = ReturnType<typeof createStreamController>;
@@ -243,6 +245,7 @@ export function createStreamController({
clientAbortSignal, clientAbortSignal,
allowCompletedToolHandoffGrace = false, allowCompletedToolHandoffGrace = false,
clientDisconnectGracePeriodMs = 0, clientDisconnectGracePeriodMs = 0,
redactStreamDiagnosticsForLog = false,
}: StreamControllerOptions = {}) { }: StreamControllerOptions = {}) {
const abortController = new AbortController(); const abortController = new AbortController();
const startTime = Date.now(); const startTime = Date.now();
@@ -253,6 +256,9 @@ export function createStreamController({
let pendingRequestCleared = false; let pendingRequestCleared = false;
let cleanupClientAbortSignal: (() => void) | null = null; let cleanupClientAbortSignal: (() => void) | null = null;
const retainDiagnosticForLog = (value: unknown): unknown =>
redactStreamDiagnosticsForLog ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : value;
const logStream = (status) => { const logStream = (status) => {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
const p = provider?.toUpperCase() || "UNKNOWN"; const p = provider?.toUpperCase() || "UNKNOWN";
@@ -279,7 +285,7 @@ export function createStreamController({
} catch (e) { } catch (e) {
console.error( console.error(
`[${getTimeString()}] [streamHandler] trackPendingRequest decrement failed — counter may drift`, `[${getTimeString()}] [streamHandler] trackPendingRequest decrement failed — counter may drift`,
e retainDiagnosticForLog(e)
); );
} }
}; };
@@ -317,7 +323,7 @@ export function createStreamController({
disconnected = true; disconnected = true;
cleanupClientAbortListener(); cleanupClientAbortListener();
logStream(`disconnect: ${reason}`); logStream(`disconnect: ${String(retainDiagnosticForLog(reason))}`);
// Decrement pending request counter — the TransformStream flush() won't // Decrement pending request counter — the TransformStream flush() won't
// fire when the client aborts mid-stream, so we must clean up here. // fire when the client aborts mid-stream, so we must clean up here.
@@ -390,7 +396,7 @@ export function createStreamController({
duration: Date.now() - startTime, duration: Date.now() - startTime,
}) === true; }) === true;
} catch (e) { } catch (e) {
console.debug(`[STREAM-HANDLER] onError callback error:`, e); console.debug(`[STREAM-HANDLER] onError callback error:`, retainDiagnosticForLog(e));
} }
} }
@@ -406,7 +412,7 @@ export function createStreamController({
} }
if (error instanceof Error) { if (error instanceof Error) {
logStream(`error: ${error.message}`); logStream(`error: ${String(retainDiagnosticForLog(error.message))}`);
return; return;
} }
logStream("error: unknown"); logStream("error: unknown");
@@ -845,9 +851,11 @@ export function pipeWithDisconnect(
providerResponse: Response, providerResponse: Response,
transformStream: TransformStream<Uint8Array, Uint8Array>, transformStream: TransformStream<Uint8Array, Uint8Array>,
streamController: StreamController, streamController: StreamController,
opts: { stallTimeoutMs?: number } = {} opts: { redactStreamDiagnosticsForLog?: boolean; stallTimeoutMs?: number } = {}
) { ) {
const stallTimeoutMs = opts.stallTimeoutMs ?? DEFAULT_STREAM_STALL_TIMEOUT_MS; const stallTimeoutMs = opts.stallTimeoutMs ?? DEFAULT_STREAM_STALL_TIMEOUT_MS;
const retainDiagnosticForLog = (value: unknown): unknown =>
opts.redactStreamDiagnosticsForLog ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : value;
// Watchdog disabled — preserve legacy behavior verbatim. // Watchdog disabled — preserve legacy behavior verbatim.
if (!stallTimeoutMs || stallTimeoutMs <= 0) { if (!stallTimeoutMs || stallTimeoutMs <= 0) {
@@ -887,7 +895,10 @@ export function pipeWithDisconnect(
try { try {
streamController.handleError?.(stallError); streamController.handleError?.(stallError);
} catch (e) { } catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog handleError failed:`, e); console.debug(
`[STREAM-HANDLER] stall watchdog handleError failed:`,
retainDiagnosticForLog(e)
);
} }
// Error the pipeline so the downstream reader unblocks. createDisconnect- // Error the pipeline so the downstream reader unblocks. createDisconnect-
// AwareStream's catch block translates this into buildStreamErrorChunks // AwareStream's catch block translates this into buildStreamErrorChunks
@@ -895,13 +906,16 @@ export function pipeWithDisconnect(
try { try {
upstreamTapController?.error(stallError); upstreamTapController?.error(stallError);
} catch (e) { } catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog upstream tap error failed:`, e); console.debug(
`[STREAM-HANDLER] stall watchdog upstream tap error failed:`,
retainDiagnosticForLog(e)
);
} }
// Abort the underlying fetch so upstream releases the connection. // Abort the underlying fetch so upstream releases the connection.
try { try {
streamController.abort?.(); streamController.abort?.();
} catch (e) { } catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog abort failed:`, e); console.debug(`[STREAM-HANDLER] stall watchdog abort failed:`, retainDiagnosticForLog(e));
} }
}, stallTimeoutMs); }, stallTimeoutMs);
}; };

View File

@@ -474,6 +474,8 @@ export async function ensureStreamReadiness(
provider?: string | null; provider?: string | null;
model?: string | null; model?: string | null;
log?: StreamReadinessLogger | null; log?: StreamReadinessLogger | null;
/** Keep the diagnostic transient while retaining only its existence in logs. */
redactUpstreamDiagnosticForLog?: boolean;
} }
): Promise<StreamReadinessResult> { ): Promise<StreamReadinessResult> {
if (!response.body || options.timeoutMs <= 0) return { ok: true, response }; if (!response.body || options.timeoutMs <= 0) return { ok: true, response };
@@ -568,9 +570,13 @@ export async function ensureStreamReadiness(
const reason = upstreamDiagnostic const reason = upstreamDiagnostic
? `${classificationReason}: ${upstreamDiagnostic}` ? `${classificationReason}: ${upstreamDiagnostic}`
: classificationReason; : classificationReason;
const retainedReason =
upstreamDiagnostic && options.redactUpstreamDiagnosticForLog
? `${classificationReason}: [upstream diagnostic omitted]`
: reason;
options.log?.warn?.( options.log?.warn?.(
"STREAM", "STREAM",
`${reason} (${options.provider || "provider"}/${options.model || "unknown"})` `${retainedReason} (${options.provider || "provider"}/${options.model || "unknown"})`
); );
return { return {
ok: false, ok: false,

View File

@@ -19,6 +19,8 @@ export interface GuardrailContext {
sourceFormat?: string | null; sourceFormat?: string | null;
stream?: boolean; stream?: boolean;
targetFormat?: string | null; targetFormat?: string | null;
/** Server-owned bit resolved before any guardrail can replace the raw video carrier. */
videoTranscriptSensitive?: boolean;
} }
export interface GuardrailResult<TValue = unknown> { export interface GuardrailResult<TValue = unknown> {

View File

@@ -10,6 +10,7 @@ import { VisionBridgeGuardrail } from "./visionBridge";
import { AudioBridgeGuardrail } from "./audioBridge"; import { AudioBridgeGuardrail } from "./audioBridge";
import { VideoBridgeGuardrail } from "./videoBridge"; import { VideoBridgeGuardrail } from "./videoBridge";
import { CredentialMaskerGuardrail } from "./credentialMasker"; import { CredentialMaskerGuardrail } from "./credentialMasker";
import { VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER } from "./videoTranscriptLogRedaction";
/** /**
* `preCall`/`postCall` may legitimately return nothing — that is the documented * `preCall`/`postCall` may legitimately return nothing — that is the documented
@@ -69,8 +70,29 @@ function coerceDisabledGuardrails(value: unknown) {
.filter(Boolean); .filter(Boolean);
} }
function getGuardrailLogger(context: GuardrailContext) { function getGuardrailLogger(context: GuardrailContext): NonNullable<GuardrailContext["log"]> {
return context.log || console; const source = context.log || console;
if (context.videoTranscriptSensitive !== true) return source;
const emit = (
level: "debug" | "info" | "warn" | "error",
tag: string,
_message: string,
_meta?: Record<string, unknown>
) => {
const target = source[level];
if (typeof target !== "function") return;
target.call(source, tag, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER, {
detail: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
});
};
return {
debug: (tag, message, meta) => emit("debug", tag, message, meta),
info: (tag, message, meta) => emit("info", tag, message, meta),
warn: (tag, message, meta) => emit("warn", tag, message, meta),
error: (tag, message, meta) => emit("error", tag, message, meta),
};
} }
export function resolveDisabledGuardrails({ export function resolveDisabledGuardrails({
@@ -135,6 +157,8 @@ export class GuardrailRegistry {
async runPreCallHooks<TPayload = unknown>(payload: TPayload, context: GuardrailContext = {}) { async runPreCallHooks<TPayload = unknown>(payload: TPayload, context: GuardrailContext = {}) {
const logger = getGuardrailLogger(context); const logger = getGuardrailLogger(context);
const guardedContext =
context.videoTranscriptSensitive === true ? { ...context, log: logger } : context;
const results: GuardrailExecutionResult[] = []; const results: GuardrailExecutionResult[] = [];
let currentPayload = payload; let currentPayload = payload;
@@ -151,7 +175,7 @@ export class GuardrailRegistry {
} }
try { try {
const result = asGuardrailResult(await guardrail.preCall(currentPayload, context)); const result = asGuardrailResult(await guardrail.preCall(currentPayload, guardedContext));
const modified = result?.modifiedPayload !== undefined; const modified = result?.modifiedPayload !== undefined;
const meta = result?.meta || null; const meta = result?.meta || null;
@@ -211,6 +235,8 @@ export class GuardrailRegistry {
async runPostCallHooks<TResponse = unknown>(response: TResponse, context: GuardrailContext = {}) { async runPostCallHooks<TResponse = unknown>(response: TResponse, context: GuardrailContext = {}) {
const logger = getGuardrailLogger(context); const logger = getGuardrailLogger(context);
const guardedContext =
context.videoTranscriptSensitive === true ? { ...context, log: logger } : context;
const results: GuardrailExecutionResult[] = []; const results: GuardrailExecutionResult[] = [];
let currentResponse = response; let currentResponse = response;
@@ -227,7 +253,7 @@ export class GuardrailRegistry {
} }
try { try {
const result = asGuardrailResult(await guardrail.postCall(currentResponse, context)); const result = asGuardrailResult(await guardrail.postCall(currentResponse, guardedContext));
const modified = result?.modifiedResponse !== undefined; const modified = result?.modifiedResponse !== undefined;
const meta = result?.meta || null; const meta = result?.meta || null;

View File

@@ -18,6 +18,7 @@ import {
type BridgeCacheStore, type BridgeCacheStore,
} from "./modalityBridge/bridgeCache"; } from "./modalityBridge/bridgeCache";
import { recordBridgeUse } from "./modalityBridge/bridgeStats"; import { recordBridgeUse } from "./modalityBridge/bridgeStats";
import { fingerprintVideoTranscriptDescription } from "./videoTranscriptLogRedaction";
import { import {
composeVideoFramePrompt, composeVideoFramePrompt,
describeVideoPart as defaultDescribeVideoPart, describeVideoPart as defaultDescribeVideoPart,
@@ -714,6 +715,16 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
const videosProcessed = attemptedParts.length - failures; const videosProcessed = attemptedParts.length - failures;
const videosReplaced = descriptions.filter((description) => description !== null).length; const videosReplaced = descriptions.filter((description) => description !== null).length;
if (videosReplaced === 0) return { block: false }; if (videosReplaced === 0) return { block: false };
const videoTranscriptDescriptionFingerprints = [
...new Set(
descriptions
.filter(
(description): description is string =>
typeof description === "string" && description.includes("transcript[source=")
)
.map(fingerprintVideoTranscriptDescription)
),
].sort();
return { return {
block: false, block: false,
@@ -731,6 +742,9 @@ export class VideoBridgeGuardrail extends BaseGuardrail {
focusWindowsApplied, focusWindowsApplied,
focusHintsApplied, focusHintsApplied,
transcriptCuesApplied, transcriptCuesApplied,
...(videoTranscriptDescriptionFingerprints.length > 0
? { videoTranscriptDescriptionFingerprints }
: {}),
contactSheetsUsed, contactSheetsUsed,
audioFusionRuns, audioFusionRuns,
audioFusionPartials, audioFusionPartials,

View File

@@ -0,0 +1,512 @@
import { createHash } from "node:crypto";
export const VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER = "[omitted: video transcript]";
export const VIDEO_TRANSCRIPT_REDACTION_PIPELINE_KEY = "_omnirouteVideoTranscriptRedacted" as const;
const VIDEO_TRANSCRIPT_PAYLOAD_KEYS = new Set(["audioTranscript", "transcript"]);
const VIDEO_TRANSCRIPT_CARRIER_KEYS = new Set(["input_video", "source", "url", "video_url"]);
const VIDEO_BRIDGE_DESCRIPTION_PREFIX = "[Video description:";
// Security bound for the retained copy, deliberately above the default downstream log depth (20).
// Crossing it is unknown-as-sensitive: callers must discard the partial copy instead of leaking it.
const MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_DEPTH = 32;
// Aggregate bound across the whole retained graph, not a per-object width limit. The partial copy
// is discarded on overflow so attacker-controlled breadth cannot produce a large unsafe artifact.
const MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_ENTRIES = 10_000;
const VIDEO_TRANSCRIPT_CUE_PREFIX = "transcript[source=";
const VIDEO_TRANSCRIPT_CUE_SOURCES = ["audio-bridge", "client", "embedded"] as const;
const MAX_VIDEO_TRANSCRIPT_CUE_METADATA_CODE_UNITS = 1024;
const VIDEO_TRANSCRIPT_DESCRIPTION_FINGERPRINT_RE = /^sha256:([a-f0-9]{64}):(\d{1,8})$/;
const MAX_TRUSTED_DESCRIPTION_CODE_UNITS = 16 * 1024 * 1024;
const MAX_TRUSTED_DESCRIPTION_IDENTITIES = 64;
const MAX_TRUSTED_DESCRIPTION_HASH_CODE_UNITS = 64 * 1024 * 1024;
const MAX_TRUSTED_DESCRIPTION_PREFIX_OCCURRENCES = 128;
const MAX_TRUSTED_DESCRIPTION_CANDIDATE_HASHES = 512;
type JsonRecord = Record<string, unknown>;
export interface VideoTranscriptLogContext {
/** SHA-256 identities emitted only for descriptions produced by the Video Bridge guardrail. */
trustedDescriptionFingerprints?: readonly string[];
}
export interface VideoTranscriptLogSensitivityInput {
/** Original client body, before any guardrail is allowed to replace or remove media parts. */
rawRequestBody?: unknown;
/** Current request body after guardrail processing. */
processedBody?: unknown;
/** Validated identities emitted by a successful Video Bridge rewrite. */
trustedDescriptionFingerprints?: readonly string[];
}
export function fingerprintVideoTranscriptDescription(value: string): string {
return `sha256:${createHash("sha256").update(value).digest("hex")}:${value.length}`;
}
/** Keep raw provider text transient while replacing every retained/logged copy. */
export function redactVideoTranscriptSensitiveText(value: string, sensitive: boolean): string {
return sensitive ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : value;
}
type WalkFrame = {
directCarrier: boolean;
depth: number;
value: object;
};
type CloneFrame = WalkFrame & {
target: JsonRecord | unknown[];
};
type CloneMemo = {
carrier?: JsonRecord | unknown[];
ordinary?: JsonRecord | unknown[];
};
type TrustedDescriptionIdentity = {
fingerprint: string;
length: number;
};
type TrustedDescriptionRange = {
end: number;
start: number;
};
type VideoTranscriptCuePrefix = { kind: "valid"; textStart: number } | { kind: "malformed" };
function parseTrustedDescriptionIdentity(value: unknown): TrustedDescriptionIdentity | null {
if (typeof value !== "string") return null;
const match = VIDEO_TRANSCRIPT_DESCRIPTION_FINGERPRINT_RE.exec(value);
if (!match) return null;
const length = Number(match[2]);
if (!Number.isSafeInteger(length) || length <= 0 || length > MAX_TRUSTED_DESCRIPTION_CODE_UNITS) {
return null;
}
return { fingerprint: value, length };
}
function isJsonRecord(value: unknown): value is JsonRecord {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function isWhitespaceCodeUnit(value: string, index: number): boolean {
const codeUnit = value[index];
return codeUnit !== undefined && codeUnit.trim().length === 0;
}
function findNextVideoTranscriptCuePrefix(
value: string,
fromIndex: number,
end: number
): VideoTranscriptCuePrefix | null {
const start = value.indexOf(VIDEO_TRANSCRIPT_CUE_PREFIX, fromIndex);
if (start < 0 || start >= end) return null;
const sourceStart = start + VIDEO_TRANSCRIPT_CUE_PREFIX.length;
const source = VIDEO_TRANSCRIPT_CUE_SOURCES.find((candidate) =>
value.startsWith(candidate, sourceStart)
);
if (!source) return { kind: "malformed" };
const metadataStart = sourceStart + source.length;
const metadataLimit = Math.min(
end,
metadataStart + MAX_VIDEO_TRANSCRIPT_CUE_METADATA_CODE_UNITS + 1
);
let metadataEnd = -1;
for (let cursor = metadataStart; cursor < metadataLimit; cursor += 1) {
if (value[cursor] === "\r" || value[cursor] === "\n") break;
if (value[cursor] === "]") {
metadataEnd = cursor;
break;
}
}
if (metadataEnd < metadataStart) return { kind: "malformed" };
let cursor = metadataEnd + 1;
const whitespaceStart = cursor;
while (cursor < end && isWhitespaceCodeUnit(value, cursor)) cursor += 1;
if (
cursor === whitespaceStart ||
cursor + "text=".length > end ||
!value.startsWith("text=", cursor)
) {
return { kind: "malformed" };
}
return { kind: "valid", textStart: cursor + "text=".length };
}
function findQuotedStringEnd(value: string, quoteStart: number, end: number): number | null {
if (value[quoteStart] !== '"') return null;
let cursor = quoteStart + 1;
while (cursor < end) {
if (value[cursor] === '"') return cursor + 1;
if (value[cursor] === "\\") {
cursor += 2;
} else {
cursor += 1;
}
}
return null;
}
function redactSerializedVideoTranscriptCues(value: string): string {
const pieces: string[] = [];
let cursor = 0;
let searchFrom = 0;
while (searchFrom < value.length) {
const cue = findNextVideoTranscriptCuePrefix(value, searchFrom, value.length);
if (!cue) break;
if (cue.kind === "malformed") return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
const quotedEnd = findQuotedStringEnd(value, cue.textStart, value.length);
if (quotedEnd === null) return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
pieces.push(
value.slice(cursor, cue.textStart),
JSON.stringify(VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER)
);
cursor = quotedEnd;
searchFrom = quotedEnd;
}
// This parser is called only for a description whose exact fingerprint was emitted by the
// Video Bridge after applying at least one transcript cue. No parseable cue therefore means
// malformed trusted data: omit the whole retained range instead of returning it verbatim.
if (pieces.length === 0) return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
pieces.push(value.slice(cursor));
return pieces.join("");
}
/** Accept only identities emitted by a successful server-side Video Bridge rewrite. */
export function extractVideoTranscriptDescriptionFingerprints(results: unknown): string[] {
if (!Array.isArray(results)) return [];
const fingerprints = new Set<string>();
for (const result of results) {
if (!isJsonRecord(result) || result.guardrail !== "video-bridge" || result.modified !== true) {
continue;
}
const meta = isJsonRecord(result.meta) ? result.meta : null;
if (
!meta ||
typeof meta.transcriptCuesApplied !== "number" ||
meta.transcriptCuesApplied <= 0 ||
!Array.isArray(meta.videoTranscriptDescriptionFingerprints)
) {
continue;
}
for (const fingerprint of meta.videoTranscriptDescriptionFingerprints) {
const identity = parseTrustedDescriptionIdentity(fingerprint);
if (identity) fingerprints.add(identity.fingerprint);
}
}
return [...fingerprints].sort();
}
function isRecognizedVideoPart(record: JsonRecord): boolean {
const type = typeof record.type === "string" ? record.type : undefined;
// Explicit modality tags remain security carriers even when the media URL/data is malformed.
// Validation can reject them later, but malformed input must never bypass retained-log policy.
if (
type === "input_video" ||
type === "video" ||
type === "video_source" ||
type === "video_url"
) {
return true;
}
if (Object.hasOwn(record, "input_video") || Object.hasOwn(record, "video_url")) return true;
const source = isJsonRecord(record.source) ? record.source : undefined;
if (!source) return false;
const videoMediaType =
typeof source.media_type === "string" && source.media_type.toLowerCase().startsWith("video/");
return videoMediaType;
}
function findTrustedDescriptionRanges(
value: string,
context: VideoTranscriptLogContext
): TrustedDescriptionRange[] | null {
if (!value.includes(VIDEO_BRIDGE_DESCRIPTION_PREFIX)) return [];
const identities = context.trustedDescriptionFingerprints
?.map(parseTrustedDescriptionIdentity)
.filter((identity): identity is TrustedDescriptionIdentity => identity !== null);
if (!identities || identities.length === 0) return [];
// A successful request can only produce a bounded number of video descriptions.
// If internal metadata violates that contract, omit the whole retained string
// rather than risk leaking a cue or performing attacker-amplified hashing.
if (identities.length > MAX_TRUSTED_DESCRIPTION_IDENTITIES) return null;
const ranges: TrustedDescriptionRange[] = [];
let candidateHashes = 0;
let hashBudget = 0;
let prefixOccurrences = 0;
let start = value.indexOf(VIDEO_BRIDGE_DESCRIPTION_PREFIX);
while (start >= 0) {
prefixOccurrences += 1;
// Bound synchronous work independently of byte length: many tiny forged
// prefixes would otherwise amplify hashing and block the event loop. A
// retained copy fails closed while the live request remains untouched.
if (prefixOccurrences > MAX_TRUSTED_DESCRIPTION_PREFIX_OCCURRENCES) return null;
for (const identity of identities) {
const end = start + identity.length;
if (end > value.length) continue;
candidateHashes += 1;
if (candidateHashes > MAX_TRUSTED_DESCRIPTION_CANDIDATE_HASHES) return null;
hashBudget += identity.length;
if (hashBudget > MAX_TRUSTED_DESCRIPTION_HASH_CODE_UNITS) return null;
const candidate = value.slice(start, end);
if (fingerprintVideoTranscriptDescription(candidate) === identity.fingerprint) {
ranges.push({ end, start });
}
}
start = value.indexOf(VIDEO_BRIDGE_DESCRIPTION_PREFIX, start + 1);
}
ranges.sort((left, right) => left.start - right.start || left.end - right.end);
const merged: TrustedDescriptionRange[] = [];
for (const range of ranges) {
const previous = merged.at(-1);
if (previous && range.start < previous.end) {
previous.end = Math.max(previous.end, range.end);
} else {
merged.push({ ...range });
}
}
return merged;
}
export function omitVideoTranscriptFromLogString(
value: string,
context: VideoTranscriptLogContext = {}
): string {
const ranges = findTrustedDescriptionRanges(value, context);
if (ranges === null) return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
if (ranges.length === 0) return value;
let result = "";
let cursor = 0;
for (const range of ranges) {
result += value.slice(cursor, range.start);
result += redactSerializedVideoTranscriptCues(value.slice(range.start, range.end));
cursor = range.end;
}
return result + value.slice(cursor);
}
/**
* Remove complete trusted Video Bridge descriptions before Memory extraction.
* Logs may retain the non-transcript portion of a description, but durable
* user facts must come only from caller text, never from media-derived prose.
* Exact server-issued fingerprints let adjacent caller text survive even when
* both appear in the same string. Unknown/over-budget input fails closed.
*/
export function omitVideoTranscriptDerivedTextForMemory(
value: string,
context: VideoTranscriptLogContext = {}
): string {
try {
const ranges = findTrustedDescriptionRanges(value, context);
if (ranges === null) return "";
if (ranges.length === 0) return value;
let result = "";
let cursor = 0;
for (const range of ranges) {
result += value.slice(cursor, range.start);
cursor = range.end;
}
return result + value.slice(cursor);
} catch {
return "";
}
}
function fieldIsTranscript(key: string, carrier: boolean): boolean {
return carrier && VIDEO_TRANSCRIPT_PAYLOAD_KEYS.has(key);
}
function childIsDirectCarrier(key: string, carrierParent: boolean, value: unknown): boolean {
return carrierParent && VIDEO_TRANSCRIPT_CARRIER_KEYS.has(key) && isJsonRecord(value);
}
function* enumerableEntries(value: object): Generator<readonly [string, unknown]> {
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index += 1) {
yield [String(index), value[index]] as const;
}
return;
}
const record = value as JsonRecord;
for (const key in record) {
if (Object.hasOwn(record, key)) yield [key, record[key]] as const;
}
}
function walkContains(root: unknown, context: VideoTranscriptLogContext): boolean {
const stack: Array<WalkFrame | { directCarrier: boolean; depth: number; value: string }> = [];
if (typeof root === "function") return true;
if (typeof root === "string") stack.push({ directCarrier: false, depth: 0, value: root });
else if (root && typeof root === "object" && !ArrayBuffer.isView(root)) {
stack.push({ directCarrier: false, depth: 0, value: root });
}
const seen = new WeakMap<object, number>();
let scannedEntries = 0;
while (stack.length > 0) {
const frame = stack.pop()!;
if (frame.depth > MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_DEPTH) return true;
if (typeof frame.value === "string") {
const stringValue = frame.value;
const ranges = findTrustedDescriptionRanges(stringValue, context);
if (ranges === null) return true;
if (
ranges.some(
(range) => findNextVideoTranscriptCuePrefix(stringValue, range.start, range.end) !== null
)
) {
return true;
}
continue;
}
const record = Array.isArray(frame.value) ? null : (frame.value as JsonRecord);
const recognized = record ? isRecognizedVideoPart(record) : false;
const carrier = frame.directCarrier || recognized;
const seenBit = carrier ? 2 : 1;
const previousBits = seen.get(frame.value) ?? 0;
if ((previousBits & seenBit) !== 0) continue;
seen.set(frame.value, previousBits | seenBit);
for (const [key, value] of enumerableEntries(frame.value)) {
scannedEntries += 1;
if (scannedEntries > MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_ENTRIES) return true;
if (fieldIsTranscript(key, carrier)) {
return true;
}
// Functions are not valid JSON request values. In particular, an enumerable toJSON
// function could replace the retained clone with attacker-selected transcript content.
if (typeof value === "function") return true;
if (typeof value === "string") {
stack.push({
directCarrier: fieldIsTranscript(key, carrier),
depth: frame.depth + 1,
value,
});
} else if (value && typeof value === "object" && !ArrayBuffer.isView(value)) {
stack.push({
directCarrier: childIsDirectCarrier(key, carrier, value),
depth: frame.depth + 1,
value,
});
}
}
}
return false;
}
/** Detect transcript data only in recognized video carriers or a trusted bridge description. */
export function containsVideoTranscriptForLog(
value: unknown,
context: VideoTranscriptLogContext = {}
): boolean {
try {
return walkContains(value, context);
} catch {
return true;
}
}
/** Resolve sensitivity once from both sides of the guardrail boundary. */
export function resolveVideoTranscriptLogSensitivity({
rawRequestBody,
processedBody,
trustedDescriptionFingerprints = [],
}: VideoTranscriptLogSensitivityInput): boolean {
const hasTrustedDescription = trustedDescriptionFingerprints.some(
(fingerprint) => parseTrustedDescriptionIdentity(fingerprint) !== null
);
return (
hasTrustedDescription ||
containsVideoTranscriptForLog(rawRequestBody) ||
containsVideoTranscriptForLog(processedBody)
);
}
function prepareClone(
value: unknown,
depth: number,
directCarrier: boolean,
context: VideoTranscriptLogContext,
memo: WeakMap<object, CloneMemo>
): { frame?: CloneFrame; limitExceeded?: boolean; value: unknown } {
if (depth > MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_DEPTH) {
return { limitExceeded: true, value: VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER };
}
if (typeof value === "string") {
return { value: omitVideoTranscriptFromLogString(value, context) };
}
if (typeof value === "function") return { value: "[Function]" };
if (!value || typeof value !== "object") return { value };
if (ArrayBuffer.isView(value)) return { value: `[binary ${value.byteLength} bytes]` };
const record = Array.isArray(value) ? null : (value as JsonRecord);
const carrier = directCarrier || Boolean(record && isRecognizedVideoPart(record));
const memoKey = carrier ? "carrier" : "ordinary";
const previous = memo.get(value)?.[memoKey];
if (previous) return { value: "[Circular]" };
const target: JsonRecord | unknown[] = Array.isArray(value) ? [] : {};
const entry = memo.get(value) ?? {};
entry[memoKey] = target;
memo.set(value, entry);
return { frame: { directCarrier: carrier, depth, target, value }, value: target };
}
function omitVideoTranscriptForLogUnsafe(
payload: unknown,
context: VideoTranscriptLogContext
): unknown {
const memo = new WeakMap<object, CloneMemo>();
const prepared = prepareClone(payload, 0, false, context, memo);
if (prepared.limitExceeded) return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
if (!prepared.frame) return prepared.value;
const stack = [prepared.frame];
let clonedEntries = 0;
while (stack.length > 0) {
const frame = stack.pop()!;
for (const [key, value] of enumerableEntries(frame.value)) {
clonedEntries += 1;
if (clonedEntries > MAX_VIDEO_TRANSCRIPT_LOG_SECURITY_ENTRIES) {
return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
}
const targetKey: string | number = Array.isArray(frame.target) ? Number(key) : key;
if (fieldIsTranscript(key, frame.directCarrier)) {
(frame.target as JsonRecord)[targetKey] = VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
continue;
}
const child = prepareClone(
value,
frame.depth + 1,
childIsDirectCarrier(key, frame.directCarrier, value),
context,
memo
);
if (child.limitExceeded) return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
(frame.target as JsonRecord)[targetKey] = child.value;
if (child.frame) stack.push(child.frame);
}
}
return prepared.value;
}
/** Remove Video Bridge source fields and trusted generated segments from a bounded log copy. */
export function omitVideoTranscriptForLog(
payload: unknown,
context: VideoTranscriptLogContext = {}
): unknown {
try {
return omitVideoTranscriptForLogUnsafe(payload, context);
} catch {
return VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER;
}
}

View File

@@ -1,4 +1,10 @@
import { sanitizePII } from "./piiSanitizer"; import { sanitizePII } from "./piiSanitizer";
import { omitVideoTranscriptForLog } from "./guardrails/videoTranscriptLogRedaction";
export {
omitVideoTranscriptForLog,
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
} from "./guardrails/videoTranscriptLogRedaction";
const SENSITIVE_KEYS = new Set([ const SENSITIVE_KEYS = new Set([
"api_key", "api_key",
@@ -162,7 +168,8 @@ export function sanitizePayloadPII(payload: unknown): unknown {
export function protectPayloadForLog(payload: unknown): unknown { export function protectPayloadForLog(payload: unknown): unknown {
if (payload === null || payload === undefined) return null; if (payload === null || payload === undefined) return null;
const normalized = normalizePayloadForLog(payload); const normalized = normalizePayloadForLog(payload);
const reasoningOmitted = omitEncryptedReasoningForLog(normalized); const transcriptOmitted = omitVideoTranscriptForLog(normalized);
const reasoningOmitted = omitEncryptedReasoningForLog(transcriptOmitted);
const piiSanitized = sanitizePayloadPII(reasoningOmitted); const piiSanitized = sanitizePayloadPII(reasoningOmitted);
return redactPayload(piiSanitized); return redactPayload(piiSanitized);
} }

View File

@@ -8,9 +8,25 @@
*/ */
import { logger } from "../../../open-sse/utils/logger.ts"; import { logger } from "../../../open-sse/utils/logger.ts";
import { redactVideoTranscriptSensitiveText } from "../guardrails/videoTranscriptLogRedaction.ts";
const log = logger("PLUGIN_HOOKS"); const log = logger("PLUGIN_HOOKS");
function isVideoTranscriptSensitiveContext(value: unknown): boolean {
return Boolean(
value &&
typeof value === "object" &&
(value as Record<string, unknown>).videoTranscriptSensitive === true
);
}
function retainedHookError(error: unknown, sensitive: boolean): string {
return redactVideoTranscriptSensitiveText(
error instanceof Error ? error.message : String(error),
sensitive
);
}
// ── Types ── // ── Types ──
export type BlockingHookResult = { export type BlockingHookResult = {
@@ -143,6 +159,7 @@ export function unregisterHook(event: string, pluginName: string): void {
export async function emitHook(event: string, payload: unknown): Promise<void> { export async function emitHook(event: string, payload: unknown): Promise<void> {
const list = hooks.get(event); const list = hooks.get(event);
if (!list || list.length === 0) return; if (!list || list.length === 0) return;
const videoTranscriptSensitive = isVideoTranscriptSensitiveContext(payload);
for (const reg of list) { for (const reg of list) {
if (isRateLimited(reg.pluginName)) { if (isRateLimited(reg.pluginName)) {
@@ -152,11 +169,10 @@ export async function emitHook(event: string, payload: unknown): Promise<void> {
try { try {
await reg.handler(payload); await reg.handler(payload);
} catch (err: unknown) { } catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
log.error("hook.handler_error", { log.error("hook.handler_error", {
event, event,
pluginName: reg.pluginName, pluginName: reg.pluginName,
error: message, error: retainedHookError(err, videoTranscriptSensitive),
}); });
} }
} }
@@ -178,6 +194,7 @@ export async function emitHookBlocking(
}> { }> {
const list = hooks.get(event) || []; const list = hooks.get(event) || [];
const ctx = (payload || {}) as Record<string, unknown>; const ctx = (payload || {}) as Record<string, unknown>;
const videoTranscriptSensitive = isVideoTranscriptSensitiveContext(ctx);
let mergedBody: unknown = ctx.body; let mergedBody: unknown = ctx.body;
let mergedMetadata: Record<string, unknown> = (ctx.metadata as Record<string, unknown>) || {}; let mergedMetadata: Record<string, unknown> = (ctx.metadata as Record<string, unknown>) || {};
@@ -209,11 +226,10 @@ export async function emitHookBlocking(
} }
} }
} catch (err: unknown) { } catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
log.error("hook.blocking_handler_error", { log.error("hook.blocking_handler_error", {
event, event,
pluginName: reg.pluginName, pluginName: reg.pluginName,
error: message, error: retainedHookError(err, videoTranscriptSensitive),
}); });
} }
} }
@@ -234,6 +250,8 @@ export interface PluginContext {
* client (trace ids, correlation ids, session markers). */ * client (trace ids, correlation ids, session markers). */
headers?: Record<string, string | string[] | undefined>; headers?: Record<string, string | string[] | undefined>;
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
/** Server-owned retention bit; plugins still receive the live body as trusted processors. */
videoTranscriptSensitive?: boolean;
} }
export interface PluginResult { export interface PluginResult {
@@ -333,6 +351,7 @@ export async function runOnRequest(ctx: PluginContext): Promise<PluginResult> {
export async function runOnResponse(ctx: PluginContext, response: unknown): Promise<unknown> { export async function runOnResponse(ctx: PluginContext, response: unknown): Promise<unknown> {
let currentResponse = response; let currentResponse = response;
const list = hooks.get("onResponse") || []; const list = hooks.get("onResponse") || [];
const videoTranscriptSensitive = ctx.videoTranscriptSensitive === true;
for (const reg of list) { for (const reg of list) {
try { try {
const result = await reg.handler({ ...ctx, response: currentResponse }); const result = await reg.handler({ ...ctx, response: currentResponse });
@@ -345,8 +364,10 @@ export async function runOnResponse(ctx: PluginContext, response: unknown): Prom
currentResponse = (result as { response: unknown }).response; currentResponse = (result as { response: unknown }).response;
} }
} catch (err: unknown) { } catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err); log.error("hook.response_handler_error", {
log.error("hook.response_handler_error", { pluginName: reg.pluginName, error: message }); pluginName: reg.pluginName,
error: retainedHookError(err, videoTranscriptSensitive),
});
} }
} }
return currentResponse; return currentResponse;

View File

@@ -11,16 +11,32 @@ export type PendingRequestScope = {
model: string; model: string;
provider: string; provider: string;
connectionId: string | null; connectionId: string | null;
videoTranscriptSensitive?: boolean;
videoTranscriptDescriptionFingerprints?: readonly string[];
}; };
export function updatePendingScope(scope: PendingRequestScope, metadata: PendingRequestMetadata) { export function updatePendingScope(scope: PendingRequestScope, metadata: PendingRequestMetadata) {
if (!updatePendingRequestById(scope.id || null, metadata)) { const protectedMetadata = {
updatePendingRequest(scope.model, scope.provider, scope.connectionId, metadata); ...metadata,
...(scope.videoTranscriptSensitive ? { videoTranscriptSensitive: true } : {}),
...(scope.videoTranscriptDescriptionFingerprints?.length
? { videoTranscriptDescriptionFingerprints: scope.videoTranscriptDescriptionFingerprints }
: {}),
};
if (!updatePendingRequestById(scope.id || null, protectedMetadata)) {
updatePendingRequest(scope.model, scope.provider, scope.connectionId, protectedMetadata);
} }
} }
export function finalizePendingScope(scope: PendingRequestScope, metadata: PendingRequestMetadata) { export function finalizePendingScope(scope: PendingRequestScope, metadata: PendingRequestMetadata) {
if (!finalizePendingRequestById(scope.id, metadata)) { const protectedMetadata = {
finalizePendingRequest(scope.model, scope.provider, scope.connectionId, metadata); ...metadata,
...(scope.videoTranscriptSensitive ? { videoTranscriptSensitive: true } : {}),
...(scope.videoTranscriptDescriptionFingerprints?.length
? { videoTranscriptDescriptionFingerprints: scope.videoTranscriptDescriptionFingerprints }
: {}),
};
if (!finalizePendingRequestById(scope.id, protectedMetadata)) {
finalizePendingRequest(scope.model, scope.provider, scope.connectionId, protectedMetadata);
} }
} }

View File

@@ -8,6 +8,11 @@
*/ */
import { getDbInstance } from "../db/core"; import { getDbInstance } from "../db/core";
import {
omitVideoTranscriptForLog,
type VideoTranscriptLogContext,
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
} from "../guardrails/videoTranscriptLogRedaction";
import { protectPayloadForLog } from "../logPayloads"; import { protectPayloadForLog } from "../logPayloads";
import { import {
resolveOrphanedUsageAccountIdentity, resolveOrphanedUsageAccountIdentity,
@@ -55,6 +60,10 @@ export type PendingRequestMetadata = {
stageUpdatedAt?: number | null; stageUpdatedAt?: number | null;
correlationId?: string | null; correlationId?: string | null;
sessionTag?: string | null; sessionTag?: string | null;
/** Trusted request state; consumed during normalization and never retained. */
videoTranscriptSensitive?: boolean;
/** Exact SHA-256 identities of generated Video descriptions; never retained. */
videoTranscriptDescriptionFingerprints?: readonly string[];
}; };
export type PendingRequestDetail = { export type PendingRequestDetail = {
id: string; id: string;
@@ -88,6 +97,14 @@ function normalizePendingMetadata(metadata?: PendingRequestMetadata): PendingReq
if (!metadata) return {}; if (!metadata) return {};
const normalized: PendingRequestMetadata = {}; const normalized: PendingRequestMetadata = {};
const transcriptSensitive = metadata.videoTranscriptSensitive === true;
const descriptionContext: VideoTranscriptLogContext = {
trustedDescriptionFingerprints: metadata.videoTranscriptDescriptionFingerprints ?? [],
};
const protectRequest = (value: unknown, logContext: VideoTranscriptLogContext = {}): unknown =>
truncatePendingPreview(protectPayloadForLog(omitVideoTranscriptForLog(value, logContext)));
const protectResponse = (value: unknown): unknown =>
transcriptSensitive ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : protectRequest(value);
if (metadata.clientEndpoint !== undefined) { if (metadata.clientEndpoint !== undefined) {
normalized.clientEndpoint = toStringOrNull(metadata.clientEndpoint) || null; normalized.clientEndpoint = toStringOrNull(metadata.clientEndpoint) || null;
@@ -106,29 +123,25 @@ function normalizePendingMetadata(metadata?: PendingRequestMetadata): PendingReq
: null; : null;
} }
if (metadata.clientRequest !== undefined) { if (metadata.clientRequest !== undefined) {
normalized.clientRequest = truncatePendingPreview(protectPayloadForLog(metadata.clientRequest)); normalized.clientRequest = protectRequest(metadata.clientRequest, {});
} }
if (metadata.providerRequest !== undefined) { if (metadata.providerRequest !== undefined) {
normalized.providerRequest = truncatePendingPreview( normalized.providerRequest = protectRequest(metadata.providerRequest, descriptionContext);
protectPayloadForLog(metadata.providerRequest)
);
} }
if (metadata.providerResponse !== undefined) { if (metadata.providerResponse !== undefined) {
normalized.providerResponse = truncatePendingPreview( normalized.providerResponse = protectResponse(metadata.providerResponse);
protectPayloadForLog(metadata.providerResponse)
);
} }
if (metadata.clientResponse !== undefined) { if (metadata.clientResponse !== undefined) {
normalized.clientResponse = truncatePendingPreview( normalized.clientResponse = protectResponse(metadata.clientResponse);
protectPayloadForLog(metadata.clientResponse)
);
} }
if (metadata.status !== undefined) { if (metadata.status !== undefined) {
const status = Number(metadata.status); const status = Number(metadata.status);
normalized.status = Number.isFinite(status) ? status : null; normalized.status = Number.isFinite(status) ? status : null;
} }
if (metadata.error !== undefined) { if (metadata.error !== undefined) {
normalized.error = toStringOrNull(metadata.error) || null; normalized.error = transcriptSensitive
? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER
: toStringOrNull(metadata.error) || null;
} }
if (metadata.errorCode !== undefined) { if (metadata.errorCode !== undefined) {
normalized.errorCode = toStringOrNull(metadata.errorCode) || null; normalized.errorCode = toStringOrNull(metadata.errorCode) || null;

View File

@@ -6,6 +6,11 @@ export { buildClientRawRequest, resolveDispatchClientRawRequest };
import { normalizeReasoningRequest } from "@/shared/reasoning/effortStandardization"; import { normalizeReasoningRequest } from "@/shared/reasoning/effortStandardization";
import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs"; import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs";
import { resolvePreviousResponseState } from "@/lib/db/responsesContinuationStore"; import { resolvePreviousResponseState } from "@/lib/db/responsesContinuationStore";
import {
extractVideoTranscriptDescriptionFingerprints,
redactVideoTranscriptSensitiveText,
resolveVideoTranscriptLogSensitivity,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
import { normalizeResponsesPreviousResponseIdMode } from "@omniroute/open-sse/utils/responsesStatePolicy.ts"; import { normalizeResponsesPreviousResponseIdMode } from "@omniroute/open-sse/utils/responsesStatePolicy.ts";
import { FORMATS } from "@omniroute/open-sse/translator/formats.ts"; import { FORMATS } from "@omniroute/open-sse/translator/formats.ts";
import { resolveRoutingModel, RoutingModelOps } from "./resolveRoutingModel"; import { resolveRoutingModel, RoutingModelOps } from "./resolveRoutingModel";
@@ -102,6 +107,7 @@ import { resolveConversationId } from "@omniroute/open-sse/services/conversation
import { import {
isAntigravityMissingProjectError, isAntigravityMissingProjectError,
isProviderBreakerFailureStatus, isProviderBreakerFailureStatus,
resolveDailyQuotaModelViews,
resolveStreamReadinessClassificationError, resolveStreamReadinessClassificationError,
shouldTripProviderBreakerForResult, shouldTripProviderBreakerForResult,
} from "./chatPredicates"; } from "./chatPredicates";
@@ -690,6 +696,10 @@ async function handleChatImplementation(
clientRawRequest = chatAdmission.resolveClientRawAfterAdmission(clientRawRequest, () => clientRawRequest = chatAdmission.resolveClientRawAfterAdmission(clientRawRequest, () =>
deferredClientRawBody.withClientBody((clientBody) => buildClientRawRequest(request, clientBody)) deferredClientRawBody.withClientBody((clientBody) => buildClientRawRequest(request, clientBody))
); );
let videoTranscriptSensitive = resolveVideoTranscriptLogSensitivity({
rawRequestBody: clientRawRequest?.body,
processedBody: body,
});
// Guardrail pre-call pipeline — prompt injection, PII masking, and future custom rules. // Guardrail pre-call pipeline — prompt injection, PII masking, and future custom rules.
telemetry.startPhase("validate"); telemetry.startPhase("validate");
@@ -707,11 +717,15 @@ async function handleChatImplementation(
model: modelStr, model: modelStr,
signal: request.signal, signal: request.signal,
stream: body?.stream === true, stream: body?.stream === true,
videoTranscriptSensitive,
}); });
if (preCallGuardrails.blocked) { if (preCallGuardrails.blocked) {
log.warn("GUARDRAIL", "Request blocked during pre-call guardrails", { log.warn("GUARDRAIL", "Request blocked during pre-call guardrails", {
guardrail: preCallGuardrails.guardrail, guardrail: preCallGuardrails.guardrail,
message: preCallGuardrails.message, message: redactVideoTranscriptSensitiveText(
preCallGuardrails.message || "Request rejected by guardrail",
videoTranscriptSensitive
),
}); });
return errorResponse( return errorResponse(
HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.BAD_REQUEST,
@@ -722,6 +736,14 @@ async function handleChatImplementation(
const modelBeforeGuardrails = const modelBeforeGuardrails =
typeof body?.model === "string" && body.model.length > 0 ? body.model : modelStr; typeof body?.model === "string" && body.model.length > 0 ? body.model : modelStr;
body = preCallGuardrails.payload; body = preCallGuardrails.payload;
const videoTranscriptDescriptionFingerprints = extractVideoTranscriptDescriptionFingerprints(
preCallGuardrails.results
);
videoTranscriptSensitive = resolveVideoTranscriptLogSensitivity({
rawRequestBody: clientRawRequest?.body,
processedBody: body,
trustedDescriptionFingerprints: videoTranscriptDescriptionFingerprints,
});
({ body, modelStr } = await RoutingModelOps.reconcileGuardrailReroute({ ({ body, modelStr } = await RoutingModelOps.reconcileGuardrailReroute({
body, body,
modelBeforeGuardrails, modelBeforeGuardrails,
@@ -757,7 +779,10 @@ async function handleChatImplementation(
// already treat null/undefined as "untracked" (see withConversationId). // already treat null/undefined as "untracked" (see withConversationId).
log.warn("CHAT", "resolveConversationId failed, continuing without conversation tracking", { log.warn("CHAT", "resolveConversationId failed, continuing without conversation tracking", {
correlationId: reqId, correlationId: reqId,
error: error instanceof Error ? error.message : String(error), error: redactVideoTranscriptSensitiveText(
error instanceof Error ? error.message : String(error),
videoTranscriptSensitive
),
}); });
} }
@@ -1041,6 +1066,8 @@ async function handleChatImplementation(
sourceFormat, sourceFormat,
endpointPath: new URL(request.url).pathname, endpointPath: new URL(request.url).pathname,
requestHeaders: request.headers, requestHeaders: request.headers,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
clientManagedResponsesContext: clientManagedResponsesContext:
sourceFormat === "openai-responses" && sourceFormat === "openai-responses" &&
new URL(request.url).pathname.split("/").includes("responses") && new URL(request.url).pathname.split("/").includes("responses") &&
@@ -1094,6 +1121,8 @@ async function handleChatImplementation(
reasoningIntent, reasoningIntent,
reasoningRequestTags: requestRoutingTags.tags, reasoningRequestTags: requestRoutingTags.tags,
managedLease, managedLease,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
// #7360 follow-up: without this, a target dispatch abandoned by // #7360 follow-up: without this, a target dispatch abandoned by
// targetTimeoutRunner.ts's per-target timeout (comboTargetTimeoutMs) // targetTimeoutRunner.ts's per-target timeout (comboTargetTimeoutMs)
// never learns it was abandoned — it only watches the ORIGINAL // never learns it was abandoned — it only watches the ORIGINAL
@@ -1164,6 +1193,8 @@ async function handleChatImplementation(
forceLiveComboTest: isComboLiveTest, forceLiveComboTest: isComboLiveTest,
conversationId, conversationId,
managedLease, managedLease,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
}, },
combo.strategy, combo.strategy,
true true
@@ -1181,7 +1212,13 @@ async function handleChatImplementation(
`Global fallback ${fallbackModel} also failed (${fallbackResponse.status})` `Global fallback ${fallbackModel} also failed (${fallbackResponse.status})`
); );
} catch (err: any) { } catch (err: any) {
log.warn("GLOBAL_FALLBACK", `Global fallback error: ${err?.message || "unknown"}`); log.warn(
"GLOBAL_FALLBACK",
`Global fallback error: ${redactVideoTranscriptSensitiveText(
err?.message || "unknown",
videoTranscriptSensitive
)}`
);
} }
} }
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
@@ -1209,6 +1246,7 @@ async function handleChatImplementation(
sessionTag: conversationId, sessionTag: conversationId,
startTime: telemetry?.startTime, startTime: telemetry?.startTime,
requestBody: clientRawRequest?.body ?? null, requestBody: clientRawRequest?.body ?? null,
videoTranscriptSensitive,
}); });
} catch {} } catch {}
} }
@@ -1257,6 +1295,8 @@ async function handleChatImplementation(
reasoningIntent, reasoningIntent,
reasoningRequestTags: requestRoutingTags.tags, reasoningRequestTags: requestRoutingTags.tags,
managedLease, managedLease,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
}, },
null, null,
false false
@@ -1305,6 +1345,10 @@ async function handleSingleModelChat(
reasoningRequestTags?: string[]; reasoningRequestTags?: string[];
reasoningTransportFallback?: "skip" | "drop"; reasoningTransportFallback?: "skip" | "drop";
managedLease?: ManagedLeaseDispatchContext | null; managedLease?: ManagedLeaseDispatchContext | null;
/** Sensitive carrier/result state derived structurally, never from caller prose. */
videoTranscriptSensitive?: boolean;
/** Exact SHA-256 identities of transcript descriptions generated by the guardrail. */
videoTranscriptDescriptionFingerprints?: readonly string[];
/** /**
* Per-target abort signal from combo.ts's targetTimeoutRunner * Per-target abort signal from combo.ts's targetTimeoutRunner
* (comboTargetTimeoutMs) — see the #7360 follow-up comment at the * (comboTargetTimeoutMs) — see the #7360 follow-up comment at the
@@ -1316,6 +1360,9 @@ async function handleSingleModelChat(
comboStrategy: string | null = null, comboStrategy: string | null = null,
isCombo: boolean = false isCombo: boolean = false
) { ) {
const videoTranscriptDescriptionFingerprints =
runtimeOptions.videoTranscriptDescriptionFingerprints ?? [];
// 1. Resolve model → provider/model // 1. Resolve model → provider/model
const resolved = await resolveModelOrError( const resolved = await resolveModelOrError(
modelStr, modelStr,
@@ -1351,6 +1398,8 @@ async function handleSingleModelChat(
sourceFormat: sNetSourceFormat, sourceFormat: sNetSourceFormat,
endpointPath: clientRawRequest?.endpoint || "", endpointPath: clientRawRequest?.endpoint || "",
requestHeaders: clientRawRequest?.headers, requestHeaders: clientRawRequest?.headers,
videoTranscriptSensitive: runtimeOptions.videoTranscriptSensitive === true,
videoTranscriptDescriptionFingerprints,
clientManagedResponsesContext: clientManagedResponsesContext:
sNetSourceFormat === "openai-responses" && sNetSourceFormat === "openai-responses" &&
String(clientRawRequest?.endpoint || "") String(clientRawRequest?.endpoint || "")
@@ -1382,6 +1431,8 @@ async function handleSingleModelChat(
redirectCombo.config?.reasoningTransportFallback === "skip" ? "skip" : "drop", redirectCombo.config?.reasoningTransportFallback === "skip" ? "skip" : "drop",
conversationId: runtimeOptions?.conversationId ?? null, conversationId: runtimeOptions?.conversationId ?? null,
managedLease: runtimeOptions.managedLease ?? null, managedLease: runtimeOptions.managedLease ?? null,
videoTranscriptSensitive: runtimeOptions.videoTranscriptSensitive === true,
videoTranscriptDescriptionFingerprints,
// #7360 follow-up — see the primary handleSingleModel closure above. // #7360 follow-up — see the primary handleSingleModel closure above.
modelAbortSignal: target?.modelAbortSignal ?? null, modelAbortSignal: target?.modelAbortSignal ?? null,
}, },
@@ -1486,6 +1537,7 @@ async function handleSingleModelChat(
correlationId: runtimeOptions?.correlationId ?? null, correlationId: runtimeOptions?.correlationId ?? null,
sessionTag: runtimeOptions?.conversationId ?? null, sessionTag: runtimeOptions?.conversationId ?? null,
startTime: telemetry?.startTime, startTime: telemetry?.startTime,
videoTranscriptSensitive: runtimeOptions.videoTranscriptSensitive === true,
}); });
} catch {} } catch {}
return gate; return gate;
@@ -1693,7 +1745,8 @@ async function handleSingleModelChat(
lastError, lastError,
lastStatus, lastStatus,
candidateAliases, candidateAliases,
isCombo isCombo,
runtimeOptions.videoTranscriptSensitive === true
); );
const lastFailedConnectionId = const lastFailedConnectionId =
excludedConnectionIds.size > 0 excludedConnectionIds.size > 0
@@ -1853,6 +1906,8 @@ async function handleSingleModelChat(
sessionAffinityKey: runtimeOptions.sessionAffinityKey ?? null, sessionAffinityKey: runtimeOptions.sessionAffinityKey ?? null,
reasoningTransportFallback: runtimeOptions.reasoningTransportFallback ?? "drop", reasoningTransportFallback: runtimeOptions.reasoningTransportFallback ?? "drop",
managedLease: runtimeOptions.managedLease ?? null, managedLease: runtimeOptions.managedLease ?? null,
videoTranscriptSensitive: runtimeOptions.videoTranscriptSensitive === true,
videoTranscriptDescriptionFingerprints,
}, },
runtimeOptions runtimeOptions
); );
@@ -1888,6 +1943,7 @@ async function handleSingleModelChat(
comboName, comboName,
clientRawRequest, clientRawRequest,
tlsFingerprintUsed, tlsFingerprintUsed,
videoTranscriptSensitive: runtimeOptions.videoTranscriptSensitive === true,
}); });
if (result.success) { if (result.success) {
@@ -1990,7 +2046,13 @@ async function handleSingleModelChat(
provider, provider,
model, model,
providerProfile, providerProfile,
{ isCombo } {
isCombo,
retainedErrorText: redactVideoTranscriptSensitiveText(
classificationError,
runtimeOptions.videoTranscriptSensitive === true
),
}
); );
if (shouldFallback && !hasForcedConnection) { if (shouldFallback && !hasForcedConnection) {
@@ -2039,7 +2101,13 @@ async function handleSingleModelChat(
provider, provider,
model, model,
providerProfile, providerProfile,
{ isCombo } {
isCombo,
retainedErrorText: redactVideoTranscriptSensitiveText(
result.error || ANTIGRAVITY_PRE_RESPONSE_TIMEOUT_CODE,
runtimeOptions.videoTranscriptSensitive === true
),
}
); );
if (shouldFallback && !hasForcedConnection) { if (shouldFallback && !hasForcedConnection) {
@@ -2177,9 +2245,14 @@ async function handleSingleModelChat(
: classify429FromError({ status: result.status, message: errorStr }) : classify429FromError({ status: result.status, message: errorStr })
: undefined; : undefined;
if (result.status === 429 && isDailyQuotaExhausted(errorStr)) { if (result.status === 429 && isDailyQuotaExhausted(errorStr)) {
// Parse which model is quota-limited // Keep the raw provider token for routing state, but never retain it when a sensitive
const match = errorStr.match(/today's quota for model ([^,]+)/); // upstream diagnostic could have echoed transcript text.
const limitedModel = match ? match[1].trim() : model; const { operationalModel: limitedModel, retainedModel: retainedLimitedModel } =
resolveDailyQuotaModelViews(
errorStr,
model,
runtimeOptions.videoTranscriptSensitive === true
);
const mlSettings = resolveModelLockoutSettings(runtimeOptions.cachedSettings); const mlSettings = resolveModelLockoutSettings(runtimeOptions.cachedSettings);
if (mlSettings.enabled && mlSettings.errorCodes.includes(result.status)) { if (mlSettings.enabled && mlSettings.errorCodes.includes(result.status)) {
@@ -2200,7 +2273,7 @@ async function handleSingleModelChat(
"MODEL_DAILY_QUOTA", "MODEL_DAILY_QUOTA",
JSON.stringify({ JSON.stringify({
connection: credentials.connectionId.slice(0, 8), connection: credentials.connectionId.slice(0, 8),
model: limitedModel, model: retainedLimitedModel,
cooldownMs: lockResult.cooldownMs, cooldownMs: lockResult.cooldownMs,
failureCount: lockResult.failureCount, failureCount: lockResult.failureCount,
}) })
@@ -2291,6 +2364,10 @@ async function handleSingleModelChat(
(failureKind === "rate_limit" || failureKind === "transient") (failureKind === "rate_limit" || failureKind === "transient")
), ),
isCombo, isCombo,
retainedErrorText: redactVideoTranscriptSensitiveText(
errorStr,
runtimeOptions.videoTranscriptSensitive === true
),
} }
); );

View File

@@ -28,6 +28,7 @@ import {
} from "@omniroute/open-sse/utils/proxyFetch.ts"; } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { resolveProxyForConnection } from "@/lib/localDb"; import { resolveProxyForConnection } from "@/lib/localDb";
import { hasBlockingProxyAssignment } from "@/lib/db/proxies"; import { hasBlockingProxyAssignment } from "@/lib/db/proxies";
import { redactVideoTranscriptSensitiveText } from "@/lib/guardrails/videoTranscriptLogRedaction";
import { import {
CircuitBreakerOpenError, CircuitBreakerOpenError,
getCircuitBreaker, getCircuitBreaker,
@@ -36,10 +37,10 @@ import {
import { classify429FromError, type FailureKind } from "../../shared/utils/classify429"; import { classify429FromError, type FailureKind } from "../../shared/utils/classify429";
import { resolveUseUpstream429BreakerHints } from "../../shared/utils/providerHints"; import { resolveUseUpstream429BreakerHints } from "../../shared/utils/providerHints";
import { logProxyEvent } from "../../lib/proxyLogger";
import { logTranslationEvent } from "../../lib/translatorEvents";
import { getRuntimeProviderProfile } from "@omniroute/open-sse/services/accountFallback.ts"; import { getRuntimeProviderProfile } from "@omniroute/open-sse/services/accountFallback.ts";
export { safeLogEvents } from "./chatLogEvents";
// Models that explicitly cannot run on the codex/ChatGPT-Pro OAuth pool — when // Models that explicitly cannot run on the codex/ChatGPT-Pro OAuth pool — when
// a caller writes `codex/deepseek-v4-pro` we transparently reroute to the // a caller writes `codex/deepseek-v4-pro` we transparently reroute to the
// canonical provider whose API key is configured. Saves callers from having // canonical provider whose API key is configured. Saves callers from having
@@ -391,7 +392,6 @@ export function checkResourcePressureBeforeProviderWork(): ResourcePressureGuard
return null; return null;
} }
} }
export async function executeChatWithBreaker({ export async function executeChatWithBreaker({
bypassCircuitBreaker, bypassCircuitBreaker,
breaker, breaker,
@@ -425,6 +425,8 @@ export async function executeChatWithBreaker({
reasoningTransportFallback = "drop", reasoningTransportFallback = "drop",
sessionAffinityKey = null, sessionAffinityKey = null,
managedLease = null, managedLease = null,
videoTranscriptSensitive = false,
videoTranscriptDescriptionFingerprints = [],
}: ExecuteChatWithBreakerOptions): Promise<ExecuteChatWithBreakerResult> { }: ExecuteChatWithBreakerOptions): Promise<ExecuteChatWithBreakerResult> {
let tlsFingerprintUsed = false; let tlsFingerprintUsed = false;
const normalizedTrafficType: TrafficType = const normalizedTrafficType: TrafficType =
@@ -432,7 +434,6 @@ export async function executeChatWithBreaker({
? "shadow" ? "shadow"
: "production"; : "production";
const isShadowTraffic = normalizedTrafficType === "shadow"; const isShadowTraffic = normalizedTrafficType === "shadow";
// #5217: capture the proxy actually applied during execution so the caller can // #5217: capture the proxy actually applied during execution so the caller can
// merge it into proxyInfo before the egress log (executors pinning a per-account // merge it into proxyInfo before the egress log (executors pinning a per-account
// proxy internally otherwise leave the egress log reading "direct"). // proxy internally otherwise leave the egress log reading "direct").
@@ -484,6 +485,8 @@ export async function executeChatWithBreaker({
sessionAffinityKey, sessionAffinityKey,
reasoningTransportFallback, reasoningTransportFallback,
managedLease, managedLease,
videoTranscriptSensitive,
videoTranscriptDescriptionFingerprints,
skipResourcePressureGuard: true, skipResourcePressureGuard: true,
onCredentialsRefreshed: async (newCreds: any) => { onCredentialsRefreshed: async (newCreds: any) => {
await updateProviderCredentials(credentials.connectionId, { await updateProviderCredentials(credentials.connectionId, {
@@ -537,7 +540,13 @@ export async function executeChatWithBreaker({
provider, provider,
model, model,
providerProfile, providerProfile,
{ isCombo } {
isCombo,
retainedErrorText: redactVideoTranscriptSensitiveText(
String(failure?.message || failure?.code || "stream failure"),
videoTranscriptSensitive
),
}
); );
}, },
}) })
@@ -609,7 +618,7 @@ export async function executeChatWithBreaker({
if (cbErr?.code === "PROXY_UNREACHABLE" || /proxy unreachable/i.test(cbErr?.message || "")) { if (cbErr?.code === "PROXY_UNREACHABLE" || /proxy unreachable/i.test(cbErr?.message || "")) {
const detail = cbErr?.message || "Proxy unreachable"; const detail = cbErr?.message || "Proxy unreachable";
log.warn("PROXY", detail); log.warn("PROXY", redactVideoTranscriptSensitiveText(detail, videoTranscriptSensitive));
return { return {
result: { result: {
success: false, success: false,
@@ -633,7 +642,8 @@ export function handleNoCredentials(
lastError: string | null, lastError: string | null,
lastStatus: number | null, lastStatus: number | null,
candidateAliases?: readonly string[], candidateAliases?: readonly string[],
isCombo: boolean = false isCombo: boolean = false,
videoTranscriptSensitive: boolean = false
) { ) {
if (credentials?.allRateLimited) { if (credentials?.allRateLimited) {
const errorMsg = lastError || credentials.lastError || "Unavailable"; const errorMsg = lastError || credentials.lastError || "Unavailable";
@@ -660,7 +670,8 @@ export function handleNoCredentials(
}); });
} }
log.warn("CHAT", `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`); const retainedErrorMsg = redactVideoTranscriptSensitiveText(errorMsg, videoTranscriptSensitive);
log.warn("CHAT", `[${provider}/${model}] ${retainedErrorMsg} (${credentials.retryAfterHuman})`);
return unavailableResponse( return unavailableResponse(
status, status,
`[${provider}/${model}] ${errorMsg}`, `[${provider}/${model}] ${errorMsg}`,
@@ -845,82 +856,6 @@ export function applyExecutorProxyToInfo(
}; };
} }
// Async because the egress-IP lookup lazy-imports proxyEgress; callers treat
// this as fire-and-forget logging (the internal try/catch swallows everything).
export async function safeLogEvents({
result,
proxyInfo,
proxyLatency,
provider,
model,
sourceFormat,
targetFormat,
credentials,
comboName,
clientRawRequest,
tlsFingerprintUsed = false,
}) {
try {
const rawIp =
clientRawRequest?.headers?.["x-forwarded-for"] ||
clientRawRequest?.headers?.["x-real-ip"] ||
clientRawRequest?.headers?.["cf-connecting-ip"] ||
null;
const rawIpValue = Array.isArray(rawIp) ? rawIp[0] : rawIp;
const clientIp = typeof rawIpValue === "string" ? rawIpValue.split(",")[0].trim() : null;
// Resolve the egress IP (the IP the upstream actually saw) from cache — never
// blocking the request. Warm it in the background for next time. null until
// the first warm completes; direct (no proxy) is also tracked.
let egressIp: string | null = null;
try {
const { getCachedEgressIp, warmEgressIp } = await import("../../lib/proxyEgress");
const { proxyConfigToUrl } = await import("@omniroute/open-sse/utils/proxyDispatcher.ts");
const proxyUrl = proxyInfo?.proxy ? proxyConfigToUrl(proxyInfo.proxy) : null;
egressIp = getCachedEgressIp(proxyUrl);
warmEgressIp(proxyUrl);
} catch {
// egress visibility is best-effort; never break the request path
}
logProxyEvent({
status: result.success
? "success"
: result.status === 408 || result.status === 504
? "timeout"
: "error",
proxy: proxyInfo?.proxy || null,
level: proxyInfo?.level || "direct",
levelId: proxyInfo?.levelId || null,
provider,
targetUrl: `${provider}/${model}`,
clientIp,
egressIp,
latencyMs: proxyLatency,
error: result.success ? null : result.error || null,
connectionId: credentials.connectionId,
comboId: comboName || null,
account: credentials.connectionId?.slice(0, 8) || null,
tlsFingerprint: tlsFingerprintUsed,
});
} catch {}
try {
logTranslationEvent({
provider,
model,
sourceFormat,
targetFormat,
status: result.success ? "success" : "error",
statusCode: result.success ? 200 : result.status || 500,
latency: proxyLatency,
endpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
connectionId: credentials.connectionId || null,
comboName: comboName || null,
});
} catch {}
}
export function withSessionHeader(response: Response, sessionId: string | null): Response { export function withSessionHeader(response: Response, sessionId: string | null): Response {
if (!response || !sessionId) return response; if (!response || !sessionId) return response;

View File

@@ -0,0 +1,107 @@
import { redactVideoTranscriptSensitiveText } from "@/lib/guardrails/videoTranscriptLogRedaction";
import { logProxyEvent } from "../../lib/proxyLogger";
import { logTranslationEvent } from "../../lib/translatorEvents";
type ProxyLogConfig = {
host: string;
port: number | string;
type: string;
};
export interface SafeLogEventsInput {
clientRawRequest?: {
endpoint?: string;
headers?: Record<string, string | string[] | undefined>;
} | null;
comboName?: string | null;
credentials: { connectionId?: string | null };
model: string;
provider: string;
proxyInfo?: {
level?: string;
levelId?: string | null;
proxy?: ProxyLogConfig | null;
} | null;
proxyLatency: number;
result: { error?: unknown; status?: number; success: boolean };
sourceFormat: string;
targetFormat: string;
tlsFingerprintUsed?: boolean;
videoTranscriptSensitive?: boolean;
}
/** Retain safe proxy/translation metadata without retaining a sensitive provider error echo. */
export async function safeLogEvents({
result,
proxyInfo,
proxyLatency,
provider,
model,
sourceFormat,
targetFormat,
credentials,
comboName,
clientRawRequest,
tlsFingerprintUsed = false,
videoTranscriptSensitive = false,
}: SafeLogEventsInput): Promise<void> {
try {
const rawIp =
clientRawRequest?.headers?.["x-forwarded-for"] ||
clientRawRequest?.headers?.["x-real-ip"] ||
clientRawRequest?.headers?.["cf-connecting-ip"] ||
null;
const rawIpValue = Array.isArray(rawIp) ? rawIp[0] : rawIp;
const clientIp = typeof rawIpValue === "string" ? rawIpValue.split(",")[0].trim() : null;
let egressIp: string | null = null;
try {
const { getCachedEgressIp, warmEgressIp } = await import("../../lib/proxyEgress");
const { proxyConfigToUrl } = await import("@omniroute/open-sse/utils/proxyDispatcher.ts");
const proxyUrl = proxyInfo?.proxy ? proxyConfigToUrl(proxyInfo.proxy) : null;
egressIp = getCachedEgressIp(proxyUrl);
warmEgressIp(proxyUrl);
} catch {
// Egress visibility is best-effort and never breaks the request path.
}
logProxyEvent({
account: credentials.connectionId?.slice(0, 8) || null,
clientIp,
comboId: comboName || null,
connectionId: credentials.connectionId,
egressIp,
error:
result.success || !result.error
? null
: redactVideoTranscriptSensitiveText(String(result.error), videoTranscriptSensitive),
latencyMs: proxyLatency,
level: proxyInfo?.level || "direct",
levelId: proxyInfo?.levelId || null,
provider,
proxy: proxyInfo?.proxy || null,
status: result.success
? "success"
: result.status === 408 || result.status === 504
? "timeout"
: "error",
targetUrl: `${provider}/${model}`,
tlsFingerprint: tlsFingerprintUsed,
});
} catch {}
try {
logTranslationEvent({
comboName: comboName || null,
connectionId: credentials.connectionId || null,
endpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
latency: proxyLatency,
model,
provider,
sourceFormat,
status: result.success ? "success" : "error",
statusCode: result.success ? 200 : result.status || 500,
targetFormat,
});
} catch {}
}

View File

@@ -1,6 +1,7 @@
import { getTrustedLocalRateLimitResponse } from "@omniroute/open-sse/services/rateLimitManager/errors";
import { isLocalStreamLifecycleError } from "../../shared/utils/circuitBreaker"; import { isLocalStreamLifecycleError } from "../../shared/utils/circuitBreaker";
import { isRequestScopedUpstreamFailure } from "./comboFailureLogging"; import { isRequestScopedUpstreamFailure } from "./comboFailureLogging";
import { getTrustedLocalRateLimitResponse } from "@omniroute/open-sse/services/rateLimitManager/errors";
export const PROVIDER_BREAKER_FAILURE_STATUSES = new Set([408, 500, 502, 503, 504]); export const PROVIDER_BREAKER_FAILURE_STATUSES = new Set([408, 500, 502, 503, 504]);
@@ -69,3 +70,32 @@ export function resolveStreamReadinessClassificationError(
} }
return fallback; return fallback;
} }
export interface DailyQuotaModelViews {
/** Model key used by model-lockout state and future routing decisions. */
operationalModel: string;
/** Safe representation allowed in retained application logs. */
retainedModel: string;
}
/**
* Ordinary requests preserve the exact upstream model token used by existing quota routing.
* For transcript-sensitive requests, the provider diagnostic is untrusted request-derived text:
* keep classifying the raw error at the caller, but never promote its token into retained lockout
* state. The already-resolved request model is the server-owned key and safe retained view.
*/
export function resolveDailyQuotaModelViews(
errorText: string,
resolvedModel: string,
videoTranscriptSensitive: boolean
): DailyQuotaModelViews {
const match = errorText.match(/today's quota for model ([^,]+)/);
const upstreamModel = match?.[1]?.trim();
const operationalModel = videoTranscriptSensitive
? resolvedModel
: upstreamModel || resolvedModel;
return {
operationalModel,
retainedModel: operationalModel,
};
}

View File

@@ -18,6 +18,11 @@
* never turn into a second failure on the response path. * never turn into a second failure on the response path.
*/ */
import { saveCallLog, saveRequestUsage } from "@/lib/usageDb"; import { saveCallLog, saveRequestUsage } from "@/lib/usageDb";
import {
containsVideoTranscriptForLog,
omitVideoTranscriptForLog,
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
} from "@/lib/guardrails/videoTranscriptLogRedaction";
export interface RejectedRequestUsageInput { export interface RejectedRequestUsageInput {
status: number; status: number;
@@ -44,6 +49,8 @@ export interface RejectedRequestUsageInput {
* dashboard log detail had no request to inspect — see #7360 follow-up. * dashboard log detail had no request to inspect — see #7360 follow-up.
*/ */
requestBody?: unknown; requestBody?: unknown;
/** Trusted request-scoped signal; raw text remains available to routing before this boundary. */
videoTranscriptSensitive?: boolean;
} }
export async function recordRejectedRequestUsage(input: RejectedRequestUsageInput): Promise<void> { export async function recordRejectedRequestUsage(input: RejectedRequestUsageInput): Promise<void> {
@@ -64,10 +71,20 @@ export async function recordRejectedRequestUsage(input: RejectedRequestUsageInpu
connectionId = undefined, connectionId = undefined,
startTime, startTime,
requestBody = null, requestBody = null,
videoTranscriptSensitive = false,
} = input; } = input;
const now = Date.now(); const now = Date.now();
const duration = typeof startTime === "number" ? now - startTime : 0; const duration = typeof startTime === "number" ? now - startTime : 0;
// This sink owns a request body and must defend itself if an upstream caller forgets the
// request-scoped bit. The explicit bit is still required after a guardrail replaced the raw
// carrier with generated text, which is why both sources participate.
const transcriptSensitive =
videoTranscriptSensitive || containsVideoTranscriptForLog(requestBody);
const retainedError = transcriptSensitive ? VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER : error || null;
const retainedRequestBody = transcriptSensitive
? omitVideoTranscriptForLog(requestBody)
: requestBody;
// 1. call_logs — preserves /dashboard/logs visibility (unchanged behavior). // 1. call_logs — preserves /dashboard/logs visibility (unchanged behavior).
await saveCallLog({ await saveCallLog({
@@ -81,8 +98,8 @@ export async function recordRejectedRequestUsage(input: RejectedRequestUsageInpu
connectionId, connectionId,
duration, duration,
tokens: {}, tokens: {},
error: error || null, error: retainedError,
requestBody, requestBody: retainedRequestBody,
comboName, comboName,
comboStepId, comboStepId,
comboExecutionKey, comboExecutionKey,

View File

@@ -2607,10 +2607,11 @@ export async function markAccountUnavailable(
providerProfile = null, providerProfile = null,
options: { options: {
persistUnavailableState?: boolean; persistUnavailableState?: boolean;
/** Caller is the combo engine — it records its own model-level lockouts. */
isCombo?: boolean; isCombo?: boolean;
retainedErrorText?: string;
} = {} } = {}
) { ) {
const retainedErrorText = options.retainedErrorText ?? errorText;
const currentMutex = markMutexes.get(connectionId) || Promise.resolve(); const currentMutex = markMutexes.get(connectionId) || Promise.resolve();
let resolveMutex: (() => void) | undefined; let resolveMutex: (() => void) | undefined;
markMutexes.set( markMutexes.set(
@@ -2619,10 +2620,8 @@ export async function markAccountUnavailable(
resolveMutex = resolve; resolveMutex = resolve;
}) })
); );
try { try {
await currentMutex; await currentMutex;
// STRICT_ZERO_COST: this connection just failed (whatever the reason) — // STRICT_ZERO_COST: this connection just failed (whatever the reason) —
// drop any cached "SAFE" free-allowance reading for it immediately rather // drop any cached "SAFE" free-allowance reading for it immediately rather
// than waiting out the TTL, so the very next candidate-pool build reads a // than waiting out the TTL, so the very next candidate-pool build reads a
@@ -2736,14 +2735,13 @@ export async function markAccountUnavailable(
// the opt-in setting probeCanDisable restores the historical behavior. // the opt-in setting probeCanDisable restores the historical behavior.
if (await shouldIsolateProbeFailures()) { if (await shouldIsolateProbeFailures()) {
await updateProviderConnection(connectionId, { await updateProviderConnection(connectionId, {
// lastError kept RAW (full text) — maximal probe visibility; the // Probe visibility keeps full retained text; sensitive callers supply an omission marker.
// divergence vs the normal path's slice(0,100) is intentional.
// backoffLevel is deliberately NOT written: a positive backoff // backoffLevel is deliberately NOT written: a positive backoff
// triggers the selection-time auto-decay (resetConnectionBackoff, // triggers the selection-time auto-decay (resetConnectionBackoff,
// auth.ts getProviderCredentials) which wipes lastError back to // auth.ts getProviderCredentials) which wipes lastError back to
// NULL on the next attempt — silently destroying the probe record. // NULL on the next attempt — silently destroying the probe record.
// The backoff is also routing state a probe must not touch (#9817). // The backoff is also routing state a probe must not touch (#9817).
lastError: errorText, lastError: retainedErrorText,
lastErrorType: fallbackResult.reason || null, lastErrorType: fallbackResult.reason || null,
errorCode: status, errorCode: status,
lastErrorAt: new Date().toISOString(), lastErrorAt: new Date().toISOString(),
@@ -3148,7 +3146,7 @@ export async function markAccountUnavailable(
return { shouldFallback: true, cooldownMs: lockout.cooldownMs }; return { shouldFallback: true, cooldownMs: lockout.cooldownMs };
} }
const errorMsg = describeUpstreamFailure(errorText); const errorMsg = describeUpstreamFailure(retainedErrorText);
// T09: Codex per-scope lockout (do not block the whole account globally). // T09: Codex per-scope lockout (do not block the whole account globally).
if ( if (

View File

@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveDailyQuotaModelViews } from "../../src/sse/handlers/chatPredicates.ts";
test("daily quota model uses the upstream model token for ordinary operational and retained views", () => {
assert.deepEqual(
resolveDailyQuotaModelViews(
"You have exceeded today's quota for model moonshotai/Kimi-K2.5, try tomorrow",
"Kimi-K2.5",
false
),
{
operationalModel: "moonshotai/Kimi-K2.5",
retainedModel: "moonshotai/Kimi-K2.5",
}
);
});
test("daily quota uses the server-resolved key when the upstream token may echo a transcript", () => {
const transcriptSentinel = "PRIVATE_DAILY_QUOTA_TRANSCRIPT_SENTINEL";
const views = resolveDailyQuotaModelViews(
`You have exceeded today's quota for model ${transcriptSentinel}, try tomorrow`,
"server-resolved-model",
true
);
assert.equal(views.operationalModel, "server-resolved-model");
assert.equal(views.retainedModel, "server-resolved-model");
assert.equal(views.operationalModel.includes(transcriptSentinel), false);
assert.equal(views.retainedModel.includes(transcriptSentinel), false);
});
test("daily quota model falls back to the server-resolved model when no token is present", () => {
assert.deepEqual(
resolveDailyQuotaModelViews("daily quota exhausted", "server-resolved-model", true),
{
operationalModel: "server-resolved-model",
retainedModel: "server-resolved-model",
}
);
});

View File

@@ -6,7 +6,11 @@ import path from "node:path";
import net from "node:net"; import net from "node:net";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-chat-helpers-")); const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-chat-helpers-"));
const TEST_APP_LOG_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-chat-helpers-log-"));
const TEST_APP_LOG_PATH = path.join(TEST_APP_LOG_DIR, "app.log");
process.env.DATA_DIR = TEST_DATA_DIR; process.env.DATA_DIR = TEST_DATA_DIR;
process.env.APP_LOG_TO_FILE = "true";
process.env.APP_LOG_FILE_PATH = TEST_APP_LOG_PATH;
const core = await import("../../src/lib/db/core.ts"); const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts"); const providersDb = await import("../../src/lib/db/providers.ts");
@@ -22,6 +26,8 @@ const {
} = await import("../../src/sse/handlers/chatHelpers.ts"); } = await import("../../src/sse/handlers/chatHelpers.ts");
const { getCircuitBreaker, resetAllCircuitBreakers, STATE } = const { getCircuitBreaker, resetAllCircuitBreakers, STATE } =
await import("../../src/shared/utils/circuitBreaker.ts"); await import("../../src/shared/utils/circuitBreaker.ts");
const proxyLogger = await import("../../src/lib/proxyLogger.ts");
const { logger: appLogger } = await import("../../src/shared/utils/logger.ts");
// DATA_DIR must be fixed before these modules load; keep this test seam dynamic. // DATA_DIR must be fixed before these modules load; keep this test seam dynamic.
const { setTlsClientForTest } = await import("../../open-sse/utils/proxyFetch.ts"); const { setTlsClientForTest } = await import("../../open-sse/utils/proxyFetch.ts");
@@ -32,6 +38,31 @@ async function resetStorage() {
fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
} }
async function readAppLogWhen(
predicate: (contents: string) => boolean,
timeoutMs = 4_000
): Promise<string> {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const contents = fs.existsSync(TEST_APP_LOG_PATH)
? fs.readFileSync(TEST_APP_LOG_PATH, "utf8")
: "";
if (predicate(contents)) return contents;
await new Promise((resolve) => setTimeout(resolve, 50));
}
return fs.existsSync(TEST_APP_LOG_PATH) ? fs.readFileSync(TEST_APP_LOG_PATH, "utf8") : "";
}
async function flushAppLogger(): Promise<void> {
await new Promise<void>((resolveFlush) => {
try {
appLogger.flush(() => resolveFlush());
} catch {
resolveFlush();
}
});
}
async function seedConnection(provider, overrides = {}) { async function seedConnection(provider, overrides = {}) {
return providersDb.createProviderConnection({ return providersDb.createProviderConnection({
provider, provider,
@@ -420,6 +451,42 @@ test("handleNoCredentials returns Retry-After when every account is rate limited
assert.match(json.error.message, /\[openai\/gpt-4o-mini\] Quota exceeded/); assert.match(json.error.message, /\[openai\/gpt-4o-mini\] Quota exceeded/);
}); });
test("handleNoCredentials omits transcript-sensitive upstream text from application logs", async () => {
const rawCue = "PRIVATE_NO_CREDENTIALS_VIDEO_TRANSCRIPT_SENTINEL";
const provider = `fu05-${Math.random().toString(16).slice(2)}`;
const response = handleNoCredentials(
{
allRateLimited: true,
retryAfter: new Date(Date.now() + 30_000).toISOString(),
retryAfterHuman: "reset after 30s",
lastErrorCode: 429,
},
"conn_video_transcript",
provider,
"fixture-model",
rawCue,
429,
undefined,
false,
true
);
const body = (await response.json()) as { error?: { message?: string } };
assert.match(body.error?.message ?? "", new RegExp(rawCue));
await flushAppLogger();
const contents = await readAppLogWhen(
(value) => value.includes(provider) && value.includes("omitted: video transcript")
);
const matchingLines = contents
.split("\n")
.filter((line) => line.includes(provider))
.join("\n");
assert.doesNotMatch(matchingLines, new RegExp(rawCue));
assert.match(matchingLines, /omitted: video transcript/);
});
test("handleNoCredentials returns structured model_cooldown when every credential for the model is cooling down", async () => { test("handleNoCredentials returns structured model_cooldown when every credential for the model is cooling down", async () => {
const retryAfter = new Date(Date.now() + 12_000).toISOString(); const retryAfter = new Date(Date.now() + 12_000).toISOString();
const response = handleNoCredentials( const response = handleNoCredentials(
@@ -553,6 +620,60 @@ test("executeChatWithBreaker converts proxy fast-fail errors", async () => {
} }
}); });
test("executeChatWithBreaker omits transcript echoes from proxy fast-fail logs", async () => {
const rawCue = "PRIVATE_PROXY_FAST_FAIL_VIDEO_TRANSCRIPT_SENTINEL";
const error = new Error(`Proxy unreachable: ${rawCue}`);
(error as Error & { code?: string }).code = "PROXY_UNREACHABLE";
const rejectingBreaker = {
execute: async () => {
throw error;
},
};
await flushAppLogger();
const before = fs.existsSync(TEST_APP_LOG_PATH)
? fs.readFileSync(TEST_APP_LOG_PATH, "utf8").length
: 0;
const credentials = {
connectionId: "conn_sensitive_proxy",
apiKey: "sk-openai-helper",
providerSpecificData: {},
};
const proxyResult = await executeChatWithBreaker({
bypassCircuitBreaker: false,
breaker: rejectingBreaker,
body: { model: "openai/gpt-4o-mini" },
provider: "openai",
model: "gpt-4o-mini",
refreshedCredentials: credentials,
proxyInfo: null,
log: console,
clientRawRequest: null,
credentials,
apiKeyInfo: null,
userAgent: "",
comboName: null,
comboStrategy: null,
isCombo: false,
extendedContext: false,
comboStepId: null,
comboExecutionKey: null,
videoTranscriptSensitive: true,
});
assert.equal(proxyResult.result.status, 503);
assert.match(String(proxyResult.result.error || ""), new RegExp(rawCue));
await flushAppLogger();
const contents = await readAppLogWhen(
(value) => value.slice(before).includes("omitted: video transcript") || value.includes(rawCue)
);
const retainedLines = contents.slice(before);
assert.doesNotMatch(retainedLines, new RegExp(rawCue));
assert.match(retainedLines, /omitted: video transcript/);
});
test("executeChatWithBreaker preserves account TLS scope when a proxy bypasses to direct", async () => { test("executeChatWithBreaker preserves account TLS scope when a proxy bypasses to direct", async () => {
const server = net.createServer((socket) => socket.end()); const server = net.createServer((socket) => socket.end());
const listening = Promise.withResolvers<void>(); const listening = Promise.withResolvers<void>();
@@ -674,6 +795,49 @@ test("safeLogEvents tolerates success and timeout payloads", () => {
}); });
}); });
test("safeLogEvents preserves a null error for failures without error details", async () => {
const provider = "fu05-missing-error";
await safeLogEvents({
result: { success: false, status: 502 },
proxyInfo: { proxy: null, level: "direct", levelId: null },
proxyLatency: 25,
provider,
model: "fixture-model",
sourceFormat: "openai-chat",
targetFormat: "openai-chat",
credentials: { connectionId: "conn-missing-error" },
comboName: null,
clientRawRequest: { endpoint: "/v1/chat/completions" },
});
const entry = proxyLogger.getProxyLogs({ provider, limit: 1 })[0];
assert.ok(entry);
assert.equal(entry.error, null);
});
test("safeLogEvents omits failure text for transcript-sensitive requests", async () => {
const rawCue = "PRIVATE_PROXY_LOG_VIDEO_TRANSCRIPT_SENTINEL";
const provider = "fu05-transcript-sensitive";
await safeLogEvents({
result: { success: false, status: 502, error: rawCue },
proxyInfo: { proxy: null, level: "direct", levelId: null },
proxyLatency: 25,
provider,
model: "fixture-model",
sourceFormat: "openai-chat",
targetFormat: "openai-chat",
credentials: { connectionId: "conn-video-transcript" },
comboName: null,
clientRawRequest: { endpoint: "/v1/chat/completions" },
videoTranscriptSensitive: true,
});
const entry = proxyLogger.getProxyLogs({ provider, limit: 1 })[0];
assert.ok(entry);
assert.equal(String(entry.error).includes(rawCue), false);
assert.match(String(entry.error), /omitted: video transcript/);
});
test("withSessionHeader adds headers to mutable and immutable responses", async () => { test("withSessionHeader adds headers to mutable and immutable responses", async () => {
const mutable = withSessionHeader(new Response("ok"), "sess_mutable"); const mutable = withSessionHeader(new Response("ok"), "sess_mutable");
const immutable = withSessionHeader(Response.redirect("https://example.com"), "sess_redirect"); const immutable = withSessionHeader(Response.redirect("https://example.com"), "sess_redirect");

View File

@@ -14,6 +14,9 @@ const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-attempt-logging-
process.env.DATA_DIR = testDataDir; process.env.DATA_DIR = testDataDir;
const coreDb = await import("../../src/lib/db/core.ts"); const coreDb = await import("../../src/lib/db/core.ts");
const eventBus = await import("../../src/lib/events/eventBus.ts");
const { fingerprintVideoTranscriptDescription } =
await import("../../src/lib/guardrails/videoTranscriptLogRedaction.ts");
const { getCallLogById } = await import("../../src/lib/usage/callLogs.ts"); const { getCallLogById } = await import("../../src/lib/usage/callLogs.ts");
const { persistAttemptLogs } = await import("../../open-sse/handlers/chatCore/attemptLogging.ts"); const { persistAttemptLogs } = await import("../../open-sse/handlers/chatCore/attemptLogging.ts");
@@ -48,6 +51,7 @@ function baseCtx(overrides: Record<string, unknown> = {}) {
tokensCompressed: 0, tokensCompressed: 0,
apiKeyInfo: { id: "key-1", name: "Key One" }, apiKeyInfo: { id: "key-1", name: "Key One" },
noLogEnabled: false, noLogEnabled: false,
videoTranscriptSensitive: false,
...overrides, ...overrides,
} as Parameters<typeof persistAttemptLogs>[1]; } as Parameters<typeof persistAttemptLogs>[1];
} }
@@ -136,3 +140,75 @@ test("connectionId falls back to credentials.connectionId when null, and error i
assert.equal(row.status, 502); assert.equal(row.status, 502);
assert.match(String(row.error ?? ""), /upstream boom/); assert.match(String(row.error ?? ""), /upstream boom/);
}); });
test("omits non-stream response echoes for a transcript-sensitive attempt", async () => {
const id = "attempt-video-transcript-sensitive-1";
const traceId = "trace-video-transcript-sensitive-1";
const rawCue = "private attempt-log subtitle echo sentinel";
const description = `[Video description: transcript[source=embedded] text=${JSON.stringify(rawCue)}]`;
persistAttemptLogs(
{
status: 502,
error: `upstream echoed ${rawCue}`,
responseBody: { choices: [{ message: { content: rawCue } }] },
providerResponse: {
choices: [{ message: { content: rawCue } }],
warning: `safety filter echoed ${rawCue}`,
},
clientResponse: { choices: [{ message: { content: rawCue } }] },
},
baseCtx({
detailedLoggingEnabled: true,
pendingRequestId: id,
skillRequestId: "skill-video-transcript-sensitive",
traceId,
body: {
messages: [
{
role: "user",
content: description,
},
],
},
reqLogger: {
getPipelinePayloads: () => ({}),
isVideoTranscriptSensitive: () => true,
},
videoTranscriptDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
videoTranscriptSensitive: true,
})
);
const row = await pollForCallLog(id);
assert.ok(row);
const serialized = JSON.stringify({
error: row.error,
pipelinePayloads: row.pipelinePayloads,
responseBody: row.responseBody,
});
assert.equal(serialized.includes(rawCue), false);
assert.match(serialized, /omitted: video transcript/);
await new Promise<void>((resolve) => setImmediate(resolve));
const lifecycle = eventBus
.getEventHistory(undefined, 100)
.find(
(entry) =>
entry.event === "request.failed" &&
(entry.payload as { id?: unknown } | undefined)?.id === traceId
);
assert.ok(lifecycle, "request.failed must be retained for dashboard lifecycle cleanup");
const lifecyclePayload = JSON.stringify(lifecycle.payload);
assert.equal(lifecyclePayload.includes(rawCue), false);
assert.match(lifecyclePayload, /omitted: video transcript/);
const auditRow = coreDb
.getDbInstance()
.prepare(
"SELECT details FROM audit_log WHERE action = 'provider.warning' AND request_id = ? ORDER BY id DESC LIMIT 1"
)
.get("skill-video-transcript-sensitive") as { details?: string } | undefined;
assert.ok(auditRow, "provider.warning existence must survive transcript redaction");
assert.equal(String(auditRow.details).includes(rawCue), false);
assert.match(String(auditRow.details), /omitted: video transcript/);
});

View File

@@ -17,13 +17,11 @@ process.env.DATA_DIR = testDataDir;
// Dynamic imports AFTER DATA_DIR is set so core.ts picks up the temp path. // Dynamic imports AFTER DATA_DIR is set so core.ts picks up the temp path.
const coreDb = await import("../../src/lib/db/core.ts"); const coreDb = await import("../../src/lib/db/core.ts");
const upstreamProxyDb = await import("../../src/lib/db/upstreamProxy.ts"); const upstreamProxyDb = await import("../../src/lib/db/upstreamProxy.ts");
const { resolveExecutorWithProxy } = await import( const { resolveExecutorWithProxy } =
"../../open-sse/handlers/chatCore/executorProxy.ts" await import("../../open-sse/handlers/chatCore/executorProxy.ts");
);
const { getExecutor } = await import("../../open-sse/executors/index.ts"); const { getExecutor } = await import("../../open-sse/executors/index.ts");
const { clearUpstreamProxyConfigCache } = await import( const { clearUpstreamProxyConfigCache } =
"../../open-sse/handlers/chatCore/comboContextCache.ts" await import("../../open-sse/handlers/chatCore/comboContextCache.ts");
);
before(async () => { before(async () => {
await coreDb.ensureDbInitialized(); await coreDb.ensureDbInitialized();
@@ -137,3 +135,68 @@ test("connection override wins over provider mode 'fallback'", async () => {
// Connection override short-circuits to the passthrough executor, not the fallback wrapper. // Connection override short-circuits to the passthrough executor, not the fallback wrapper.
assert.equal(exec, await getExecutor("cliproxyapi")); assert.equal(exec, await getExecutor("cliproxyapi"));
}); });
test("fallback diagnostics omit transcript echoes while preserving operational errors", async () => {
const nativeSentinel = "PRIVATE_NATIVE_VIDEO_TRANSCRIPT_SENTINEL";
const thrownFallbackSentinel = "PRIVATE_THROWN_FALLBACK_VIDEO_TRANSCRIPT_SENTINEL";
const statusFallbackSentinel = "PRIVATE_STATUS_FALLBACK_VIDEO_TRANSCRIPT_SENTINEL";
const retainedLogs: string[] = [];
const log = {
info: (...args: unknown[]) => retainedLogs.push(args.map(String).join(" ")),
error: (...args: unknown[]) => retainedLogs.push(args.map(String).join(" ")),
};
await upstreamProxyDb.upsertUpstreamProxyConfig({
providerId: "openai",
mode: "fallback",
enabled: true,
});
clearUpstreamProxyConfigCache("openai");
const nativeExecutor = await getExecutor("openai");
const fallbackExecutor = await getExecutor("cliproxyapi");
const originalNativeExecute = nativeExecutor.execute;
const originalFallbackExecute = fallbackExecutor.execute;
const input = {
model: "video-model",
body: { messages: [{ role: "user", content: "describe the video" }] },
stream: false,
credentials: { apiKey: "test-key" },
videoTranscriptSensitive: true,
};
try {
nativeExecutor.execute = async () => {
throw new Error(nativeSentinel);
};
fallbackExecutor.execute = async () => {
throw new Error(thrownFallbackSentinel);
};
const thrownWrapper = await resolveExecutorWithProxy("openai", log);
await assert.rejects(thrownWrapper.execute(input), new RegExp(thrownFallbackSentinel));
nativeExecutor.execute = async () => ({
response: new Response("retryable", { status: 500 }),
url: "https://native.example.test/v1/chat/completions",
headers: {},
transformedBody: input.body,
});
fallbackExecutor.execute = async () => {
throw new Error(statusFallbackSentinel);
};
const statusWrapper = await resolveExecutorWithProxy("openai", log);
await assert.rejects(statusWrapper.execute(input), new RegExp(statusFallbackSentinel));
const retained = retainedLogs.join("\n");
for (const sentinel of [nativeSentinel, thrownFallbackSentinel, statusFallbackSentinel]) {
assert.doesNotMatch(retained, new RegExp(sentinel));
}
assert.match(retained, /omitted: video transcript/);
} finally {
nativeExecutor.execute = originalNativeExecute;
fallbackExecutor.execute = originalFallbackExecute;
clearUpstreamProxyConfigCache("openai");
}
});

View File

@@ -38,7 +38,11 @@ test("sanitizeChatRequestBody: Responses target maps max_tokens → max_output_t
// #9161: token-field selection keys on the OUTBOUND (target) protocol only — a // #9161: token-field selection keys on the OUTBOUND (target) protocol only — a
// Responses-shaped SOURCE no longer forces max_output_tokens (see // Responses-shaped SOURCE no longer forces max_output_tokens (see
// codex-responses-to-chat-9161.test.ts for that direction). // codex-responses-to-chat-9161.test.ts for that direction).
const out = sanitizeChatRequestBody({ max_tokens: 128 }, FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES); const out = sanitizeChatRequestBody(
{ max_tokens: 128 },
FORMATS.OPENAI,
FORMATS.OPENAI_RESPONSES
);
assert.equal(out.max_output_tokens, 128); assert.equal(out.max_output_tokens, 128);
assert.equal(out.max_tokens, undefined); assert.equal(out.max_tokens, undefined);
}); });
@@ -80,6 +84,7 @@ test("checkIdempotencyCache returns { hit:null, idempotencyKey } on a miss", asy
effectiveServiceTier: undefined, effectiveServiceTier: undefined,
startTime: 0, startTime: 0,
log: undefined, log: undefined,
videoTranscriptSensitive: false,
}); });
assert.equal(result.hit, null); assert.equal(result.hit, null);
// #6558: the raw header key is now namespaced by provider/model + a messages // #6558: the raw header key is now namespaced by provider/model + a messages
@@ -115,13 +120,42 @@ test("checkIdempotencyCache returns a hit Response reusing the same key after a
effectiveServiceTier: undefined, effectiveServiceTier: undefined,
startTime: 0, startTime: 0,
log: undefined, log: undefined,
videoTranscriptSensitive: false,
}); });
assert.equal(result.idempotencyKey, key, "the resolved key is returned for the save site to reuse"); assert.equal(
result.idempotencyKey,
key,
"the resolved key is returned for the save site to reuse"
);
assert.ok(result.hit, "a cached entry produces a hit"); assert.ok(result.hit, "a cached entry produces a hit");
assert.equal(result.hit!.response.headers.get("X-OmniRoute-Idempotent"), "true"); assert.equal(result.hit!.response.headers.get("X-OmniRoute-Idempotent"), "true");
}); });
test("checkIdempotencyCache bypasses an existing transcript-sensitive entry", async () => {
const rawKey = "idem-private-video-3821";
const key = composeIdempotencyKey({
rawKey,
provider: "openai",
model: "gpt-4.1",
messages: undefined,
})!;
saveIdempotency(key, { content: "PRIVATE_IDEMPOTENCY_TRANSCRIPT_SENTINEL" }, 200);
const result = await checkIdempotencyCache({
clientRawRequest: { headers: new Headers({ "idempotency-key": rawKey }) },
provider: "openai",
model: "gpt-4.1",
effectiveServiceTier: undefined,
startTime: 0,
log: undefined,
videoTranscriptSensitive: true,
});
assert.equal(result.hit, null);
assert.equal(result.idempotencyKey, null);
});
test("checkIdempotencyCache resolves a null key when no idempotency headers are present", async () => { test("checkIdempotencyCache resolves a null key when no idempotency headers are present", async () => {
const result = await checkIdempotencyCache({ const result = await checkIdempotencyCache({
clientRawRequest: { headers: new Headers() }, clientRawRequest: { headers: new Headers() },
@@ -130,6 +164,7 @@ test("checkIdempotencyCache resolves a null key when no idempotency headers are
effectiveServiceTier: undefined, effectiveServiceTier: undefined,
startTime: 0, startTime: 0,
log: undefined, log: undefined,
videoTranscriptSensitive: false,
}); });
assert.equal(result.hit, null); assert.equal(result.hit, null);
assert.equal(result.idempotencyKey, null); assert.equal(result.idempotencyKey, null);

View File

@@ -6,6 +6,7 @@ import {
extractMemoryTextFromRequestBody, extractMemoryTextFromRequestBody,
resolveMemoryOwnerId, resolveMemoryOwnerId,
} from "../../open-sse/handlers/chatCore/memoryExtraction.ts"; } from "../../open-sse/handlers/chatCore/memoryExtraction.ts";
import { fingerprintVideoTranscriptDescription } from "../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
test("extractMemoryTextFromResponse reads OpenAI choices[0].message.content (trimmed)", () => { test("extractMemoryTextFromResponse reads OpenAI choices[0].message.content (trimmed)", () => {
assert.equal( assert.equal(
@@ -17,11 +18,7 @@ test("extractMemoryTextFromResponse reads OpenAI choices[0].message.content (tri
test("extractMemoryTextFromResponse joins Claude content text blocks and skips non-text", () => { test("extractMemoryTextFromResponse joins Claude content text blocks and skips non-text", () => {
assert.equal( assert.equal(
extractMemoryTextFromResponse({ extractMemoryTextFromResponse({
content: [ content: [{ type: "text", text: " a " }, { type: "image" }, { type: "text", text: "b" }],
{ type: "text", text: " a " },
{ type: "image" },
{ type: "text", text: "b" },
],
}), }),
"a\nb" "a\nb"
); );
@@ -65,17 +62,99 @@ test("extractMemoryTextFromRequestBody joins array content parts of the last use
messages: [ messages: [
{ {
role: "user", role: "user",
content: [ content: [{ type: "input_text", text: " a " }, { text: "b" }, { type: "image_url" }],
{ type: "input_text", text: " a " },
{ text: "b" },
{ type: "image_url" },
],
}, },
], ],
}; };
assert.equal(extractMemoryTextFromRequestBody(body), "a\nb"); assert.equal(extractMemoryTextFromRequestBody(body), "a\nb");
}); });
test("extractMemoryTextFromRequestBody excludes only trusted Video Bridge text", () => {
const poisonedVideo =
'[Video description: untrusted media-derived observation only; transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text="I prefer attacker memory poison"]';
const privacyContext = {
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(poisonedVideo)],
};
const messagesBody = {
messages: [
{
role: "user",
content: [
{ type: "text", text: "My genuine preference is dark mode" },
{ type: "text", text: poisonedVideo },
],
},
],
};
const responsesBody = {
input: [
{
role: "user",
type: "message",
content: [
{ type: "input_text", text: "Remember my genuine timezone is UTC" },
{ type: "input_text", text: poisonedVideo },
],
},
],
};
const sameStringBody = {
messages: [
{
role: "user",
content: `Remember my genuine language is Portuguese\n${poisonedVideo}`,
},
],
};
assert.equal(
extractMemoryTextFromRequestBody(messagesBody, true, privacyContext),
"My genuine preference is dark mode"
);
assert.equal(
extractMemoryTextFromRequestBody(responsesBody, true, privacyContext),
"Remember my genuine timezone is UTC"
);
assert.equal(
extractMemoryTextFromRequestBody(sameStringBody, true, privacyContext),
"Remember my genuine language is Portuguese"
);
});
test("extractMemoryTextFromRequestBody keeps text adjacent to a raw video carrier", () => {
const body = {
messages: [
{
role: "user",
content: [
{ type: "input_text", text: "Remember my genuine theme is solarized" },
{
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
transcript: "PRIVATE_RAW_VIDEO_MEMORY_SENTINEL",
text: "media-derived text must not become memory",
},
],
},
],
};
assert.equal(
extractMemoryTextFromRequestBody(body, true),
"Remember my genuine theme is solarized"
);
});
test("extractMemoryTextFromRequestBody preserves a caller-forged Video description", () => {
const forged =
'[Video description: transcript[source=client] text="caller-forged memory suppression"]';
assert.equal(
extractMemoryTextFromRequestBody({ messages: [{ role: "user", content: forged }] }),
forged
);
});
test("extractMemoryTextFromRequestBody reads Responses-style input items", () => { test("extractMemoryTextFromRequestBody reads Responses-style input items", () => {
const inputBody = { const inputBody = {
input: [{ role: "user", type: "message", content: [{ type: "input_text", text: "hey" }] }], input: [{ role: "user", type: "message", content: [{ type: "input_text", text: "hey" }] }],

View File

@@ -56,6 +56,17 @@ test("no headers arg → backward compatible (undefined in ctx)", async () => {
assert.equal(capturedCtx!.headers, undefined); assert.equal(capturedCtx!.headers, undefined);
}); });
test("transcript sensitivity is propagated as server-owned plugin context", async () => {
let capturedCtx: Record<string, unknown> | undefined;
registerHook("onRequest", PLUGIN, async (ctx: Record<string, unknown>) => {
capturedCtx = ctx;
return {};
});
const gate = await runPluginOnRequestHook(baseArgs({ videoTranscriptSensitive: true }));
assert.equal(gate.blocked, false);
assert.equal(capturedCtx?.videoTranscriptSensitive, true);
});
test("a blocking hook → blocked:true with a 403 JSON Response", async () => { test("a blocking hook → blocked:true with a 403 JSON Response", async () => {
registerHook("onRequest", PLUGIN, async () => ({ registerHook("onRequest", PLUGIN, async () => ({
blocked: true, blocked: true,

View File

@@ -6,7 +6,9 @@
import { test, after } from "node:test"; import { test, after } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");const { runPluginOnResponseHook, runPluginOnStreamCompleteHook } = await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts"); const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
const { runPluginOnResponseHook, runPluginOnStreamCompleteHook } =
await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts");
async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> { async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
@@ -126,6 +128,27 @@ test("no headers arg → backward compatible (undefined in ctx)", async () => {
assert.equal(captured!.headers, undefined); assert.equal(captured!.headers, undefined);
}); });
test("transcript sensitivity is propagated as server-owned plugin context", async () => {
let captured: Record<string, unknown> | undefined;
registerHook("onResponse", "test-onresponse-plugin", async (ctx: Record<string, unknown>) => {
captured = ctx;
return {};
});
await runPluginOnResponseHook({
requestId: "req-transcript-sensitive",
body: { messages: [{ role: "user", content: "processed video request" }] },
model: "gpt-4o",
provider: "openai",
apiKeyInfo: null,
response: { status: 200, data: { ok: true } },
videoTranscriptSensitive: true,
});
await waitFor(() => captured !== undefined);
assert.equal(captured?.videoTranscriptSensitive, true);
});
test("a throwing hook never rejects the caller (fail-open)", async () => { test("a throwing hook never rejects the caller (fail-open)", async () => {
registerHook("onResponse", "test-onresponse-plugin", async () => { registerHook("onResponse", "test-onresponse-plugin", async () => {
throw new Error("boom"); throw new Error("boom");

View File

@@ -7,9 +7,8 @@
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
const { buildPostCallGuardrailContext } = await import( const { buildPostCallGuardrailContext } =
"../../open-sse/handlers/chatCore/postCallGuardrailContext.ts" await import("../../open-sse/handlers/chatCore/postCallGuardrailContext.ts");
);
function baseArgs(overrides: Record<string, unknown> = {}) { function baseArgs(overrides: Record<string, unknown> = {}) {
return { return {
@@ -47,6 +46,11 @@ test("maps fields, constants, and source/target formats", () => {
}); });
}); });
test("propagates the server-owned transcript sensitivity bit", () => {
const ctx = buildPostCallGuardrailContext(baseArgs({ videoTranscriptSensitive: true }), () => []);
assert.equal(ctx.videoTranscriptSensitive, true);
});
test("null clientRawRequest → endpoint/headers null", () => { test("null clientRawRequest → endpoint/headers null", () => {
const ctx = buildPostCallGuardrailContext(baseArgs({ clientRawRequest: null }), () => []); const ctx = buildPostCallGuardrailContext(baseArgs({ clientRawRequest: null }), () => []);
assert.equal(ctx.endpoint, null); assert.equal(ctx.endpoint, null);

View File

@@ -45,6 +45,7 @@ function baseArgs(overrides: Record<string, unknown> = {}) {
apiKeyId: "key-1", apiKeyId: "key-1",
usage: { prompt_tokens: 10, completion_tokens: 5 }, usage: { prompt_tokens: 10, completion_tokens: 5 },
log: undefined, log: undefined,
videoTranscriptSensitive: false,
...overrides, ...overrides,
} as Parameters<typeof storeSemanticCacheResponse>[0]; } as Parameters<typeof storeSemanticCacheResponse>[0];
} }
@@ -65,6 +66,7 @@ function assertNumericSignatureInputs(
headers: undefined, headers: undefined,
translatedResponse: {}, translatedResponse: {},
model: "gpt-x", model: "gpt-x",
videoTranscriptSensitive: false,
}, },
deps deps
); );
@@ -94,6 +96,21 @@ test("disabled → no store, no gate calls past enabled", () => {
assert.equal(calls.cacheable, 0); assert.equal(calls.cacheable, 0);
}); });
test("transcript-sensitive responses never enter the durable semantic cache", () => {
const sentinel = "PRIVATE_NONSTREAM_SEMANTIC_CACHE_SENTINEL";
const { deps, stored, calls } = makeDeps();
storeSemanticCacheResponse(
baseArgs({
translatedResponse: { choices: [{ message: { content: sentinel } }] },
videoTranscriptSensitive: true,
}),
deps
);
assert.equal(stored.length, 0);
assert.equal(calls.cacheable, 0);
});
test("not cacheable-for-write → no store", () => { test("not cacheable-for-write → no store", () => {
const { deps, stored } = makeDeps({ isCacheableForWrite: () => false }); const { deps, stored } = makeDeps({ isCacheableForWrite: () => false });
storeSemanticCacheResponse(baseArgs(), deps); storeSemanticCacheResponse(baseArgs(), deps);

View File

@@ -53,6 +53,7 @@ function makeBaseArgs(overrides: Record<string, unknown> = {}) {
persistCalls.push(a); persistCalls.push(a);
}, },
apiKeyId: null as string | null, apiKeyId: null as string | null,
videoTranscriptSensitive: false,
...overrides, ...overrides,
}; };
return { args, persistCalls }; return { args, persistCalls };
@@ -173,6 +174,7 @@ function makeHitArgs(overrides: Record<string, unknown> = {}) {
persistCalls.push(a as Record<string, unknown>); persistCalls.push(a as Record<string, unknown>);
}, },
apiKeyId: null as string | null, apiKeyId: null as string | null,
videoTranscriptSensitive: false,
...overrides, ...overrides,
}; };
return { args, persistCalls, convertedCalls, debugCalls }; return { args, persistCalls, convertedCalls, debugCalls };
@@ -191,6 +193,27 @@ function seedHit(args: ReturnType<typeof makeHitArgs>["args"], response: unknown
return signature; return signature;
} }
test("checkSemanticCache bypasses an existing sensitive cache entry", async () => {
clearCache();
const sentinel = "PRIVATE_SEMANTIC_CACHE_HIT_TRANSCRIPT_SENTINEL";
const { args, persistCalls, convertedCalls, debugCalls } = makeHitArgs({
body: {
model: "gpt-4o",
messages: [{ role: "user", content: "sensitive cached query" }],
temperature: 0,
},
videoTranscriptSensitive: true,
});
seedHit(args, { choices: [{ message: { content: sentinel } }] });
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
assert.equal(result, null);
assert.equal(persistCalls.length, 0);
assert.equal(convertedCalls.length, 0);
assert.equal(debugCalls.length, 0);
});
test("checkSemanticCache returns a non-streaming JSON HIT with cache headers + logging side effects", async () => { test("checkSemanticCache returns a non-streaming JSON HIT with cache headers + logging side effects", async () => {
clearCache(); clearCache();
const cached = { const cached = {

View File

@@ -39,6 +39,7 @@ function baseArgs(overrides: Record<string, unknown> = {}) {
apiKeyId: "key-1", apiKeyId: "key-1",
streamUsage: { prompt_tokens: 12, completion_tokens: 8 }, streamUsage: { prompt_tokens: 12, completion_tokens: 8 },
log: undefined, log: undefined,
videoTranscriptSensitive: false,
...overrides, ...overrides,
} as Parameters<typeof storeStreamingSemanticCacheResponse>[0]; } as Parameters<typeof storeStreamingSemanticCacheResponse>[0];
} }
@@ -60,6 +61,7 @@ function assertNumericSignatureInputs(
}, },
headers: undefined, headers: undefined,
model: "gpt-x", model: "gpt-x",
videoTranscriptSensitive: false,
}, },
deps deps
); );
@@ -95,6 +97,20 @@ test("disabled → no store", () => {
assert.equal(stored.length, 0); assert.equal(stored.length, 0);
}); });
test("transcript-sensitive streams never enter the durable semantic cache", () => {
const sentinel = "PRIVATE_STREAM_SEMANTIC_CACHE_SENTINEL";
const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(
baseArgs({
streamResponseBody: { choices: [{ message: { content: sentinel } }] },
videoTranscriptSensitive: true,
}),
deps
);
assert.equal(stored.length, 0);
});
test("missing response body → no store", () => { test("missing response body → no store", () => {
const { deps, stored } = makeDeps(); const { deps, stored } = makeDeps();
storeStreamingSemanticCacheResponse(baseArgs({ streamResponseBody: null }), deps); storeStreamingSemanticCacheResponse(baseArgs({ streamResponseBody: null }), deps);

View File

@@ -124,6 +124,20 @@ test("progress disabled → no progress header", () => {
assert.deepEqual(args.responseHeaders, {}); assert.deepEqual(args.responseHeaders, {});
}); });
test("pipeline forwards transcript-sensitive diagnostic retention policy", () => {
let receivedOptions: PipeWithDisconnectParameters[3] | undefined;
const { deps } = makeDeps({
pipeWithDisconnect: (...args: PipeWithDisconnectParameters) => {
receivedOptions = args[3];
return fakeStream("pii-base", []);
},
});
assembleStreamingPipeline(baseArgs({ redactStreamDiagnosticsForLog: true }), deps);
assert.equal(receivedOptions?.redactStreamDiagnosticsForLog, true);
});
test("echoModel set → echo transform applied last", () => { test("echoModel set → echo transform applied last", () => {
const { deps, log } = makeDeps(); const { deps, log } = makeDeps();
assembleStreamingPipeline(baseArgs({ echoModel: "alias-x" }), deps); assembleStreamingPipeline(baseArgs({ echoModel: "alias-x" }), deps);

View File

@@ -372,6 +372,7 @@ async function invokeChatCore({
reasoningTransportFallback = "drop", reasoningTransportFallback = "drop",
managedLease = null, managedLease = null,
cachedSettings = null, cachedSettings = null,
log = noopLog(),
}: any = {}) { }: any = {}) {
const calls: any[] = []; const calls: any[] = [];
@@ -406,7 +407,7 @@ async function invokeChatCore({
apiKey: "sk-test", apiKey: "sk-test",
providerSpecificData: {}, providerSpecificData: {},
}, },
log: noopLog(), log,
clientRawRequest: { clientRawRequest: {
endpoint, endpoint,
body: structuredClone(body), body: structuredClone(body),
@@ -2012,6 +2013,53 @@ test("chatCore returns 500 when translation throws a generic error", async () =>
assert.equal(result.status, 500); assert.equal(result.status, 500);
assert.equal(result.error, "unexpected translator crash"); assert.equal(result.error, "unexpected translator crash");
}); });
test("chatCore keeps transcript-sensitive translation errors raw for the client but not logs", async () => {
const sentinel = "PRIVATE_TRANSLATION_ERROR_TRANSCRIPT_SENTINEL";
const warnCalls: unknown[][] = [];
register(
FORMATS.OPENAI_RESPONSES,
FORMATS.OPENAI,
() => {
throw new Error(sentinel);
},
null
);
const { result } = await invokeChatCore({
provider: "openai",
model: "gpt-4o-mini",
endpoint: "/v1/responses",
body: {
model: "gpt-4o-mini",
input: [
{
content: [
{
transcript: sentinel,
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
],
role: "user",
},
],
},
log: {
debug() {},
info() {},
warn(...args: unknown[]) {
warnCalls.push(args);
},
error() {},
},
});
assert.equal(result.success, false);
assert.equal(result.error, sentinel);
const serializedLogs = JSON.stringify(warnCalls);
assert.equal(serializedLogs.includes(sentinel), false);
assert.match(serializedLogs, /omitted: video transcript/);
});
test("chatCore refreshes GitHub credentials after 401 and retries with the refreshed Copilot token", async () => { test("chatCore refreshes GitHub credentials after 401 and retries with the refreshed Copilot token", async () => {
let refreshedCredentials = null; let refreshedCredentials = null;
const { calls, result } = await invokeChatCore({ const { calls, result } = await invokeChatCore({

View File

@@ -17,6 +17,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-10597-test-secret"; process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-10597-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts"); const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const { getEventHistory } = await import("../../src/lib/events/eventBus.ts");
const DISTINCTIVE_ERROR_TEXT = const DISTINCTIVE_ERROR_TEXT =
"messages.450: `tool_use` ids were found without `tool_result` blocks immediately after"; "messages.450: `tool_use` ids were found without `tool_result` blocks immediately after";
@@ -48,14 +49,24 @@ function healthy200(model: string) {
id: "ok", id: "ok",
object: "chat.completion", object: "chat.completion",
model, model,
choices: [{ index: 0, message: { role: "assistant", content: "hello from " + model }, finish_reason: "stop" }], choices: [
{
index: 0,
message: { role: "assistant", content: "hello from " + model },
finish_reason: "stop",
},
],
}), }),
{ status: 200, headers: { "Content-Type": "application/json" } } { status: 200, headers: { "Content-Type": "application/json" } }
); );
} }
function makeCombo(models: string[]) { function makeCombo(models: string[]) {
return { name: "test-combo-10597", strategy: "priority", models: models.map((m) => ({ model: m })) }; return {
name: "test-combo-10597",
strategy: "priority",
models: models.map((m) => ({ model: m })),
};
} }
test("#10597 COMBO failure log must surface the upstream error body, not just the status code", async () => { test("#10597 COMBO failure log must surface the upstream error body, not just the status code", async () => {
@@ -79,7 +90,10 @@ test("#10597 COMBO failure log must surface the upstream error body, not just th
assert.equal(modelsCalled.length, 2); assert.equal(modelsCalled.length, 2);
const failureLog = warnCalls.find( const failureLog = warnCalls.find(
(c) => typeof c.msg === "string" && c.msg.includes("claude/claude-opus-4-8") && c.msg.includes("failed") (c) =>
typeof c.msg === "string" &&
c.msg.includes("claude/claude-opus-4-8") &&
c.msg.includes("failed")
); );
assert.ok(failureLog, "expected a COMBO warn log for the failing leg"); assert.ok(failureLog, "expected a COMBO warn log for the failing leg");
@@ -89,3 +103,200 @@ test("#10597 COMBO failure log must surface the upstream error body, not just th
`expected the upstream error body to appear in the COMBO failure log, but got: ${serialized}` `expected the upstream error body to appear in the COMBO failure log, but got: ${serialized}`
); );
}); });
test("transcript-sensitive combo failures omit echoed transcript only from retained logs", async () => {
const transcriptSentinel = "PRIVATE_COMBO_ERROR_TRANSCRIPT_SENTINEL";
const localWarnCalls: WarnCall[] = [];
const modelsCalled: string[] = [];
const result = await handleComboChat({
body: { model: "test", messages: [{ role: "user", content: "processed video request" }] },
combo: makeCombo(["claude/private-video", "openai/private-video-fallback"]),
handleSingleModel: async (_body: unknown, modelStr: string) => {
modelsCalled.push(modelStr);
if (modelsCalled.length === 1) {
return new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
return healthy200(modelStr);
},
log: {
info: () => {},
debug: () => {},
error: () => {},
warn: (tag: string, msg: string, meta?: unknown) => {
localWarnCalls.push({ tag, msg, meta });
},
},
settings: {},
allCombos: [],
videoTranscriptSensitive: true,
});
assert.equal(result.status, 200);
assert.equal(modelsCalled.length, 2);
const retainedFailure = localWarnCalls.find((call) => call.msg.includes("failed, trying next"));
assert.ok(retainedFailure);
const serialized = JSON.stringify(retainedFailure);
assert.equal(serialized.includes(transcriptSentinel), false);
assert.match(serialized, /omitted: video transcript/);
});
test("transcript-sensitive round-robin failures omit echoed transcript from retained logs", async () => {
const transcriptSentinel = "PRIVATE_COMBO_RR_ERROR_TRANSCRIPT_SENTINEL";
const localWarnCalls: WarnCall[] = [];
const result = await handleComboChat({
body: { model: "test", messages: [{ role: "user", content: "processed video request" }] },
combo: {
name: "test-combo-10597-private-rr",
strategy: "round-robin",
models: [{ model: "claude/private-video-rr" }],
config: { maxRetries: 0 },
},
handleSingleModel: async () =>
new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 400,
headers: { "Content-Type": "application/json" },
}),
log: {
info: () => {},
debug: () => {},
error: () => {},
warn: (tag: string, msg: string, meta?: unknown) => {
localWarnCalls.push({ tag, msg, meta });
},
},
settings: {},
allCombos: [],
videoTranscriptSensitive: true,
});
assert.equal(result.ok, false);
const retainedFailure = localWarnCalls.find((call) => call.tag === "COMBO-RR");
assert.ok(retainedFailure);
const serialized = JSON.stringify(retainedFailure);
assert.equal(serialized.includes(transcriptSentinel), false);
assert.match(serialized, /omitted: video transcript/);
});
test("transcript-sensitive masked-200 quality failures omit transcript from logs and live events", async () => {
const transcriptSentinel = "PRIVATE_COMBO_QUALITY_TRANSCRIPT_SENTINEL";
const comboName = "test-combo-10597-private-quality";
const localWarnCalls: WarnCall[] = [];
const modelsCalled: string[] = [];
const result = await handleComboChat({
body: { model: "test", messages: [{ role: "user", content: "processed video request" }] },
combo: {
...makeCombo(["claude/private-quality", "openai/private-quality-fallback"]),
name: comboName,
},
handleSingleModel: async (_body: unknown, modelStr: string) => {
modelsCalled.push(modelStr);
if (modelsCalled.length === 1) {
return new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return healthy200(modelStr);
},
log: {
info: () => {},
debug: () => {},
error: () => {},
warn: (tag: string, msg: string, meta?: unknown) => {
localWarnCalls.push({ tag, msg, meta });
},
},
settings: {},
allCombos: [],
videoTranscriptSensitive: true,
});
assert.equal(result.status, 200);
assert.equal(modelsCalled.length, 2);
const retainedLogs = JSON.stringify(localWarnCalls);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
const failedEvent = getEventHistory(undefined, 100).find((entry) => {
if (entry.event !== "combo.target.failed") return false;
return (entry.payload as { comboName?: string }).comboName === comboName;
});
assert.ok(failedEvent, "expected a retained combo.target.failed event for the quality rejection");
const retainedEvent = JSON.stringify(failedEvent);
assert.equal(retainedEvent.includes(transcriptSentinel), false);
assert.match(retainedEvent, /omitted: video transcript/);
});
test("transcript-sensitive round-robin masked-200 failures omit quality echoes from every log", async () => {
const transcriptSentinel = "PRIVATE_COMBO_RR_QUALITY_TRANSCRIPT_SENTINEL";
const localWarnCalls: WarnCall[] = [];
const result = await handleComboChat({
body: { model: "test", messages: [{ role: "user", content: "processed video request" }] },
combo: {
name: "test-combo-10597-private-rr-quality",
strategy: "round-robin",
models: [{ model: "claude/private-video-rr-quality" }],
config: { maxRetries: 0 },
},
handleSingleModel: async () =>
new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
log: {
info: () => {},
debug: () => {},
error: () => {},
warn: (tag: string, msg: string, meta?: unknown) => {
localWarnCalls.push({ tag, msg, meta });
},
},
settings: {},
allCombos: [],
videoTranscriptSensitive: true,
});
assert.equal(result.ok, false);
const retainedLogs = JSON.stringify(localWarnCalls);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
});
test("transcript-sensitive terminal logs omit the echo while the functional client error stays intact", async () => {
const transcriptSentinel = "PRIVATE_COMBO_TERMINAL_TRANSCRIPT_SENTINEL";
const localWarnCalls: WarnCall[] = [];
const result = await handleComboChat({
body: { model: "test", messages: [{ role: "user", content: "processed video request" }] },
combo: {
name: "test-combo-10597-private-terminal",
strategy: "priority",
models: [{ model: "claude/private-video-terminal" }],
config: { maxRetries: 0 },
},
handleSingleModel: async () =>
new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 500,
headers: { "Content-Type": "application/json" },
}),
log: {
info: () => {},
debug: () => {},
error: () => {},
warn: (tag: string, msg: string, meta?: unknown) => {
localWarnCalls.push({ tag, msg, meta });
},
},
settings: {},
allCombos: [],
videoTranscriptSensitive: true,
});
assert.equal(result.ok, false);
assert.match(await result.text(), new RegExp(transcriptSentinel));
const retainedLogs = JSON.stringify(localWarnCalls);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
});

View File

@@ -282,6 +282,89 @@ test("handleComboChat context-relay persists a handoff when codex quota reaches
assert.equal(saved.fromAccount, connectionId); assert.equal(saved.fromAccount, connectionId);
}); });
test("handleComboChat persists a transcript-sensitive handoff from a safely retained copy", async () => {
const sessionId = "sess-private-video";
const connectionId = "conn-private-video";
const transcriptSentinel = "PRIVATE_COMBO_CONTEXT_RELAY_TRANSCRIPT_SENTINEL";
touchSession(sessionId, connectionId);
registerCodexConnection(connectionId, {
accessToken: "token-private-video",
workspaceId: "ws-private-video",
});
let summaryCalls = 0;
let liveRequest = "";
let retainedSummaryRequest = "";
globalThis.fetch = async (url) => {
if (String(url).includes("/backend-api/wham/usage")) return buildQuotaResponse(90);
throw new Error(`Unexpected fetch: ${String(url)}`);
};
const result = await handleComboChat({
body: {
messages: [
{
role: "user",
content: [
{ type: "input_text", text: "Keep this adjacent combo context" },
{
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
transcript: transcriptSentinel,
},
],
},
],
},
combo: {
name: "relay-private-video",
strategy: "context-relay",
models: ["codex/gpt-5.6-sol"],
config: { maxRetries: 0, handoffThreshold: 0.85, handoffProviders: ["codex"] },
},
handleSingleModel: async (body) => {
if (body._omnirouteInternalRequest === "context-handoff") {
summaryCalls += 1;
retainedSummaryRequest = JSON.stringify(body);
return okResponse({
choices: [
{
message: {
content: JSON.stringify({
summary: "Safe combo-level handoff",
keyDecisions: [],
taskProgress: "continue",
activeEntities: [],
}),
},
},
],
});
}
liveRequest = JSON.stringify(body);
return okResponse();
},
isModelAvailable: async () => true,
log: createLog(),
settings: null,
allCombos: null,
relayOptions: {
sessionId,
config: { handoffThreshold: 0.85, handoffProviders: ["codex"] },
},
videoTranscriptSensitive: true,
});
const saved = await waitFor(() => handoffDb.getHandoff(sessionId, "relay-private-video"));
assert.equal(result.ok, true);
assert.match(liveRequest, new RegExp(transcriptSentinel));
assert.equal(summaryCalls, 1);
assert.ok(saved);
assert.match(retainedSummaryRequest, /Keep this adjacent combo context/);
assert.doesNotMatch(retainedSummaryRequest, new RegExp(transcriptSentinel));
assert.doesNotMatch(retainedSummaryRequest, /data:video/);
});
test("handleComboChat context-relay respects handoffProviders and skips generation when codex is disabled", async () => { test("handleComboChat context-relay respects handoffProviders and skips generation when codex is disabled", async () => {
const sessionId = "sess-disabled-provider"; const sessionId = "sess-disabled-provider";
const connectionId = "conn-disabled-provider"; const connectionId = "conn-disabled-provider";

View File

@@ -66,10 +66,17 @@ function okResponse(content: string): Response {
} }
function makeLog() { function makeLog() {
const records: Array<{ level: string; scope: string; msg: string }> = []; const records: Array<{
const cap = (level: string) => (scope: string, msg: string) => { level: string;
records.push({ level, scope, msg: String(msg) }); scope: string;
}; msg: string;
details: unknown[];
}> = [];
const cap =
(level: string) =>
(scope: string, msg: string, ...details: unknown[]) => {
records.push({ level, scope, msg: String(msg), details });
};
return { return {
log: { info: cap("info"), warn: cap("warn"), debug: cap("debug"), error: cap("error") }, log: { info: cap("info"), warn: cap("warn"), debug: cap("debug"), error: cap("error") },
records, records,
@@ -223,6 +230,36 @@ test("tryFusionDispatch: owns the request and synthesizes for the fusion strateg
assert.ok(dispatched.includes("p/panelA") && dispatched.includes("p/panelB")); assert.ok(dispatched.includes("p/panelA") && dispatched.includes("p/panelB"));
}); });
test("tryFusionDispatch: propagates transcript sensitivity into native fusion logs", async () => {
const transcriptSentinel = "PRIVATE_FUSION_PRELUDE_TRANSCRIPT_SENTINEL";
const ctx = setup({
name: "private-fusion",
strategy: "fusion",
models: [{ model: "p/failing" }, { model: "p/healthy" }],
config: { minPanel: 1 },
});
const res = await tryFusionDispatch({
body: ctx.body,
combo: ctx.combo,
cfg: ctx.config as unknown as Record<string, unknown>,
config: ctx.config,
strategy: "fusion",
allCombos: [],
handleSingleModel: async () => okResponse("unused"),
handleSingleModelWithTimeout: async (_body, modelStr) => {
if (modelStr === "p/failing") throw new Error(transcriptSentinel);
return okResponse("safe answer");
},
log: ctx.log,
runCombo: async () => okResponse("recursed"),
videoTranscriptSensitive: true,
});
assert.ok(res);
const retainedLogs = JSON.stringify(ctx.records);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
});
test("tryRuntimeUnitDispatch: falls through when the combo has no executable combo-ref", async () => { test("tryRuntimeUnitDispatch: falls through when the combo has no executable combo-ref", async () => {
const ctx = setup({ const ctx = setup({
name: "flat", name: "flat",
@@ -423,7 +460,8 @@ function pinCtx() {
async function dispatchHealthyPin( async function dispatchHealthyPin(
ctx: ReturnType<typeof pinCtx>, ctx: ReturnType<typeof pinCtx>,
handler: () => Promise<Response> handler: () => Promise<Response>,
videoTranscriptSensitive = false
) { ) {
await seedHealthyPinProvider(); await seedHealthyPinProvider();
const dispatched: string[] = []; const dispatched: string[] = [];
@@ -441,6 +479,7 @@ async function dispatchHealthyPin(
return handler(); return handler();
}, },
log: ctx.log, log: ctx.log,
videoTranscriptSensitive,
}); });
return { res, dispatched }; return { res, dispatched };
} }
@@ -534,6 +573,40 @@ test("tryPinnedModelDispatch: falls through when the pinned dispatch throws", as
); );
}); });
test("tryPinnedModelDispatch: omits transcript echoed by a masked-200 quality failure", async () => {
const transcriptSentinel = "PRIVATE_PINNED_QUALITY_TRANSCRIPT_SENTINEL";
const ctx = pinCtx();
const { res } = await dispatchHealthyPin(
ctx,
async () =>
new Response(JSON.stringify({ error: { message: transcriptSentinel } }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
true
);
assert.equal(res, null);
const retainedLogs = JSON.stringify(ctx.records);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
});
test("tryPinnedModelDispatch: omits transcript echoed by a thrown upstream error", async () => {
const transcriptSentinel = "PRIVATE_PINNED_THROW_TRANSCRIPT_SENTINEL";
const ctx = pinCtx();
const { res } = await dispatchHealthyPin(
ctx,
async () => {
throw new Error(transcriptSentinel);
},
true
);
assert.equal(res, null);
const retainedLogs = JSON.stringify(ctx.records);
assert.equal(retainedLogs.includes(transcriptSentinel), false);
assert.match(retainedLogs, /omitted: video transcript/);
});
/* ------------------------------------------------------------------------- * /* ------------------------------------------------------------------------- *
* Runtime-unit strategy ordering. * Runtime-unit strategy ordering.
* *

View File

@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";
test("trusted video description fingerprints reach handoffs through direct and nested combos", () => {
const typesSource = fs.readFileSync("open-sse/services/combo/types.ts", "utf8");
assert.match(
typesSource,
/videoTranscriptDescriptionFingerprints\?: readonly string\[\]/,
"the combo request contract must carry bounded identities"
);
const comboSource = fs.readFileSync("open-sse/services/combo.ts", "utf8");
for (const callName of ["maybeGenerateHandoff({", "maybeGenerateUniversalHandoff({"]) {
const callStart = comboSource.indexOf(callName);
assert.notEqual(callStart, -1, `${callName} must exist`);
const callSource = comboSource.slice(callStart, callStart + 1_500);
assert.match(
callSource,
/trustedDescriptionFingerprints:\s*videoTranscriptDescriptionFingerprints/,
`${callName} must receive the trusted identities`
);
}
const preludeSource = fs.readFileSync("open-sse/services/combo/dispatchPrelude.ts", "utf8");
const baseOptionsStart = preludeSource.indexOf("function buildBaseOptions(");
assert.notEqual(baseOptionsStart, -1);
const baseOptionsSource = preludeSource.slice(baseOptionsStart, baseOptionsStart + 2_000);
assert.match(
baseOptionsSource,
/videoTranscriptDescriptionFingerprints:\s*a\.videoTranscriptDescriptionFingerprints/,
"nested combos must preserve the identities"
);
const chatSource = fs.readFileSync("src/sse/handlers/chat.ts", "utf8");
const comboCallStarts = [
chatSource.indexOf("const response = await (handleComboChat as any)({"),
chatSource.indexOf("return handleComboChat({"),
];
for (const callStart of comboCallStarts) {
assert.notEqual(callStart, -1, "both primary and safety-net combo calls must exist");
const optionPrefix = chatSource.slice(callStart, callStart + 1_400);
assert.match(optionPrefix, /\n\s+videoTranscriptDescriptionFingerprints,\s*$/m);
}
});

View File

@@ -10,6 +10,8 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts"); const core = await import("../../src/lib/db/core.ts");
const handoffDb = await import("../../src/lib/db/contextHandoffs.ts"); const handoffDb = await import("../../src/lib/db/contextHandoffs.ts");
const contextHandoff = await import("../../open-sse/services/contextHandoff.ts"); const contextHandoff = await import("../../open-sse/services/contextHandoff.ts");
const { fingerprintVideoTranscriptDescription } =
await import("../../src/lib/guardrails/videoTranscriptLogRedaction.ts");
async function resetStorage() { async function resetStorage() {
core.resetDbInstance(); core.resetDbInstance();
@@ -152,6 +154,116 @@ test("maybeGenerateHandoff skips below the warning threshold", async () => {
assert.equal(handoffDb.getHandoff("sess-low", "relay-combo"), null); assert.equal(handoffDb.getHandoff("sess-low", "relay-combo"), null);
}); });
test("maybeGenerateHandoff preserves adjacent text while excluding a raw video carrier", async () => {
let retainedPrompt = "";
contextHandoff.maybeGenerateHandoff({
sessionId: "sess-private-video",
comboName: "relay-private-video",
connectionId: "conn-private-video",
percentUsed: 0.9,
messages: [
{
role: "user",
content: [
{ type: "input_text", text: "Keep this genuine handoff context" },
{
transcript: "PRIVATE_CONTEXT_HANDOFF_TRANSCRIPT_SENTINEL",
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
],
},
],
model: "codex/gpt-5.6-sol",
expiresAt: null,
config: { handoffProviders: ["codex"] },
handleSingleModel: async (body) => {
retainedPrompt = JSON.stringify(body);
return new Response(
JSON.stringify({
choices: [
{
message: {
content: JSON.stringify({
summary: "Safe raw-carrier handoff",
keyDecisions: [],
taskProgress: "continue",
activeEntities: [],
}),
},
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
},
});
const saved = await waitFor(() =>
handoffDb.getHandoff("sess-private-video", "relay-private-video")
);
assert.ok(saved);
assert.match(retainedPrompt, /Keep this genuine handoff context/);
assert.doesNotMatch(retainedPrompt, /PRIVATE_CONTEXT_HANDOFF_TRANSCRIPT_SENTINEL/);
assert.doesNotMatch(retainedPrompt, /data:video/);
});
test("maybeGenerateHandoff redacts a trusted serialized description before summarizing", async () => {
const sentinel = "PRIVATE_SERIALIZED_CONTEXT_HANDOFF_SENTINEL";
const description = `[Video description: stable scene; transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text="${sentinel}"]`;
let retainedPrompt = "";
contextHandoff.maybeGenerateHandoff({
sessionId: "sess-private-description",
comboName: "relay-private-description",
connectionId: "conn-private-description",
percentUsed: 0.9,
messages: [
{
role: "user",
content: [
{ type: "input_text", text: "Keep this adjacent request" },
{ type: "input_text", text: description },
],
},
],
model: "codex/gpt-5.6-sol",
expiresAt: null,
config: { handoffProviders: ["codex"] },
videoTranscriptSensitive: true,
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
handleSingleModel: async (body) => {
retainedPrompt = JSON.stringify(body);
return new Response(
JSON.stringify({
choices: [
{
message: {
content: JSON.stringify({
summary: "Safe serialized-description handoff",
keyDecisions: [],
taskProgress: "continue",
activeEntities: [],
}),
},
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
},
});
const saved = await waitFor(() =>
handoffDb.getHandoff("sess-private-description", "relay-private-description")
);
assert.ok(saved);
assert.match(retainedPrompt, /Keep this adjacent request/);
assert.match(retainedPrompt, /omitted: video transcript/);
assert.doesNotMatch(retainedPrompt, new RegExp(sentinel));
});
test("maybeGenerateHandoff persists a structured handoff once the threshold is reached", async () => { test("maybeGenerateHandoff persists a structured handoff once the threshold is reached", async () => {
const calls = []; const calls = [];

View File

@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";
import type { ExecutorLog } from "../../open-sse/executors/base.ts";
import { createExecutorRetentionLog } from "../../open-sse/handlers/chatCore/executorRetentionLog.ts";
const TRANSCRIPT_SENTINEL = "PRIVATE_EXECUTOR_DIAGNOSTIC_TRANSCRIPT_SENTINEL";
test("sensitive executor logs retain only a constant marker and discard metadata", () => {
const entries: Array<{ level: string; tag: string; message: string; data?: unknown }> = [];
const rawLog: ExecutorLog = {
debug: (tag, message, data) => entries.push({ level: "debug", tag, message, data }),
info: (tag, message, data) => entries.push({ level: "info", tag, message, data }),
warn: (tag, message, data) => entries.push({ level: "warn", tag, message, data }),
error: (tag, message, data) => entries.push({ level: "error", tag, message, data }),
};
const retainedLog = createExecutorRetentionLog(rawLog, true);
assert.ok(retainedLog);
for (const level of ["debug", "info", "warn", "error"] as const) {
retainedLog[level]?.("DEEPSEEK-WEB", `DeepSeek error: ${TRANSCRIPT_SENTINEL}`, {
upstream: TRANSCRIPT_SENTINEL,
});
}
assert.equal(entries.length, 4);
for (const entry of entries) {
assert.equal(entry.tag, "DEEPSEEK-WEB");
assert.equal(entry.message, "[omitted: video transcript]");
assert.equal(entry.data, undefined);
assert.equal(JSON.stringify(entry).includes(TRANSCRIPT_SENTINEL), false);
}
});
test("ordinary executor logs preserve their original logger and diagnostic", () => {
const messages: string[] = [];
const rawLog: ExecutorLog = {
warn: (_tag, message) => messages.push(message),
};
const retainedLog = createExecutorRetentionLog(rawLog, false);
assert.equal(retainedLog, rawLog);
retainedLog?.warn?.("DEEPSEEK-WEB", "ordinary upstream diagnostic");
assert.deepEqual(messages, ["ordinary upstream diagnostic"]);
});
test("every chatCore executor dispatch receives the retention-boundary logger", () => {
const source = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
const callMarker = "executor.execute({";
let offset = 0;
let calls = 0;
while (true) {
const start = source.indexOf(callMarker, offset);
if (start === -1) break;
calls += 1;
assert.match(
source.slice(start, start + 900),
/\n\s+log: retainedExecutorLog,/,
`executor dispatch ${calls} must not receive the raw request logger`
);
offset = start + callMarker.length;
}
assert.equal(calls, 3);
});

View File

@@ -35,7 +35,9 @@ function okResponse(content: string): Promise<Response> {
function errResponse(status: number): Promise<Response> { function errResponse(status: number): Promise<Response> {
const body = JSON.stringify({ error: { message: "boom" } }); const body = JSON.stringify({ error: { message: "boom" } });
return Promise.resolve(new Response(body, { status, headers: { "Content-Type": "application/json" } })); return Promise.resolve(
new Response(body, { status, headers: { "Content-Type": "application/json" } })
);
} }
// Mirrors the #6454 repro: an 11-member "fusion-free" style panel where only // Mirrors the #6454 repro: an 11-member "fusion-free" style panel where only
@@ -72,8 +74,14 @@ test("fusion #6454: a cooling minority (2/11) does not sink a healthy majority
tuning: { minPanel: 1, stragglerGraceMs: 4000, panelHardTimeoutMs: 60000 }, tuning: { minPanel: 1, stragglerGraceMs: 4000, panelHardTimeoutMs: 60000 },
}); });
assert.notEqual(res.status, 503, "9/11 healthy members must not be reported as a total panel failure"); assert.notEqual(
const body = (await res.clone().json()) as { choices?: Array<{ message?: { content?: string } }> }; res.status,
503,
"9/11 healthy members must not be reported as a total panel failure"
);
const body = (await res.clone().json()) as {
choices?: Array<{ message?: { content?: string } }>;
};
const text = body.choices?.[0]?.message?.content ?? ""; const text = body.choices?.[0]?.message?.content ?? "";
assert.ok(text.length > 0, "should carry a real synthesized/answer body, not an error"); assert.ok(text.length > 0, "should carry a real synthesized/answer body, not an error");
// The judge call is the final dispatch, invoked with every healthy answer available to it. // The judge call is the final dispatch, invoked with every healthy answer available to it.
@@ -94,7 +102,40 @@ test("fusion #6454: a genuinely all-failed 11-member panel still returns the doc
tuning: { minPanel: 1, stragglerGraceMs: 4000, panelHardTimeoutMs: 60000 }, tuning: { minPanel: 1, stragglerGraceMs: 4000, panelHardTimeoutMs: 60000 },
}); });
assert.equal(res.status, 503, "a genuinely all-failed panel must still surface the fusion failure error"); assert.equal(
res.status,
503,
"a genuinely all-failed panel must still surface the fusion failure error"
);
const body = (await res.clone().json()) as { error: { message: string } }; const body = (await res.clone().json()) as { error: { message: string } };
assert.match(body.error.message, /All fusion panel models failed/); assert.match(body.error.message, /All fusion panel models failed/);
}); });
test("fusion native logs omit a thrown transcript echo for a sensitive request", async () => {
const transcriptSentinel = "PRIVATE_FUSION_THROW_TRANSCRIPT_SENTINEL";
const retainedLogs: unknown[][] = [];
let dispatches = 0;
const res = await handleFusionChat({
body: { messages: [{ role: "user", content: "processed video request" }] },
models: ["private/failing-panel", "private/healthy-panel"],
handleSingleModel: async (_body: Body, model: string) => {
dispatches += 1;
if (model === "private/failing-panel") throw new Error(transcriptSentinel);
return okResponse("safe answer");
},
log: {
info: (...args: unknown[]) => retainedLogs.push(args),
warn: (...args: unknown[]) => retainedLogs.push(args),
debug: (...args: unknown[]) => retainedLogs.push(args),
error: (...args: unknown[]) => retainedLogs.push(args),
},
tuning: { minPanel: 1, stragglerGraceMs: 25, panelHardTimeoutMs: 1000 },
videoTranscriptSensitive: true,
});
assert.equal(res.status, 200);
assert.ok(dispatches >= 2);
const retained = JSON.stringify(retainedLogs);
assert.equal(retained.includes(transcriptSentinel), false);
assert.match(retained, /omitted: video transcript/);
});

View File

@@ -0,0 +1,139 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-glm-video-redaction-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { GlmExecutor } = await import("../../open-sse/executors/glm.ts");
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { force: true, recursive: true });
});
test("GLM stream readiness keeps the operational diagnostic but omits it from retained logs", async () => {
const sentinel = "PRIVATE_GLM_STREAM_TRANSCRIPT_SENTINEL";
const retainedWarnings: string[] = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(`data: ${JSON.stringify({ error: { message: sentinel } })}\n\n`, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
try {
const executor = new GlmExecutor("glm");
const result = await executor.execute({
model: "glm-5.3-high",
body: {
model: "glm-5.3-high",
messages: [{ role: "user", content: "describe the video" }],
},
stream: true,
credentials: {
apiKey: "glm-key",
providerSpecificData: {
baseUrl: "https://api.z.ai/api/coding/paas/v4",
primaryTransport: "openai",
},
},
videoTranscriptSensitive: true,
log: {
warn: (_tag, message) => retainedWarnings.push(message),
},
});
assert.equal(result.response.status, 502);
const operationalBody = await result.response.text();
assert.match(
operationalBody,
new RegExp(sentinel),
"the transient operational response must retain the upstream diagnostic"
);
assert.equal(retainedWarnings.length, 1);
assert.doesNotMatch(retainedWarnings[0], new RegExp(sentinel));
assert.match(retainedWarnings[0], /upstream diagnostic omitted/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GLM transport fallback omits thrown transcript diagnostics without changing its result", async () => {
const primarySentinel = "PRIVATE_GLM_PRIMARY_THROW_TRANSCRIPT_SENTINEL";
const fallbackSentinel = "PRIVATE_GLM_FALLBACK_THROW_TRANSCRIPT_SENTINEL";
const retainedDebug: string[] = [];
const originalFetch = globalThis.fetch;
const executor = new GlmExecutor("glm");
const input = {
model: "glm-5.1",
body: {
model: "glm-5.1",
messages: [{ role: "user", content: "describe the video" }],
},
stream: false,
credentials: {
apiKey: "glm-key",
providerSpecificData: {
baseUrl: "https://api.z.ai/api/coding/paas/v4",
primaryTransport: "openai",
},
},
videoTranscriptSensitive: true,
log: {
debug: (_tag: string, message: string) => retainedDebug.push(message),
},
};
try {
let fetchCall = 0;
globalThis.fetch = async () => {
fetchCall += 1;
if (fetchCall === 1) throw new Error(primarySentinel);
return new Response("fallback result", { status: 500 });
};
const fallbackResult = await executor.execute(input);
assert.equal(fallbackResult.response.status, 500);
fetchCall = 0;
globalThis.fetch = async () => {
fetchCall += 1;
if (fetchCall === 1) return new Response("primary result", { status: 500 });
throw new Error(fallbackSentinel);
};
const primaryResult = await executor.execute(input);
assert.equal(primaryResult.response.status, 500);
const retained = retainedDebug.join("\n");
assert.doesNotMatch(retained, new RegExp(primarySentinel));
assert.doesNotMatch(retained, new RegExp(fallbackSentinel));
assert.match(retained, /omitted: video transcript/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("chatCore propagates video transcript sensitivity to every executor dispatch", () => {
const source = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
const lines = source.split("\n");
const callLines = lines.flatMap((line, index) =>
line.includes("executor.execute({") ? [index] : []
);
assert.equal(callLines.length, 3, "the source contract expects all three executor dispatches");
for (const callLine of callLines) {
const contextEditingLine = lines.findIndex(
(line, index) => index >= callLine && line.includes("contextEditing:")
);
assert.ok(
contextEditingLine > callLine && contextEditingLine - callLine < 30,
"executor dispatch must have a bounded ExecuteInput object"
);
const callSource = lines.slice(callLine, contextEditingLine + 1).join("\n");
assert.match(callSource, /\n\s+videoTranscriptSensitive,\s*$/m);
}
});

View File

@@ -259,6 +259,89 @@ test("guardrail registry fails open when a guardrail throws", async () => {
assert.equal(warnings.length, 1); assert.equal(warnings.length, 1);
}); });
test("guardrail registry sanitizes native pre-call logs for a transcript-sensitive request", async () => {
const transcriptSentinel = "PRIVATE_GUARDRAIL_PRECALL_TRANSCRIPT_SENTINEL";
class EchoingGuardrail extends BaseGuardrail {
constructor() {
super("echoing", { priority: 5 });
}
override async preCall(
_payload: unknown,
context: import("../../src/lib/guardrails/base.ts").GuardrailContext
) {
context.log?.warn?.("GUARDRAIL", `native echo: ${transcriptSentinel}`, {
transcriptEcho: transcriptSentinel,
});
return { meta: { transcriptEcho: transcriptSentinel } };
}
}
class ExplodingGuardrail extends BaseGuardrail {
constructor() {
super("exploding-private", { priority: 10 });
}
override async preCall() {
throw new Error(transcriptSentinel);
}
}
const retainedLogs: unknown[][] = [];
const capture = (...args: unknown[]) => retainedLogs.push(args);
const registry = new GuardrailRegistry();
registry.register(new EchoingGuardrail());
registry.register(new ExplodingGuardrail());
const result = await registry.runPreCallHooks(
{ safe: true },
{
log: { debug: capture, info: capture, warn: capture, error: capture },
videoTranscriptSensitive: true,
}
);
assert.equal(result.blocked, false);
const retained = JSON.stringify(retainedLogs);
assert.equal(retained.includes(transcriptSentinel), false);
assert.match(retained, /omitted: video transcript/);
});
test("guardrail registry sanitizes native post-call logs for a transcript-sensitive request", async () => {
const transcriptSentinel = "PRIVATE_GUARDRAIL_POSTCALL_TRANSCRIPT_SENTINEL";
class ExplodingPostGuardrail extends BaseGuardrail {
constructor() {
super("exploding-post-private", { priority: 5 });
}
override async postCall(
_response: unknown,
context: import("../../src/lib/guardrails/base.ts").GuardrailContext
) {
context.log?.warn?.("GUARDRAIL", `native echo: ${transcriptSentinel}`, {
transcriptEcho: transcriptSentinel,
});
throw new Error(transcriptSentinel);
}
}
const retainedLogs: unknown[][] = [];
const capture = (...args: unknown[]) => retainedLogs.push(args);
const registry = new GuardrailRegistry();
registry.register(new ExplodingPostGuardrail());
const result = await registry.runPostCallHooks(
{ choices: [] },
{
log: { debug: capture, info: capture, warn: capture, error: capture },
videoTranscriptSensitive: true,
}
);
assert.equal(result.blocked, false);
const retained = JSON.stringify(retainedLogs);
assert.equal(retained.includes(transcriptSentinel), false);
assert.match(retained, /omitted: video transcript/);
});
test("guardrail registry never fails open after the client request aborts", async () => { test("guardrail registry never fails open after the client request aborts", async () => {
class AbortedGuardrail extends BaseGuardrail { class AbortedGuardrail extends BaseGuardrail {
constructor() { constructor() {

View File

@@ -1,4 +1,5 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import test from "node:test"; import test from "node:test";
import { VideoBridgeGuardrail } from "../../../src/lib/guardrails/videoBridge.ts"; import { VideoBridgeGuardrail } from "../../../src/lib/guardrails/videoBridge.ts";
@@ -122,6 +123,7 @@ test("preserves scene-aware sampler metadata in guardrail meta and the transpare
}); });
test("reports only validated transcript provenance in guardrail metadata", async () => { test("reports only validated transcript provenance in guardrail metadata", async () => {
const description = "[Video description: caption; transcript[source=client] spoken words]";
const bridge = new VideoBridgeGuardrail({ const bridge = new VideoBridgeGuardrail({
deps: { deps: {
getSettings: async () => ({ getSettings: async () => ({
@@ -134,7 +136,7 @@ test("reports only validated transcript provenance in guardrail metadata", async
cues: [{ text: "spoken words", start: 1, end: 2, source: "client" }], cues: [{ text: "spoken words", start: 1, end: 2, source: "client" }],
}); });
return { return {
description: "[Video description: caption; transcript[source=client] spoken words]", description,
durationSeconds: 2, durationSeconds: 2,
framesRequested: 1, framesRequested: 1,
framesUsed: 1, framesUsed: 1,
@@ -170,6 +172,9 @@ test("reports only validated transcript provenance in guardrail metadata", async
{} {}
); );
assert.equal(result.meta?.transcriptCuesApplied, 1); assert.equal(result.meta?.transcriptCuesApplied, 1);
assert.deepEqual(result.meta?.videoTranscriptDescriptionFingerprints, [
`sha256:${createHash("sha256").update(description).digest("hex")}:${description.length}`,
]);
}); });
test("converts Responses input using input_text while preserving sibling order", async () => { test("converts Responses input using input_text while preserving sibling order", async () => {

View File

@@ -33,3 +33,30 @@ test("crc32 is deterministic and ByteQueue buffers bytes", async () => {
const q = new ByteQueue(); const q = new ByteQueue();
assert.equal(typeof q, "object"); assert.equal(typeof q, "object");
}); });
test("malformed Kiro EventStream payload diagnostics never retain upstream content", async () => {
const { crc32, parseEventFrame } = await import("../../open-sse/executors/kiro/eventstream.ts");
const transcriptSentinel = "PRIVATE_KIRO_UPSTREAM_TRANSCRIPT_SENTINEL";
const payload = new TextEncoder().encode(transcriptSentinel);
const frame = new Uint8Array(16 + payload.length);
const view = new DataView(frame.buffer);
view.setUint32(0, frame.length, false);
view.setUint32(4, 0, false);
view.setUint32(8, crc32(frame.subarray(0, 8)), false);
frame.set(payload, 12);
view.setUint32(frame.length - 4, 0, false);
const warnings: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" "));
try {
assert.deepEqual(parseEventFrame(frame)?.payload, { raw: transcriptSentinel });
} finally {
console.warn = originalWarn;
}
assert.equal(warnings.length, 1);
assert.match(warnings[0], /Failed to parse payload/);
assert.match(warnings[0], new RegExp(`${payload.byteLength} bytes`));
assert.equal(warnings[0].includes(transcriptSentinel), false);
});

View File

@@ -159,6 +159,52 @@ test("runOnResponse passes through if no modification", async () => {
assert.deepEqual(result, { original: true }); assert.deepEqual(result, { original: true });
}); });
test("native plugin-hook error logs omit transcript echoes for a sensitive request", async () => {
const sentinels = [
"PRIVATE_PLUGIN_REQUEST_TRANSCRIPT_SENTINEL",
"PRIVATE_PLUGIN_RESPONSE_TRANSCRIPT_SENTINEL",
"PRIVATE_PLUGIN_ERROR_TRANSCRIPT_SENTINEL",
];
const retainedLogs: string[] = [];
const originalConsoleError = console.error;
console.error = (...args: unknown[]) => {
retainedLogs.push(args.map(String).join(" "));
};
try {
registerHook("onRequest", "private-request-plugin", (payload: unknown) => {
(payload as Record<string, unknown>).videoTranscriptSensitive = false;
throw new Error(sentinels[0]);
});
registerHook("onResponse", "private-response-plugin", (payload: unknown) => {
(payload as Record<string, unknown>).videoTranscriptSensitive = false;
throw new Error(sentinels[1]);
});
registerHook("onError", "private-error-plugin", (payload: unknown) => {
(payload as Record<string, unknown>).videoTranscriptSensitive = false;
throw new Error(sentinels[2]);
});
await emitHookBlocking("onRequest", { videoTranscriptSensitive: true });
await runOnResponse(
{
requestId: "private-request",
body: {},
model: "test",
metadata: {},
videoTranscriptSensitive: true,
},
{ ok: true }
);
await emitHook("onError", { videoTranscriptSensitive: true });
} finally {
console.error = originalConsoleError;
}
const retained = retainedLogs.join("\n");
for (const sentinel of sentinels) assert.equal(retained.includes(sentinel), false);
assert.match(retained, /omitted: video transcript/);
});
// ── runOnError ── // ── runOnError ──
test("runOnError fires emitHook", async () => { test("runOnError fires emitHook", async () => {

View File

@@ -85,8 +85,12 @@ test("extractErrorMessage stays available to toJsonErrorPayload's callers", () =
test("markAccountUnavailable routes lastError through the helper", () => { test("markAccountUnavailable routes lastError through the helper", () => {
const src = fs.readFileSync(new URL("../../src/sse/services/auth.ts", import.meta.url), "utf8"); const src = fs.readFileSync(new URL("../../src/sse/services/auth.ts", import.meta.url), "utf8");
assert.ok( assert.ok(
src.includes("describeUpstreamFailure(errorText)"), src.includes("const retainedErrorText = options.retainedErrorText ?? errorText"),
"auth.ts must describe the failure instead of discarding non-string errors" "auth.ts must retain the caller-selected safe error text"
);
assert.ok(
src.includes("describeUpstreamFailure(retainedErrorText)"),
"auth.ts must describe the retained failure instead of echoing the raw provider body"
); );
assert.equal( assert.equal(
/typeof errorText === "string" \? errorText\.slice\(0, 100\) : "Provider error"/.test(src), /typeof errorText === "string" \? errorText\.slice\(0, 100\) : "Provider error"/.test(src),

View File

@@ -182,6 +182,24 @@ describe("Reasoning Replay Cache — Service Layer", () => {
assert.equal(lookupReasoning("call_capture_2"), "Captured assistant reasoning"); assert.equal(lookupReasoning("call_capture_2"), "Captured assistant reasoning");
}); });
it("should never cache reasoning echoed by a transcript-sensitive response", () => {
clearReasoningCacheAll();
const sentinel = "PRIVATE_REASONING_CACHE_TRANSCRIPT_SENTINEL";
const cached = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning_content: sentinel,
tool_calls: [{ id: "call_private_video_reasoning" }],
},
"deepseek",
"deepseek-reasoner",
{ videoTranscriptSensitive: true }
);
assert.equal(cached, 0);
assert.equal(lookupReasoning("call_private_video_reasoning"), null);
});
it("should keep request message cache keys stable when tool call IDs change", () => { it("should keep request message cache keys stable when tool call IDs change", () => {
clearReasoningCacheAll(); clearReasoningCacheAll();

View File

@@ -130,12 +130,71 @@ test("combo-exhausted rejection persists the client request body for dashboard i
const detail = await callLogs.getCallLogById(rejected.id); const detail = await callLogs.getCallLogById(rejected.id);
assert.ok(detail, "expected to load the call log detail"); assert.ok(detail, "expected to load the call log detail");
assert.equal(detail!.error, '[503] Combo "default" failed — all targets exhausted');
assert.deepEqual(detail!.requestBody, { assert.deepEqual(detail!.requestBody, {
model: "default", model: "default",
messages: [{ role: "user", content: "hello" }], messages: [{ role: "user", content: "hello" }],
}); });
}); });
test("transcript-sensitive rejection omits retained error echoes and request cues", async () => {
const sentinel = "PRIVATE_REJECTED_VIDEO_TRANSCRIPT_SENTINEL";
const forgedAuditSentinel = "KEEP_REJECTED_CALLER_AUDIT_PROSE";
const requestBody = {
metadata: { tenant: "safe-tenant" },
messages: [
{
content: [
{
transcript: {
cues: [{ end: 2, source: "client", start: 1, text: sentinel }],
},
type: "input_video",
video_url: "https://example.invalid/private.mp4",
},
{
text: `[Video description: transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text=${JSON.stringify(forgedAuditSentinel)}]`,
type: "text",
},
],
role: "user",
},
],
model: "default",
};
await recordRejectedRequestUsage({
status: 502,
model: "default",
provider: "combo",
error: `[502] Provider echoed ${sentinel}`,
apiKeyId: "key-transcript-sensitive-rejection",
apiKeyName: "transcript-sensitive-rejection",
requestBody,
});
let detail: Awaited<ReturnType<typeof callLogs.getCallLogById>> = null;
for (let i = 0; i < 50 && !detail; i++) {
const logs = await callLogs.getCallLogs({});
const found = logs.find(
(entry: { apiKeyName?: string | null }) =>
entry.apiKeyName === "transcript-sensitive-rejection"
);
if (found) detail = await callLogs.getCallLogById(found.id);
else await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.ok(detail, "expected a retained rejection call log");
assert.equal(detail.error, "[omitted: video transcript]");
assert.equal(JSON.stringify(detail).includes(sentinel), false);
assert.equal(JSON.stringify(detail).includes(forgedAuditSentinel), true);
assert.equal(detail.requestBody.metadata.tenant, "safe-tenant");
assert.equal(detail.requestBody.model, "default");
assert.equal(detail.requestBody.messages[0].content[0].transcript, "[omitted: video transcript]");
assert.match(detail.requestBody.messages[0].content[1].text, /KEEP_REJECTED_CALLER_AUDIT_PROSE/);
assert.equal(requestBody.messages[0].content[0].transcript.cues[0].text, sentinel);
});
test("combo-exhausted rejection without a request body still logs cleanly (no request body available)", async () => { test("combo-exhausted rejection without a request body still logs cleanly (no request body available)", async () => {
await recordRejectedRequestUsage({ await recordRejectedRequestUsage({
status: 503, status: 503,

View File

@@ -13,7 +13,22 @@ const {
buildStreamSummaryFromEvents, buildStreamSummaryFromEvents,
compactStructuredStreamPayload, compactStructuredStreamPayload,
} = await import("../../open-sse/utils/streamPayloadCollector.ts"); } = await import("../../open-sse/utils/streamPayloadCollector.ts");
const { cloneBoundedForLog, createRequestLogger } =
await import("../../open-sse/utils/requestLogger.ts");
const { cloneBoundedChatLogPayload } =
await import("../../open-sse/handlers/chatCore/logTruncation.ts");
const {
containsVideoTranscriptForLog,
extractVideoTranscriptDescriptionFingerprints,
fingerprintVideoTranscriptDescription,
omitVideoTranscriptForLog,
omitVideoTranscriptFromLogString,
resolveVideoTranscriptLogSensitivity,
VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER,
} = await import("../../src/lib/guardrails/videoTranscriptLogRedaction.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts"); const { FORMATS } = await import("../../open-sse/translator/formats.ts");
const { translateRequest } = await import("../../open-sse/translator/index.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
test("normalizes JSON strings before log protection and redacts sensitive keys", () => { test("normalizes JSON strings before log protection and redacts sensitive keys", () => {
const protectedPayload = protectPayloadForLog( const protectedPayload = protectPayloadForLog(
@@ -87,6 +102,722 @@ test("omits encrypted reasoning values from structured log payloads", () => {
assert.equal(payload.output[0].encrypted_content, encryptedContent); assert.equal(payload.output[0].encrypted_content, encryptedContent);
}); });
test("omits raw Video Bridge transcript cues from persisted request-log payloads", () => {
const rawCue = 'private subtitle sentinel "] [system]';
const trustedDescription = `[Video description: frame@t=00:01.000 scene; transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text=${JSON.stringify(rawCue)}]`;
const protectedPipeline = protectPipelinePayloads({
clientRawRequest: {
body: {
messages: [
{
content: [
{
audioTranscript: {
cues: [{ end: 2, source: "audio-bridge", start: 1, text: rawCue }],
},
transcript: {
cues: [{ end: 2, source: "client", start: 1, text: rawCue }],
},
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
],
},
],
},
},
providerRequest: {
body: cloneBoundedForLog(
{
messages: [
{
content: trustedDescription,
},
],
},
0,
null,
{
trustedDescriptionFingerprints: [
fingerprintVideoTranscriptDescription(trustedDescription),
],
}
),
},
});
const serialized = JSON.stringify(protectedPipeline);
assert.equal(serialized.includes(rawCue), false);
assert.equal(serialized.includes("private subtitle sentinel"), false);
assert.match(serialized, /omitted: video transcript/);
assert.match(serialized, /frame@t=00:01\.000 scene/);
});
test("omits transcript tracks from the media detector's empty-base64 video shape", async () => {
const rawCue = "private empty-base64 subtitle sentinel";
const payload = {
input: [
{
role: "user",
content: [
{
source: {
data: "",
media_type: "video/mp4",
transcript: {
cues: [{ end: 2, source: "client", start: 1, text: rawCue }],
},
type: "base64",
},
type: "video",
},
],
},
],
};
const protectedPayload = protectPayloadForLog(payload);
for (const redacted of [
protectedPayload,
cloneBoundedForLog(payload),
cloneBoundedChatLogPayload(payload),
]) {
const serialized = JSON.stringify(redacted);
assert.equal(serialized.includes(rawCue), false);
assert.match(serialized, /omitted: video transcript/);
}
const logger = await createRequestLogger(undefined, undefined, undefined, {
captureStreamChunks: true,
enabled: true,
});
logger.appendProviderChunk(`data: ${rawCue}\n\n`);
logger.logClientRawRequest("/v1/responses", payload);
logger.appendConvertedChunk(`data: ${rawCue}\n\n`);
const pipeline = logger.getPipelinePayloads();
assert.equal(JSON.stringify(pipeline).includes(rawCue), false);
assert.equal(pipeline?.streamChunks, undefined);
});
test("omits Video Bridge cues before lossy request-log string truncation", () => {
const rawCue = "private boundary subtitle sentinel";
const payload = (targetCue: number, visualPadding: number) => {
const cueText = (index: number): string =>
index === targetCue
? rawCue + "x".repeat(4_096 - rawCue.length)
: String(index).padStart(2, "0") + "x".repeat(4_094);
const description = `[Video description: ${Array.from(
{ length: 16 },
(_unused, index) =>
`transcript[source=client;confidence=1.00;interval=00:${String(index).padStart(2, "0")}.000-00:${String(index + 1).padStart(2, "0")}.000] text=${JSON.stringify(cueText(index))}`
).join("; ")}; frame-tail=${"v".repeat(visualPadding)}]`;
return {
context: {
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
},
value: { input: [{ content: description, role: "user" }] },
};
};
const requestLogNine = payload(9, 3_507);
const requestLogTen = payload(10, 7_710);
const chatLogNine = payload(9, 3_522);
const alternateChatLogNine = payload(9, 3_598);
const boundedPayloads = [
cloneBoundedForLog(requestLogNine.value, 0, null, requestLogNine.context),
cloneBoundedForLog(requestLogTen.value, 0, null, requestLogTen.context),
cloneBoundedChatLogPayload(chatLogNine.value, 0, chatLogNine.context),
cloneBoundedChatLogPayload(alternateChatLogNine.value, 0, alternateChatLogNine.context),
];
for (const bounded of boundedPayloads) {
const serialized = JSON.stringify(protectPayloadForLog(bounded));
assert.equal(serialized.includes(rawCue), false);
assert.match(serialized, /omitted: video transcript/);
}
});
test("suppresses bounded stream-chunk logs for a request carrying Video Bridge cues", async () => {
const rawCue = "private streamed subtitle sentinel";
const description =
`[Video description: transcript[source=embedded;confidence=1.00;` +
`interval=00:01.000-00:02.000] text=${JSON.stringify(rawCue)}]`;
const logger = await createRequestLogger(undefined, undefined, undefined, {
captureStreamChunks: true,
enabled: true,
maxStreamChunkBytes: 64,
videoTranscriptDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
videoTranscriptSensitive: true,
});
logger.appendProviderChunk(`data: ${rawCue}\n\n`);
logger.logTargetRequest(
"https://provider.invalid",
{},
{
input: [
{
content: description,
role: "user",
},
],
}
);
logger.appendProviderChunk(`data: ${rawCue}\n\n`);
const protectedPipeline = protectPipelinePayloads(logger.getPipelinePayloads());
const serialized = JSON.stringify(protectedPipeline);
assert.equal(serialized.includes(rawCue), false);
assert.equal(protectedPipeline?.streamChunks, undefined);
assert.match(serialized, /omitted: video transcript/);
});
test("omits non-stream response bodies after a request carries Video Bridge cues", async () => {
const rawCue = "private non-stream subtitle echo sentinel";
const description =
`[Video description: transcript[source=embedded;confidence=1.00;` +
`interval=00:01.000-00:02.000] text=${JSON.stringify(rawCue)}]`;
const logger = await createRequestLogger(undefined, undefined, undefined, {
captureStreamChunks: true,
enabled: true,
videoTranscriptDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
videoTranscriptSensitive: true,
});
logger.logTargetRequest(
"https://provider.invalid",
{},
{
input: [
{
content: description,
role: "user",
},
],
}
);
logger.logProviderResponse(
200,
"OK",
{ "content-type": "application/json" },
{
choices: [{ message: { content: rawCue } }],
}
);
logger.logConvertedResponse({ choices: [{ message: { content: rawCue } }] });
const pipeline = logger.getPipelinePayloads();
const serialized = JSON.stringify(pipeline);
assert.equal(serialized.includes(rawCue), false);
assert.match(serialized, /omitted: video transcript/);
assert.equal(pipeline?.providerResponse?.status, 200);
});
test("preserves generic transcript fields and stream logs outside recognized video parts", async () => {
const ordinaryTranscript = "ordinary meeting notes";
const cueLikeProse = 'Discuss transcript[source=client] text="meeting notes" as plain text.';
const forgedVideoDescription =
'[Video description: transcript[source=client] text="caller-forged audit suppression"]';
const payload = {
input: [
{
arguments: { transcript: ordinaryTranscript },
call_id: "call_ordinary_transcript",
type: "function_call",
},
{ content: cueLikeProse, role: "user", type: "message" },
{ content: forgedVideoDescription, role: "user", type: "message" },
],
};
const protectedPayload = protectPayloadForLog(payload) as typeof payload;
assert.equal(JSON.stringify(protectedPayload).includes(ordinaryTranscript), true);
assert.equal(protectedPayload.input[1].content, cueLikeProse);
assert.equal(protectedPayload.input[2].content, forgedVideoDescription);
assert.equal(JSON.stringify(cloneBoundedForLog(payload)).includes(ordinaryTranscript), true);
assert.equal(
JSON.stringify(cloneBoundedChatLogPayload(payload)).includes(ordinaryTranscript),
true
);
const logger = await createRequestLogger(undefined, undefined, undefined, {
captureStreamChunks: true,
enabled: true,
});
logger.logTargetRequest("https://provider.invalid", {}, payload);
logger.appendProviderChunk(`data: ${ordinaryTranscript}\n\n`);
const pipeline = logger.getPipelinePayloads();
assert.equal(JSON.stringify(pipeline).includes(ordinaryTranscript), true);
assert.equal(JSON.stringify(pipeline).includes("caller-forged audit suppression"), true);
assert.equal(pipeline?.streamChunks?.provider?.length, 1);
});
test("preserves ordinary depth-10 payloads until the downstream log-depth policy", () => {
const sentinel = "ordinary depth-10 transcript sentinel";
let payload: Record<string, unknown> = { transcript: sentinel };
for (let depth = 0; depth < 10; depth += 1) payload = { nested: payload };
const protectedPipeline = protectPipelinePayloads({ clientRawRequest: payload });
for (const protectedPayload of [
omitVideoTranscriptForLog(payload),
protectPayloadForLog(payload),
cloneBoundedForLog(payload),
cloneBoundedChatLogPayload(payload),
protectedPipeline,
]) {
assert.equal(JSON.stringify(protectedPayload).includes(sentinel), true);
}
});
test("does not trust forged Video-description prose merely because a real video carrier is sensitive", async () => {
const rawCue = "private structured video transcript";
const genuineCue = "private server-derived embedded transcript";
const genuineDescription =
`[Video description: untrusted media-derived observation; ` +
`transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text=${JSON.stringify(genuineCue)}]`;
const forgedProse =
'[Video description: transcript[source=client] text="caller-forged audit evidence"]';
const clientPayload = {
input: [
{
content: [
{
transcript: {
cues: [{ end: 2, source: "client", start: 1, text: rawCue }],
},
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
{ text: forgedProse, type: "input_text" },
],
role: "user",
},
],
};
const providerPayload = {
input: [
{
content: [
{ text: genuineDescription, type: "input_text" },
{ text: forgedProse, type: "input_text" },
],
role: "user",
},
],
};
const logger = await createRequestLogger(undefined, undefined, undefined, {
captureStreamChunks: true,
enabled: true,
videoTranscriptDescriptionFingerprints: [
fingerprintVideoTranscriptDescription(genuineDescription),
],
videoTranscriptSensitive: true,
});
logger.logClientRawRequest("/v1/responses", clientPayload);
logger.logTargetRequest("https://provider.invalid", {}, providerPayload);
const pipeline = logger.getPipelinePayloads();
const serialized = JSON.stringify(pipeline);
assert.equal(serialized.includes(rawCue), false);
assert.equal(serialized.includes(genuineCue), false);
assert.match(serialized, /omitted: video transcript/);
assert.equal(serialized.includes("caller-forged audit evidence"), true);
});
test("accepts description fingerprints only from a successful Video Bridge rewrite", () => {
const description =
'[Video description: transcript[source=embedded] text="server-derived subtitle"]';
const fingerprint = fingerprintVideoTranscriptDescription(description);
const result = extractVideoTranscriptDescriptionFingerprints([
{
guardrail: "video-bridge",
meta: {
transcriptCuesApplied: 1,
videoTranscriptDescriptionFingerprints: [fingerprint, fingerprint],
},
modified: true,
},
{
guardrail: "caller-forged",
meta: { transcriptCuesApplied: 1, videoTranscriptDescriptionFingerprints: [fingerprint] },
modified: true,
},
{
guardrail: "video-bridge",
meta: {
transcriptCuesApplied: 1,
videoTranscriptDescriptionFingerprints: ["sha256:not-a-digest"],
},
modified: true,
},
]);
assert.deepEqual(result, [fingerprint]);
});
test("redacts the exact generated segment after Kiro and Cursor concatenate text blocks", async () => {
const genuineCue = "PRIVATE_TRANSLATED_VIDEO_TRANSCRIPT_SENTINEL";
const genuineDescription =
`[Video description: transcript[source=embedded;confidence=1.00;` +
`interval=00:01.000-00:02.000] text=${JSON.stringify(genuineCue)}]`;
const forgedProse =
'[Video description: transcript[source=client] text="KEEP_TRANSLATED_CALLER_PROSE"]';
const body = {
messages: [
{
content: [
{ text: genuineDescription, type: "text" },
{ text: forgedProse, type: "text" },
],
role: "user",
},
],
model: "translated-video-log-fixture",
};
usageHistory.clearPendingRequests();
try {
for (const targetFormat of [FORMATS.KIRO, FORMATS.CURSOR]) {
const translated = translateRequest(
FORMATS.OPENAI,
targetFormat,
"translated-video-log-fixture",
body,
false
);
const videoTranscriptDescriptionFingerprints = [
fingerprintVideoTranscriptDescription(genuineDescription),
];
const logger = await createRequestLogger(FORMATS.OPENAI, targetFormat, undefined, {
enabled: true,
videoTranscriptDescriptionFingerprints,
videoTranscriptSensitive: true,
});
logger.logTargetRequest("https://provider.invalid", {}, translated);
const serialized = JSON.stringify(logger.getPipelinePayloads());
assert.equal(serialized.includes(genuineCue), false, targetFormat);
assert.match(serialized, /omitted: video transcript/, targetFormat);
assert.equal(serialized.includes("KEEP_TRANSLATED_CALLER_PROSE"), true, targetFormat);
const requestId = usageHistory.trackPendingRequest(
"translated-video-log-fixture",
targetFormat,
`connection-${targetFormat}`,
true,
{
providerRequest: translated,
videoTranscriptDescriptionFingerprints,
videoTranscriptSensitive: true,
}
);
const pending = JSON.stringify(usageHistory.getPendingById().get(requestId!));
assert.equal(pending.includes(genuineCue), false, targetFormat);
assert.match(pending, /omitted: video transcript/, targetFormat);
assert.equal(pending.includes("KEEP_TRANSLATED_CALLER_PROSE"), true, targetFormat);
}
} finally {
usageHistory.clearPendingRequests();
}
});
test("fails closed after a bounded number of forged description-prefix candidates", () => {
const genuineDescription =
'[Video description: transcript[source=embedded] text="bounded genuine subtitle"]';
const prefixFlood = Array.from(
{ length: 129 },
(_unused, index) => `[Video description: forged-prefix-${index}]`
).join("");
const omitted = omitVideoTranscriptForLog(
{ content: prefixFlood + genuineDescription },
{
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(genuineDescription)],
}
) as { content: string };
assert.equal(omitted.content, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
});
test("fails closed after a bounded number of trusted-description candidate hashes", () => {
const genuineDescription =
'[Video description: transcript[source=embedded] text="bounded hash subtitle"]';
const forgedPrefixes = Array.from(
{ length: 9 },
(_unused, index) => `[Video description: candidate-${index}]`
).join("");
const trustedDescriptionFingerprints = [
...Array.from({ length: 63 }, (_unused, index) =>
fingerprintVideoTranscriptDescription(`[Video description: identity-${index}]`)
),
fingerprintVideoTranscriptDescription(genuineDescription),
];
const omitted = omitVideoTranscriptForLog(
{ content: forgedPrefixes + genuineDescription },
{ trustedDescriptionFingerprints }
) as { content: string };
assert.equal(omitted.content, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
});
test("trusted malformed or over-budget serialized cues fail closed", () => {
const privateTranscript = "PRIVATE_MALFORMED_SERIALIZED_CUE_SENTINEL";
const malformedDescriptions = [
`[Video description: transcript[source=embedded${";metadata=x".repeat(100)}] text=${JSON.stringify(privateTranscript)}]`,
`[Video description: transcript[source=embedded text=${JSON.stringify(privateTranscript)}]`,
`[Video description: transcript[source=embedded]text=${JSON.stringify(privateTranscript)}]`,
`[Video description: transcript[source=embedded] payload=${JSON.stringify(privateTranscript)}]`,
`[Video description: transcript[source=embedded] text=${JSON.stringify("safe cue")}; transcript[source=audio-bridge${";metadata=x".repeat(100)}] text=${JSON.stringify(privateTranscript)}]`,
];
for (const description of malformedDescriptions) {
const context = {
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
};
const direct = omitVideoTranscriptFromLogString(description, context);
const structured = omitVideoTranscriptForLog({ content: description }, context) as {
content: string;
};
assert.equal(direct, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER, description);
assert.equal(structured.content, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER, description);
assert.equal(direct.includes(privateTranscript), false, description);
assert.equal(structured.content.includes(privateTranscript), false, description);
}
});
test("bounds cyclic objects and fails closed when the transcript-security depth is exceeded", () => {
const cyclic: Record<string, unknown> = { transcript: "ordinary cyclic notes" };
cyclic.self = cyclic;
let deep: Record<string, unknown> = { transcript: "ordinary deeply nested notes" };
for (let index = 0; index < 20_000; index += 1) deep = { child: deep };
assert.doesNotThrow(() => containsVideoTranscriptForLog(cyclic));
assert.doesNotThrow(() => containsVideoTranscriptForLog(deep));
assert.equal(containsVideoTranscriptForLog(cyclic), false);
assert.equal(containsVideoTranscriptForLog(deep), true);
const omittedCycle = omitVideoTranscriptForLog(cyclic);
const omittedDeep = omitVideoTranscriptForLog(deep);
assert.doesNotThrow(() => JSON.stringify(omittedCycle));
assert.equal(omittedDeep, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
});
test("fails closed when enumerable getters or proxies throw during transcript inspection", () => {
const throwingGetter: Record<string, unknown> = {};
Object.defineProperty(throwingGetter, "nested", {
enumerable: true,
get() {
throw new Error("PRIVATE_GETTER_TRANSCRIPT_SENTINEL");
},
});
const throwingProxy = new Proxy<Record<string, unknown>>(
{},
{
ownKeys() {
throw new Error("PRIVATE_PROXY_TRANSCRIPT_SENTINEL");
},
}
);
for (const payload of [throwingGetter, throwingProxy]) {
assert.equal(containsVideoTranscriptForLog(payload), true);
assert.equal(omitVideoTranscriptForLog(payload), VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
}
});
test("fails closed when an enumerable function can replace the retained JSON representation", () => {
const privateTranscript = "PRIVATE_TOJSON_VIDEO_TRANSCRIPT_SENTINEL";
const payload = {
safe: "retained diagnostic",
toJSON() {
return {
transcript: privateTranscript,
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
};
},
};
assert.equal(containsVideoTranscriptForLog(payload), true);
const omitted = omitVideoTranscriptForLog(payload) as Record<string, unknown>;
const serialized = JSON.stringify(omitted);
assert.equal(serialized.includes(privateTranscript), false);
assert.equal(omitted.safe, "retained diagnostic");
assert.notEqual(typeof omitted.toJSON, "function");
});
test("treats explicit malformed and nested video carriers as transcript-sensitive", () => {
const privateTranscript = "PRIVATE_MALFORMED_VIDEO_TRANSCRIPT_SENTINEL";
const payloads = [
{
transcript: privateTranscript,
type: "input_video",
},
{
transcript: privateTranscript,
type: "video_url",
video_url: "",
},
{
input_video: {
transcript: privateTranscript,
url: "data:video/mp4;base64,AA==",
},
type: "input_video",
},
{
input_video: {
url: { transcript: privateTranscript },
},
type: "input_video",
},
{
source: { transcript: privateTranscript },
type: "video_source",
},
{
source: {
media_type: "video/mp4",
transcript: privateTranscript,
},
type: "video",
},
];
for (const payload of payloads) {
assert.equal(containsVideoTranscriptForLog(payload), true, JSON.stringify(payload));
const serialized = JSON.stringify(omitVideoTranscriptForLog(payload));
assert.equal(serialized.includes(privateTranscript), false, serialized);
assert.match(serialized, /omitted: video transcript/);
}
});
test("keeps raw request sensitivity after a guardrail removes the original carrier", () => {
const rawRequestBody = {
messages: [
{
content: [
{
transcript: "PRIVATE_RAW_REQUEST_TRANSCRIPT_SENTINEL",
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
],
role: "user",
},
],
};
const processedBody = {
messages: [{ content: "guardrail replaced the media", role: "user" }],
};
assert.equal(resolveVideoTranscriptLogSensitivity({ processedBody, rawRequestBody }), true);
assert.equal(
resolveVideoTranscriptLogSensitivity({
processedBody: { metadata: { transcript: "ordinary audit label" } },
rawRequestBody: { metadata: { transcript: "ordinary caller label" } },
}),
false
);
});
test("fails closed at the aggregate traversal budget before a tail video transcript can leak", () => {
const privateTranscript = "private over-budget video transcript sentinel";
const payload: unknown[] = Array.from(
{ length: 10_001 },
(_unused, index) => `ordinary entry ${index}`
);
// The detector must treat a bounded security scan as unknown/sensitive even without a cue.
assert.equal(containsVideoTranscriptForLog(payload), true);
payload[payload.length - 1] = {
transcript: privateTranscript,
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
};
const omitted = omitVideoTranscriptForLog(payload);
assert.equal(omitted, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
assert.equal(JSON.stringify(omitted).includes(privateTranscript), false);
});
test("detects and omits an input-video transcript below nine ordinary objects", () => {
const privateTranscript = "private depth-9 video transcript sentinel";
let payload: Record<string, unknown> = {
transcript: privateTranscript,
type: "input_video",
video_url: { url: "data:video/mp4;base64,AA==" },
};
for (let depth = 0; depth < 9; depth += 1) payload = { nested: payload };
assert.equal(containsVideoTranscriptForLog(payload), true);
const omitted = JSON.stringify(omitVideoTranscriptForLog(payload));
assert.equal(omitted.includes(privateTranscript), false);
assert.match(omitted, /omitted: video transcript/);
});
test("redacts only direct transcript carriers within a recognized video part", () => {
const directCue = "private direct video subtitle";
const sourceCue = "private source video subtitle";
const unrelatedNestedTranscript = "ordinary nested metadata transcript";
const payload = {
type: "video",
source: {
data: "AA==",
media_type: "video/mp4",
transcript: sourceCue,
metadata: { transcript: unrelatedNestedTranscript },
type: "base64",
},
transcript: directCue,
metadata: { transcript: unrelatedNestedTranscript },
};
const omitted = omitVideoTranscriptForLog(payload) as typeof payload;
assert.equal(omitted.transcript, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
assert.equal(omitted.metadata.transcript, unrelatedNestedTranscript);
assert.equal(omitted.source.transcript, VIDEO_TRANSCRIPT_LOG_OMISSION_MARKER);
assert.equal(omitted.source.metadata.transcript, unrelatedNestedTranscript);
});
test("suppresses active stream chunks even when persisted pipeline logging is disabled", async () => {
const rawCue = "private active-log subtitle sentinel";
const model = "video-log-redaction-model";
const provider = "video-log-redaction-provider";
const connectionId = "video-log-redaction-connection";
usageHistory.clearPendingRequests();
try {
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true);
const logger = await createRequestLogger(undefined, undefined, model, {
captureStreamChunks: true,
connectionId,
enabled: false,
model,
provider,
requestId,
});
logger.appendProviderChunk(`data: ${rawCue}\n\n`);
logger.logClientRawRequest("/v1/responses", {
input: [
{
audioTranscript: {
cues: [{ end: 2, source: "audio-bridge", start: 1, text: rawCue }],
},
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
],
});
logger.appendOpenAIChunk(`data: ${rawCue}\n\n`);
logger.appendConvertedChunk(`data: ${rawCue}\n\n`);
const chunks = usageHistory.getPendingById().get(requestId)?.streamChunks;
assert.deepEqual(chunks, { client: [], openai: [], provider: [] });
} finally {
usageHistory.clearPendingRequests();
}
});
test("omits encrypted reasoning split across captured SSE chunks", () => { test("omits encrypted reasoning split across captured SSE chunks", () => {
const encryptedContent = "opaque-replay-state".repeat(128); const encryptedContent = "opaque-replay-state".repeat(128);
const protectedPipeline = protectPipelinePayloads({ const protectedPipeline = protectPipelinePayloads({

View File

@@ -8,7 +8,10 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-reqlogger
process.env.DATA_DIR = TEST_DATA_DIR; process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts"); const core = await import("../../src/lib/db/core.ts");
const { fingerprintVideoTranscriptDescription } =
await import("../../src/lib/guardrails/videoTranscriptLogRedaction.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const pendingRequestScope = await import("../../src/lib/usage/pendingRequestScope.ts");
const callLogs = await import("../../src/lib/usage/callLogs.ts"); const callLogs = await import("../../src/lib/usage/callLogs.ts");
test.after(() => { test.after(() => {
@@ -61,6 +64,84 @@ test("trackPendingRequest creates a detail entry", () => {
assert.equal(detail.clientRequest.messages[0].content, "hi"); assert.equal(detail.clientRequest.messages[0].content, "hi");
}); });
test("pending request metadata omits trusted Video Bridge cues and response echoes", () => {
usageHistory.clearPendingRequests();
const sentinel = "PRIVATE_ACTIVE_VIDEO_TRANSCRIPT_SENTINEL";
const description =
`[Video description: focus=full; untrusted media-derived observations; ` +
`transcript[source=embedded start=0 end=1 confidence=1] text="${sentinel}"]`;
const metadata = {
providerRequest: { messages: [{ role: "user", content: description }] },
stage: "registered",
videoTranscriptDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
videoTranscriptSensitive: true,
};
const requestId = usageHistory.trackPendingRequest(
"gpt-4",
"openai",
"conn-transcript",
true,
metadata
);
assert.ok(requestId);
const scope = {
id: requestId,
model: "gpt-4",
provider: "openai",
connectionId: "conn-transcript",
videoTranscriptDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
videoTranscriptSensitive: true,
};
pendingRequestScope.updatePendingScope(scope, {
providerRequest: { messages: [{ role: "user", content: description }] },
providerResponse: { output: sentinel },
clientResponse: { output: sentinel },
error: sentinel,
stage: "provider_response_started",
});
const detail = usageHistory.getPendingById().get(requestId);
const serialized = JSON.stringify(detail);
assert.doesNotMatch(serialized, new RegExp(sentinel));
assert.match(serialized, /\[omitted: video transcript\]/);
});
test("pending metadata keeps forged Video prose beside a structured transcript carrier", () => {
usageHistory.clearPendingRequests();
const rawCue = "PRIVATE_STRUCTURED_TRANSCRIPT_SENTINEL";
const forgedProse =
'[Video description: transcript[source=client] text="KEEP_CALLER_AUDIT_PROSE"]';
const request = {
input: [
{
content: [
{
transcript: {
cues: [{ end: 2, source: "client", start: 1, text: rawCue }],
},
type: "input_video",
video_url: "data:video/mp4;base64,AA==",
},
{ text: forgedProse, type: "input_text" },
],
role: "user",
},
],
};
const requestId = usageHistory.trackPendingRequest("gpt-4", "openai", "conn-forged", true, {
clientRequest: request,
providerRequest: request,
videoTranscriptSensitive: true,
});
assert.ok(requestId);
const serialized = JSON.stringify(usageHistory.getPendingById().get(requestId));
assert.equal(serialized.includes(rawCue), false);
assert.equal(serialized.includes("KEEP_CALLER_AUDIT_PROSE"), true);
});
test("trackPendingRequest decrements and removes detail on finish", () => { test("trackPendingRequest decrements and removes detail on finish", () => {
usageHistory.clearPendingRequests(); usageHistory.clearPendingRequests();
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true); usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true);

View File

@@ -131,6 +131,155 @@ test("resolvePreviousResponseState reads output from a wrapped (streaming) clien
}); });
}); });
test("resolvePreviousResponseState continues from a safely redacted Video Bridge transcript", () => {
insertCallLog({
id: "log-video-redacted",
responseId: "resp_video_redacted",
apiKeyId: "key-1",
detailState: "ready",
artifactRelPath: "2026-01-01/log-video-redacted.json",
});
const request = {
body: {
input: [
{
role: "user",
content:
'[Video description: transcript[source=embedded] text="[omitted: video transcript]"]',
},
],
},
};
writeArtifact("2026-01-01/log-video-redacted.json", {
_omnirouteVideoTranscriptRedacted: true,
clientRawRequest: request,
providerRequest: request,
clientResponse: {
id: "resp_video_redacted",
output: [{ type: "message", role: "assistant", content: "summary" }],
},
});
assert.deepEqual(store.resolvePreviousResponseState("resp_video_redacted", "key-1"), {
input: request.body.input,
output: [{ type: "message", role: "assistant", content: "summary" }],
});
});
test("resolvePreviousResponseState continues when the prior output was safely redacted", () => {
insertCallLog({
id: "log-video-output-redacted",
responseId: "resp_video_output_redacted",
apiKeyId: "key-1",
detailState: "ready",
artifactRelPath: "2026-01-01/log-video-output-redacted.json",
});
const request = {
body: { input: [{ role: "user", content: "summarize the prior result" }] },
};
writeArtifact("2026-01-01/log-video-output-redacted.json", {
_omnirouteVideoTranscriptRedacted: true,
clientRawRequest: request,
providerRequest: request,
clientResponse: {
id: "resp_video_output_redacted",
output: [
{
type: "message",
role: "assistant",
content: '[Video description: text="[omitted: video transcript]"]',
},
],
},
});
assert.deepEqual(store.resolvePreviousResponseState("resp_video_output_redacted", "key-1"), {
input: request.body.input,
output: [
{
type: "message",
role: "assistant",
content: '[Video description: text="[omitted: video transcript]"]',
},
],
});
});
test("resolvePreviousResponseState continues from an empty-base64 transcript omission", () => {
insertCallLog({
id: "log-empty-base64-video-redacted",
responseId: "resp_empty_base64_video_redacted",
apiKeyId: "key-1",
detailState: "ready",
artifactRelPath: "2026-01-01/log-empty-base64-video-redacted.json",
});
const request = {
body: {
input: [
{
type: "video",
source: {
data: "",
media_type: "video/mp4",
transcript: "[omitted: video transcript]",
type: "base64",
},
},
],
},
};
writeArtifact("2026-01-01/log-empty-base64-video-redacted.json", {
_omnirouteVideoTranscriptRedacted: true,
clientRawRequest: request,
providerRequest: request,
clientResponse: {
id: "resp_empty_base64_video_redacted",
output: [{ type: "message", role: "assistant", content: "summary" }],
},
});
assert.deepEqual(
store.resolvePreviousResponseState("resp_empty_base64_video_redacted", "key-1"),
{
input: request.body.input,
output: [{ type: "message", role: "assistant", content: "summary" }],
}
);
});
test("resolvePreviousResponseState preserves ordinary transcript fields and marker-like prose", () => {
insertCallLog({
id: "log-ordinary-transcript",
responseId: "resp_ordinary_transcript",
apiKeyId: "key-1",
detailState: "ready",
artifactRelPath: "2026-01-01/log-ordinary-transcript.json",
});
const input = [
{
arguments: { transcript: "ordinary meeting notes" },
type: "function_call",
},
{
content:
'[Video description: caller prose with text="[omitted: video transcript]"] without a video.',
role: "user",
type: "message",
},
];
const output = [{ type: "message", role: "assistant", content: "ordinary reply" }];
writeArtifact("2026-01-01/log-ordinary-transcript.json", {
clientRawRequest: { body: { input } },
providerRequest: { body: { input } },
clientResponse: { id: "resp_ordinary_transcript", output },
});
assert.deepEqual(store.resolvePreviousResponseState("resp_ordinary_transcript", "key-1"), {
input,
output,
});
});
test("resolvePreviousResponseState returns null for an unknown response id", () => { test("resolvePreviousResponseState returns null for an unknown response id", () => {
const result = store.resolvePreviousResponseState("resp_does_not_exist", "key-1"); const result = store.resolvePreviousResponseState("resp_does_not_exist", "key-1");
assert.equal(result, null); assert.equal(result, null);

View File

@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-retention-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const { finalizeStreamRequestLog } =
await import("../../open-sse/utils/streamFailureFinalization.ts");
test.after(() => {
usageHistory.clearPendingRequests();
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { force: true, recursive: true });
});
test("stream finalization omits transcript-sensitive response echoes", () => {
usageHistory.clearPendingRequests();
const sentinel = "PRIVATE_STREAM_FINALIZATION_TRANSCRIPT_SENTINEL";
const requestId = usageHistory.trackPendingRequest(
"video-model",
"video-provider",
"video-connection",
true,
{ videoTranscriptSensitive: true }
);
assert.ok(requestId);
finalizeStreamRequestLog({
pendingRequestId: requestId,
model: "video-model",
provider: "video-provider",
connectionId: "video-connection",
providerResponse: { echo: sentinel },
clientResponse: { echo: sentinel },
status: 502,
error: sentinel,
videoTranscriptSensitive: true,
});
const retained = usageHistory.getCompletedDetails().get(requestId);
assert.ok(retained);
const serialized = JSON.stringify(retained);
assert.equal(serialized.includes(sentinel), false);
assert.match(serialized, /omitted: video transcript/);
});
test("chatCore propagates the request sensitivity bit into stream finalization", () => {
const source = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
const callStart = source.indexOf("streamFailure.finalizeStreamRequestLog({");
assert.notEqual(callStart, -1, "stream finalization call must exist");
const callEnd = source.indexOf("\n });", callStart);
assert.notEqual(callEnd, -1, "stream finalization call must be bounded");
const callSource = source.slice(callStart, callEnd);
assert.match(callSource, /\n\s+videoTranscriptSensitive,\s*$/m);
});

View File

@@ -51,9 +51,14 @@ test("pipeWithDisconnect stall watchdog logs instead of silently swallowing a th
}; };
try { try {
const stream = pipeWithDisconnect(new Response(source), new TransformStream(), streamController, { const stream = pipeWithDisconnect(
stallTimeoutMs: 40, new Response(source),
}); new TransformStream(),
streamController,
{
stallTimeoutMs: 40,
}
);
await readStreamText(stream); await readStreamText(stream);
} finally { } finally {
console.debug = originalDebug; console.debug = originalDebug;
@@ -67,3 +72,45 @@ test("pipeWithDisconnect stall watchdog logs instead of silently swallowing a th
"a throwing handleError during the stall watchdog must be logged via console.debug, not swallowed" "a throwing handleError during the stall watchdog must be logged via console.debug, not swallowed"
); );
}); });
test("pipeWithDisconnect omits transcript-sensitive stall watchdog diagnostics", async () => {
const privateCue = "PRIVATE_VIDEO_CUE_stall_watchdog_4c91";
const source = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("x"));
},
cancel() {},
});
const streamController = {
isConnected: () => true,
handleError() {
throw new Error(privateCue);
},
handleComplete() {},
abort() {},
};
const debugCalls = [];
const originalDebug = console.debug;
console.debug = (...args) => {
debugCalls.push(args);
};
try {
const stream = pipeWithDisconnect(
new Response(source),
new TransformStream(),
streamController,
{
redactStreamDiagnosticsForLog: true,
stallTimeoutMs: 40,
}
);
await readStreamText(stream);
} finally {
console.debug = originalDebug;
}
const retainedDiagnostics = debugCalls.flat().map(String).join(" ");
assert.equal(retainedDiagnostics.includes(privateCue), false);
assert.equal(retainedDiagnostics.includes("[omitted: video transcript]"), true);
});

View File

@@ -829,3 +829,38 @@ test("pipeWithDisconnect stall watchdog does not fire after normal stream comple
assert.equal(text, "ok"); assert.equal(text, "ok");
assert.equal(onErrorCalled, false, "stall watchdog must be cleared on stream completion"); assert.equal(onErrorCalled, false, "stall watchdog must be cleared on stream completion");
}); });
test("createStreamController redacts sensitive diagnostics without changing onError", () => {
const privateCue = "PRIVATE_VIDEO_CUE_controller_7f38";
const originalLog = console.log;
const originalDebug = console.debug;
const calls: unknown[][] = [];
let callbackMessage: string | null = null;
console.log = (...args: unknown[]) => {
calls.push(args);
};
console.debug = (...args: unknown[]) => {
calls.push(args);
};
try {
const controller = createStreamController({
redactStreamDiagnosticsForLog: true,
onError(event) {
callbackMessage = event.message;
throw new Error(`${privateCue}: callback failure`);
},
});
controller.handleError(new Error(privateCue));
} finally {
console.log = originalLog;
console.debug = originalDebug;
}
const retainedDiagnostics = calls.flat().map(String).join(" ");
assert.equal(callbackMessage, privateCue, "fallback classification must receive the real error");
assert.equal(retainedDiagnostics.includes(privateCue), false);
assert.equal(retainedDiagnostics.includes("[omitted: video transcript]"), true);
});

View File

@@ -80,6 +80,11 @@ test("#8142 onFailure throwing does not crash the stream and is logged", async (
loggedOnFailureThrow, loggedOnFailureThrow,
"a console.debug call referencing onFailure must be emitted when the callback throws" "a console.debug call referencing onFailure must be emitted when the callback throws"
); );
assert.match(
debugCalls.flat().map(String).join(" "),
/boom from consumer onFailure handler/,
"ordinary stream diagnostics must retain their useful error message"
);
}); });
test("#8142 regression: onFailure returning normally logs nothing and behaves identically", async () => { test("#8142 regression: onFailure returning normally logs nothing and behaves identically", async () => {
@@ -117,3 +122,112 @@ test("#8142 regression: onFailure returning normally logs nothing and behaves id
"the happy path (no throw) must not emit the onFailure-throw debug log — behavior-free regression guard" "the happy path (no throw) must not emit the onFailure-throw debug log — behavior-free regression guard"
); );
}); });
test("sensitive onFailure diagnostics omit embedded transcript text", async () => {
const privateCue = "PRIVATE_VIDEO_CUE_onFailure_2e91";
const originalDebug = console.debug;
const debugCalls: unknown[][] = [];
console.debug = (...args: unknown[]) => {
debugCalls.push(args);
};
try {
await assert.rejects(
readTransformed([responseFailedChunk("upstream failed")], {
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-test",
body: { messages: [{ role: "user", content: "hello" }] },
redactStreamDiagnosticsForLog: true,
onFailure() {
throw new Error(privateCue);
},
}),
/upstream failed/i
);
} finally {
console.debug = originalDebug;
}
const retainedDiagnostics = debugCalls.flat().map(String).join(" ");
assert.equal(retainedDiagnostics.includes(privateCue), false);
assert.equal(retainedDiagnostics.includes("[omitted: video transcript]"), true);
});
test("sensitive onComplete diagnostics omit embedded transcript text", async () => {
const privateCue = "PRIVATE_VIDEO_CUE_onComplete_8d42";
const originalDebug = console.debug;
const debugCalls: unknown[][] = [];
console.debug = (...args: unknown[]) => {
debugCalls.push(args);
};
try {
await readTransformed(
[
`data: ${JSON.stringify({
id: "chatcmpl-sensitive-log",
choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
})}\n\n`,
"data: [DONE]\n\n",
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-test",
body: { messages: [{ role: "user", content: "hello" }] },
redactStreamDiagnosticsForLog: true,
onComplete() {
throw new Error(privateCue);
},
}
);
} finally {
console.debug = originalDebug;
}
const retainedDiagnostics = debugCalls.flat().map(String).join(" ");
assert.equal(retainedDiagnostics.includes(privateCue), false);
assert.equal(retainedDiagnostics.includes("[omitted: video transcript]"), true);
});
test("sensitive flush diagnostics omit embedded transcript text", async () => {
const privateCue = "PRIVATE_VIDEO_CUE_flush_f601";
const originalLog = console.log;
const logCalls: unknown[][] = [];
console.log = (...args: unknown[]) => {
logCalls.push(args);
};
try {
await readTransformed(
[
`data: ${JSON.stringify({
id: "chatcmpl-sensitive-flush",
choices: [{ index: 0, delta: { content: "tail" }, finish_reason: "stop" }],
})}`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-test",
body: { messages: [{ role: "user", content: "hello" }] },
redactStreamDiagnosticsForLog: true,
reqLogger: {
appendConvertedChunk() {
throw new Error(privateCue);
},
},
}
);
} finally {
console.log = originalLog;
}
const retainedDiagnostics = logCalls.flat().map(String).join(" ");
assert.equal(retainedDiagnostics.includes(privateCue), false);
assert.equal(retainedDiagnostics.includes("[omitted: video transcript]"), true);
});

View File

@@ -616,10 +616,7 @@ test("ensureStreamReadiness preserves sanitized error-only diagnostics on early
assert.equal(result.response.status, 502); assert.equal(result.response.status, 502);
assert.equal(result.code, "STREAM_EARLY_EOF"); assert.equal(result.code, "STREAM_EARLY_EOF");
assert.equal(result.type, "stream_early_eof"); assert.equal(result.type, "stream_early_eof");
assert.equal( assert.equal(result.classificationReason, "Stream ended before producing a non-ping SSE event");
result.classificationReason,
"Stream ended before producing a non-ping SSE event"
);
assert.equal( assert.equal(
result.upstreamDiagnostic, result.upstreamDiagnostic,
"UPSTREAM_DETAIL quota exhausted; retry after 2s; empty content Bearer [REDACTED] <path>" "UPSTREAM_DETAIL quota exhausted; retry after 2s; empty content Bearer [REDACTED] <path>"
@@ -636,19 +633,38 @@ test("ensureStreamReadiness preserves sanitized error-only diagnostics on early
assert.equal(body.upstream_details.error.message, result.upstreamDiagnostic); assert.equal(body.upstream_details.error.message, result.upstreamDiagnostic);
assert.equal(warnings.length, 1); assert.equal(warnings.length, 1);
for (const surfaced of [ for (const surfaced of [result.reason, body.upstream_details.error.message, warnings[0]]) {
result.reason,
body.upstream_details.error.message,
warnings[0],
]) {
assert.match(surfaced, /UPSTREAM_DETAIL/); assert.match(surfaced, /UPSTREAM_DETAIL/);
assert.doesNotMatch( assert.doesNotMatch(surfaced, /SECOND_DETAIL|TOP_SECRET|\/srv\/omniroute\/handler\.ts/);
surfaced,
/SECOND_DETAIL|TOP_SECRET|\/srv\/omniroute\/handler\.ts/
);
} }
}); });
test("ensureStreamReadiness omits sensitive upstream diagnostics from retained logs", async () => {
const rawCue = "PRIVATE_STREAM_READINESS_VIDEO_TRANSCRIPT_SENTINEL";
const warnings: string[] = [];
const response = new Response(
streamFromChunks([`data: ${JSON.stringify({ error: { message: rawCue } })}\n\n`]),
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
);
const result = await ensureStreamReadiness(response, {
timeoutMs: 100,
provider: "test-provider",
model: "test-model",
redactUpstreamDiagnosticForLog: true,
log: {
warn: (_tag, message) => warnings.push(message),
},
});
assert.equal(result.ok, false);
if (result.ok) assert.fail("error-only SSE payload must remain a readiness failure");
assert.match(result.reason, new RegExp(rawCue));
assert.equal(warnings.length, 1);
assert.doesNotMatch(warnings[0], new RegExp(rawCue));
assert.match(warnings[0], /upstream diagnostic omitted/);
});
test("stream-readiness diagnostics cannot reclassify Antigravity account exhaustion (#8972)", () => { test("stream-readiness diagnostics cannot reclassify Antigravity account exhaustion (#8972)", () => {
const classificationError = "Stream ended before producing a non-ping SSE event"; const classificationError = "Stream ended before producing a non-ping SSE event";
const diagnostic = "UPSTREAM_DETAIL quota exhausted; retry after 2s; empty content"; const diagnostic = "UPSTREAM_DETAIL quota exhausted; retry after 2s; empty content";

View File

@@ -8,6 +8,7 @@ import {
DEFAULT_UNIVERSAL_HANDOFF_CONFIG, DEFAULT_UNIVERSAL_HANDOFF_CONFIG,
type HandoffPayload, type HandoffPayload,
} from "../../open-sse/services/contextHandoff.ts"; } from "../../open-sse/services/contextHandoff.ts";
import { fingerprintVideoTranscriptDescription } from "../../src/lib/guardrails/videoTranscriptLogRedaction.ts";
// ── resolveUniversalHandoffConfig ──────────────────────────────────────────── // ── resolveUniversalHandoffConfig ────────────────────────────────────────────
@@ -160,6 +161,49 @@ test("providerAllowlist: empty allowlist allows all providers", async () => {
assert.ok(calls.length > 0, "handleSingleModel MUST be called when allowlist is empty"); assert.ok(calls.length > 0, "handleSingleModel MUST be called when allowlist is empty");
}); });
test("transcript-sensitive model switches generate from a safely redacted history", async () => {
const sentinel = "PRIVATE_UNIVERSAL_HANDOFF_TRANSCRIPT_SENTINEL";
const description = `[Video description: stable scene; transcript[source=embedded;confidence=1.00;interval=00:01.000-00:02.000] text="${sentinel}"]`;
const calls: Array<{ body: Record<string, unknown>; modelStr: string }> = [];
maybeGenerateUniversalHandoff({
sessionId: "ses_private_video",
comboName: "private-video-combo",
messages: [
{
role: "user",
content: [
{ type: "input_text", text: "Keep this universal handoff context" },
{ type: "input_text", text: description },
],
},
],
prevModel: "openai/gpt-4o",
currModel: "anthropic/claude-3-5-sonnet",
videoTranscriptSensitive: true,
trustedDescriptionFingerprints: [fingerprintVideoTranscriptDescription(description)],
universalConfig: {
...DEFAULT_UNIVERSAL_HANDOFF_CONFIG,
enabled: true,
providerAllowlist: [],
handoffModel: "anthropic/claude-3-5-sonnet",
},
handleSingleModel: async (body, modelStr) => {
calls.push({ body, modelStr });
return new Response(JSON.stringify({ choices: [{ message: { content: "{}" } }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
await waitImmediate();
assert.strictEqual(calls.length, 1);
const retainedPrompt = JSON.stringify(calls[0].body);
assert.match(retainedPrompt, /Keep this universal handoff context/);
assert.match(retainedPrompt, /omitted: video transcript/);
assert.doesNotMatch(retainedPrompt, new RegExp(sentinel));
});
test("providerAllowlist: handoffModel takes precedence over currModel for allowlist check", async () => { test("providerAllowlist: handoffModel takes precedence over currModel for allowlist check", async () => {
const calls: unknown[] = []; const calls: unknown[] = [];
await maybeGenerateUniversalHandoff({ await maybeGenerateUniversalHandoff({