llm-output-guard 0.5.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.
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.
@@ -526,7 +578,9 @@ output starts to reach.
526
578
 
527
579
  ## Stability
528
580
 
529
- What semver means for this package specifically.
581
+ What semver means for this package specifically. These rules bind from **1.0.0**
582
+ onward; under `0.x` they described an intent, and the surface was frozen — export
583
+ by export — in the 1.0.0 release.
530
584
 
531
585
  **The public API is:** everything exported from `llm-output-guard`, plus
532
586
  `outputGuard` / `OutputGuardOptions` / `DegenerateAction` from `./ai-sdk` and
@@ -534,7 +588,8 @@ What semver means for this package specifically.
534
588
  Each subpath is its own contract; the two adapters share an internal base type
535
589
  today and are free to diverge, so an option added to one is not a promise about
536
590
  the other. Anything not exported from those three entry points is internal, has
537
- no stability guarantee, and may move in any release.
591
+ no stability guarantee, and may move in any release. The list is asserted in
592
+ `test/surface.test.ts`, so an export cannot join it by accident.
538
593
 
539
594
  **Threshold and preset values are behaviour, not implementation.** This is the
540
595
  interesting case, so it gets a rule of its own:
package/dist/index.cjs CHANGED
@@ -579,12 +579,10 @@ exports.compressibilityScore = compressibilityScore;
579
579
  exports.compressionRatio = compressionRatio;
580
580
  exports.createStreamGuard = createStreamGuard;
581
581
  exports.emptinessScore = emptinessScore;
582
- exports.findGap = findGap;
583
582
  exports.guardStream = guardStream;
584
583
  exports.jsonScore = jsonScore;
585
584
  exports.languageMismatchScore = languageMismatchScore;
586
585
  exports.languageProfile = languageProfile;
587
- exports.percentile = percentile;
588
586
  exports.presets = presets;
589
587
  exports.repetitionScore = repetitionScore;
590
588
  exports.shortnessScore = shortnessScore;
package/dist/index.d.cts CHANGED
@@ -102,19 +102,6 @@ interface Summary {
102
102
  /** Everything that would make the number above untrustworthy. */
103
103
  caveats: string[];
104
104
  }
105
- /** Linear-interpolated percentile. `sorted` must be ascending. */
106
- declare function percentile(sorted: number[], p: number): number;
107
- /**
108
- * The widest empty stretch in the upper tail, if there is one.
109
- *
110
- * A genuinely bimodal detector -- healthy output clustered near zero, a
111
- * handful of failures far above it -- leaves a visible hole between the two.
112
- * That hole is worth far more than a percentile, because it is evidence about
113
- * *this* detector on *your* traffic rather than an assumption about rarity.
114
- * Searching only above the median keeps the ordinary spread of healthy scores
115
- * from being mistaken for a separation.
116
- */
117
- declare function findGap(sorted: number[], minWidth?: number): Gap | null;
118
105
  /**
119
106
  * Summarise one detector's scores.
120
107
  *
@@ -412,4 +399,4 @@ declare function languageProfile(text: string): Record<string, number>;
412
399
  declare function languageMismatchScore(text: string, expected: string, options?: LanguageOptions): number;
413
400
  declare const supportedLanguages: string[];
414
401
 
415
- export { type Calibration, type CalibrationOptions, CheckOptions, type CompressibilityOptions, DegenerateOutputError, type Distribution, type Gap, type JsonOptions, type JsonResult, type LanguageOptions, ReasonCode, type RepetitionOptions, type ScoreSample, type Summary, type TailLoopOptions, type TailLoopResult, TokenMode, type TruncationOptions, Verdict, assertOutput, calibrate, checkOutput, compressibilityScore, compressionRatio, emptinessScore, findGap, jsonScore, languageMismatchScore, languageProfile, percentile, presets, repetitionScore, shortnessScore, stripFence, summarise, supportedLanguages, tailLoopDetail, tailLoopScore, truncationScore };
402
+ export { type Calibration, type CalibrationOptions, CheckOptions, type CompressibilityOptions, DegenerateOutputError, type Distribution, type Gap, type JsonOptions, type JsonResult, type LanguageOptions, ReasonCode, type RepetitionOptions, type ScoreSample, type Summary, type TailLoopOptions, type TailLoopResult, TokenMode, type TruncationOptions, Verdict, assertOutput, calibrate, checkOutput, compressibilityScore, compressionRatio, emptinessScore, jsonScore, languageMismatchScore, languageProfile, presets, repetitionScore, shortnessScore, stripFence, summarise, supportedLanguages, tailLoopDetail, tailLoopScore, truncationScore };
package/dist/index.d.ts CHANGED
@@ -102,19 +102,6 @@ interface Summary {
102
102
  /** Everything that would make the number above untrustworthy. */
103
103
  caveats: string[];
104
104
  }
105
- /** Linear-interpolated percentile. `sorted` must be ascending. */
106
- declare function percentile(sorted: number[], p: number): number;
107
- /**
108
- * The widest empty stretch in the upper tail, if there is one.
109
- *
110
- * A genuinely bimodal detector -- healthy output clustered near zero, a
111
- * handful of failures far above it -- leaves a visible hole between the two.
112
- * That hole is worth far more than a percentile, because it is evidence about
113
- * *this* detector on *your* traffic rather than an assumption about rarity.
114
- * Searching only above the median keeps the ordinary spread of healthy scores
115
- * from being mistaken for a separation.
116
- */
117
- declare function findGap(sorted: number[], minWidth?: number): Gap | null;
118
105
  /**
119
106
  * Summarise one detector's scores.
120
107
  *
@@ -412,4 +399,4 @@ declare function languageProfile(text: string): Record<string, number>;
412
399
  declare function languageMismatchScore(text: string, expected: string, options?: LanguageOptions): number;
413
400
  declare const supportedLanguages: string[];
414
401
 
415
- export { type Calibration, type CalibrationOptions, CheckOptions, type CompressibilityOptions, DegenerateOutputError, type Distribution, type Gap, type JsonOptions, type JsonResult, type LanguageOptions, ReasonCode, type RepetitionOptions, type ScoreSample, type Summary, type TailLoopOptions, type TailLoopResult, TokenMode, type TruncationOptions, Verdict, assertOutput, calibrate, checkOutput, compressibilityScore, compressionRatio, emptinessScore, findGap, jsonScore, languageMismatchScore, languageProfile, percentile, presets, repetitionScore, shortnessScore, stripFence, summarise, supportedLanguages, tailLoopDetail, tailLoopScore, truncationScore };
402
+ export { type Calibration, type CalibrationOptions, CheckOptions, type CompressibilityOptions, DegenerateOutputError, type Distribution, type Gap, type JsonOptions, type JsonResult, type LanguageOptions, ReasonCode, type RepetitionOptions, type ScoreSample, type Summary, type TailLoopOptions, type TailLoopResult, TokenMode, type TruncationOptions, Verdict, assertOutput, calibrate, checkOutput, compressibilityScore, compressionRatio, emptinessScore, jsonScore, languageMismatchScore, languageProfile, presets, repetitionScore, shortnessScore, stripFence, summarise, supportedLanguages, tailLoopDetail, tailLoopScore, truncationScore };
package/dist/index.js CHANGED
@@ -143,6 +143,6 @@ var presets = {
143
143
  }
144
144
  };
145
145
 
146
- export { calibrate, findGap, percentile, presets, summarise };
146
+ export { calibrate, presets, summarise };
147
147
  //# sourceMappingURL=index.js.map
148
148
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-output-guard",
3
- "version": "0.5.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"