openclaw/src/agents/payload-redaction.ts
Vincent Koc 9c86a9fd23
fix(gateway): support image_url in OpenAI chat completions (#34068)
* fix(gateway): parse image_url in openai chat completions

* test(gateway): cover openai chat completions image_url flows

* docs(changelog): note openai image_url chat completions fix (#17685)

* fix(gateway): harden openai image_url parsing and limits

* test(gateway): add openai image_url regression coverage

* docs(changelog): expand #17685 openai chat completions note

* Gateway: make OpenAI image_url URL fetch opt-in and configurable

* Diagnostics: redact image base64 payload data in trace logs

* Changelog: note OpenAI image_url hardening follow-ups

* Gateway: enforce OpenAI image_url total budget incrementally

* Gateway: scope OpenAI image_url extraction to the active turn

* Update CHANGELOG.md
2026-03-06 00:35:50 -05:00

65 lines
1.8 KiB
TypeScript

import crypto from "node:crypto";
import { estimateBase64DecodedBytes } from "../media/base64.js";
export const REDACTED_IMAGE_DATA = "<redacted>";
function toLowerTrimmed(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function hasImageMime(record: Record<string, unknown>): boolean {
const candidates = [
toLowerTrimmed(record.mimeType),
toLowerTrimmed(record.media_type),
toLowerTrimmed(record.mime_type),
];
return candidates.some((value) => value.startsWith("image/"));
}
function shouldRedactImageData(record: Record<string, unknown>): record is Record<string, string> {
if (typeof record.data !== "string") {
return false;
}
const type = toLowerTrimmed(record.type);
return type === "image" || hasImageMime(record);
}
function digestBase64Payload(data: string): string {
return crypto.createHash("sha256").update(data).digest("hex");
}
/**
* Redacts image/base64 payload data from diagnostic objects before persistence.
*/
export function redactImageDataForDiagnostics(value: unknown): unknown {
const seen = new WeakSet<object>();
const visit = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((entry) => visit(entry));
}
if (!input || typeof input !== "object") {
return input;
}
if (seen.has(input)) {
return "[Circular]";
}
seen.add(input);
const record = input as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const [key, val] of Object.entries(record)) {
out[key] = visit(val);
}
if (shouldRedactImageData(record)) {
out.data = REDACTED_IMAGE_DATA;
out.bytes = estimateBase64DecodedBytes(record.data);
out.sha256 = digestBase64Payload(record.data);
}
return out;
};
return visit(value);
}