File size: 4,512 Bytes
ff49634 df94b74 ff49634 df94b74 ff49634 df94b74 ff49634 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | /**
* Normalize model output before JSON parsing.
* Reasoning models (GLM, DeepSeek, etc.) often wrap chain-of-thought in tags
* that are NOT HTML error pages.
*/
const THINK_TAG = "think";
const REASONING_BLOCK_PATTERNS: RegExp[] = [
/<think>[\s\S]*?<\/redacted_thinking>/gi,
/<thinking>[\s\S]*?<\/thinking>/gi,
new RegExp(`<${THINK_TAG}>[\\s\\S]*?</${THINK_TAG}>`, "gi"),
];
const UNCLOSED_REASONING_PREFIXES = [
/^<think>[\s\S]*/i,
/^<thinking>[\s\S]*/i,
new RegExp(`^<${THINK_TAG}>[\\s\\S]*`, "i"),
];
export function stripReasoningBlocks(content: string): string {
let text = content;
for (const pattern of REASONING_BLOCK_PATTERNS) {
text = text.replace(pattern, "");
}
for (const pattern of UNCLOSED_REASONING_PREFIXES) {
text = text.replace(pattern, "");
}
return text.trim();
}
const TRUNCATED_JSON_ERROR = "JSON response was truncated";
/** Detect incomplete JSON — valid start but stream/token limit cut off before closing braces. */
export function isLikelyTruncatedJson(content: string): boolean {
const trimmed = stripReasoningBlocks(content).trim();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false;
if (trimmed.endsWith("}") || trimmed.endsWith("]")) {
try {
JSON.parse(trimmed);
return false;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return /unterminated string|unexpected eof|unexpected end|expected .* at end/i.test(msg);
}
}
try {
JSON.parse(trimmed);
return false;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
if (/unterminated string|unexpected eof|unexpected end|expected .* at end/i.test(msg)) {
return true;
}
}
return true;
}
function extractJsonCandidate(content: string): string {
const trimmed = content.trim();
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (fenced?.[1]) return fenced[1].trim();
const objIdx = trimmed.indexOf("{");
const arrIdx = trimmed.indexOf("[");
let start = -1;
if (objIdx === -1) start = arrIdx;
else if (arrIdx === -1) start = objIdx;
else start = Math.min(objIdx, arrIdx);
if (start < 0) return trimmed;
const slice = trimmed.slice(start);
const lastBrace = slice.lastIndexOf("}");
const lastBracket = slice.lastIndexOf("]");
const end = Math.max(lastBrace, lastBracket);
if (end === -1) return slice;
return slice.slice(0, end + 1);
}
export function parseAiJsonResponse(content: string): unknown {
const normalized = stripReasoningBlocks(content);
if (!normalized) throw new Error("Empty response from AI");
const candidates = [normalized, extractJsonCandidate(normalized)];
const uniqueCandidates = [...new Set(candidates.filter(Boolean))];
let lastError: Error | undefined;
for (const candidate of uniqueCandidates) {
try {
return JSON.parse(candidate);
} catch (err: unknown) {
lastError = err instanceof Error ? err : new Error(String(err));
const markdownCleaned = candidate
.replace(/^```json\s*/i, "")
.replace(/```\s*$/, "")
.trim();
if (markdownCleaned !== candidate) {
try {
return JSON.parse(markdownCleaned);
} catch (inner: unknown) {
lastError = inner instanceof Error ? inner : new Error(String(inner));
}
}
}
}
if (isLikelyTruncatedJson(normalized)) {
throw new Error(
`${TRUNCATED_JSON_ERROR} before completion (usually max_tokens too low). Preview: ${normalized.slice(0, 200)}`,
);
}
throw new Error(
`Failed to parse AI response as JSON: ${lastError?.message ?? "unknown error"}. Preview: ${normalized.slice(0, 200)}`,
);
}
export { TRUNCATED_JSON_ERROR };
export function extractContentFromCompletionBody(body: string): string | null {
const trimmed = body.trim();
if (!trimmed.startsWith("{")) return null;
try {
const json = JSON.parse(trimmed) as {
choices?: Array<{ message?: { content?: string } }>;
};
const content = json.choices?.[0]?.message?.content;
return typeof content === "string" ? content : null;
} catch {
return null;
}
}
export function looksLikeHtmlErrorPage(content: string): boolean {
const head = content.trim().slice(0, 300).toLowerCase();
return (
head.startsWith("<!doctype") ||
head.startsWith("<html") ||
head.startsWith("<head") ||
head.startsWith("<body") ||
/^<\?(xml|php)/.test(head)
);
}
|