schema: previous_line_items on the model; say what the finish_reason rule is

Migration 0034 has the column and the Prisma model did not, so a regenerated
client and the database disagreed.

The extraction comment claimed only a finished reply is accepted while the code
accepts an absent finish_reason. The code is right — failing every call on a
provider that omits an optional field would be a certain outage in exchange for
a case that also requires syntactically complete JSON, and the harm a truncated
reply does is closed on the write side. The prose now states the rule as it is:
a reason that is PRESENT must be `stop`.
This commit is contained in:
2026-08-21 18:23:32 +10:00
parent d4ff68363a
commit b5569b8ccc
2 changed files with 14 additions and 5 deletions
+12 -5
View File
@@ -185,11 +185,18 @@ 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 }[] };
// 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.
// The rule is: a finish_reason that is PRESENT must be `stop`. An absent
// one is accepted, deliberately — providers differ on whether they send the
// field, and failing every call on a provider that omits it would be a
// certain outage in exchange for a case that also requires the model to
// emit syntactically complete JSON. The harm a truncated reply does — a
// partial item list replacing a good one — is closed on the write side
// instead, by the guard in `routeFood`/`createFromBatch`.
//
// `length` is the common one: a truncated reply still parses once the JSON
// happens to close, quietly dropping the tail of a long receipt. Any other
// reason (`content_filter`, `tool_calls`) is a different reply shape that
// the strict schema never constrained.
const finish = raw.choices?.[0]?.finish_reason;
if (finish && finish !== "stop") {
throw new ExtractionError(