llm-output-guard 0.1.0 → 0.1.1

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/dist/index.cjs CHANGED
@@ -200,6 +200,16 @@ function checkOutput(text, options = {}) {
200
200
  scores[code] = score;
201
201
  if (score > threshold) reasons.push({ code, score, threshold, message });
202
202
  };
203
+ if (typeof text !== "string") {
204
+ scores.EMPTY = 1;
205
+ reasons.push({
206
+ code: "EMPTY",
207
+ score: 1,
208
+ threshold: 0.5,
209
+ message: `Response was ${text === null ? "null" : typeof text}, not a string.`
210
+ });
211
+ return { ok: false, reasons, scores };
212
+ }
203
213
  const empty = emptinessScore(text);
204
214
  add("EMPTY", empty, 0.5, "Response contains no usable content.");
205
215
  if (empty >= 1) {
@@ -1 +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"]}
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 * 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 } 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"]}
package/dist/index.d.cts CHANGED
@@ -54,8 +54,11 @@ interface CheckOptions {
54
54
  *
55
55
  * Every detector runs even after one fails, so `reasons` shows the full picture
56
56
  * rather than whichever check happened to be ordered first.
57
+ *
58
+ * Never throws. A `null`, `undefined`, or otherwise non-string input is a
59
+ * verdict (`EMPTY`), not an exception -- see the guard below for why.
57
60
  */
58
- declare function checkOutput(text: string, options?: CheckOptions): Verdict;
61
+ declare function checkOutput(text: string | null | undefined, options?: CheckOptions): Verdict;
59
62
  /** Error thrown by {@link assertOutput}, carrying the full verdict. */
60
63
  declare class DegenerateOutputError extends Error {
61
64
  readonly verdict: Verdict;
@@ -67,7 +70,7 @@ declare class DegenerateOutputError extends Error {
67
70
  * Throwing wrapper, for dropping straight into an existing retry or fallback
68
71
  * chain that already keys off thrown errors.
69
72
  */
70
- declare function assertOutput(text: string, options?: CheckOptions): string;
73
+ declare function assertOutput(text: string | null | undefined, options?: CheckOptions): string;
71
74
 
72
75
  /**
73
76
  * Starting configurations. Tuned against the fixture corpus in test/fixtures,
@@ -154,12 +157,22 @@ interface CompressibilityOptions {
154
157
  * This is not a real compressor; it only needs to move monotonically
155
158
  * with redundancy, which is all the score requires.
156
159
  *
157
- * Healthy prose lands around 0.30-0.55. Degenerate output collapses toward 0.
160
+ * Measured against the fixture corpus: healthy output lands at 0.67-0.97,
161
+ * degenerate collapse at 0.007-0.042, and tail loops in between at 0.17-0.20.
162
+ * The gap either side of that middle band is what the pivot below trades on.
158
163
  */
159
164
  declare function compressionRatio(text: string, options?: CompressibilityOptions): number;
160
165
  /**
161
166
  * Suspicion score derived from {@link compressionRatio}.
162
167
  * `pivot` is the ratio treated as fully healthy; lower ratios scale up toward 1.
168
+ *
169
+ * At the default 0.32 every healthy fixture clamps to exactly 0, with the
170
+ * nearest one still twice the pivot away -- so this detector is deliberately
171
+ * tuned for outright entropy collapse and abstains on everything milder.
172
+ * Tail loops score 0.37-0.48 here and are left to `tailLoopScore`, which
173
+ * separates them far more cleanly (0.90 against a healthy max of 0.00).
174
+ * Raising the pivot would make this fire on loops too, buying redundant
175
+ * coverage with the margin that currently makes a false positive so unlikely.
163
176
  */
164
177
  declare function compressibilityScore(text: string, options?: CompressibilityOptions & {
165
178
  pivot?: number;
package/dist/index.d.ts CHANGED
@@ -54,8 +54,11 @@ interface CheckOptions {
54
54
  *
55
55
  * Every detector runs even after one fails, so `reasons` shows the full picture
56
56
  * rather than whichever check happened to be ordered first.
57
+ *
58
+ * Never throws. A `null`, `undefined`, or otherwise non-string input is a
59
+ * verdict (`EMPTY`), not an exception -- see the guard below for why.
57
60
  */
58
- declare function checkOutput(text: string, options?: CheckOptions): Verdict;
61
+ declare function checkOutput(text: string | null | undefined, options?: CheckOptions): Verdict;
59
62
  /** Error thrown by {@link assertOutput}, carrying the full verdict. */
60
63
  declare class DegenerateOutputError extends Error {
61
64
  readonly verdict: Verdict;
@@ -67,7 +70,7 @@ declare class DegenerateOutputError extends Error {
67
70
  * Throwing wrapper, for dropping straight into an existing retry or fallback
68
71
  * chain that already keys off thrown errors.
69
72
  */
70
- declare function assertOutput(text: string, options?: CheckOptions): string;
73
+ declare function assertOutput(text: string | null | undefined, options?: CheckOptions): string;
71
74
 
72
75
  /**
73
76
  * Starting configurations. Tuned against the fixture corpus in test/fixtures,
@@ -154,12 +157,22 @@ interface CompressibilityOptions {
154
157
  * This is not a real compressor; it only needs to move monotonically
155
158
  * with redundancy, which is all the score requires.
156
159
  *
157
- * Healthy prose lands around 0.30-0.55. Degenerate output collapses toward 0.
160
+ * Measured against the fixture corpus: healthy output lands at 0.67-0.97,
161
+ * degenerate collapse at 0.007-0.042, and tail loops in between at 0.17-0.20.
162
+ * The gap either side of that middle band is what the pivot below trades on.
158
163
  */
159
164
  declare function compressionRatio(text: string, options?: CompressibilityOptions): number;
160
165
  /**
161
166
  * Suspicion score derived from {@link compressionRatio}.
162
167
  * `pivot` is the ratio treated as fully healthy; lower ratios scale up toward 1.
168
+ *
169
+ * At the default 0.32 every healthy fixture clamps to exactly 0, with the
170
+ * nearest one still twice the pivot away -- so this detector is deliberately
171
+ * tuned for outright entropy collapse and abstains on everything milder.
172
+ * Tail loops score 0.37-0.48 here and are left to `tailLoopScore`, which
173
+ * separates them far more cleanly (0.90 against a healthy max of 0.00).
174
+ * Raising the pivot would make this fire on loops too, buying redundant
175
+ * coverage with the margin that currently makes a false positive so unlikely.
163
176
  */
164
177
  declare function compressibilityScore(text: string, options?: CompressibilityOptions & {
165
178
  pivot?: number;
package/dist/index.js CHANGED
@@ -198,6 +198,16 @@ function checkOutput(text, options = {}) {
198
198
  scores[code] = score;
199
199
  if (score > threshold) reasons.push({ code, score, threshold, message });
200
200
  };
201
+ if (typeof text !== "string") {
202
+ scores.EMPTY = 1;
203
+ reasons.push({
204
+ code: "EMPTY",
205
+ score: 1,
206
+ threshold: 0.5,
207
+ message: `Response was ${text === null ? "null" : typeof text}, not a string.`
208
+ });
209
+ return { ok: false, reasons, scores };
210
+ }
201
211
  const empty = emptinessScore(text);
202
212
  add("EMPTY", empty, 0.5, "Response contains no usable content.");
203
213
  if (empty >= 1) {
package/dist/index.js.map CHANGED
@@ -1 +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.js","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"]}
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.js","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 } 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-output-guard",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Detect degenerate LLM output that arrives with a 200 OK. Zero dependencies, deterministic, composes with any retry or fallback layer.",
5
5
  "keywords": [
6
6
  "llm",