// lib/json-utils.js — tolerant JSON extraction for LLM responses. // Local models often wrap JSON in prose or code fences, or leave trailing commas. export function extractJson(text) { if (text == null) throw new Error("Model returned an empty response."); let s = String(text).trim(); // Strip code fences anywhere s = s.replace(/```(?:json|javascript|js)?/gi, "```"); const fenced = s.match(/```([\s\S]*?)```/); if (fenced && fenced[1].trim().match(/^[\[{]/)) s = fenced[1].trim(); // Slice from first brace/bracket to its matching end; try whichever starts first, first. const candidates = []; const firstObj = s.indexOf("{"); const firstArr = s.indexOf("["); const objSlice = firstObj !== -1 ? s.slice(firstObj, s.lastIndexOf("}") + 1) : null; const arrSlice = firstArr !== -1 ? s.slice(firstArr, s.lastIndexOf("]") + 1) : null; if (firstArr !== -1 && (firstObj === -1 || firstArr < firstObj)) { if (arrSlice) candidates.push(arrSlice); if (objSlice) candidates.push(objSlice); } else { if (objSlice) candidates.push(objSlice); if (arrSlice) candidates.push(arrSlice); } candidates.unshift(s); let lastErr = null; for (const c of candidates) { if (!c) continue; for (const attempt of [c, repair(c)]) { try { return JSON.parse(attempt); } catch (e) { lastErr = e; } } } const preview = s.slice(0, 300).replace(/\s+/g, " "); throw new Error( "Could not read the model's response as JSON. Try again, lower the temperature, or use a stronger model. Response began: " + preview ); } function repair(s) { let out = s; // Smart quotes -> straight quotes out = out.replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'"); // Remove trailing commas before } or ] out = out.replace(/,\s*([}\]])/g, "$1"); // Remove JS-style comments out = out.replace(/^\s*\/\/.*$/gm, ""); // Control characters inside strings break JSON.parse; replace bare newlines in strings out = sanitizeNewlinesInStrings(out); return out; } function sanitizeNewlinesInStrings(s) { let result = ""; let inStr = false; let escaped = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (inStr) { if (escaped) { result += ch; escaped = false; continue; } if (ch === "\\") { result += ch; escaped = true; continue; } if (ch === '"') { inStr = false; result += ch; continue; } if (ch === "\n") { result += "\\n"; continue; } if (ch === "\r") { continue; } if (ch === "\t") { result += "\\t"; continue; } result += ch; } else { if (ch === '"') inStr = true; result += ch; } } return result; }