// Assembles a redacted debug snapshot of the /ask chat — the conversation state // plus a trace of recent raw provider calls (request / response / finish reason) // — as a pretty JSON string for offline debugging (truncation, empty answers, // "trapped in context"). Pure + unit-testable. An API key is NEVER included: the // inputs don't carry one, and `redactKeys` scrubs anything key-shaped as a // defense-in-depth backstop. import type { UiMessage } from "./askConversation"; import type { DebugCall } from "./askProvider"; export type DebugExportInput = { provider: string; model: string; searchMode: string; markdownOn: boolean; reportMode: boolean; strictGrounding: boolean; detached: boolean; maxAnswerTokens: number; messages: UiMessage[]; report: string; grounding: { label: string; videoCount: number; truncated: boolean } | null; contextOverride: string | null; trace: DebugCall[]; now?: number; // injectable timestamp for tests }; // Provider API-key shapes: Anthropic (sk-ant-…), OpenAI (sk-…), Gemini (AIza…). const KEY_PATTERNS = [ /sk-ant-[A-Za-z0-9_-]{10,}/g, /sk-[A-Za-z0-9_-]{16,}/g, /AIza[A-Za-z0-9_-]{10,}/g, ]; export function redactKeys(s: string): string { return KEY_PATTERNS.reduce((acc, re) => acc.replace(re, "[REDACTED]"), s); } export function buildDebugExport(input: DebugExportInput): string { const obj = { meta: { kind: "ytdlp-transcript-browser/ask-debug", version: 1, exportedAt: new Date(input.now ?? Date.now()).toISOString(), provider: input.provider, model: input.model, searchMode: input.searchMode, markdownOn: input.markdownOn, reportMode: input.reportMode, strictGrounding: input.strictGrounding, detached: input.detached, maxAnswerTokens: input.maxAnswerTokens, }, grounding: input.grounding, contextOverride: input.contextOverride, report: input.report, messages: input.messages, // Recent raw provider calls (finish reasons, payloads). No API key. trace: input.trace, }; return redactKeys(JSON.stringify(obj, null, 2)); }