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// Leniently extract a JSON object from an LLM response: prefer a fenced
// ```json block, fall back to the first balanced {...} span. Returns null if
// nothing parses, so callers can degrade gracefully rather than crash.
export function extractJson<T = unknown>(text: string): T | null {
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = fenced ? fenced[1] : text;
// Try the whole candidate first.
try {
return JSON.parse(candidate.trim()) as T;
} catch {
// Fall back to the first balanced object.
}
const start = candidate.indexOf("{");
if (start === -1) return null;
let depth = 0;
let inStr = false;
let esc = false;
for (let i = start; i < candidate.length; i++) {
const c = candidate[i];
if (inStr) {
if (esc) esc = false;
else if (c === "\\") esc = true;
else if (c === '"') inStr = false;
} else if (c === '"') inStr = true;
else if (c === "{") depth++;
else if (c === "}") {
depth--;
if (depth === 0) {
try {
return JSON.parse(candidate.slice(start, i + 1)) as T;
} catch {
return null;
}
}
}
}
return null;
}