feat(video): redact transcript fields in the in-memory pending-request snapshot (#12430 item 6)

trackPendingRequest (open-sse/handlers/chatCore.ts) stored the raw client
body (with video transcript/audioTranscript cues) under `clientRequest`,
live-exposed via /api/usage/call-logs (pendingDetails), /api/logs/[id] and
/api/conversations while a request is in-flight. P2a redacted the persisted
detailed-log snapshot but not this in-memory copy.

Add redactPendingBody() to videoBridgeSnapshotRedaction.ts (sibling to
logClientRawRequestRedacted from P2a): when videoBridgeObserved, returns the
redacted clone from redactVideoTranscriptFieldsForLog; otherwise returns the
exact same reference. Wire it into the trackPendingRequest call site
(chatCore.ts:934), keeping the file within its frozen 5976-line budget
(5971 -> 5974).
This commit is contained in:
diegosouzapw
2026-09-03 08:26:26 -03:00
parent cb38bfa6ee
commit 5fc9e37d22
3 changed files with 77 additions and 3 deletions

View File

@@ -360,7 +360,10 @@ import { deleteSessionAccountAffinity } from "@/lib/db/sessionAccountAffinity";
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
import { guardrailRegistry } from "@/lib/guardrails";
import type { VideoBridgeLogRedactionEntry } from "@/lib/guardrails/videoBridge";
import { logClientRawRequestRedacted } from "@/lib/guardrails/videoBridgeSnapshotRedaction";
import {
logClientRawRequestRedacted,
redactPendingBody,
} from "@/lib/guardrails/videoBridgeSnapshotRedaction";
import {
shouldPreserveCacheControl,
resolveConnectionCacheOverride,
@@ -928,7 +931,7 @@ export async function handleChatCore({
const pendingRequestId =
trackPendingRequest(model, provider, pendingConnId, true, {
clientEndpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
clientRequest: clientRawRequest?.body ?? body,
clientRequest: redactPendingBody(clientRawRequest?.body ?? body, videoBridgeObserved),
providerRequest: initialProviderRequest,
stage: "registered",
correlationId,

View File

@@ -133,3 +133,17 @@ export function logClientRawRequestRedacted(
clientRawRequest.headers
);
}
/**
* Call-site wrapper for the `clientRequest` field stored by `trackPendingRequest`
* (open-sse/handlers/chatCore.ts): the sibling in-memory leak to
* `logClientRawRequestRedacted` above — same raw body, but live-exposed via
* /api/usage/call-logs (pendingDetails), /api/logs/[id] and /api/conversations
* while the request is in-flight, not just in the persisted detailed-log
* snapshot. Identical observed/non-observed branching: a non-observed request
* keeps the exact same reference (no clone); an observed one gets the redacted
* clone.
*/
export function redactPendingBody(clientRequest: unknown, videoBridgeObserved: boolean): unknown {
return videoBridgeObserved ? redactVideoTranscriptFieldsForLog(clientRequest) : clientRequest;
}

View File

@@ -7,7 +7,10 @@
import assert from "node:assert/strict";
import test from "node:test";
import { redactVideoTranscriptFieldsForLog } from "../../../src/lib/guardrails/videoBridgeSnapshotRedaction.ts";
import {
redactVideoTranscriptFieldsForLog,
redactPendingBody,
} from "../../../src/lib/guardrails/videoBridgeSnapshotRedaction.ts";
// Heavy import is fine here (test only, never in the production module under test) — used
// solely to prove the local placeholder literal never drifts from the canonical P1 constant.
import { VIDEO_TRANSCRIPT_REDACTION_PLACEHOLDER } from "../../../src/lib/guardrails/videoBridgeHelpers.ts";
@@ -205,3 +208,57 @@ test("the redaction placeholder matches the canonical P1 constant (no drift)", (
const part = contentAt(result, "messages", 0)[0];
assert.equal(part.transcript, VIDEO_TRANSCRIPT_REDACTION_PLACEHOLDER);
});
// #12430 item 6 (P2c): the sibling in-memory leak. `trackPendingRequest`
// (open-sse/handlers/chatCore.ts) stores the same raw client body under
// `clientRequest`, live-exposed via /api/usage/call-logs (pendingDetails),
// /api/logs/[id] and /api/conversations while the request is in-flight. This
// helper is the guarded call-site wrapper chatCore.ts uses, mirroring
// logClientRawRequestRedacted's observed/non-observed branching.
test("redactPendingBody: observed=true delegates to redactVideoTranscriptFieldsForLog", () => {
const body = {
messages: [
{
role: "user",
content: [
{
type: "input_video",
video_url: "https://example.com/clip.mp4",
transcript: { cues: [{ text: "pending secret" }] },
},
],
},
],
};
const result = redactPendingBody(body, true);
assert.notEqual(
result,
body,
"observed path must return a new structure, not the same reference"
);
const part = contentAt(result, "messages", 0)[0];
assert.equal(part.transcript, VIDEO_TRANSCRIPT_REDACTION_PLACEHOLDER);
assert.ok(!JSON.stringify(result).includes("pending secret"));
assert.deepEqual(result, redactVideoTranscriptFieldsForLog(body));
});
test("redactPendingBody: observed=false returns the SAME reference unchanged", () => {
const body = {
messages: [
{
role: "user",
content: [
{
type: "input_video",
video_url: "https://example.com/clip.mp4",
transcript: { cues: [{ text: "not observed" }] },
},
],
},
],
};
const result = redactPendingBody(body, false);
assert.equal(result, body, "non-observed path must return the exact same reference");
});