llm-output-guard 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/ai-sdk.cjs +419 -0
- package/dist/ai-sdk.cjs.map +1 -0
- package/dist/ai-sdk.d.cts +91 -0
- package/dist/ai-sdk.d.ts +91 -0
- package/dist/ai-sdk.js +73 -0
- package/dist/ai-sdk.js.map +1 -0
- package/dist/chunk-KJNWKUM7.js +369 -0
- package/dist/chunk-KJNWKUM7.js.map +1 -0
- package/dist/index.d.cts +3 -149
- package/dist/index.d.ts +3 -149
- package/dist/index.js +2 -366
- package/dist/index.js.map +1 -1
- package/dist/stream-4a1hXc1m.d.cts +150 -0
- package/dist/stream-4a1hXc1m.d.ts +150 -0
- package/package.json +17 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
type ReasonCode = 'EMPTY' | 'TOO_SHORT' | 'REPETITION' | 'TAIL_LOOP' | 'LOW_ENTROPY' | 'TRUNCATED' | 'INVALID_JSON' | 'LANG_MISMATCH';
|
|
2
|
+
interface Reason {
|
|
3
|
+
code: ReasonCode;
|
|
4
|
+
/** 0..1. Higher means more suspicious. */
|
|
5
|
+
score: number;
|
|
6
|
+
/** The threshold this score crossed. */
|
|
7
|
+
threshold: number;
|
|
8
|
+
/** Human-readable explanation, safe to log. */
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
interface Verdict {
|
|
12
|
+
/** True when nothing crossed its threshold. */
|
|
13
|
+
ok: boolean;
|
|
14
|
+
/** Every failing signal, not just the first -- more useful when debugging. */
|
|
15
|
+
reasons: Reason[];
|
|
16
|
+
/** Every score computed, including passing ones. Feed these to your metrics. */
|
|
17
|
+
scores: Partial<Record<ReasonCode, number>>;
|
|
18
|
+
/** Parsed JSON payload when `json` was enabled and parsing succeeded. */
|
|
19
|
+
json?: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface CheckOptions {
|
|
22
|
+
/** Minimum acceptable length in characters. Set 0 to disable. Default 1. */
|
|
23
|
+
minLength?: number;
|
|
24
|
+
/** Duplicate-n-gram threshold. Set null to disable. Default 0.35. */
|
|
25
|
+
maxRepetition?: number | null;
|
|
26
|
+
/** Tail-loop threshold. Set null to disable. Default 0.5. */
|
|
27
|
+
maxTailLoop?: number | null;
|
|
28
|
+
/** Compressibility threshold. Set null to disable. Default 0.75. */
|
|
29
|
+
maxCompressibility?: number | null;
|
|
30
|
+
/** Truncation threshold. Set null to disable. Default null. */
|
|
31
|
+
maxTruncation?: number | null;
|
|
32
|
+
/** Provider stop reason, used by the truncation detector when present. */
|
|
33
|
+
finishReason?: string;
|
|
34
|
+
/** Require a parseable JSON payload. Default false. */
|
|
35
|
+
expectJson?: boolean;
|
|
36
|
+
/** Allow the JSON payload to be wrapped in a fence. Default true. */
|
|
37
|
+
allowJsonFence?: boolean;
|
|
38
|
+
/** Top-level keys the JSON payload must contain. */
|
|
39
|
+
requiredKeys?: string[];
|
|
40
|
+
/** Expected language code ('id' | 'en' | 'es'). Off by default. */
|
|
41
|
+
expectLang?: string | null;
|
|
42
|
+
/** Language-mismatch threshold. Default 0.6. */
|
|
43
|
+
maxLangMismatch?: number;
|
|
44
|
+
/** N-gram size for the repetition detector. Default 3. */
|
|
45
|
+
ngram?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StreamGuardOptions extends CheckOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Characters of *new* text between checks. Default 400.
|
|
51
|
+
*
|
|
52
|
+
* Checking on every chunk would re-scan the buffer per token and turn a
|
|
53
|
+
* linear stream into quadratic work. Batching costs a little detection
|
|
54
|
+
* latency and buys a bounded cost per stream.
|
|
55
|
+
*/
|
|
56
|
+
checkEvery?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Characters that must arrive before any judgement. Default 240.
|
|
59
|
+
*
|
|
60
|
+
* A loop is not visible in the first sentence, and neither is its absence.
|
|
61
|
+
* Below this the guard abstains rather than guessing -- the same rule the
|
|
62
|
+
* detectors already follow for short samples.
|
|
63
|
+
*/
|
|
64
|
+
warmup?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Trailing characters each mid-stream check looks at. Default 2000.
|
|
67
|
+
*
|
|
68
|
+
* Two reasons, and the second matters more. Cost: without a window every
|
|
69
|
+
* check re-scans the whole buffer, so a stream costs quadratic work in its
|
|
70
|
+
* own length. Sensitivity: a model that produced four healthy paragraphs
|
|
71
|
+
* and then began looping is diluted to nothing when measured across all
|
|
72
|
+
* five, which is the same reasoning that makes `tailLoopScore` a separate
|
|
73
|
+
* detector from `repetitionScore`. Recent text is the text in question.
|
|
74
|
+
*/
|
|
75
|
+
window?: number;
|
|
76
|
+
}
|
|
77
|
+
interface StreamGuard {
|
|
78
|
+
/**
|
|
79
|
+
* Feed the next chunk.
|
|
80
|
+
*
|
|
81
|
+
* Returns a verdict only on the chunks where a check actually ran, and
|
|
82
|
+
* `null` on the rest -- so `null` means "not judged yet", never "healthy".
|
|
83
|
+
* Read `.ok` on what you get back.
|
|
84
|
+
*/
|
|
85
|
+
push(chunk: string): Verdict | null;
|
|
86
|
+
/**
|
|
87
|
+
* Full check on the complete text, including the detectors deferred above.
|
|
88
|
+
* Pass the provider's stop reason if you have it; truncation keys off it.
|
|
89
|
+
*/
|
|
90
|
+
end(finishReason?: string): Verdict;
|
|
91
|
+
/** Everything pushed so far. */
|
|
92
|
+
readonly text: string;
|
|
93
|
+
/** How many mid-stream checks have run. Useful when tuning `checkEvery`. */
|
|
94
|
+
readonly checks: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Watches a response as it arrives and reports degeneration before it finishes.
|
|
98
|
+
*
|
|
99
|
+
* The reason to bother: a model that has started looping will keep looping
|
|
100
|
+
* until it hits `max_tokens`, and you pay for every one of those tokens plus
|
|
101
|
+
* the latency of waiting for them. Catching it at character 300 of a 4000
|
|
102
|
+
* character run and aborting turns a slow bad answer into a fast one.
|
|
103
|
+
*
|
|
104
|
+
* This never aborts anything itself -- it holds no controller and knows
|
|
105
|
+
* nothing about your provider. It tells you; you decide.
|
|
106
|
+
*/
|
|
107
|
+
declare function createStreamGuard(options?: StreamGuardOptions): StreamGuard;
|
|
108
|
+
interface GuardStreamOptions extends StreamGuardOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Called the first time a mid-stream check fails. Abort your request here.
|
|
111
|
+
*
|
|
112
|
+
* The guard deliberately does not own the AbortController: the thing that
|
|
113
|
+
* knows how to cancel a generation is the code that started it, and a
|
|
114
|
+
* detection library that reaches into your transport is a library you
|
|
115
|
+
* cannot use with the next transport.
|
|
116
|
+
*/
|
|
117
|
+
onDegenerate?: (verdict: Verdict) => void;
|
|
118
|
+
/**
|
|
119
|
+
* Called once with the final verdict when the source ends normally. Skipped
|
|
120
|
+
* when the stream was cut short, because a verdict on a deliberately
|
|
121
|
+
* abandoned response would describe your own abort, not the model.
|
|
122
|
+
*/
|
|
123
|
+
onEnd?: (verdict: Verdict) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Stop yielding once degeneration is detected. Default true.
|
|
126
|
+
*
|
|
127
|
+
* Set false to keep passing chunks through while still being told -- useful
|
|
128
|
+
* for a logging-only rollout, where you want the signal without changing
|
|
129
|
+
* what the user sees.
|
|
130
|
+
*/
|
|
131
|
+
stopOnDegenerate?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Wraps a chunk stream and cuts it off when the model starts looping.
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const controller = new AbortController();
|
|
138
|
+
* const guarded = guardStream(model.textStream, {
|
|
139
|
+
* ...presets.chat,
|
|
140
|
+
* onDegenerate: () => controller.abort(),
|
|
141
|
+
* });
|
|
142
|
+
* for await (const chunk of guarded) process.stdout.write(chunk);
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* Yields the source's chunks unchanged until then, so it drops into an
|
|
146
|
+
* existing loop without touching what you do with the text.
|
|
147
|
+
*/
|
|
148
|
+
declare function guardStream(source: AsyncIterable<string>, options?: GuardStreamOptions): AsyncGenerator<string, void, undefined>;
|
|
149
|
+
|
|
150
|
+
export { type CheckOptions as C, type GuardStreamOptions as G, type Reason as R, type StreamGuardOptions as S, type Verdict as V, type ReasonCode as a, type StreamGuard as b, createStreamGuard as c, guardStream as g };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-output-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"import": "./dist/index.js",
|
|
26
26
|
"require": "./dist/index.cjs"
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"./ai-sdk": {
|
|
29
|
+
"types": "./dist/ai-sdk.d.ts",
|
|
30
|
+
"import": "./dist/ai-sdk.js",
|
|
31
|
+
"require": "./dist/ai-sdk.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
28
34
|
},
|
|
29
35
|
"files": [
|
|
30
36
|
"dist"
|
|
@@ -41,9 +47,18 @@
|
|
|
41
47
|
"seed": "node scripts/seed-fixtures.mjs",
|
|
42
48
|
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
43
49
|
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"ai": ">=4"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"ai": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
44
58
|
"devDependencies": {
|
|
45
59
|
"@changesets/cli": "^2.27.0",
|
|
46
60
|
"@types/node": "^20.19.43",
|
|
61
|
+
"ai": "^7.0.62",
|
|
47
62
|
"tsup": "^8.0.0",
|
|
48
63
|
"tsx": "^4.7.0",
|
|
49
64
|
"typescript": "^5.4.0",
|