Files
HealthLog/src/lib/ai/json-extract.ts
T
Marc-André Bombeck a463206774 fix(ai): stop prefilling the assistant turn on Anthropic JSON calls
The Anthropic client steered JSON replies by appending an assistant turn
containing an opening brace and gluing the brace back onto the reply.
Current Claude models (the 4.6 / 4.7 / 4.8 family, Opus 5, Sonnet 5,
Fable 5 and 5.1) reject an assistant prefill with HTTP 400, so every
JSON surface on the provider (briefing, status cards, document and lab
extraction) failed for a self-hoster on a current model while plain chat
kept working.

Drop the prefill on every model. The request now carries a plain
instruction on the last user turn to reply with a single JSON object and
nothing else, and the reply is narrowed to the outermost object body via
a shared extractJsonObject helper, so a fenced or prefixed answer still
parses. The insights stripJsonFences helper delegates to the same
function rather than keeping a second copy. The tool-use path is
unchanged.

Reported independently by @sweidinger.
2026-09-02 10:28:05 +02:00

27 lines
971 B
TypeScript

/**
* Isolate the JSON object body of a model reply.
*
* Providers without a native JSON mode (Anthropic, the local endpoints)
* are steered by a prompt instruction alone, and a compliant model still
* routinely wraps its object in a ```json fence or puts a sentence in
* front of it. This helper drops a leading/trailing fence and narrows to
* the outermost `{` ... `}` span so `JSON.parse` sees the object.
*
* It is a no-op on already-clean JSON and on fence-free prose with no
* braces: both return the trimmed input, so a prose fallback downstream
* still works.
*/
export function extractJsonObject(raw: string): string {
let text = raw.trim();
const fenced = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
if (fenced) {
text = fenced[1].trim();
}
const first = text.indexOf("{");
const last = text.lastIndexOf("}");
if (first !== -1 && last !== -1 && last > first) {
return text.slice(first, last + 1);
}
return text;
}