llm-output-guard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edwin Satya Yudistira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # llm-output-guard
2
+
3
+ Detect LLM responses that failed **while returning `200 OK`**.
4
+
5
+ Zero runtime dependencies. Deterministic. Composes with whatever retry or fallback layer you already have.
6
+
7
+ ```bash
8
+ npm i llm-output-guard
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Why this exists
14
+
15
+ A model in my interview-question pipeline started returning garbage — the same clause repeated until it hit the token ceiling. Every layer reported success:
16
+
17
+ - the provider returned `200`
18
+ - the SDK parsed the envelope without complaint
19
+ - the response had non-zero length
20
+
21
+ So the retry policy saw nothing worth acting on. The bad response was cached, served, and counted as a success. What surfaced instead was a *latency* problem, because downstream code kept retrying around a response that was technically fine.
22
+
23
+ Retry and fallback libraries key off **transport** signals: `429`, `5xx`, timeouts. None of them look at whether the content means anything. That is the gap this package fills.
24
+
25
+ ## What it is not
26
+
27
+ This is **not** another fallback chain. Those exist and they are good:
28
+
29
+ - [`cockatiel`](https://github.com/connor4312/cockatiel) — retry, circuit breaker, timeout, bulkhead
30
+ - [`ai-fallback`](https://www.npmjs.com/package/ai-fallback) — model fallback for the Vercel AI SDK
31
+
32
+ `llm-output-guard` produces the *signal* those layers are missing. Use them together.
33
+
34
+ ---
35
+
36
+ ## Usage
37
+
38
+ ```ts
39
+ import { checkOutput, presets } from 'llm-output-guard';
40
+
41
+ const text = await callModel(prompt);
42
+ const verdict = checkOutput(text, presets.chat);
43
+
44
+ if (!verdict.ok) {
45
+ console.warn('degenerate output', verdict.reasons);
46
+ // fall through to your next provider
47
+ }
48
+ ```
49
+
50
+ ### Throwing form, for existing retry layers
51
+
52
+ ```ts
53
+ import { assertOutput, DegenerateOutputError, presets } from 'llm-output-guard';
54
+ import { retry, handleWhen, ExponentialBackoff } from 'cockatiel';
55
+
56
+ const policy = retry(
57
+ handleWhen((err) => err instanceof DegenerateOutputError || isTransport(err)),
58
+ { maxAttempts: 3, backoff: new ExponentialBackoff() },
59
+ );
60
+
61
+ const text = await policy.execute(async () =>
62
+ assertOutput(await callModel(prompt), presets.chat),
63
+ );
64
+ ```
65
+
66
+ `DegenerateOutputError` carries `.retryable === true` and the full `.verdict`.
67
+
68
+ ### Structured output
69
+
70
+ ```ts
71
+ const verdict = checkOutput(raw, {
72
+ ...presets.strictJson,
73
+ requiredKeys: ['score', 'notes', 'followUp'],
74
+ });
75
+
76
+ if (verdict.ok) use(verdict.json); // already parsed, fence stripped
77
+ ```
78
+
79
+ ---
80
+
81
+ ## The verdict
82
+
83
+ ```ts
84
+ {
85
+ ok: false,
86
+ reasons: [
87
+ { code: 'REPETITION', score: 0.83, threshold: 0.4, message: '83% of 3-grams are duplicates.' },
88
+ { code: 'TAIL_LOOP', score: 0.90, threshold: 0.5, message: 'Response ends in a repeating block…' },
89
+ ],
90
+ scores: { EMPTY: 0, TOO_SHORT: 0, REPETITION: 0.83, TAIL_LOOP: 0.90, LOW_ENTROPY: 0.41 },
91
+ }
92
+ ```
93
+
94
+ Every detector runs even after one fails, so `reasons` shows the whole picture instead of whichever check happened to be ordered first. `scores` includes passing detectors too — send them to your metrics and you will know your real degeneration rate within a day.
95
+
96
+ ## Detectors
97
+
98
+ | Code | Catches | Signal |
99
+ |---|---|---|
100
+ | `EMPTY` | Whitespace, lone punctuation, `{}`, empty fences | Content presence |
101
+ | `TOO_SHORT` | Non-empty but useless | Length vs. minimum |
102
+ | `REPETITION` | Loops and stutters | Duplicate n-gram fraction |
103
+ | `TAIL_LOOP` | Good start, then a stuck ending | Periodicity in the trailing window |
104
+ | `LOW_ENTROPY` | Character-level collapse, token artifacts | Hand-rolled LZ77 compression ratio |
105
+ | `TRUNCATED` | Cut off mid-thought | `finish_reason`, unbalanced fences/brackets |
106
+ | `INVALID_JSON` | Prose around the payload, missing keys | Parse + key contract |
107
+ | `LANG_MISMATCH` | Answered in the wrong language | Function-word profile (coarse, opt-in) |
108
+
109
+ Every detector is exported on its own if you only want one.
110
+
111
+ ## Presets
112
+
113
+ `chat` · `strictJson` · `longForm` · `lenient`
114
+
115
+ They are starting points calibrated against the fixture corpus in this repo — not universal truths. Log your scores for a week, then set your own thresholds.
116
+
117
+ ---
118
+
119
+ ## On thresholds
120
+
121
+ A miss is annoying. **A false positive is worse**: a healthy response gets discarded and retried against a slower provider for nothing.
122
+
123
+ So the corpus carries deliberate traps — markdown tables, repeated-prefix lists, code blocks, rhetorical refrains — all of which a naive detector flags. `npm run calibrate` prints the margin between the worst healthy score and the weakest degenerate one:
124
+
125
+ ```
126
+ === REPETITION ===
127
+ healthy max : 0.073 (code-python-snippet)
128
+ degenerate min: 0.771 (tail-loop-after-good-start)
129
+ margin : 0.698 OK
130
+ ```
131
+
132
+ If that margin ever goes thin, the answer is a better detector, not a nudged threshold.
133
+
134
+ ## Growing the corpus
135
+
136
+ ```bash
137
+ GROQ_API_KEY=… node scripts/generate-fixtures.mjs --model llama-3.1-8b-instant --n 8
138
+ ```
139
+
140
+ Output lands in `test/fixtures/raw/` **unreviewed**. Read each one, label it, then move it into `bad/` or `good/`. Nothing is auto-promoted: a fixture you have not read is a threshold you cannot defend.
141
+
142
+ ## Design notes
143
+
144
+ - **Zero runtime dependencies**, enforced in CI. Node ≥ 18, works on edge, browser, Deno, Bun.
145
+ - **Hand-rolled LZ77** rather than `node:zlib`, so the package stays runtime-agnostic. It is not a real compressor; it only needs to move monotonically with redundancy.
146
+ - **Pure and synchronous.** No network, no clock, no randomness — safe on a hot path, trivial to test.
147
+ - **Scores, not booleans.** Detectors report 0–1 and leave the threshold decision to you.
148
+ - **Abstains rather than guesses.** Samples too short to judge score 0.
149
+
150
+ ## Limitations
151
+
152
+ - Not a hallucination detector. It measures *shape*, never truth.
153
+ - Language detection is a function-word heuristic covering `id`/`en`/`es`. Opt-in, and unreliable under 25 words.
154
+ - Truncation from a missing full stop is weak evidence, scored 0.55 and left below the default thresholds on purpose. Lower `maxTruncation` to ~0.5 to catch it, and expect false positives.
155
+ - Thresholds calibrated on the bundled corpus. Yours will differ.
156
+
157
+ ## License
158
+
159
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,344 @@
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
+ var supportedLanguages = Object.keys(PROFILES);
181
+
182
+ // src/check.ts
183
+ var DEFAULTS = {
184
+ minLength: 1,
185
+ maxRepetition: 0.35,
186
+ maxTailLoop: 0.5,
187
+ maxCompressibility: 0.75,
188
+ maxTruncation: null,
189
+ expectJson: false,
190
+ allowJsonFence: true,
191
+ maxLangMismatch: 0.6,
192
+ ngram: 3
193
+ };
194
+ function checkOutput(text, options = {}) {
195
+ const opts = { ...DEFAULTS, ...options };
196
+ const reasons = [];
197
+ const scores = {};
198
+ let parsedJson;
199
+ const add = (code, score, threshold, message) => {
200
+ scores[code] = score;
201
+ if (score > threshold) reasons.push({ code, score, threshold, message });
202
+ };
203
+ const empty = emptinessScore(text);
204
+ add("EMPTY", empty, 0.5, "Response contains no usable content.");
205
+ if (empty >= 1) {
206
+ return { ok: false, reasons, scores };
207
+ }
208
+ if (opts.minLength > 0) {
209
+ add(
210
+ "TOO_SHORT",
211
+ shortnessScore(text, opts.minLength),
212
+ 0,
213
+ `Response is ${text.trim().length} chars, below the ${opts.minLength} minimum.`
214
+ );
215
+ }
216
+ if (opts.maxRepetition != null) {
217
+ const s = repetitionScore(text, { n: opts.ngram });
218
+ add(
219
+ "REPETITION",
220
+ s,
221
+ opts.maxRepetition,
222
+ `${Math.round(s * 100)}% of ${opts.ngram}-grams are duplicates.`
223
+ );
224
+ }
225
+ if (opts.maxTailLoop != null) {
226
+ const s = tailLoopScore(text);
227
+ add(
228
+ "TAIL_LOOP",
229
+ s,
230
+ opts.maxTailLoop,
231
+ `Response ends in a repeating block covering ${Math.round(s * 100)}% of the tail.`
232
+ );
233
+ }
234
+ if (opts.maxCompressibility != null) {
235
+ const s = compressibilityScore(text);
236
+ add(
237
+ "LOW_ENTROPY",
238
+ s,
239
+ opts.maxCompressibility,
240
+ "Response is far more compressible than natural language."
241
+ );
242
+ }
243
+ if (opts.maxTruncation != null || opts.finishReason) {
244
+ const s = truncationScore(text, { finishReason: opts.finishReason });
245
+ add(
246
+ "TRUNCATED",
247
+ s,
248
+ opts.maxTruncation ?? 0.75,
249
+ `Response appears cut off near: "${excerpt(text.trim().slice(-60), 60)}"`
250
+ );
251
+ }
252
+ if (opts.expectJson) {
253
+ const result = jsonScore(text, {
254
+ allowFence: opts.allowJsonFence,
255
+ requiredKeys: opts.requiredKeys
256
+ });
257
+ parsedJson = result.value;
258
+ add(
259
+ "INVALID_JSON",
260
+ result.score,
261
+ 0,
262
+ result.reason === "missing-keys" ? `JSON is missing required keys: ${result.missingKeys?.join(", ")}.` : "Response is not parseable JSON."
263
+ );
264
+ }
265
+ if (opts.expectLang) {
266
+ const s = languageMismatchScore(text, opts.expectLang);
267
+ add(
268
+ "LANG_MISMATCH",
269
+ s,
270
+ opts.maxLangMismatch,
271
+ `Response does not look like '${opts.expectLang}'.`
272
+ );
273
+ }
274
+ return { ok: reasons.length === 0, reasons, scores, json: parsedJson };
275
+ }
276
+ var DegenerateOutputError = class extends Error {
277
+ verdict;
278
+ /** Marks this as safe to retry against another provider. */
279
+ retryable = true;
280
+ constructor(verdict) {
281
+ super(`Degenerate LLM output: ${verdict.reasons.map((r) => r.code).join(", ")}`);
282
+ this.name = "DegenerateOutputError";
283
+ this.verdict = verdict;
284
+ }
285
+ };
286
+ function assertOutput(text, options = {}) {
287
+ const verdict = checkOutput(text, options);
288
+ if (!verdict.ok) throw new DegenerateOutputError(verdict);
289
+ return text;
290
+ }
291
+
292
+ // src/presets.ts
293
+ var presets = {
294
+ /** Conversational replies. Tolerant of quoted or listed repetition. */
295
+ chat: {
296
+ minLength: 12,
297
+ maxRepetition: 0.4,
298
+ maxTailLoop: 0.5,
299
+ maxCompressibility: 0.75
300
+ },
301
+ /** Structured output. Parseability is non-negotiable; prose checks relax. */
302
+ strictJson: {
303
+ minLength: 2,
304
+ expectJson: true,
305
+ maxRepetition: 0.6,
306
+ maxTailLoop: 0.6,
307
+ maxCompressibility: null,
308
+ maxTruncation: 0.75
309
+ },
310
+ /** Long-form generation, where truncation matters most. */
311
+ longForm: {
312
+ minLength: 200,
313
+ maxRepetition: 0.35,
314
+ maxTailLoop: 0.4,
315
+ maxCompressibility: 0.7,
316
+ maxTruncation: 0.75
317
+ },
318
+ /** Catches only unambiguous garbage. Use when false positives cost more than misses. */
319
+ lenient: {
320
+ minLength: 1,
321
+ maxRepetition: 0.7,
322
+ maxTailLoop: 0.7,
323
+ maxCompressibility: 0.9
324
+ }
325
+ };
326
+
327
+ exports.DegenerateOutputError = DegenerateOutputError;
328
+ exports.assertOutput = assertOutput;
329
+ exports.checkOutput = checkOutput;
330
+ exports.compressibilityScore = compressibilityScore;
331
+ exports.compressionRatio = compressionRatio;
332
+ exports.emptinessScore = emptinessScore;
333
+ exports.jsonScore = jsonScore;
334
+ exports.languageMismatchScore = languageMismatchScore;
335
+ exports.languageProfile = languageProfile;
336
+ exports.presets = presets;
337
+ exports.repetitionScore = repetitionScore;
338
+ exports.shortnessScore = shortnessScore;
339
+ exports.stripFence = stripFence;
340
+ exports.supportedLanguages = supportedLanguages;
341
+ exports.tailLoopScore = tailLoopScore;
342
+ exports.truncationScore = truncationScore;
343
+ //# sourceMappingURL=index.cjs.map
344
+ //# sourceMappingURL=index.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/presets.ts"],"names":[],"mappings":"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","file":"index.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 * Healthy prose lands around 0.30-0.55. Degenerate output collapses toward 0.\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 */\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 */\nexport function checkOutput(text: string, options: CheckOptions = {}): 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 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(text: string, options: CheckOptions = {}): string {\n const verdict = checkOutput(text, options);\n if (!verdict.ok) throw new DegenerateOutputError(verdict);\n return text;\n}\n","import type { CheckOptions } from './types.js';\n\n/**\n * Starting configurations. Tuned against the fixture corpus in test/fixtures,\n * so treat them as calibrated defaults rather than arbitrary numbers -- and\n * still re-tune against your own traffic before trusting them in production.\n */\nexport const presets = {\n /** Conversational replies. Tolerant of quoted or listed repetition. */\n chat: {\n minLength: 12,\n maxRepetition: 0.4,\n maxTailLoop: 0.5,\n maxCompressibility: 0.75,\n } satisfies CheckOptions,\n\n /** Structured output. Parseability is non-negotiable; prose checks relax. */\n strictJson: {\n minLength: 2,\n expectJson: true,\n maxRepetition: 0.6,\n maxTailLoop: 0.6,\n maxCompressibility: null,\n maxTruncation: 0.75,\n } satisfies CheckOptions,\n\n /** Long-form generation, where truncation matters most. */\n longForm: {\n minLength: 200,\n maxRepetition: 0.35,\n maxTailLoop: 0.4,\n maxCompressibility: 0.7,\n maxTruncation: 0.75,\n } satisfies CheckOptions,\n\n /** Catches only unambiguous garbage. Use when false positives cost more than misses. */\n lenient: {\n minLength: 1,\n maxRepetition: 0.7,\n maxTailLoop: 0.7,\n maxCompressibility: 0.9,\n } satisfies CheckOptions,\n} as const;\n"]}