llm-output-guard 0.2.0 → 0.4.0

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
@@ -131,6 +131,42 @@ for await (const chunk of stream) {
131
131
  const final = guard.end(finishReason); // full check, all detectors
132
132
  ```
133
133
 
134
+ ### Vercel AI SDK
135
+
136
+ One wrap, and both `generateText` and `streamText` are guarded:
137
+
138
+ ```ts
139
+ import { wrapLanguageModel } from 'ai';
140
+ import { outputGuard } from 'llm-output-guard/ai-sdk';
141
+ import { presets } from 'llm-output-guard';
142
+
143
+ const model = wrapLanguageModel({
144
+ model: groq('llama-3.3-70b-versatile'),
145
+ middleware: outputGuard({ ...presets.chat, onDegenerate: 'abort' }),
146
+ });
147
+ ```
148
+
149
+ On `streamText` this cancels the provider's stream mid-generation. Driven
150
+ through the real SDK against a looping model, the provider was asked for **17
151
+ of 137 parts** before the guard cut it off — the rest was never generated and
152
+ never billed. On `generateText` the tokens are already bought, so it throws
153
+ `DegenerateOutputError` instead, which your fallback layer can act on.
154
+
155
+ `onDegenerate` takes `'throw'` (default, also cancels the stream), `'abort'`
156
+ (stop cleanly, keep what arrived), or `'ignore'`. Start with `'ignore'` plus
157
+ `onVerdict` to watch your own traffic before letting a threshold fail anything:
158
+
159
+ ```ts
160
+ outputGuard({
161
+ ...presets.chat,
162
+ onDegenerate: 'ignore',
163
+ onVerdict: (verdict, { streaming }) => metrics.record(verdict.scores, { streaming }),
164
+ });
165
+ ```
166
+
167
+ `ai` is an **optional peer dependency** — importing the subpath does not pull it
168
+ in, and the main entry point has no peers at all.
169
+
134
170
  **What runs when.** Mid-stream only the redundancy detectors are meaningful:
135
171
  partial output is genuinely short, genuinely cut off, and genuinely not valid
136
172
  JSON, so `TOO_SHORT`, `TRUNCATED`, `INVALID_JSON` and `LANG_MISMATCH` would
@@ -179,6 +215,50 @@ They are starting points calibrated against the fixture corpus in this repo —
179
215
 
180
216
  ---
181
217
 
218
+ ## Calibrating against your own traffic
219
+
220
+ The shipped presets are tuned on the fixture corpus, which is not your traffic.
221
+ Log your scores for a week, then let the CLI read them back:
222
+
223
+ ```bash
224
+ npx llm-output-guard calibrate scores.jsonl
225
+ # or: cat scores.jsonl | npx llm-output-guard calibrate --fpr 0.001
226
+ ```
227
+
228
+ ```
229
+ 8,000 verdicts — flagging budget 0.10% of traffic
230
+ ! sample is too small for a 0.10% rate: it rests on the top ~8 scores, and
231
+ ~10,000 verdicts are needed before that tail means anything
232
+
233
+ REPETITION n=7,993
234
+ p50 0.000 p90 0.000 p99 0.100 p99.9 0.944 max 0.991
235
+ gap 0.114 -> 0.705 (15 above, 0.19% of traffic)
236
+ suggest maxRepetition: 0.409
237
+
238
+ TAIL_LOOP n=7,993
239
+ p50 0.000 p90 0.000 p99 0.000 p99.9 0.000 max 0.789
240
+ gap 0.000 -> 0.789 (1 above, 0.01% of traffic)
241
+ suggest maxTailLoop: 0.394
242
+ ! the separation rests on 1 sample; treat it as a lead to confirm, not a
243
+ calibrated threshold
244
+ ```
245
+
246
+ Input is JSONL and the parsing is deliberately forgiving — a bare scores
247
+ object, a whole `Verdict`, or either of those buried in a wider log record all
248
+ work, because a calibration step you have to reshape your logs for is one you
249
+ will not run. `--json` emits the same analysis as data.
250
+
251
+ **What it can and cannot tell you.** The corpus can compute a real margin
252
+ because every fixture is labelled. Your logs are not, and no arithmetic
253
+ recovers a label that was never written down. So these numbers bound *false
254
+ positives* — how much of your own traffic a threshold would flag — on the
255
+ assumption that degeneration is rare in it. They say nothing about what a
256
+ threshold catches; a detector that never fires has a perfect false-positive
257
+ rate. The `gap` line is the exception worth trusting, because a hole between
258
+ the bulk and a cluster of outliers is real separation observed in your data
259
+ rather than an assumption about rarity — and when that hole rests on one or
260
+ two samples, the report says so.
261
+
182
262
  ## On thresholds
183
263
 
184
264
  A miss is annoying. **A false positive is worse**: a healthy response gets discarded and retried against a slower provider for nothing.
@@ -0,0 +1,419 @@
1
+ 'use strict';
2
+
3
+ // src/internal/tokenize.ts
4
+ function words(text) {
5
+ return text.toLowerCase().match(/[\p{L}\p{N}']+/gu) ?? [];
6
+ }
7
+ function clamp01(n) {
8
+ if (Number.isNaN(n)) return 0;
9
+ return n < 0 ? 0 : n > 1 ? 1 : n;
10
+ }
11
+ function excerpt(text, max = 80) {
12
+ const flat = text.replace(/\s+/g, " ").trim();
13
+ return flat.length <= max ? flat : flat.slice(0, max) + "\u2026";
14
+ }
15
+
16
+ // src/detectors/repetition.ts
17
+ function repetitionScore(text, options = {}) {
18
+ const { n = 3, maxSample = 8e3 } = options;
19
+ const w = words(text.slice(0, maxSample));
20
+ if (w.length < n * 4) return 0;
21
+ const seen = /* @__PURE__ */ new Set();
22
+ let total = 0;
23
+ for (let i = 0; i + n <= w.length; i++) {
24
+ seen.add(w.slice(i, i + n).join(" "));
25
+ total++;
26
+ }
27
+ if (total === 0) return 0;
28
+ return clamp01(1 - seen.size / total);
29
+ }
30
+ function tailLoopScore(text, options = {}) {
31
+ const { tailWords = 200, maxPeriod = 40, minRepeats = 3 } = options;
32
+ const all = words(text);
33
+ const tail = all.slice(-tailWords);
34
+ if (tail.length < minRepeats * 2) return 0;
35
+ let best = 0;
36
+ const periodCap = Math.min(maxPeriod, Math.floor(tail.length / minRepeats));
37
+ for (let p = 1; p <= periodCap; p++) {
38
+ const block = tail.slice(tail.length - p);
39
+ let repeats = 1;
40
+ let cursor = tail.length - p;
41
+ while (cursor - p >= 0) {
42
+ let same = true;
43
+ for (let k = 0; k < p; k++) {
44
+ if (tail[cursor - p + k] !== block[k]) {
45
+ same = false;
46
+ break;
47
+ }
48
+ }
49
+ if (!same) break;
50
+ repeats++;
51
+ cursor -= p;
52
+ }
53
+ if (repeats >= minRepeats) {
54
+ best = Math.max(best, clamp01(repeats * p / tail.length));
55
+ }
56
+ }
57
+ return best;
58
+ }
59
+
60
+ // src/detectors/compressibility.ts
61
+ function compressionRatio(text, options = {}) {
62
+ const { window = 1024, maxSample = 4e3, minMatch = 4 } = options;
63
+ const s = text.slice(0, maxSample);
64
+ if (s.length < 64) return 1;
65
+ let i = 0;
66
+ let emitted = 0;
67
+ while (i < s.length) {
68
+ let bestLen = 0;
69
+ const start = i > window ? i - window : 0;
70
+ for (let j = start; j < i; j++) {
71
+ let k = 0;
72
+ while (k < 255 && i + k < s.length && s[j + k] === s[i + k]) k++;
73
+ if (k > bestLen) {
74
+ bestLen = k;
75
+ if (bestLen >= 255) break;
76
+ }
77
+ }
78
+ emitted++;
79
+ i += bestLen >= minMatch ? bestLen : 1;
80
+ }
81
+ return emitted / s.length;
82
+ }
83
+ function compressibilityScore(text, options = {}) {
84
+ const { pivot = 0.32, ...rest } = options;
85
+ if (text.trim().length < 64) return 0;
86
+ return clamp01(1 - compressionRatio(text, rest) / pivot);
87
+ }
88
+
89
+ // src/detectors/emptiness.ts
90
+ function emptinessScore(text) {
91
+ const trimmed = text.trim();
92
+ if (trimmed.length === 0) return 1;
93
+ if (words(trimmed).length === 0) return 1;
94
+ const stripped = trimmed.replace(/```[a-z]*\s*```/gi, "").replace(/^[{}[\]"'\s,.:;!?-]+$/g, "");
95
+ return stripped.trim().length === 0 ? 1 : 0;
96
+ }
97
+ function shortnessScore(text, minChars) {
98
+ if (minChars <= 0) return 0;
99
+ const len = text.trim().length;
100
+ if (len >= minChars) return 0;
101
+ return 1 - len / minChars;
102
+ }
103
+
104
+ // src/detectors/truncation.ts
105
+ var LENGTH_STOPS = /* @__PURE__ */ new Set(["length", "max_tokens", "maxtokens", "max_output_tokens", "token_limit"]);
106
+ var TERMINAL = /[.!?"'`\u2019\u201d)\]}:;\u3002\uff01\uff1f]\s*$/;
107
+ function truncationScore(text, options = {}) {
108
+ const { finishReason } = options;
109
+ if (finishReason && LENGTH_STOPS.has(finishReason.toLowerCase())) return 1;
110
+ const trimmed = text.trim();
111
+ if (trimmed.length === 0) return 0;
112
+ let score = 0;
113
+ const fences = (trimmed.match(/```/g) ?? []).length;
114
+ if (fences % 2 === 1) score = Math.max(score, 0.9);
115
+ for (const [open, close] of [["{", "}"], ["[", "]"], ["(", ")"]]) {
116
+ const opens = trimmed.split(open).length - 1;
117
+ const closes = trimmed.split(close).length - 1;
118
+ if (opens > closes) score = Math.max(score, 0.8);
119
+ }
120
+ if (!TERMINAL.test(trimmed)) score = Math.max(score, 0.55);
121
+ return score;
122
+ }
123
+
124
+ // src/detectors/json.ts
125
+ function stripFence(text) {
126
+ const fenced = text.trim().match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
127
+ return fenced ? fenced[1] : text.trim();
128
+ }
129
+ function jsonScore(text, options = {}) {
130
+ const { allowFence = true, requiredKeys = [] } = options;
131
+ const candidate = allowFence ? stripFence(text) : text.trim();
132
+ let value;
133
+ try {
134
+ value = JSON.parse(candidate);
135
+ } catch {
136
+ return { score: 1, reason: "unparseable" };
137
+ }
138
+ if (requiredKeys.length > 0) {
139
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
140
+ return { score: 1, value, reason: "missing-keys", missingKeys: [...requiredKeys] };
141
+ }
142
+ const record = value;
143
+ const missing = requiredKeys.filter((k) => !(k in record));
144
+ if (missing.length > 0) {
145
+ return { score: 1, value, reason: "missing-keys", missingKeys: missing };
146
+ }
147
+ }
148
+ return { score: 0, value };
149
+ }
150
+
151
+ // src/detectors/language.ts
152
+ var PROFILES = {
153
+ id: /* @__PURE__ */ new Set(["yang", "dan", "di", "untuk", "dengan", "ini", "itu", "dari", "pada", "tidak", "adalah", "akan", "bisa", "kita", "saya", "atau", "juga", "dalam", "sudah", "ke"]),
154
+ en: /* @__PURE__ */ new Set(["the", "and", "of", "to", "in", "is", "that", "for", "it", "with", "as", "this", "are", "be", "you", "on", "not", "or", "can", "we"]),
155
+ es: /* @__PURE__ */ new Set(["el", "la", "de", "que", "y", "en", "los", "un", "por", "con", "las", "para", "una", "es", "no", "se", "del", "al", "lo", "como"])
156
+ };
157
+ function languageProfile(text) {
158
+ const w = words(text);
159
+ const out = {};
160
+ if (w.length === 0) return out;
161
+ for (const [lang, set] of Object.entries(PROFILES)) {
162
+ let hits = 0;
163
+ for (const token of w) if (set.has(token)) hits++;
164
+ out[lang] = hits / w.length;
165
+ }
166
+ return out;
167
+ }
168
+ function languageMismatchScore(text, expected, options = {}) {
169
+ const { minWords = 25 } = options;
170
+ if (!(expected in PROFILES)) return 0;
171
+ const w = words(text);
172
+ if (w.length < minWords) return 0;
173
+ const profile = languageProfile(text);
174
+ const target = profile[expected] ?? 0;
175
+ const best = Math.max(...Object.values(profile));
176
+ if (best === 0) return 0;
177
+ if (target >= best) return 0;
178
+ return Math.min(1, (best - target) / best);
179
+ }
180
+
181
+ // src/check.ts
182
+ var DEFAULTS = {
183
+ minLength: 1,
184
+ maxRepetition: 0.35,
185
+ maxTailLoop: 0.5,
186
+ maxCompressibility: 0.75,
187
+ maxTruncation: null,
188
+ expectJson: false,
189
+ allowJsonFence: true,
190
+ maxLangMismatch: 0.6,
191
+ ngram: 3
192
+ };
193
+ function checkOutput(text, options = {}) {
194
+ const opts = { ...DEFAULTS, ...options };
195
+ const reasons = [];
196
+ const scores = {};
197
+ let parsedJson;
198
+ const add = (code, score, threshold, message) => {
199
+ scores[code] = score;
200
+ if (score > threshold) reasons.push({ code, score, threshold, message });
201
+ };
202
+ if (typeof text !== "string") {
203
+ scores.EMPTY = 1;
204
+ reasons.push({
205
+ code: "EMPTY",
206
+ score: 1,
207
+ threshold: 0.5,
208
+ message: `Response was ${text === null ? "null" : typeof text}, not a string.`
209
+ });
210
+ return { ok: false, reasons, scores };
211
+ }
212
+ const empty = emptinessScore(text);
213
+ add("EMPTY", empty, 0.5, "Response contains no usable content.");
214
+ if (empty >= 1) {
215
+ return { ok: false, reasons, scores };
216
+ }
217
+ if (opts.minLength > 0) {
218
+ add(
219
+ "TOO_SHORT",
220
+ shortnessScore(text, opts.minLength),
221
+ 0,
222
+ `Response is ${text.trim().length} chars, below the ${opts.minLength} minimum.`
223
+ );
224
+ }
225
+ if (opts.maxRepetition != null) {
226
+ const s = repetitionScore(text, { n: opts.ngram });
227
+ add(
228
+ "REPETITION",
229
+ s,
230
+ opts.maxRepetition,
231
+ `${Math.round(s * 100)}% of ${opts.ngram}-grams are duplicates.`
232
+ );
233
+ }
234
+ if (opts.maxTailLoop != null) {
235
+ const s = tailLoopScore(text);
236
+ add(
237
+ "TAIL_LOOP",
238
+ s,
239
+ opts.maxTailLoop,
240
+ `Response ends in a repeating block covering ${Math.round(s * 100)}% of the tail.`
241
+ );
242
+ }
243
+ if (opts.maxCompressibility != null) {
244
+ const s = compressibilityScore(text);
245
+ add(
246
+ "LOW_ENTROPY",
247
+ s,
248
+ opts.maxCompressibility,
249
+ "Response is far more compressible than natural language."
250
+ );
251
+ }
252
+ if (opts.maxTruncation != null || opts.finishReason) {
253
+ const s = truncationScore(text, { finishReason: opts.finishReason });
254
+ add(
255
+ "TRUNCATED",
256
+ s,
257
+ opts.maxTruncation ?? 0.75,
258
+ `Response appears cut off near: "${excerpt(text.trim().slice(-60), 60)}"`
259
+ );
260
+ }
261
+ if (opts.expectJson) {
262
+ const result = jsonScore(text, {
263
+ allowFence: opts.allowJsonFence,
264
+ requiredKeys: opts.requiredKeys
265
+ });
266
+ parsedJson = result.value;
267
+ add(
268
+ "INVALID_JSON",
269
+ result.score,
270
+ 0,
271
+ result.reason === "missing-keys" ? `JSON is missing required keys: ${result.missingKeys?.join(", ")}.` : "Response is not parseable JSON."
272
+ );
273
+ }
274
+ if (opts.expectLang) {
275
+ const s = languageMismatchScore(text, opts.expectLang);
276
+ add(
277
+ "LANG_MISMATCH",
278
+ s,
279
+ opts.maxLangMismatch,
280
+ `Response does not look like '${opts.expectLang}'.`
281
+ );
282
+ }
283
+ return { ok: reasons.length === 0, reasons, scores, json: parsedJson };
284
+ }
285
+ var DegenerateOutputError = class extends Error {
286
+ verdict;
287
+ /** Marks this as safe to retry against another provider. */
288
+ retryable = true;
289
+ constructor(verdict) {
290
+ super(`Degenerate LLM output: ${verdict.reasons.map((r) => r.code).join(", ")}`);
291
+ this.name = "DegenerateOutputError";
292
+ this.verdict = verdict;
293
+ }
294
+ };
295
+
296
+ // src/stream.ts
297
+ var DEFERRED_TO_END = {
298
+ minLength: 0,
299
+ maxTruncation: null,
300
+ expectJson: false,
301
+ expectLang: null,
302
+ finishReason: void 0,
303
+ /*
304
+ * LOW_ENTROPY is deferred for a second reason: cost. The LZ77 pass is
305
+ * 0.4ms at 500 characters and 11ms at its 4000-character sample cap, which
306
+ * is 100x the other two detectors combined -- affordable once per response,
307
+ * ruinous every few hundred characters of every stream.
308
+ *
309
+ * Nothing is lost by waiting. Every fixture it catches alone is caught
310
+ * earlier here by REPETITION, because character-level collapse is also
311
+ * n-gram collapse; and the one signal it owns outright -- a response that
312
+ * is uniformly redundant end to end -- is a statement about the finished
313
+ * text, which is exactly when it now runs.
314
+ */
315
+ maxCompressibility: null
316
+ };
317
+ function createStreamGuard(options = {}) {
318
+ const { checkEvery = 400, warmup = 240, window = 2e3, ...checkOptions } = options;
319
+ let text = "";
320
+ let sinceCheck = 0;
321
+ let checks = 0;
322
+ const due = () => checks === 0 ? text.length >= warmup : sinceCheck >= checkEvery;
323
+ return {
324
+ get text() {
325
+ return text;
326
+ },
327
+ get checks() {
328
+ return checks;
329
+ },
330
+ push(chunk) {
331
+ if (typeof chunk !== "string" || chunk.length === 0) return null;
332
+ text += chunk;
333
+ sinceCheck += chunk.length;
334
+ if (!due()) return null;
335
+ sinceCheck = 0;
336
+ checks += 1;
337
+ const recent = text.length > window ? text.slice(-window) : text;
338
+ return checkOutput(recent, { ...checkOptions, ...DEFERRED_TO_END });
339
+ },
340
+ end(finishReason) {
341
+ return checkOutput(text, {
342
+ ...checkOptions,
343
+ finishReason: finishReason ?? checkOptions.finishReason
344
+ });
345
+ }
346
+ };
347
+ }
348
+
349
+ // src/ai-sdk.ts
350
+ function finishReasonOf(value) {
351
+ if (typeof value === "string") return value;
352
+ if (value && typeof value === "object") return value.unified ?? value.raw;
353
+ return void 0;
354
+ }
355
+ function outputGuard(options = {}) {
356
+ const { onDegenerate = "throw", onVerdict, ...guardOptions } = options;
357
+ const act = (verdict, streaming) => {
358
+ onVerdict?.(verdict, { streaming });
359
+ if (verdict.ok || onDegenerate === "ignore") return;
360
+ if (onDegenerate === "throw") throw new DegenerateOutputError(verdict);
361
+ };
362
+ return {
363
+ /**
364
+ * Non-streaming. The tokens are already bought by the time this runs, so
365
+ * all it can do is stop a bad answer from being used as a good one.
366
+ */
367
+ async wrapGenerate({
368
+ doGenerate
369
+ }) {
370
+ const result = await doGenerate();
371
+ const text = (result.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
372
+ act(
373
+ checkOutput(text, {
374
+ ...guardOptions,
375
+ finishReason: finishReasonOf(result.finishReason) ?? guardOptions.finishReason
376
+ }),
377
+ false
378
+ );
379
+ return result;
380
+ },
381
+ async wrapStream({
382
+ doStream
383
+ }) {
384
+ const result = await doStream();
385
+ const guard = createStreamGuard(guardOptions);
386
+ let fired = false;
387
+ let finishReason;
388
+ const guarded = result.stream.pipeThrough(
389
+ new TransformStream({
390
+ transform(part, controller) {
391
+ controller.enqueue(part);
392
+ if (part.type === "finish") finishReason = part.finishReason;
393
+ if (part.type !== "text-delta" || fired) return;
394
+ const verdict = guard.push(part.delta ?? "");
395
+ if (!verdict || verdict.ok) return;
396
+ fired = true;
397
+ onVerdict?.(verdict, { streaming: true });
398
+ if (onDegenerate === "ignore") return;
399
+ if (onDegenerate === "throw") {
400
+ controller.error(new DegenerateOutputError(verdict));
401
+ } else {
402
+ controller.terminate();
403
+ }
404
+ },
405
+ flush() {
406
+ if (!fired) {
407
+ onVerdict?.(guard.end(finishReasonOf(finishReason)), { streaming: true });
408
+ }
409
+ }
410
+ })
411
+ );
412
+ return { ...result, stream: guarded };
413
+ }
414
+ };
415
+ }
416
+
417
+ exports.outputGuard = outputGuard;
418
+ //# sourceMappingURL=ai-sdk.cjs.map
419
+ //# sourceMappingURL=ai-sdk.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/tokenize.ts","../src/detectors/repetition.ts","../src/detectors/compressibility.ts","../src/detectors/emptiness.ts","../src/detectors/truncation.ts","../src/detectors/json.ts","../src/detectors/language.ts","../src/check.ts","../src/stream.ts","../src/ai-sdk.ts"],"names":[],"mappings":"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","file":"ai-sdk.cjs","sourcesContent":["/** Word tokenizer that works across scripts (Latin, Cyrillic, and friends). */\nexport function words(text: string): string[] {\n return text.toLowerCase().match(/[\\p{L}\\p{N}']+/gu) ?? [];\n}\n\n/** Clamp a raw signal into the 0..1 suspicion range. */\nexport function clamp01(n: number): number {\n if (Number.isNaN(n)) return 0;\n return n < 0 ? 0 : n > 1 ? 1 : n;\n}\n\n/** Short, safe excerpt for messages. Never leaks a full response into logs. */\nexport function excerpt(text: string, max = 80): string {\n const flat = text.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max) + '\\u2026';\n}\n","import { words, clamp01 } from '../internal/tokenize.js';\n\nexport interface RepetitionOptions {\n /** N-gram size. 3 suits prose; 2 is noisy, 4 misses short loops. */\n n?: number;\n /** Only analyse the first N characters. Keeps cost bounded on long outputs. */\n maxSample?: number;\n}\n\n/**\n * Fraction of n-grams that are duplicates. 0 = every n-gram unique, 1 = total collapse.\n *\n * Healthy prose sits near 0.00-0.10. A model stuck in a loop passes 0.5 quickly.\n * Returns 0 for text too short to judge rather than guessing.\n */\nexport function repetitionScore(text: string, options: RepetitionOptions = {}): number {\n const { n = 3, maxSample = 8000 } = options;\n const w = words(text.slice(0, maxSample));\n if (w.length < n * 4) return 0;\n\n const seen = new Set<string>();\n let total = 0;\n for (let i = 0; i + n <= w.length; i++) {\n seen.add(w.slice(i, i + n).join(' '));\n total++;\n }\n if (total === 0) return 0;\n return clamp01(1 - seen.size / total);\n}\n\nexport interface TailLoopOptions {\n /** How many trailing words to inspect. */\n tailWords?: number;\n /** Longest loop period to look for, in words. */\n maxPeriod?: number;\n /** A block must repeat at least this many times to count as a loop. */\n minRepeats?: number;\n}\n\n/**\n * Detects the specific failure where a model terminates in a repeating tail --\n * the same clause emitted over and over until max_tokens runs out.\n *\n * Whole-output repetition misses this when the first half of the response was fine.\n * Returns the fraction of the inspected tail covered by the loop.\n */\nexport function tailLoopScore(text: string, options: TailLoopOptions = {}): number {\n const { tailWords = 200, maxPeriod = 40, minRepeats = 3 } = options;\n const all = words(text);\n const tail = all.slice(-tailWords);\n if (tail.length < minRepeats * 2) return 0;\n\n let best = 0;\n const periodCap = Math.min(maxPeriod, Math.floor(tail.length / minRepeats));\n for (let p = 1; p <= periodCap; p++) {\n const block = tail.slice(tail.length - p);\n let repeats = 1;\n let cursor = tail.length - p;\n while (cursor - p >= 0) {\n let same = true;\n for (let k = 0; k < p; k++) {\n if (tail[cursor - p + k] !== block[k]) { same = false; break; }\n }\n if (!same) break;\n repeats++;\n cursor -= p;\n }\n if (repeats >= minRepeats) {\n best = Math.max(best, clamp01((repeats * p) / tail.length));\n }\n }\n return best;\n}\n","import { clamp01 } from '../internal/tokenize.js';\n\nexport interface CompressibilityOptions {\n /** Sliding window for back-references, in characters. */\n window?: number;\n /** Only analyse the first N characters. */\n maxSample?: number;\n /** Shortest back-reference worth emitting. */\n minMatch?: number;\n}\n\n/**\n * Greedy LZ77 pass returning emitted-tokens / input-characters.\n *\n * Deliberately hand-rolled instead of node:zlib so the package stays\n * runtime-agnostic (browser, edge, Deno, Bun) and dependency-free.\n * This is not a real compressor; it only needs to move monotonically\n * with redundancy, which is all the score requires.\n *\n * Measured against the fixture corpus: healthy output lands at 0.67-0.97,\n * degenerate collapse at 0.007-0.042, and tail loops in between at 0.17-0.20.\n * The gap either side of that middle band is what the pivot below trades on.\n */\nexport function compressionRatio(text: string, options: CompressibilityOptions = {}): number {\n const { window = 1024, maxSample = 4000, minMatch = 4 } = options;\n const s = text.slice(0, maxSample);\n if (s.length < 64) return 1;\n\n let i = 0;\n let emitted = 0;\n while (i < s.length) {\n let bestLen = 0;\n const start = i > window ? i - window : 0;\n for (let j = start; j < i; j++) {\n let k = 0;\n while (k < 255 && i + k < s.length && s[j + k] === s[i + k]) k++;\n if (k > bestLen) {\n bestLen = k;\n if (bestLen >= 255) break;\n }\n }\n emitted++;\n i += bestLen >= minMatch ? bestLen : 1;\n }\n return emitted / s.length;\n}\n\n/**\n * Suspicion score derived from {@link compressionRatio}.\n * `pivot` is the ratio treated as fully healthy; lower ratios scale up toward 1.\n *\n * At the default 0.32 every healthy fixture clamps to exactly 0, with the\n * nearest one still twice the pivot away -- so this detector is deliberately\n * tuned for outright entropy collapse and abstains on everything milder.\n * Tail loops score 0.37-0.48 here and are left to `tailLoopScore`, which\n * separates them far more cleanly (0.90 against a healthy max of 0.00).\n * Raising the pivot would make this fire on loops too, buying redundant\n * coverage with the margin that currently makes a false positive so unlikely.\n */\nexport function compressibilityScore(\n text: string,\n options: CompressibilityOptions & { pivot?: number } = {},\n): number {\n const { pivot = 0.32, ...rest } = options;\n if (text.trim().length < 64) return 0;\n return clamp01(1 - compressionRatio(text, rest) / pivot);\n}\n","import { words } from '../internal/tokenize.js';\n\n/**\n * 1 when the response carries no usable content at all.\n *\n * Covers the cases a plain `!text` check misses: whitespace-only, a lone\n * punctuation mark, an empty code fence, or an empty JSON envelope.\n */\nexport function emptinessScore(text: string): number {\n const trimmed = text.trim();\n if (trimmed.length === 0) return 1;\n if (words(trimmed).length === 0) return 1;\n const stripped = trimmed\n .replace(/```[a-z]*\\s*```/gi, '')\n .replace(/^[{}[\\]\"'\\s,.:;!?-]+$/g, '');\n return stripped.trim().length === 0 ? 1 : 0;\n}\n\n/** 1 when the response is shorter than `minChars`, scaling down to 0 at the threshold. */\nexport function shortnessScore(text: string, minChars: number): number {\n if (minChars <= 0) return 0;\n const len = text.trim().length;\n if (len >= minChars) return 0;\n return 1 - len / minChars;\n}\n","export interface TruncationOptions {\n /**\n * The provider's own stop reason, if you have it. When this says the output\n * hit the token ceiling, that is authoritative and the heuristics are skipped.\n */\n finishReason?: string;\n}\n\nconst LENGTH_STOPS = new Set(['length', 'max_tokens', 'maxtokens', 'max_output_tokens', 'token_limit']);\nconst TERMINAL = /[.!?\"'`\\u2019\\u201d)\\]}:;\\u3002\\uff01\\uff1f]\\s*$/;\n\n/**\n * Detects output that stopped mid-thought.\n *\n * Prefers the provider's finish_reason when supplied, because that is ground\n * truth. Falls back to structural signals: unbalanced fences or brackets, or a\n * final sentence with no terminal punctuation.\n *\n * Returns a graded score, not a boolean -- a missing full stop alone is weak\n * evidence and should not sink a response on its own.\n */\nexport function truncationScore(text: string, options: TruncationOptions = {}): number {\n const { finishReason } = options;\n if (finishReason && LENGTH_STOPS.has(finishReason.toLowerCase())) return 1;\n\n const trimmed = text.trim();\n if (trimmed.length === 0) return 0;\n\n let score = 0;\n\n const fences = (trimmed.match(/```/g) ?? []).length;\n if (fences % 2 === 1) score = Math.max(score, 0.9);\n\n for (const [open, close] of [['{', '}'], ['[', ']'], ['(', ')']] as const) {\n const opens = trimmed.split(open).length - 1;\n const closes = trimmed.split(close).length - 1;\n if (opens > closes) score = Math.max(score, 0.8);\n }\n\n if (!TERMINAL.test(trimmed)) score = Math.max(score, 0.55);\n\n return score;\n}\n","export interface JsonOptions {\n /** Allow the payload to sit inside a ```json fence rather than being bare. */\n allowFence?: boolean;\n /** Top-level keys that must be present for the payload to count as valid. */\n requiredKeys?: string[];\n}\n\nexport interface JsonResult {\n /** 0 when the payload parses and satisfies requiredKeys, 1 otherwise. */\n score: number;\n /** The parsed value, when parsing succeeded. */\n value?: unknown;\n reason?: 'unparseable' | 'missing-keys';\n missingKeys?: string[];\n}\n\n/** Pull a JSON payload out of a ```json fence, or return the text unchanged. */\nexport function stripFence(text: string): string {\n const fenced = text.trim().match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n return fenced ? fenced[1] : text.trim();\n}\n\n/**\n * Structured-output check. Models that \"succeed\" while emitting prose around\n * the JSON, or an object missing half its keys, fail here.\n */\nexport function jsonScore(text: string, options: JsonOptions = {}): JsonResult {\n const { allowFence = true, requiredKeys = [] } = options;\n const candidate = allowFence ? stripFence(text) : text.trim();\n\n let value: unknown;\n try {\n value = JSON.parse(candidate);\n } catch {\n return { score: 1, reason: 'unparseable' };\n }\n\n if (requiredKeys.length > 0) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return { score: 1, value, reason: 'missing-keys', missingKeys: [...requiredKeys] };\n }\n const record = value as Record<string, unknown>;\n const missing = requiredKeys.filter((k) => !(k in record));\n if (missing.length > 0) {\n return { score: 1, value, reason: 'missing-keys', missingKeys: missing };\n }\n }\n\n return { score: 0, value };\n}\n","import { words } from '../internal/tokenize.js';\n\n/**\n * Function-word frequency profiles. Coarse by design: this catches a model\n * answering in the wrong language entirely, not dialect or register drift.\n * Off by default in every preset for exactly that reason.\n */\nconst PROFILES: Record<string, Set<string>> = {\n id: new Set(['yang', 'dan', 'di', 'untuk', 'dengan', 'ini', 'itu', 'dari', 'pada', 'tidak', 'adalah', 'akan', 'bisa', 'kita', 'saya', 'atau', 'juga', 'dalam', 'sudah', 'ke']),\n en: new Set(['the', 'and', 'of', 'to', 'in', 'is', 'that', 'for', 'it', 'with', 'as', 'this', 'are', 'be', 'you', 'on', 'not', 'or', 'can', 'we']),\n es: new Set(['el', 'la', 'de', 'que', 'y', 'en', 'los', 'un', 'por', 'con', 'las', 'para', 'una', 'es', 'no', 'se', 'del', 'al', 'lo', 'como']),\n};\n\nexport interface LanguageOptions {\n /** Below this word count the signal is unreliable and the score is 0. */\n minWords?: number;\n}\n\n/** Share of tokens matching each known profile. Not a full language detector. */\nexport function languageProfile(text: string): Record<string, number> {\n const w = words(text);\n const out: Record<string, number> = {};\n if (w.length === 0) return out;\n for (const [lang, set] of Object.entries(PROFILES)) {\n let hits = 0;\n for (const token of w) if (set.has(token)) hits++;\n out[lang] = hits / w.length;\n }\n return out;\n}\n\n/**\n * Suspicion that the response is not in `expected`.\n * Returns 0 for unknown languages or samples too short to judge --\n * silence is better than a confident wrong answer here.\n */\nexport function languageMismatchScore(\n text: string,\n expected: string,\n options: LanguageOptions = {},\n): number {\n const { minWords = 25 } = options;\n if (!(expected in PROFILES)) return 0;\n const w = words(text);\n if (w.length < minWords) return 0;\n\n const profile = languageProfile(text);\n const target = profile[expected] ?? 0;\n const best = Math.max(...Object.values(profile));\n if (best === 0) return 0;\n if (target >= best) return 0;\n return Math.min(1, (best - target) / best);\n}\n\nexport const supportedLanguages = Object.keys(PROFILES);\n","import type { CheckOptions, Reason, ReasonCode, Verdict } from './types.js';\nimport { repetitionScore, tailLoopScore } from './detectors/repetition.js';\nimport { compressibilityScore } from './detectors/compressibility.js';\nimport { emptinessScore, shortnessScore } from './detectors/emptiness.js';\nimport { truncationScore } from './detectors/truncation.js';\nimport { jsonScore } from './detectors/json.js';\nimport { languageMismatchScore } from './detectors/language.js';\nimport { excerpt } from './internal/tokenize.js';\n\nconst DEFAULTS: Required<\n Pick<CheckOptions,\n 'minLength' | 'maxRepetition' | 'maxTailLoop' | 'maxCompressibility' |\n 'maxTruncation' | 'expectJson' | 'allowJsonFence' | 'maxLangMismatch' | 'ngram'>\n> = {\n minLength: 1,\n maxRepetition: 0.35,\n maxTailLoop: 0.5,\n maxCompressibility: 0.75,\n maxTruncation: null as unknown as number,\n expectJson: false,\n allowJsonFence: true,\n maxLangMismatch: 0.6,\n ngram: 3,\n};\n\n/**\n * Runs every enabled detector and returns a structured verdict.\n *\n * Pure and synchronous: no network, no clock, no randomness. The same input\n * always produces the same verdict, which is what makes it safe to put on a\n * hot path and easy to unit test.\n *\n * Every detector runs even after one fails, so `reasons` shows the full picture\n * rather than whichever check happened to be ordered first.\n *\n * Never throws. A `null`, `undefined`, or otherwise non-string input is a\n * verdict (`EMPTY`), not an exception -- see the guard below for why.\n */\nexport function checkOutput(\n text: string | null | undefined,\n options: CheckOptions = {},\n): Verdict {\n const opts = { ...DEFAULTS, ...options };\n const reasons: Reason[] = [];\n const scores: Partial<Record<ReasonCode, number>> = {};\n let parsedJson: unknown;\n\n const add = (code: ReasonCode, score: number, threshold: number, message: string) => {\n scores[code] = score;\n if (score > threshold) reasons.push({ code, score, threshold, message });\n };\n\n /*\n * A caller who has `undefined` where the text should be is in exactly the\n * situation this package exists for: the request \"succeeded\" and produced\n * nothing. Types do not stop it -- an SDK whose field is optional, a JSON\n * envelope that shaped differently than documented, a `.content[0].text`\n * that was never there. Throwing a TypeError here would be the worst\n * possible answer, because it is not a DegenerateOutputError and so slips\n * straight through the very retry predicate the README recommends.\n */\n if (typeof text !== 'string') {\n scores.EMPTY = 1;\n reasons.push({\n code: 'EMPTY',\n score: 1,\n threshold: 0.5,\n message: `Response was ${text === null ? 'null' : typeof text}, not a string.`,\n });\n return { ok: false, reasons, scores };\n }\n\n const empty = emptinessScore(text);\n add('EMPTY', empty, 0.5, 'Response contains no usable content.');\n\n // Once the response is empty, the remaining content signals are noise.\n if (empty >= 1) {\n return { ok: false, reasons, scores };\n }\n\n if (opts.minLength > 0) {\n add(\n 'TOO_SHORT',\n shortnessScore(text, opts.minLength),\n 0,\n `Response is ${text.trim().length} chars, below the ${opts.minLength} minimum.`,\n );\n }\n\n if (opts.maxRepetition != null) {\n const s = repetitionScore(text, { n: opts.ngram });\n add('REPETITION', s, opts.maxRepetition,\n `${Math.round(s * 100)}% of ${opts.ngram}-grams are duplicates.`);\n }\n\n if (opts.maxTailLoop != null) {\n const s = tailLoopScore(text);\n add('TAIL_LOOP', s, opts.maxTailLoop,\n `Response ends in a repeating block covering ${Math.round(s * 100)}% of the tail.`);\n }\n\n if (opts.maxCompressibility != null) {\n const s = compressibilityScore(text);\n add('LOW_ENTROPY', s, opts.maxCompressibility,\n 'Response is far more compressible than natural language.');\n }\n\n if (opts.maxTruncation != null || opts.finishReason) {\n const s = truncationScore(text, { finishReason: opts.finishReason });\n add('TRUNCATED', s, opts.maxTruncation ?? 0.75,\n `Response appears cut off near: \"${excerpt(text.trim().slice(-60), 60)}\"`);\n }\n\n if (opts.expectJson) {\n const result = jsonScore(text, {\n allowFence: opts.allowJsonFence,\n requiredKeys: opts.requiredKeys,\n });\n parsedJson = result.value;\n add('INVALID_JSON', result.score, 0,\n result.reason === 'missing-keys'\n ? `JSON is missing required keys: ${result.missingKeys?.join(', ')}.`\n : 'Response is not parseable JSON.');\n }\n\n if (opts.expectLang) {\n const s = languageMismatchScore(text, opts.expectLang);\n add('LANG_MISMATCH', s, opts.maxLangMismatch,\n `Response does not look like '${opts.expectLang}'.`);\n }\n\n return { ok: reasons.length === 0, reasons, scores, json: parsedJson };\n}\n\n/** Error thrown by {@link assertOutput}, carrying the full verdict. */\nexport class DegenerateOutputError extends Error {\n readonly verdict: Verdict;\n /** Marks this as safe to retry against another provider. */\n readonly retryable = true;\n\n constructor(verdict: Verdict) {\n super(`Degenerate LLM output: ${verdict.reasons.map((r) => r.code).join(', ')}`);\n this.name = 'DegenerateOutputError';\n this.verdict = verdict;\n }\n}\n\n/**\n * Throwing wrapper, for dropping straight into an existing retry or fallback\n * chain that already keys off thrown errors.\n */\nexport function assertOutput(\n text: string | null | undefined,\n options: CheckOptions = {},\n): string {\n const verdict = checkOutput(text, options);\n if (!verdict.ok) throw new DegenerateOutputError(verdict);\n // Unreachable for non-strings: those score EMPTY 1 and throw above.\n return text as string;\n}\n","import type { CheckOptions, Verdict } from './types.js';\nimport { checkOutput } from './check.js';\n\n/**\n * Detectors that mean nothing until the response is complete.\n *\n * This is the whole problem with judging a stream. Partial output is short,\n * is cut off, and does not parse as JSON -- not because the model is failing\n * but because it has not finished talking. Run the full check on a half-built\n * response and `TOO_SHORT`, `TRUNCATED` and `INVALID_JSON` fire on every\n * healthy generation in the first few tokens, which is worse than no check at\n * all: it trains you to ignore the guard.\n *\n * What *is* meaningful early is redundancy. A model stuck in a loop is already\n * looping by the time it has emitted a few hundred characters, and no amount\n * of further generation makes it less true. So mid-stream runs exactly the\n * three detectors that measure repetition, and defers the rest to `end()`.\n */\nconst DEFERRED_TO_END: CheckOptions = {\n minLength: 0,\n maxTruncation: null,\n expectJson: false,\n expectLang: null,\n finishReason: undefined,\n\n /*\n * LOW_ENTROPY is deferred for a second reason: cost. The LZ77 pass is\n * 0.4ms at 500 characters and 11ms at its 4000-character sample cap, which\n * is 100x the other two detectors combined -- affordable once per response,\n * ruinous every few hundred characters of every stream.\n *\n * Nothing is lost by waiting. Every fixture it catches alone is caught\n * earlier here by REPETITION, because character-level collapse is also\n * n-gram collapse; and the one signal it owns outright -- a response that\n * is uniformly redundant end to end -- is a statement about the finished\n * text, which is exactly when it now runs.\n */\n maxCompressibility: null,\n};\n\nexport interface StreamGuardOptions extends CheckOptions {\n /**\n * Characters of *new* text between checks. Default 400.\n *\n * Checking on every chunk would re-scan the buffer per token and turn a\n * linear stream into quadratic work. Batching costs a little detection\n * latency and buys a bounded cost per stream.\n */\n checkEvery?: number;\n /**\n * Characters that must arrive before any judgement. Default 240.\n *\n * A loop is not visible in the first sentence, and neither is its absence.\n * Below this the guard abstains rather than guessing -- the same rule the\n * detectors already follow for short samples.\n */\n warmup?: number;\n /**\n * Trailing characters each mid-stream check looks at. Default 2000.\n *\n * Two reasons, and the second matters more. Cost: without a window every\n * check re-scans the whole buffer, so a stream costs quadratic work in its\n * own length. Sensitivity: a model that produced four healthy paragraphs\n * and then began looping is diluted to nothing when measured across all\n * five, which is the same reasoning that makes `tailLoopScore` a separate\n * detector from `repetitionScore`. Recent text is the text in question.\n */\n window?: number;\n}\n\nexport interface StreamGuard {\n /**\n * Feed the next chunk.\n *\n * Returns a verdict only on the chunks where a check actually ran, and\n * `null` on the rest -- so `null` means \"not judged yet\", never \"healthy\".\n * Read `.ok` on what you get back.\n */\n push(chunk: string): Verdict | null;\n /**\n * Full check on the complete text, including the detectors deferred above.\n * Pass the provider's stop reason if you have it; truncation keys off it.\n */\n end(finishReason?: string): Verdict;\n /** Everything pushed so far. */\n readonly text: string;\n /** How many mid-stream checks have run. Useful when tuning `checkEvery`. */\n readonly checks: number;\n}\n\n/**\n * Watches a response as it arrives and reports degeneration before it finishes.\n *\n * The reason to bother: a model that has started looping will keep looping\n * until it hits `max_tokens`, and you pay for every one of those tokens plus\n * the latency of waiting for them. Catching it at character 300 of a 4000\n * character run and aborting turns a slow bad answer into a fast one.\n *\n * This never aborts anything itself -- it holds no controller and knows\n * nothing about your provider. It tells you; you decide.\n */\nexport function createStreamGuard(options: StreamGuardOptions = {}): StreamGuard {\n const { checkEvery = 400, warmup = 240, window = 2000, ...checkOptions } = options;\n\n let text = '';\n let sinceCheck = 0;\n let checks = 0;\n\n /*\n * The first check fires as soon as `warmup` is met; `checkEvery` only\n * spaces out the ones after it. Gating the first on both would make the\n * earlier of the two settings dead -- and it is the first check that\n * decides how many wasted tokens a loop gets to emit, which is the entire\n * point of watching a stream instead of its result.\n */\n const due = () => (checks === 0 ? text.length >= warmup : sinceCheck >= checkEvery);\n\n return {\n get text() {\n return text;\n },\n get checks() {\n return checks;\n },\n\n push(chunk: string): Verdict | null {\n if (typeof chunk !== 'string' || chunk.length === 0) return null;\n\n text += chunk;\n sinceCheck += chunk.length;\n\n if (!due()) return null;\n\n sinceCheck = 0;\n checks += 1;\n // The tail, not the head -- the detectors' own `maxSample` takes the\n // first N characters, which for a stream is the part already judged.\n const recent = text.length > window ? text.slice(-window) : text;\n return checkOutput(recent, { ...checkOptions, ...DEFERRED_TO_END });\n },\n\n end(finishReason?: string): Verdict {\n return checkOutput(text, {\n ...checkOptions,\n finishReason: finishReason ?? checkOptions.finishReason,\n });\n },\n };\n}\n\nexport interface GuardStreamOptions extends StreamGuardOptions {\n /**\n * Called the first time a mid-stream check fails. Abort your request here.\n *\n * The guard deliberately does not own the AbortController: the thing that\n * knows how to cancel a generation is the code that started it, and a\n * detection library that reaches into your transport is a library you\n * cannot use with the next transport.\n */\n onDegenerate?: (verdict: Verdict) => void;\n /**\n * Called once with the final verdict when the source ends normally. Skipped\n * when the stream was cut short, because a verdict on a deliberately\n * abandoned response would describe your own abort, not the model.\n */\n onEnd?: (verdict: Verdict) => void;\n /**\n * Stop yielding once degeneration is detected. Default true.\n *\n * Set false to keep passing chunks through while still being told -- useful\n * for a logging-only rollout, where you want the signal without changing\n * what the user sees.\n */\n stopOnDegenerate?: boolean;\n}\n\n/**\n * Wraps a chunk stream and cuts it off when the model starts looping.\n *\n * ```ts\n * const controller = new AbortController();\n * const guarded = guardStream(model.textStream, {\n * ...presets.chat,\n * onDegenerate: () => controller.abort(),\n * });\n * for await (const chunk of guarded) process.stdout.write(chunk);\n * ```\n *\n * Yields the source's chunks unchanged until then, so it drops into an\n * existing loop without touching what you do with the text.\n */\nexport async function* guardStream(\n source: AsyncIterable<string>,\n options: GuardStreamOptions = {},\n): AsyncGenerator<string, void, undefined> {\n const { onDegenerate, onEnd, stopOnDegenerate = true, ...guardOptions } = options;\n const guard = createStreamGuard(guardOptions);\n let degenerate = false;\n\n for await (const chunk of source) {\n yield chunk;\n\n const verdict = guard.push(chunk);\n if (!verdict || verdict.ok || degenerate) continue;\n\n // Once, not on every subsequent check -- a loop keeps failing by\n // definition, and an abort handler called forty times is a bug report.\n degenerate = true;\n onDegenerate?.(verdict);\n if (stopOnDegenerate) return;\n }\n\n if (!degenerate) onEnd?.(guard.end());\n}\n","/**\n * Middleware adapter for the Vercel AI SDK.\n *\n * Structurally typed against the SDK rather than importing from it, so this\n * subpath adds no dependency, runtime or otherwise -- `ai` stays an optional\n * peer. The shapes below are the parts of the provider spec this touches and\n * nothing more, which is also what keeps it working across spec versions:\n * `finishReason` is a plain string in v2 and an object in v4, and both are\n * accepted here.\n */\nimport type { Verdict } from './types.js';\nimport type { StreamGuardOptions } from './stream.js';\nimport { checkOutput, DegenerateOutputError } from './check.js';\nimport { createStreamGuard } from './stream.js';\n\n/** `'stop' | 'length' | ...` in older specs, `{ unified, raw }` in v4. */\ntype FinishReasonLike = string | { unified?: string; raw?: string } | null | undefined;\n\ninterface StreamPart {\n type: string;\n /** Present on `text` content parts. */\n text?: string;\n /** Present on `text-delta` stream parts. */\n delta?: string;\n /** Present on the `finish` part. */\n finishReason?: FinishReasonLike;\n}\n\ninterface GenerateResultLike {\n content?: StreamPart[];\n finishReason?: FinishReasonLike;\n}\n\ninterface StreamResultLike {\n stream: ReadableStream<StreamPart>;\n}\n\n/** Normalises both spec shapes to what `truncationScore` expects. */\nfunction finishReasonOf(value: FinishReasonLike): string | undefined {\n if (typeof value === 'string') return value;\n if (value && typeof value === 'object') return value.unified ?? value.raw;\n return undefined;\n}\n\nexport type DegenerateAction = 'throw' | 'abort' | 'ignore';\n\nexport interface OutputGuardOptions extends StreamGuardOptions {\n /**\n * What to do when output is judged degenerate. Default `'throw'`.\n *\n * - `'throw'` errors the call with a `DegenerateOutputError`, which\n * `.retryable` marks as safe for your fallback layer to act on. On a\n * stream this also cancels the upstream request, so the tokens you have\n * not been billed for yet never get generated.\n * - `'abort'` ends the stream cleanly and keeps whatever arrived first.\n * Same token saving, no exception to handle -- use it when a partial\n * answer beats no answer.\n * - `'ignore'` reports through `onVerdict` and changes nothing. This is the\n * setting to roll out with: watch your own traffic before letting any\n * threshold fail a request.\n */\n onDegenerate?: DegenerateAction;\n /**\n * Every verdict, passing or failing, once per call. Send the scores to your\n * metrics -- a week of them is what turns the shipped thresholds into\n * thresholds you can defend for your own traffic.\n */\n onVerdict?: (verdict: Verdict, context: { streaming: boolean }) => void;\n}\n\n/**\n * Guards a model against returning degenerate output, as AI SDK middleware.\n *\n * ```ts\n * import { wrapLanguageModel } from 'ai';\n * import { outputGuard } from 'llm-output-guard/ai-sdk';\n *\n * const model = wrapLanguageModel({\n * model: groq('llama-3.3-70b-versatile'),\n * middleware: outputGuard({ ...presets.chat, onDegenerate: 'abort' }),\n * });\n * ```\n *\n * On `streamText` this is where it pays: the guard watches deltas as they\n * arrive and cancels the generation the moment a loop is detectable, rather\n * than letting the model run to `max_tokens` on your budget.\n */\nexport function outputGuard(options: OutputGuardOptions = {}) {\n const { onDegenerate = 'throw', onVerdict, ...guardOptions } = options;\n\n const act = (verdict: Verdict, streaming: boolean): void => {\n onVerdict?.(verdict, { streaming });\n if (verdict.ok || onDegenerate === 'ignore') return;\n if (onDegenerate === 'throw') throw new DegenerateOutputError(verdict);\n };\n\n return {\n /**\n * Non-streaming. The tokens are already bought by the time this runs, so\n * all it can do is stop a bad answer from being used as a good one.\n */\n async wrapGenerate<T extends GenerateResultLike>({\n doGenerate,\n }: {\n doGenerate: () => PromiseLike<T>;\n }): Promise<T> {\n const result = await doGenerate();\n const text = (result.content ?? [])\n .filter((part) => part.type === 'text')\n .map((part) => part.text ?? '')\n .join('');\n\n act(\n checkOutput(text, {\n ...guardOptions,\n finishReason: finishReasonOf(result.finishReason) ?? guardOptions.finishReason,\n }),\n false,\n );\n\n return result;\n },\n\n async wrapStream<T extends StreamResultLike>({\n doStream,\n }: {\n doStream: () => PromiseLike<T>;\n }): Promise<T> {\n const result = await doStream();\n const guard = createStreamGuard(guardOptions);\n let fired = false;\n let finishReason: FinishReasonLike;\n\n const guarded = result.stream.pipeThrough(\n new TransformStream<StreamPart, StreamPart>({\n transform(part, controller) {\n // Forward first: a chunk already generated has been paid for, and\n // withholding it buys nothing but a truncated answer.\n controller.enqueue(part);\n\n if (part.type === 'finish') finishReason = part.finishReason;\n if (part.type !== 'text-delta' || fired) return;\n\n const verdict = guard.push(part.delta ?? '');\n if (!verdict || verdict.ok) return;\n\n fired = true;\n onVerdict?.(verdict, { streaming: true });\n if (onDegenerate === 'ignore') return;\n\n /*\n * Both of these cancel the source stream, which is what actually\n * stops the provider generating -- the saving is not in skipping\n * chunks we already received but in the ones never produced.\n */\n if (onDegenerate === 'throw') {\n controller.error(new DegenerateOutputError(verdict));\n } else {\n controller.terminate();\n }\n },\n\n flush() {\n // A stream we cut short would only be reported as truncated by us,\n // describing our own abort rather than the model.\n if (!fired) {\n onVerdict?.(guard.end(finishReasonOf(finishReason)), { streaming: true });\n }\n },\n }),\n );\n\n // Everything the provider returned, with only the stream swapped -- the\n // cast is the spread losing `T`, not a change in what is handed back.\n return { ...result, stream: guarded } as T;\n },\n };\n}\n"]}