llm-output-guard 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,7 @@ Your retry layer watches for `429`, `5xx` and timeouts. It cannot see a model th
32
32
  looped until `max_tokens`, returned `{}`, stopped mid-sentence, or answered in the
33
33
  wrong language — because all of those arrive as a **successful request**.
34
34
 
35
- This produces the signal that layer is missing. Zero dependencies, ~5 KB gzipped,
35
+ This produces the signal that layer is missing. Zero dependencies, ~6 KB min+gzip,
36
36
  synchronous, no network.
37
37
 
38
38
  ```bash
@@ -267,6 +267,8 @@ because only one of those is evidence.
267
267
  ## Design notes
268
268
 
269
269
  - **Zero runtime dependencies**, enforced in CI. Node ≥ 18; works on edge, browser, Deno, Bun.
270
+ - **The size claim is a budget, not a memory.** `npm run size` bundles each entry, minifies and gzips it, and fails over budget — enforced in CI beside the zero-dependency check, because a number in prose is the cheapest thing in a repo to go stale.
271
+ - **Types resolve on old and new TypeScript alike.** Every subpath is listed in `typesVersions` as well as `exports`, so `moduleResolution: "node"` — still the default under `module: commonjs` — sees the adapters instead of only the root.
270
272
  - **Pure and synchronous.** No network, no clock, no randomness — safe on a hot path, trivial to test.
271
273
  - **Scores, not booleans.** Detectors report 0–1 and leave the threshold decision to you.
272
274
  - **Abstains rather than guesses.** Samples too short to judge score 0.
package/dist/bin.cjs CHANGED
@@ -1033,6 +1033,16 @@ function extractTurn(value) {
1033
1033
  return typeof value === "string" && value.length > 0 ? { text: value } : null;
1034
1034
  }
1035
1035
  const record = value;
1036
+ if (typeof record.role === "string" && record.role !== "assistant" && record.role !== "model") {
1037
+ return null;
1038
+ }
1039
+ const messageCalls = Array.isArray(record.tool_calls) ? record.tool_calls : Array.isArray(record.toolCalls) && typeof record.content === "string" ? record.toolCalls : null;
1040
+ if (messageCalls && (typeof record.content === "string" || record.content == null)) {
1041
+ return toTurn({ choices: [{ message: { ...record, tool_calls: messageCalls } }] });
1042
+ }
1043
+ if (typeof record.content === "string" && record.role != null) {
1044
+ return record.content.length > 0 ? { text: record.content } : null;
1045
+ }
1036
1046
  if (Array.isArray(record.toolCalls)) {
1037
1047
  const calls = record.toolCalls;
1038
1048
  const native = calls.every(
@@ -1064,11 +1074,32 @@ function extractTurn(value) {
1064
1074
  if (typeof record.text === "string") return { text: record.text };
1065
1075
  return null;
1066
1076
  }
1077
+ var TURN_KEYS = ["turns", "messages", "history", "conversation", "steps"];
1067
1078
  function extractTurns(value) {
1068
- const list = Array.isArray(value) ? value : value && typeof value === "object" && Array.isArray(value.turns) ? value.turns : null;
1069
- if (!list) return null;
1070
- const turns = list.map(extractTurn).filter((turn) => turn !== null);
1071
- return turns.length > 0 ? turns : null;
1079
+ const read = (list) => {
1080
+ if (!Array.isArray(list)) return null;
1081
+ const turns = list.map(extractTurn).filter((turn) => turn !== null);
1082
+ return turns.length > 0 ? turns : null;
1083
+ };
1084
+ if (Array.isArray(value)) return read(value);
1085
+ if (!value || typeof value !== "object") return null;
1086
+ const record = value;
1087
+ const named = (from) => {
1088
+ for (const key of TURN_KEYS) {
1089
+ const found = read(from[key]);
1090
+ if (found) return found;
1091
+ }
1092
+ return null;
1093
+ };
1094
+ const here = named(record);
1095
+ if (here) return here;
1096
+ for (const nested of Object.values(record)) {
1097
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
1098
+ const found = named(nested);
1099
+ if (found) return found;
1100
+ }
1101
+ }
1102
+ return null;
1072
1103
  }
1073
1104
  function describe2(item) {
1074
1105
  const { label, verdict } = item;
@@ -1277,6 +1308,14 @@ a native turn, or the raw envelope from any adapter this package ships:
1277
1308
  [{"text":"Reading.","toolCalls":[{"name":"read_file","arguments":{"p":"a.ts"}}]}]
1278
1309
  {"run":"job-14","turns":[{"choices":[{"message":{"content":"..."}}]}]}
1279
1310
 
1311
+ The turns are found under "turns", "messages", "history", "conversation" or
1312
+ "steps" -- at the top level or one object down -- so a request body or a logged
1313
+ run needs no reshaping. Only the model's own messages are read: system, user
1314
+ and tool messages are skipped rather than counted as turns.
1315
+
1316
+ {"turns":[{"role":"assistant","content":"\u2026","tool_calls":[\u2026]},
1317
+ {"role":"tool","content":"\u2026"}]}
1318
+
1280
1319
  A file that parses whole as one array is read as a single run, so a run logged
1281
1320
  as one pretty-printed document works without reshaping.
1282
1321