/** Coerces arbitrary provider content values into displayable text without throwing. */ export function coerceChatContentText(value: unknown): string { if (typeof value === "string") { return value; } if (value == null) { return ""; } if ( typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol" ) { return String(value); } if (typeof value === "object") { try { return JSON.stringify(value) ?? ""; } catch { return ""; } } return ""; } /** Extracts normalized plain text from string content or OpenAI-style text blocks. */ export function extractTextFromChatContent( content: unknown, opts?: { sanitizeText?: (text: string) => string; joinWith?: string; normalizeText?: (text: string) => string; }, ): string | null { const normalizeText = opts?.normalizeText ?? ((text: string) => text.replace(/\s+/g, " ").trim()); const joinWith = opts?.joinWith ?? " "; const sanitize = (text: unknown): string => { const raw = coerceChatContentText(text); const sanitized = opts?.sanitizeText ? opts.sanitizeText(raw) : raw; return coerceChatContentText(sanitized); }; const normalize = (text: unknown): string => coerceChatContentText(normalizeText(coerceChatContentText(text))); if (typeof content === "string") { const value = sanitize(content); const normalized = normalize(value); return normalized ? normalized : null; } if (!Array.isArray(content)) { return null; } const chunks: string[] = []; for (const block of content) { if (!block || typeof block !== "object") { continue; } // Non-text blocks can contain media or tool payloads; callers here need visible text only. if ((block as { type?: unknown }).type !== "text") { continue; } const text = (block as { text?: unknown }).text; const value = sanitize(text); if (value.trim()) { chunks.push(value); } } const joined = normalize(chunks.join(joinWith)); return joined ? joined : null; }