import { THINK_TAG_OPEN, THINK_TAG_CLOSE, CORRUPTION_PATTERNS, CORRUPTION_MATCH_THRESHOLD, } from "../prompts/response-rules"; import { sanitizeMessageText } from "./messageSanitizer"; export interface ParsedAssistantResponse { rawContent: string; content: string; reasoning: string; thinkingOpen: boolean; hadTranscriptLeakage: boolean; } /** * Detects if the model output looks like a system prompt loop or role-play injection. * Returns true if the content should be considered corrupted/looped. * * NOTE: This is intentionally conservative. Very few patterns are flagged to * avoid false positives. The stop-sequence approach has been removed because * it was the primary cause of response loops. */ export function isLoopedOrCorruptedResponse(text: string): boolean { if (!text || text.trim().length === 0) return false; if (CORRUPTION_PATTERNS.length === 0) return false; let matches = 0; for (const pattern of CORRUPTION_PATTERNS) { if (pattern.test(text)) { matches++; if (matches >= CORRUPTION_MATCH_THRESHOLD) return true; } } return false; } function stripLeadingMarkdownFence(text: string): string { return text.replace(/^\s*```[\w-]*\s*\n/, ""); } function stripDanglingThinkClosers(text: string): string { return text.replace(/^(?:\s*<\/think>\s*)+/i, ""); } /** * Strip Qwen3 / Qwen2.5 chat-template tokens that sometimes leak into responses. * These are never valid user-visible content. */ function stripQwenChatTemplateTokens(text: string): string { // Remove leading Qwen special tokens: <|im_start|>, <|im_end|>, // <|start_of_answered_content|>, <|start_of_answered_content>assistant:, etc. return text .replace(/^(?:\s*<\|(?:im_start|im_end|start_of_answered_content|end_of_answered_content|[a-z_]+)\|>\s*)+/gi, "") .replace(/^(?:\s*<\|start_of_answered_content\s*>\s*assistant\s*:\s*)/i, "") .replace(/^(?:\s*assistant\s*:\s*)/i, "") // bare "assistant:" at the very start .replace(/<\|(?:im_start|im_end|start_of_answered_content|end_of_answered_content|[a-z_]+)\|>/gi, ""); } function hasKnownPromptEcho(text: string): boolean { return /^\s*you are a helpful ai assistant\.?/i.test(text) || /^\s*you will be tested with prior context/i.test(text) || /^\s*you are a precise coding assistant running locally\.?/i.test(text) || /^\s*(?:#{1,6}\s*)?(?:System|Developer)\s*:\s*you are a precise coding assistant running locally\.?/i.test(text); } function stripLeadingFencedTranscriptLeakage(text: string): string { const trimmed = text.trimStart(); const fenceStripped = stripLeadingMarkdownFence(trimmed); if (!trimmed.startsWith("```")) { return text; } const leadingFence = trimmed.match(/^```[\w-]*\s*\n([\s\S]*?)\n```\s*/); if (leadingFence) { const fencedBody = leadingFence[1] ?? ""; if (hasKnownPromptEcho(fencedBody)) { const suffix = trimmed.slice(leadingFence[0].length).trimStart(); return suffix || ""; } return text; } if (hasKnownPromptEcho(fenceStripped)) { return ""; } return text; } /** * Strip prompt echo / transcript leakage from the visible assistant content. */ export function stripPromptEchoAndTranscriptLeakage(text: string): string { if (!text) return ""; const sanitized = sanitizeMessageText(text).text; const normalized = sanitized.replace(/\r\n/g, "\n"); const withoutQwen = stripQwenChatTemplateTokens(normalized); const withoutDanglingThink = stripDanglingThinkClosers(withoutQwen).trimStart(); const withoutLeadingLeakedFence = stripLeadingFencedTranscriptLeakage(withoutDanglingThink); const trimmed = withoutLeadingLeakedFence.trimStart(); const fenceStripped = stripLeadingMarkdownFence(trimmed); // Known leaked system-prompt openings: drop them entirely. if (/^\s*you are a helpful ai assistant\.?/i.test(trimmed)) return ""; if (/^\s*you will be tested with prior context/i.test(trimmed)) return ""; if (/^\s*you are a precise coding assistant running locally\.?/i.test(trimmed)) return ""; if (/^\s*(?:#{1,6}\s*)?(?:System|Developer)\s*:\s*you are a precise coding assistant running locally\.?/i.test(trimmed)) return ""; if (/^\s*(?:#{1,6}\s*)?(?:System|Developer)\s*:\s*you are a precise coding assistant running locally\.?/i.test(fenceStripped)) return ""; return withoutLeadingLeakedFence; } /** * Parse assistant response — separates visible content from reasoning. */ export function parseAssistantResponse(rawContent: string): ParsedAssistantResponse { let cursor = 0; let thinkingOpen = false; let content = ""; let reasoning = ""; while (cursor < rawContent.length) { if (!thinkingOpen) { const start = rawContent.indexOf(THINK_TAG_OPEN, cursor); if (start === -1) { content += rawContent.slice(cursor); break; } content += rawContent.slice(cursor, start); cursor = start + THINK_TAG_OPEN.length; thinkingOpen = true; continue; } const end = rawContent.indexOf(THINK_TAG_CLOSE, cursor); if (end === -1) { reasoning += rawContent.slice(cursor); cursor = rawContent.length; break; } reasoning += rawContent.slice(cursor, end); cursor = end + THINK_TAG_CLOSE.length; thinkingOpen = false; } const normalizedContent = reasoning.trim().length > 0 ? content.replace(/^\s+/, "") : content; const visibleContent = stripPromptEchoAndTranscriptLeakage(normalizedContent).trimEnd(); // Detect leakage on the visible content only (not the think block) const hadTranscriptLeakage = visibleContent.trimEnd() !== normalizedContent.trimEnd(); return { rawContent, content: visibleContent, reasoning: reasoning.trim(), thinkingOpen, hadTranscriptLeakage, }; } export function buildChatTitle(text: string): string { const normalized = text.replace(/\s+/g, " ").trim(); if (!normalized) return "New Chat"; return normalized.slice(0, 50) + (normalized.length > 50 ? "..." : ""); }