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 +68 -13
- package/dist/index.cjs +0 -2
- package/dist/index.d.cts +1 -14
- package/dist/index.d.ts +1 -14
- package/dist/index.js +1 -1
- 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
|
-
|
|
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,
|
|
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,
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-output-guard",
|
|
3
|
-
"version": "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": "
|
|
41
|
+
"llm-output-guard": "dist/bin.js"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"dist"
|