intake: only a reply that finished is a reply the schema constrained

liteLLMExtractor rejected finish_reason "length" and accepted everything else,
so a content_filter or tool_calls stop was parsed as if the strict json_schema
had held. Reject any reason present and not "stop", naming it.
This commit is contained in:
2026-08-21 17:52:25 +10:00
parent be3d10d4f1
commit 7424d13858
+13 -3
View File
@@ -185,9 +185,19 @@ export function liteLLMExtractor(opts: {
});
if (!response.ok) throw new ExtractionError(`LiteLLM returned ${response.status}`);
const raw = (await response.json()) as { choices?: { message?: { content?: string }; finish_reason?: string }[] };
// A truncated reply can still parse once the JSON is closed, which quietly
// drops the tail of a long receipt. Losing lines silently is worse than failing.
if (raw.choices?.[0]?.finish_reason === "length") throw new ExtractionError("The receipt was too long to read in one pass.");
// Only `stop` means the strict schema was satisfied. A truncated reply can
// still parse once the JSON is closed, which quietly drops the tail of a
// long receipt; a `content_filter` or `tool_calls` stop is a different reply
// shape entirely. Accepting anything but `stop` accepts a reply the schema
// never constrained — losing lines silently is worse than failing.
const finish = raw.choices?.[0]?.finish_reason;
if (finish && finish !== "stop") {
throw new ExtractionError(
finish === "length"
? "The receipt was too long to read in one pass."
: `The model stopped before it finished (${finish}).`
);
}
const content = raw.choices?.[0]?.message?.content;
if (!content) throw new ExtractionError("LiteLLM returned no content");
try {