llm-output-guard 1.11.3 → 1.12.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 +2 -2
- package/dist/anthropic.cjs +82 -3
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.js +1 -1
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WKG4QRTI.js → chunk-IXGAYS3A.js} +85 -6
- package/dist/chunk-IXGAYS3A.js.map +1 -0
- package/dist/google.cjs +82 -3
- package/dist/google.cjs.map +1 -1
- package/dist/google.js +1 -1
- package/dist/openai.cjs +109 -4
- package/dist/openai.cjs.map +1 -1
- package/dist/openai.js +28 -2
- package/dist/openai.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-WKG4QRTI.js.map +0 -1
package/README.md
CHANGED
|
@@ -276,7 +276,7 @@ because only one of those is evidence.
|
|
|
276
276
|
|
|
277
277
|
- **Zero runtime dependencies**, enforced in CI. Node ≥ 18; works on edge, browser, Deno, Bun.
|
|
278
278
|
- **The size claim is a budget, not a memory.** `npm run size` bundles each entry, minifies and gzips it, and fails over budget — enforced in CI beside the zero-dependency check, because a number in prose is the cheapest thing in a repo to go stale.
|
|
279
|
-
- **Types resolve on old and new TypeScript alike
|
|
279
|
+
- **Types resolve on old and new TypeScript alike**, and it is checked rather than assumed. `npm run check:resolution` packs the tarball and typechecks every entry point under `node`, `node16`, `nodenext` and `bundler` on TypeScript 5 and 7 — enforced in CI. `moduleResolution: "node"` ignores the `exports` map, which is why `typesVersions` is there too.
|
|
280
280
|
- **Pure and synchronous.** No network, no clock, no randomness — safe on a hot path, trivial to test.
|
|
281
281
|
- **Scores, not booleans.** Detectors report 0–1 and leave the threshold decision to you.
|
|
282
282
|
- **Abstains rather than guesses.** Samples too short to judge score 0.
|
|
@@ -349,7 +349,7 @@ re-released as 0.5.0. The rule it broke is the one in the table above.
|
|
|
349
349
|
- `PROMPT_ECHO` cannot tell a degenerate echo from a rewrite or translation — the difference is in what you asked for, not in the text.
|
|
350
350
|
- `SCRIPT_MISMATCH` and `PROMPT_ECHO` do not run mid-stream by default: both measure a property of the whole response, and a mid-stream check reads a trailing window. `earlyDocumentChecks: true` opts in, with a measured false-positive risk — see **[docs/streaming.md](docs/streaming.md)**.
|
|
351
351
|
- Tool *arguments* are measured only with `checkToolArguments: true`, non-streaming responses only.
|
|
352
|
-
- `
|
|
352
|
+
- `anthropic`'s `messages.stream()` helper is not wrapped; `create({ stream: true })` is. `openai`'s `responses.stream()` **is** — including `finalResponse()` and `done()` — but an `on()`-only consumer gets the report without an exception, because a callback has no error channel.
|
|
353
353
|
- Truncation from a missing full stop is weak evidence, scored 0.55 and deliberately left below the defaults. Lower `maxTruncation` to ~0.5 to catch it, and expect false positives.
|
|
354
354
|
- A JSON array of repeated identical records reads as a loop and fails from three records up. Set `redundancyScope: 'jsonValues'`.
|
|
355
355
|
- `AGENT_LOOP` needs an *exact* cycle. An agent circling without repeating — `build`, read a file, `build`, read another, `build` — is **not** detected: the only signal that reads it scores that 0.444 against a healthy edit/test rhythm's 0.375, a margin too small to ship. Measured, rejected, and pinned by a test.
|
package/dist/anthropic.cjs
CHANGED
|
@@ -667,6 +667,77 @@ function checkPreamble(text, options) {
|
|
|
667
667
|
return checkOutput(text, { ...options, ...TOOL_CALL_PREAMBLE });
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
// src/internal/event-stream-guard.ts
|
|
671
|
+
function guardEventStream(stream, reader, options) {
|
|
672
|
+
const { onVerdict, onDegenerate = "throw", ...checkOptions } = options;
|
|
673
|
+
const guard = createStreamGuard(checkOptions);
|
|
674
|
+
let failure = null;
|
|
675
|
+
let sawToolCall = false;
|
|
676
|
+
let finishReason;
|
|
677
|
+
let ended = false;
|
|
678
|
+
const fire = (verdict) => {
|
|
679
|
+
failure = verdict;
|
|
680
|
+
onVerdict?.(verdict, { streaming: true });
|
|
681
|
+
if (onDegenerate === "ignore") return;
|
|
682
|
+
stream.abort?.();
|
|
683
|
+
stream.controller?.abort();
|
|
684
|
+
};
|
|
685
|
+
if (typeof stream.on === "function") {
|
|
686
|
+
stream.on(reader.delta.event, ((payload) => {
|
|
687
|
+
if (failure || ended) return;
|
|
688
|
+
const verdict = guard.push(reader.delta.read(payload));
|
|
689
|
+
if (verdict && !verdict.ok) fire(verdict);
|
|
690
|
+
}));
|
|
691
|
+
for (const event of reader.toolCall.events) {
|
|
692
|
+
stream.on(event, ((payload) => {
|
|
693
|
+
if (reader.toolCall.is(payload)) sawToolCall = true;
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
for (const event of reader.terminal.events) {
|
|
697
|
+
stream.on(event, ((payload) => {
|
|
698
|
+
if (ended || failure) return;
|
|
699
|
+
ended = true;
|
|
700
|
+
finishReason = reader.terminal.finishReason(payload) ?? finishReason;
|
|
701
|
+
const verdict = sawToolCall ? checkPreamble(guard.text, checkOptions) : guard.end(finishReason);
|
|
702
|
+
if (verdict) onVerdict?.(verdict, { streaming: true });
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const ours = () => failure !== null;
|
|
707
|
+
async function* guarded() {
|
|
708
|
+
try {
|
|
709
|
+
for await (const event of stream) yield event;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (!ours()) throw error;
|
|
712
|
+
if (onDegenerate === "throw") throw new DegenerateOutputError(failure);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (ours() && onDegenerate === "throw") throw new DegenerateOutputError(failure);
|
|
716
|
+
}
|
|
717
|
+
const wrapTerminal = (fn) => async function(...args) {
|
|
718
|
+
try {
|
|
719
|
+
const result = await fn.apply(stream, args);
|
|
720
|
+
if (ours() && onDegenerate === "throw") throw new DegenerateOutputError(failure);
|
|
721
|
+
return result;
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (error instanceof DegenerateOutputError) throw error;
|
|
724
|
+
if (ours() && onDegenerate === "throw") throw new DegenerateOutputError(failure);
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return new Proxy(stream, {
|
|
729
|
+
get(target, prop) {
|
|
730
|
+
if (prop === Symbol.asyncIterator) return guarded;
|
|
731
|
+
const value = Reflect.get(target, prop, target);
|
|
732
|
+
if (typeof value !== "function") return value;
|
|
733
|
+
if (prop === "finalResponse" || prop === "done") {
|
|
734
|
+
return wrapTerminal(value);
|
|
735
|
+
}
|
|
736
|
+
return value.bind(target);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
670
741
|
// src/internal/tool-arguments.ts
|
|
671
742
|
var ARGUMENT_SCOPE = {
|
|
672
743
|
minLength: 0,
|
|
@@ -800,10 +871,18 @@ function guardClient(client, paths, options = {}) {
|
|
|
800
871
|
}
|
|
801
872
|
});
|
|
802
873
|
};
|
|
803
|
-
const wrapCreate = (create,
|
|
874
|
+
const wrapCreate = (create, guardedPath) => function(...args) {
|
|
875
|
+
const { surface } = guardedPath;
|
|
804
876
|
const prepared = surface.abortable?.(args);
|
|
805
877
|
const callArgs = prepared?.args ?? args;
|
|
806
878
|
const result = create.apply(this, callArgs);
|
|
879
|
+
if (guardedPath.eventStream && result && typeof result === "object" && typeof result[Symbol.asyncIterator] === "function") {
|
|
880
|
+
return guardEventStream(result, guardedPath.eventStream, {
|
|
881
|
+
...optionsFor(surface, callArgs[0]),
|
|
882
|
+
onVerdict,
|
|
883
|
+
onDegenerate
|
|
884
|
+
});
|
|
885
|
+
}
|
|
807
886
|
if (!result || typeof result.then !== "function") return result;
|
|
808
887
|
let checked;
|
|
809
888
|
const guarded = () => checked ??= Promise.resolve(result).then(
|
|
@@ -832,9 +911,9 @@ function guardClient(client, paths, options = {}) {
|
|
|
832
911
|
if (matching.length === 0) return typeof value === "function" ? value.bind(obj) : value;
|
|
833
912
|
const terminal = matching.find((g) => g.path.length === 1);
|
|
834
913
|
if (terminal && typeof value === "function") {
|
|
835
|
-
return wrapCreate(value.bind(obj), terminal
|
|
914
|
+
return wrapCreate(value.bind(obj), terminal);
|
|
836
915
|
}
|
|
837
|
-
const deeper = matching.filter((g) => g.path.length > 1).map((g) => ({ path: g.path.slice(1)
|
|
916
|
+
const deeper = matching.filter((g) => g.path.length > 1).map((g) => ({ ...g, path: g.path.slice(1) }));
|
|
838
917
|
if (deeper.length === 0 || !value || typeof value !== "object") {
|
|
839
918
|
return typeof value === "function" ? value.bind(obj) : value;
|
|
840
919
|
}
|