llm-output-guard 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/README.md +63 -11
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -318,18 +318,34 @@ gives you a number that describes neither.
318
318
 
319
319
  ## Detectors
320
320
 
321
- | Code | Catches | Signal |
322
- |---|---|---|
323
- | `EMPTY` | Whitespace, lone punctuation, `{}`, empty fences | Content presence |
324
- | `TOO_SHORT` | Non-empty but useless | Length vs. minimum |
325
- | `REPETITION` | Loops and stutters | Duplicate word n-gram fraction |
326
- | `TAIL_LOOP` | Good start, then a stuck ending | Periodicity in the trailing window, over words or characters |
327
- | `LOW_ENTROPY` | Character-level collapse, token artifacts | Hand-rolled LZ77 compression ratio |
328
- | `TRUNCATED` | Cut off mid-thought | `finish_reason`, unbalanced fences/brackets |
329
- | `INVALID_JSON` | Prose around the payload, missing keys | Parse + key contract |
330
- | `LANG_MISMATCH` | Answered in the wrong language | Function-word profile (coarse, opt-in) |
321
+ | Code | Catches | Signal | Exported as |
322
+ |---|---|---|---|
323
+ | `EMPTY` | Whitespace, lone punctuation, `{}`, empty fences | Content presence | `emptinessScore` |
324
+ | `TOO_SHORT` | Non-empty but useless | Length vs. minimum | `shortnessScore` |
325
+ | `REPETITION` | Loops and stutters | Duplicate word n-gram fraction | `repetitionScore` |
326
+ | `TAIL_LOOP` | Good start, then a stuck ending | Periodicity in the trailing window, over words or characters | `tailLoopScore`, `tailLoopDetail` |
327
+ | `LOW_ENTROPY` | Character-level collapse, token artifacts | Hand-rolled LZ77 compression ratio | `compressibilityScore`, `compressionRatio` |
328
+ | `TRUNCATED` | Cut off mid-thought | `finish_reason`, unbalanced fences/brackets | `truncationScore` |
329
+ | `INVALID_JSON` | Prose around the payload, missing keys | Parse + key contract | `jsonScore`, `stripFence` |
330
+ | `LANG_MISMATCH` | Answered in the wrong language | Function-word profile (coarse, opt-in) | `languageMismatchScore`, `languageProfile`, `supportedLanguages` |
331
+
332
+ Every detector is exported on its own if you only want one, and every name in
333
+ that last column is covered by semver — see **Stability**.
334
+
335
+ ```ts
336
+ import { repetitionScore, tailLoopDetail, stripFence } from 'llm-output-guard';
337
+
338
+ repetitionScore(text); // 0..1, higher is worse
339
+ repetitionScore(text, { n: 4 }); // n-gram size
340
+ tailLoopDetail(text, { mode: 'char' }); // { score, mode } — which tokenizer ran
341
+ stripFence('```json\n{"a":1}\n```'); // '{"a":1}'
342
+ ```
331
343
 
332
- Every detector is exported on its own if you only want one.
344
+ Each takes `(text, options?)` and returns a `0..1` score, with three exceptions
345
+ worth knowing: `shortnessScore(text, minChars)` takes its minimum positionally,
346
+ `stripFence` returns a string, and `jsonScore` / `tailLoopDetail` return a detail
347
+ object rather than a bare number. `supportedLanguages` is a value, not a
348
+ function — the array `['id', 'en', 'es']`.
333
349
 
334
350
  ## Presets
335
351
 
@@ -390,6 +406,42 @@ the bulk and a cluster of outliers is real separation observed in your data
390
406
  rather than an assumption about rarity — and when that hole rests on one or
391
407
  two samples, the report says so.
392
408
 
409
+ ### The same thing, as a function
410
+
411
+ The CLI is a wrapper. If your scores already live somewhere the shell cannot
412
+ reach them — a metrics store, a warehouse query, a test — call `calibrate`
413
+ directly. It takes the same flat objects the JSONL format describes:
414
+
415
+ ```ts
416
+ import { calibrate } from 'llm-output-guard';
417
+
418
+ const { n, summaries } = calibrate(
419
+ [
420
+ { REPETITION: 0.03, TAIL_LOOP: 0 },
421
+ { REPETITION: 0.91, TAIL_LOOP: 0.88, modes: { TAIL_LOOP: 'char' } },
422
+ // ...one entry per logged verdict
423
+ ],
424
+ { falsePositiveRate: 0.001 },
425
+ );
426
+
427
+ for (const s of summaries) {
428
+ s.code; // 'REPETITION'
429
+ s.mode; // 'word' | 'char', when the samples recorded one
430
+ s.suggested; // threshold flagging falsePositiveRate of this sample
431
+ s.gap; // { below, above, count, share } | null — stronger evidence
432
+ s.distribution; // { n, nonZero, min, max, p50, p90, p99, p999 }
433
+ s.caveats; // everything that makes `suggested` untrustworthy
434
+ }
435
+ ```
436
+
437
+ `modes` rides along in the same object and is not read as a score. Log it, and
438
+ `summaries` comes back segmented — one entry per `code`+`mode` — for the reason
439
+ in the paragraph above. `summarise(code, scores, options)` is exported too, for
440
+ when you have one detector's numbers already grouped.
441
+
442
+ **Read `caveats` before `suggested`.** It is where a sample too small for the
443
+ requested rate says so, and a `suggested` number carries no warning of its own.
444
+
393
445
  ## On thresholds
394
446
 
395
447
  A miss is annoying. **A false positive is worse**: a healthy response gets discarded and retried against a slower provider for nothing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-output-guard",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -38,7 +38,7 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "bin": {
41
- "llm-output-guard": "./dist/bin.js"
41
+ "llm-output-guard": "dist/bin.js"
42
42
  },
43
43
  "files": [
44
44
  "dist"