Files
accounted/lib/ai/json.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

56 lines
2.0 KiB
TypeScript

// Bounds for the candidate scan below. Real model output is already capped by
// the caller's max_tokens (roughly 33 KB of text at 8192 tokens), so genuine
// responses never come near these; they exist so pathological or adversarially
// brace-laden text cannot make the scan quadratic (compliance review A.8.28).
// Oversized or exhausted inputs fall through to the raw text and land in the
// caller's existing parse-failure path.
const MAX_SCAN_INPUT_LENGTH = 256 * 1024
const MAX_CANDIDATE_ATTEMPTS = 50
/**
* Models intermittently wrap a JSON answer in markdown fences (```json ... ```)
* or add prose around it despite a JSON-only instruction. Scan for balanced
* top-level '{'..'}' candidates (string- and escape-aware, so braces inside
* JSON string values don't end a candidate early) and return the first one
* JSON.parse accepts; prose braces around the object form unparseable
* candidates and are skipped. Returns the input unchanged when no candidate
* parses, so the caller's parse-failure path handles prose-only refusals.
* Schema validation downstream still rejects well-formed-but-wrong JSON.
*/
export function extractJsonObject(raw: string): string {
if (raw.length > MAX_SCAN_INPUT_LENGTH) return raw
let attempts = 0
let start = raw.indexOf('{')
while (start !== -1 && attempts < MAX_CANDIDATE_ATTEMPTS) {
attempts++
let depth = 0
let inString = false
let escaped = false
for (let i = start; i < raw.length; i++) {
const ch = raw[i]
if (inString) {
if (escaped) escaped = false
else if (ch === '\\') escaped = true
else if (ch === '"') inString = false
} else if (ch === '"') {
inString = true
} else if (ch === '{') {
depth++
} else if (ch === '}') {
depth--
if (depth === 0) {
const candidate = raw.slice(start, i + 1)
try {
JSON.parse(candidate)
return candidate
} catch {
break
}
}
}
}
start = raw.indexOf('{', start + 1)
}
return raw
}