import { parseToolCallsFromContent } from "./tool-call-parser"; const stripToolCallXmlBlocks = (text: string): string => { if (!text) return ""; let cleaned = text; cleaned = cleaned.replace(/[\s\S]*?<\/tool_call>/gi, ""); cleaned = cleaned.replace(/[\s\S]*?<\/use_mcp[\s_]*tool>/gi, ""); cleaned = cleaned.replace(/\n{3,}/g, "\n\n"); return cleaned.trim(); }; const collapseRepeatedVisibleContent = (text: string): string => { const trimmed = text.trim(); if (trimmed.length < 80) return text; for (let separatorLength = 0; separatorLength <= 4; separatorLength += 1) { const contentLength = trimmed.length - separatorLength; if (contentLength <= 0 || contentLength % 2 !== 0) continue; const midpoint = contentLength / 2; const first = trimmed.slice(0, midpoint).trimEnd(); const second = trimmed.slice(midpoint + separatorLength).trimStart(); if (first.length >= 40 && first === second) return first; } return text; }; const extractThinkBlocks = (text: string): { cleaned: string; extracted: string[] } => { if (!text) return { cleaned: "", extracted: [] }; const extracted: string[] = []; const visibleParts: string[] = []; let remaining = String(text); const openPrefixes = [" { let openIndex = -1; for (const prefix of openPrefixes) { const index = lower.indexOf(prefix); if (index >= 0) openIndex = openIndex === -1 ? index : Math.min(openIndex, index); } let closeIndex = -1; for (const prefix of closePrefixes) { const index = lower.indexOf(prefix); if (index >= 0) closeIndex = closeIndex === -1 ? index : Math.min(closeIndex, index); } if (openIndex === -1 && closeIndex === -1) return null; if (openIndex !== -1 && (closeIndex === -1 || openIndex < closeIndex)) return { kind: "open", index: openIndex }; return { kind: "close", index: closeIndex }; }; const parseTag = ( input: string, start: number ): { name: "think" | "thinking" | "analysis"; end: number } | null => { const closeIndex = input.indexOf(">", start); if (closeIndex < 0) return null; const tag = input.slice(start, closeIndex + 1); const open = tag.match(/^<(think|thinking|analysis)(?:\s+[^>]*)?>$/i); if (open) return { name: open[1]!.toLowerCase() as "think" | "thinking" | "analysis", end: closeIndex + 1, }; const close = tag.match(/^<\/(think|thinking|analysis)(?:\s+[^>]*)?>$/i); if (close) return { name: close[1]!.toLowerCase() as "think" | "thinking" | "analysis", end: closeIndex + 1, }; return null; }; while (remaining) { const lower = remaining.toLowerCase(); const next = findNextTag(lower); if (!next) { visibleParts.push(remaining); break; } if (next.kind === "open") { if (next.index > 0) visibleParts.push(remaining.slice(0, next.index)); const openTag = parseTag(remaining, next.index); if (!openTag) { visibleParts.push(remaining.slice(0, next.index + 1)); remaining = remaining.slice(next.index + 1); continue; } remaining = remaining.slice(openTag.end); const lowerAfter = remaining.toLowerCase(); const closeStart = lowerAfter.indexOf(` 0) { const value = remaining.slice(0, next.index).trim(); if (value) extracted.push(value); } const closeTag = parseTag(remaining, next.index); remaining = closeTag ? remaining.slice(closeTag.end) : remaining.slice(next.index + 1); } return { cleaned: visibleParts.join("").trim(), extracted }; }; export const normalizeReasoningAndContentInMessage = (message: Record): void => { const contentRaw = typeof message["content"] === "string" ? String(message["content"]) : ""; const reasoningRaw = typeof message["reasoning_content"] === "string" ? String(message["reasoning_content"]) : ""; const contentThink = extractThinkBlocks(contentRaw); const reasoningThink = extractThinkBlocks(reasoningRaw); const extracted = [...contentThink.extracted, ...reasoningThink.extracted].filter(Boolean); const nextReasoning = [reasoningThink.cleaned, extracted.join("\n")] .filter((v) => v.trim().length > 0) .join("\n"); const nextContent = contentThink.cleaned; if (nextContent !== contentRaw) message["content"] = nextContent; if (nextReasoning !== reasoningRaw) message["reasoning_content"] = nextReasoning; const strippedContent = stripToolCallXmlBlocks( typeof message["content"] === "string" ? String(message["content"]) : "" ); const strippedReasoning = stripToolCallXmlBlocks( typeof message["reasoning_content"] === "string" ? String(message["reasoning_content"]) : "" ); message["content"] = collapseRepeatedVisibleContent(strippedContent); if (strippedReasoning) { message["reasoning_content"] = strippedReasoning; } else { delete message["reasoning_content"]; } }; export const normalizeToolCallsInMessage = (message: Record): boolean => { const existing = message["tool_calls"]; const hasToolCalls = Array.isArray(existing) && existing.length > 0; if (hasToolCalls) { return false; } const content = typeof message["content"] === "string" ? String(message["content"]) : ""; const parsed = parseToolCallsFromContent(content); if (parsed.length > 0) { message["tool_calls"] = parsed; return true; } return false; };