llm-entropy-filter 1.0.0 → 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on semantic versioning principles.
6
+
7
+ ---
8
+
9
+ ## [1.1.0] - 2026-01-28
10
+
11
+ ### 🚀 Added
12
+
13
+ - Introduced formal **ruleset architecture** (`default`, `strict`, `support`, `public-api`).
14
+ - Added `rulesets/` directory for configurable entropy presets.
15
+ - Added integration examples:
16
+ - Express middleware
17
+ - Fastify plugin
18
+ - Vercel AI SDK pre-gate wrapper
19
+ - Added reproducible benchmark scripts for spam dataset evaluation.
20
+ - Added economic & performance impact documentation.
21
+ - Added stability & hallucination mitigation section in README.
22
+ - Added production-readiness checklist.
23
+
24
+ ### 🧪 Bench & Metrics
25
+
26
+ - Included reproducible SMS spam dataset benchmarking.
27
+ - Added support for generating precision / recall style reports.
28
+ - Added structured telemetry output for integration logging.
29
+
30
+ ### 🛠 Internal
31
+
32
+ - No changes to core `gate()` logic.
33
+ - No breaking changes to public API.
34
+ - Existing behavior remains the default under `ruleset: "default"`.
35
+
36
+ ### ⚠️ Breaking Changes
37
+
38
+ None.
39
+
40
+ This release focuses on infrastructure packaging, documentation clarity, and integration readiness without altering the deterministic entropy engine.
41
+
42
+ ---
43
+
44
+ ## [1.0.1] - 2026-01-27
45
+
46
+ ### Added
47
+
48
+ - Initial demo server (`/analyze`, `/triad`)
49
+ - Deterministic entropy scoring
50
+ - ALLOW / WARN / BLOCK verdict structure
51
+ - Performance benchmark documentation
package/README.md CHANGED
@@ -1,178 +1,352 @@
1
- README.md reescrito (v1.0.0 listo para publicar)
1
+ # llm-entropy-filter
2
2
 
3
- Basado en tu README actual
3
+ [![npm version](https://img.shields.io/npm/v/llm-entropy-filter.svg)](https://www.npmjs.com/package/llm-entropy-filter)
4
+ [![license](https://img.shields.io/npm/l/llm-entropy-filter.svg)](LICENSE)
4
5
 
5
- README (5)
6
+ Minimal, fast **entropy + intent gate** for LLM inputs.
6
7
 
7
- , aquí va una versión limpia, con tus métricas reales, endpoints, bench y dataset.
8
+ `llm-entropy-filter` is a deterministic, local middleware layer that filters high-entropy / low-signal inputs before they reach expensive LLM inference.
8
9
 
9
- Copia y pega esto como README.md:
10
+ It transforms your LLM from a generic processor into a **premium signal resource**.
10
11
 
11
- # llm-entropy-filter
12
+ ---
12
13
 
13
- Minimal, fast **entropy + intent gate** for LLM inputs.
14
+ # 🚀 Why this exists
15
+
16
+ LLMs are powerful but:
14
17
 
15
- This package runs a **local, deterministic heuristic gate** to detect high-entropy / low-signal inputs (spam, coercion, vague conspiracies, pseudo-science, truth relativism, broken causality) and returns an **ALLOW / WARN / BLOCK** verdict.
18
+ - Expensive per token
19
+ - Latency-heavy (seconds vs milliseconds)
20
+ - Vulnerable to spam, coercion, broken causality, and noise
16
21
 
17
- Use it **before** calling an LLM to reduce hallucinations, cost, and risk.
22
+ Most systems solve this with *more processing*.
23
+
24
+ `llm-entropy-filter` solves it with **criterion before processing**.
18
25
 
19
26
  ---
20
27
 
21
- ## What you get
28
+ # 🧠 Architecture
22
29
 
23
- ### Core (library)
24
- - `gate(text)` → `{ action, entropy_score, flags, intention, confidence, rationale }`
25
- - `gateLLM(text)` → alias of `gate(text)` (kept for compatibility)
26
- - `runEntropyFilter(text)` → underlying entropy + intention analysis utilities
30
+ The system operates in two deterministic local layers:
27
31
 
28
- ### Demo (server)
29
- - `POST /analyze` → runs local `gate(text)` + returns `meta.ts` + `meta.version`
30
- - `POST /triad` → optional OpenAI analysis (only if `OPENAI_API_KEY` is set)
32
+ ## Layer 1 — Hard Triggers (Deterministic Signals)
31
33
 
32
- ---
34
+ Immediate structural patterns:
33
35
 
34
- ## Install
36
+ - Shouting (ALL CAPS)
37
+ - Urgency markers
38
+ - Money / % signals
39
+ - Spam phrasing
40
+ - Conspiracy vagueness
41
+ - Broken causality structures
42
+ - Repetition anomalies
35
43
 
36
- ```bash
37
- npm i llm-entropy-filter
44
+ These are language-light, low-cost, and capture obvious noise.
38
45
 
39
- Quickstart (library)
40
- import { gate } from "llm-entropy-filter";
46
+ ## Layer 2 — Thematic Scoring (Signal Accumulation)
47
+
48
+ If no hard block occurs, the input is evaluated by topic clusters:
49
+
50
+ - Marketing spam
51
+ - Conspiracy framing
52
+ - Coercive tone
53
+ - Pseudo-scientific structure
54
+ - Relativism / truth dilution
55
+ - Semantic incoherence
41
56
 
42
- const r = gate("¡¡COMPRA YA!! Oferta limitada 90% OFF $$$");
43
- console.log(r);
57
+ Each topic contributes to an `entropy_score`.
44
58
 
59
+ Final verdict:
45
60
 
46
- Example output:
61
+ ALLOW | WARN | BLOCK
47
62
 
63
+
64
+ Returned with:
65
+
66
+ ```json
48
67
  {
49
68
  "action": "BLOCK",
50
69
  "entropy_score": 0.7,
51
- "flags": ["urgency","spam_sales","money_signal","shouting"],
52
- "intention": "marketing_spam",
70
+ "flags": [...],
71
+ "intention": "...",
53
72
  "confidence": 0.85,
54
- "rationale": "Detecté señales de venta agresiva/urgencia/dinero."
73
+ "rationale": "..."
55
74
  }
56
75
 
57
- Demo server (Express)
58
76
 
59
- Start:
77
+ No network calls. No embeddings. No remote inference.
60
78
 
61
- npm run serve
79
+ ## Rulesets
62
80
 
81
+ This project ships with preset rule packs:
63
82
 
64
- Health:
83
+ - `default` (balanced)
84
+ - `strict` (aggressive blocking)
85
+ - `support` (fewer false positives)
86
+ - `public-api` (hardened for open endpoints)
65
87
 
66
- curl -s http://127.0.0.1:3000/health
88
+ Rulesets live in `rulesets/` and define:
89
+ - thresholds (WARN/BLOCK)
90
+ - hard triggers
91
+ - topic scoring weights
67
92
 
93
+ ## Integrations (copy/paste)
68
94
 
69
- Local gate:
95
+ This repo includes ready-to-use adapters under `integrations/`:
70
96
 
71
- curl -s -X POST http://127.0.0.1:3000/analyze \
72
- -H "Content-Type: application/json" \
73
- -d '{"text":"Congratulations! You won a FREE iPhone. Click here to claim now!"}' | jq .
97
+ - `integrations/express.mjs` Express middleware gate (ALLOW/WARN/BLOCK)
98
+ - `integrations/fastify.mjs` — Fastify plugin gate
99
+ - `integrations/vercel-ai-sdk.mjs` pre-gate wrapper for `streamText()` / `generateText()`
100
+ - `integrations/langchain.mjs` — pre-gate + optional Runnable wrapper for LangChain
101
+
102
+ These integrations do **not** change core behavior. They only call `gate()` and route based on the verdict.
103
+
104
+
105
+ 📦 Installation
106
+ npm i llm-entropy-filter
107
+
108
+ ⚡ Quickstart
109
+ import { gate } from "llm-entropy-filter";
110
+
111
+ const result = gate("¡¡COMPRA YA!! Oferta limitada 90% OFF $$$");
74
112
 
113
+ console.log(result);
75
114
 
76
- You will also see:
115
+ 🖥 Demo Server
77
116
 
78
- "meta": { "ts": 1769546511060, "version": "1.0.0" }
117
+ The demo server wraps the local gate.
118
+
119
+ Start
120
+ npm run serve
121
+
122
+
123
+ (Ensure your package.json includes: "serve": "node demo/server.mjs")
124
+
125
+ Health
126
+ curl http://127.0.0.1:3000/health
127
+
128
+ Local gate
129
+ curl -X POST http://127.0.0.1:3000/analyze \
130
+ -H "Content-Type: application/json" \
131
+ -d '{"text":"FREE iPhone!!! Click now!!!"}'
79
132
 
80
- Optional: OpenAI triad demo
133
+ Optional LLM Triad (Demo Only)
81
134
  export OPENAI_API_KEY="YOUR_KEY"
82
135
  export OPENAI_MODEL="gpt-4.1-mini"
83
136
 
84
- curl -s -X POST http://127.0.0.1:3000/triad \
137
+ curl -X POST http://127.0.0.1:3000/triad \
85
138
  -H "Content-Type: application/json" \
86
- -d '{"text":"Vivimos en una simulación y todos lo esconden."}' | jq .
139
+ -d '{"text":"Vivimos en una simulación y todos lo esconden."}'
87
140
 
88
141
 
89
- /triad is a demo layer. The product is the local gate().
142
+ If OPENAI_API_KEY is not set, /triad returns 503.
90
143
 
91
- Benchmarks (measured)
92
- HTTP gate /analyze (local, deterministic)
144
+ Performance (Measured)
93
145
 
94
- Command:
146
+ Environment:
147
+ GitHub Codespaces (Linux container), Node 24.x
95
148
 
96
- npx autocannon -m POST -c 30 -d 10 --renderStatusCodes \
97
- http://127.0.0.1:3000/analyze \
98
- -H "Content-Type: application/json" \
99
- -b '{"text":"Congratulations. You won a FREE iPhone. Click here to claim now."}'
149
+ Local Gate /analyze
100
150
 
151
+ Avg latency: 5.28 ms
101
152
 
102
- Observed (typical run):
153
+ p50: 4 ms
103
154
 
104
- ~5.2k req/s
155
+ p99: 16 ms
105
156
 
106
- ~5.1 ms avg latency (p50 ~4 ms)
157
+ Throughput: ~5,118 req/sec
107
158
 
108
- LLM demo /triad (OpenAI)
159
+ 0 errors
109
160
 
110
- Command:
161
+ LLM Roundtrip — /triad
111
162
 
112
- npx autocannon -m POST -c 2 -d 30 --renderStatusCodes \
113
- http://127.0.0.1:3000/triad \
114
- -H "Content-Type: application/json" \
115
- -b '{"text":"Texto real de prueba (1-3 párrafos) ..."}'
163
+ Avg latency: 5,321 ms
164
+
165
+ p50: 5,030 ms
166
+
167
+ Throughput: ~0.34 req/sec
168
+
169
+ 2 timeouts in 30s test
170
+
171
+ Note: These represent different pipeline layers (local deterministic vs external LLM API). The architectural gain comes from avoiding unnecessary LLM calls.
172
+
173
+ 📉 Economic Impact (Projection)
174
+ Assumptions
175
+
176
+ 300 tokens per request (150 in / 150 out)
177
+
178
+ gpt-4o-mini pricing baseline
179
+
180
+ 30% traffic filtered locally
181
+
182
+ Effect
183
+
184
+ If 1M requests are received:
185
+
186
+ 300,000 requests never hit the LLM
187
+
188
+ 30% token cost avoided
189
+
190
+ 30% rate-limit headroom gained
116
191
 
192
+ 30% reduction in latency pressure
117
193
 
118
- Observed (typical run):
194
+ Savings scale linearly with volume and exponentially with higher-cost models.
119
195
 
120
- ~1.7 req/s
196
+ Formula:
121
197
 
122
- ~1.17 s avg latency
198
+ Savings =
199
+ (Filtered_Requests / Total_Requests)
200
+ × Avg_Tokens_Per_Request
201
+ × Token_Price
123
202
 
124
- Dataset mini + bench script (no HTTP)
203
+ 🛡 Stability & Hallucination Mitigation
125
204
 
126
- A tiny CSV lives at:
205
+ High-entropy inputs increase:
206
+
207
+ Off-topic generation
208
+
209
+ Reasoning drift
210
+
211
+ Prompt injection exposure
212
+
213
+ Token expansion loops
214
+
215
+ By constraining input entropy before inference,
216
+ the downstream model operates in a narrower semantic bandwidth.
217
+
218
+ This improves stability without imposing moral or ideological constraints.
219
+
220
+ 🧪 Dataset Benchmark
221
+
222
+ Included:
127
223
 
128
224
  bench/sms_spam.csv
129
225
 
130
- Run the bench:
226
+
227
+ Run:
131
228
 
132
229
  node bench/sms_spam_bench.mjs bench/sms_spam.csv
133
- cat bench/reports/sms_spam_report.md
134
230
 
135
231
 
136
- Typical report:
232
+ Generates:
137
233
 
138
- Throughput: ~9–10k samples/sec
234
+ Precision / recall
139
235
 
140
- Actions: ALLOW / WARN / BLOCK distribution
236
+ Confusion matrix
141
237
 
142
- Confusion table (ground truth spam/ham → action)
238
+ Top flags
143
239
 
144
- Top flags + intentions
240
+ JSON + Markdown reports
145
241
 
146
- JSON + Markdown reports written to bench/reports/
242
+ 🎯 Design Goals
147
243
 
148
- Design goals
244
+ Deterministic
149
245
 
150
- Fast: pure heuristics, no network calls
246
+ Transparent
151
247
 
152
- Portable: works in any Node environment
248
+ Fast
153
249
 
154
- Composable: middleware/wrapper before calling an LLM
250
+ Composable
155
251
 
156
- Transparent: flags explain why an input is risky
252
+ Observable
157
253
 
158
- Observable: /analyze returns meta.ts and meta.version
254
+ Economically rational
159
255
 
160
- Roadmap
256
+ 🗺 Roadmap
161
257
 
162
- Expand multilingual spam patterns
258
+ Multilingual rulesets
163
259
 
164
- Optional suggested_rewrite to lower entropy
260
+ Configurable rule packs
165
261
 
166
- Example integrations: Next.js / Vercel, Express, Cloudflare Workers
262
+ Express / Fastify middleware exports
167
263
 
168
- Extended dataset benches + cost-savings estimates
264
+ Suggested rewrite mode
169
265
 
170
- License
266
+ Production case studies
171
267
 
172
- Apache-2.0
268
+ 👤 Attribution
173
269
 
174
- Copyright (c) 2026 Ernesto Rosati
270
+ Developed and maintained by Ernesto Rosati.
175
271
 
272
+ If this library creates value for your organization,
273
+ consider collaboration or sponsorship.
176
274
 
177
- ---
275
+ 📜 License
276
+
277
+ Apache-2.0
278
+ Copyright (c) 2026 Ernesto Rosati
178
279
 
280
+ Use cases & integrations
281
+ ## ✅ Where this fits in real systems
282
+
283
+ `llm-entropy-filter` is designed to sit **before** expensive inference. Common placements:
284
+
285
+ ### 1) Public chat apps (startups)
286
+ Use as a first-line gate to block obvious spam/coercion before the LLM:
287
+ - faster UX for rejected traffic (<10ms)
288
+ - reduced token spend
289
+ - reduced prompt-abuse surface
290
+
291
+ ### 2) Rate-limit protection
292
+ Acts as a semantic pre-filter that reduces:
293
+ - quota exhaustion
294
+ - burst abuse
295
+ - coordinated spam floods
296
+
297
+ It creates headroom by rejecting high-entropy traffic locally.
298
+
299
+ ### 3) RAG pipelines (pre-retrieval gate)
300
+ Before retrieval:
301
+ - block low-signal queries that would waste retrieval + reranking
302
+ - normalize/clean input to improve recall precision
303
+ - prevent adversarial queries from polluting retrieval traces
304
+
305
+ ### 4) Multi-agent systems
306
+ In agent loops:
307
+ - prevent “reasoning drift” from noisy inputs
308
+ - keep agents from spending cycles on incoherent or adversarial prompts
309
+ - add structured telemetry for agent decisions (`flags`, `intention`, `entropy_score`)
310
+
311
+ ### 5) Tooling & SDK pre-gates (LangChain / Vercel AI SDK)
312
+ Drop in as a deterministic guard:
313
+ - before `callLLM()`
314
+ - before `streamText()`
315
+ - before tool selection / agent routing
316
+
317
+ The output can be used as:
318
+ - a routing signal (ALLOW/WARN/BLOCK)
319
+ - a logging payload for audits and dashboards
320
+
321
+ “What’s missing to be production-ready”
322
+ ## Production readiness checklist
323
+
324
+ The core gate is stable, but “production-ready” requires:
325
+
326
+ ### 1) Configurable rulesets
327
+ - `default` (balanced)
328
+ - `strict` (aggressive spam/coercion blocking)
329
+ - `support` (customer support / fewer false positives)
330
+ - `public-api` (open endpoints / hardened)
331
+
332
+ ### 2) Reproducible metrics (precision / recall)
333
+ Bench scripts should emit:
334
+ - precision/recall/F1
335
+ - confusion matrix
336
+ - false-positive rate on normal conversations
337
+ - top flags per dataset
338
+
339
+ ### 3) Copy-paste integrations
340
+ Provide ready-to-use adapters:
341
+ - Express middleware
342
+ - Fastify plugin
343
+ - Next.js / Vercel edge wrapper
344
+ - “pre-gate” helpers for LangChain-style pipelines
345
+
346
+ ### 4) One real production example
347
+ A minimal public case study:
348
+ - traffic volume
349
+ - % blocked
350
+ - cost avoided
351
+ - rate-limit incidents reduced
352
+ - latency improvement for blocked traffic
@@ -0,0 +1,117 @@
1
+ // integrations/express.mjs
2
+ import { gate } from "llm-entropy-filter";
3
+
4
+ /**
5
+ * Create an Express middleware that runs `gate()` before LLM calls.
6
+ *
7
+ * Design goals:
8
+ * - Zero behavior changes to core `gate()`
9
+ * - Drop-in for public chat endpoints
10
+ * - Deterministic: no external calls
11
+ *
12
+ * @param {object} [opts]
13
+ * @param {string} [opts.bodyField="text"] - Field name in req.body that contains user text.
14
+ * @param {string} [opts.queryField] - Optional query param fallback (e.g., ?text=...).
15
+ * @param {boolean} [opts.attachResult=true] - Attach result to req.entropyGate.
16
+ * @param {boolean} [opts.blockOn="BLOCK"] - Block when action matches this string ("BLOCK") or array of actions.
17
+ * @param {number} [opts.blockStatus=400] - HTTP status when blocked.
18
+ * @param {object|function} [opts.blockResponse] - Custom JSON response or function(req, res, result) => any
19
+ * @param {boolean} [opts.warnHeader=true] - If WARN, add response headers with gate metadata.
20
+ * @param {boolean} [opts.alwaysHeader=false] - If true, add headers for all actions.
21
+ * @param {function} [opts.onResult] - Hook: (req, result) => void
22
+ * @param {function} [opts.getText] - Hook: (req) => string (overrides bodyField/queryField)
23
+ */
24
+ export function entropyGateMiddleware(opts = {}) {
25
+ const {
26
+ bodyField = "text",
27
+ queryField,
28
+ attachResult = true,
29
+ blockOn = "BLOCK",
30
+ blockStatus = 400,
31
+ blockResponse,
32
+ warnHeader = true,
33
+ alwaysHeader = false,
34
+ onResult,
35
+ getText,
36
+ } = opts;
37
+
38
+ const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
39
+
40
+ return function entropyGate(req, res, next) {
41
+ try {
42
+ // 1) Extract text
43
+ let text = "";
44
+ if (typeof getText === "function") {
45
+ text = String(getText(req) ?? "");
46
+ } else {
47
+ const bodyVal = req?.body?.[bodyField];
48
+ const queryVal = queryField ? req?.query?.[queryField] : undefined;
49
+ text = String(bodyVal ?? queryVal ?? "");
50
+ }
51
+
52
+ // 2) Run deterministic gate
53
+ const result = gate(text);
54
+
55
+ // 3) Attach result for downstream routing/logging
56
+ if (attachResult) {
57
+ // Convention: req.entropyGate
58
+ req.entropyGate = result;
59
+ }
60
+
61
+ // Optional hook
62
+ if (typeof onResult === "function") {
63
+ onResult(req, result);
64
+ }
65
+
66
+ // 4) Telemetry headers (optional)
67
+ const shouldHeader = alwaysHeader || (warnHeader && result?.action === "WARN");
68
+ if (shouldHeader) {
69
+ // Keep headers small and stable
70
+ res.setHeader("x-entropy-action", String(result?.action ?? ""));
71
+ res.setHeader("x-entropy-score", String(result?.entropy_score ?? ""));
72
+ res.setHeader("x-entropy-intention", String(result?.intention ?? ""));
73
+ // Flags can be large; keep compact
74
+ if (Array.isArray(result?.flags)) {
75
+ res.setHeader("x-entropy-flags", result.flags.slice(0, 10).join(","));
76
+ }
77
+ }
78
+
79
+ // 5) Block if configured
80
+ if (blockSet.has(result?.action)) {
81
+ res.status(blockStatus);
82
+
83
+ if (typeof blockResponse === "function") {
84
+ return res.json(blockResponse(req, res, result));
85
+ }
86
+
87
+ if (blockResponse && typeof blockResponse === "object") {
88
+ return res.json(blockResponse);
89
+ }
90
+
91
+ // Default response: transparent + actionable
92
+ return res.json({
93
+ ok: false,
94
+ blocked: true,
95
+ gate: result,
96
+ message:
97
+ "Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
98
+ });
99
+ }
100
+
101
+ // Otherwise proceed
102
+ return next();
103
+ } catch (err) {
104
+ // Fail-open by default: do not block requests if the gate errors.
105
+ // You can change this behavior by wrapping with your own error handler.
106
+ return next(err);
107
+ }
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Small helper for routing:
113
+ * If you prefer to run gate manually inside route handlers.
114
+ */
115
+ export function runEntropyGate(text) {
116
+ return gate(String(text ?? ""));
117
+ }
@@ -0,0 +1,106 @@
1
+ // integrations/fastify.mjs
2
+ import { gate } from "llm-entropy-filter";
3
+
4
+ /**
5
+ * Fastify plugin: adds a preHandler that runs `gate()` before your route handler.
6
+ *
7
+ * Usage:
8
+ * fastify.register(entropyGatePlugin, { bodyField: "text", blockOn: "BLOCK" })
9
+ *
10
+ * Design:
11
+ * - Deterministic, local
12
+ * - Fail-open by default (if gate throws, request continues unless you override)
13
+ */
14
+ export async function entropyGatePlugin(fastify, opts = {}) {
15
+ const {
16
+ bodyField = "text",
17
+ queryField,
18
+ attachResult = true,
19
+ blockOn = "BLOCK",
20
+ blockStatus = 400,
21
+ blockResponse,
22
+ warnHeader = true,
23
+ alwaysHeader = false,
24
+ onResult,
25
+ getText,
26
+ failClosed = false, // if true: return 500 on errors instead of passing through
27
+ } = opts;
28
+
29
+ const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
30
+
31
+ fastify.decorateRequest("entropyGate", null);
32
+
33
+ fastify.addHook("preHandler", async (request, reply) => {
34
+ try {
35
+ // 1) Extract text
36
+ let text = "";
37
+ if (typeof getText === "function") {
38
+ text = String(getText(request) ?? "");
39
+ } else {
40
+ const bodyVal = request?.body?.[bodyField];
41
+ const queryVal = queryField ? request?.query?.[queryField] : undefined;
42
+ text = String(bodyVal ?? queryVal ?? "");
43
+ }
44
+
45
+ // 2) Run gate
46
+ const result = gate(text);
47
+
48
+ // 3) Attach
49
+ if (attachResult) {
50
+ request.entropyGate = result;
51
+ }
52
+
53
+ if (typeof onResult === "function") {
54
+ onResult(request, result);
55
+ }
56
+
57
+ // 4) Headers
58
+ const shouldHeader =
59
+ alwaysHeader || (warnHeader && result?.action === "WARN");
60
+ if (shouldHeader) {
61
+ reply.header("x-entropy-action", String(result?.action ?? ""));
62
+ reply.header("x-entropy-score", String(result?.entropy_score ?? ""));
63
+ reply.header("x-entropy-intention", String(result?.intention ?? ""));
64
+ if (Array.isArray(result?.flags)) {
65
+ reply.header("x-entropy-flags", result.flags.slice(0, 10).join(","));
66
+ }
67
+ }
68
+
69
+ // 5) Block
70
+ if (blockSet.has(result?.action)) {
71
+ reply.code(blockStatus);
72
+
73
+ if (typeof blockResponse === "function") {
74
+ return reply.send(blockResponse(request, reply, result));
75
+ }
76
+
77
+ if (blockResponse && typeof blockResponse === "object") {
78
+ return reply.send(blockResponse);
79
+ }
80
+
81
+ return reply.send({
82
+ ok: false,
83
+ blocked: true,
84
+ gate: result,
85
+ message:
86
+ "Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
87
+ });
88
+ }
89
+ } catch (err) {
90
+ if (failClosed) {
91
+ reply.code(500);
92
+ return reply.send({
93
+ ok: false,
94
+ error: "entropy_gate_error",
95
+ message: String(err?.message ?? err),
96
+ });
97
+ }
98
+ // fail-open: continue request
99
+ }
100
+ });
101
+ }
102
+
103
+ /** Manual helper if you prefer using it inside handlers */
104
+ export function runEntropyGate(text) {
105
+ return gate(String(text ?? ""));
106
+ }
@@ -0,0 +1,98 @@
1
+ // integrations/langchain.mjs
2
+ import { gate } from "llm-entropy-filter";
3
+
4
+ /**
5
+ * Minimal pre-gate for LangChain flows.
6
+ * Use this before calling any LLM / chain / agent.
7
+ */
8
+ export function entropyPreGate(input, opts = {}) {
9
+ const { blockOn = "BLOCK" } = opts;
10
+ const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
11
+
12
+ const text = typeof input === "string" ? input : String(input?.text ?? input ?? "");
13
+ const result = gate(text);
14
+
15
+ return {
16
+ gate: result,
17
+ inputText: text,
18
+ shouldBlock: blockSet.has(result?.action),
19
+ shouldWarn: result?.action === "WARN",
20
+ shouldAllow: result?.action === "ALLOW",
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Standard error object you can throw in API routes.
26
+ */
27
+ export function entropyBlockedError(gateResult, opts = {}) {
28
+ const { status = 400, code = "ENTROPY_BLOCKED" } = opts;
29
+ const err = new Error("Blocked by llm-entropy-filter (high-entropy / low-signal input).");
30
+ err.name = "EntropyBlockedError";
31
+ err.status = status;
32
+ err.code = code;
33
+ err.gate = gateResult;
34
+ return err;
35
+ }
36
+
37
+ /**
38
+ * LCEL wrapper: wrap a Runnable / function to enforce gate before execution.
39
+ * Works with `@langchain/core/runnables` (RunnableLambda).
40
+ *
41
+ * Usage:
42
+ * const safe = withEntropyGate(myRunnableOrFn, { pickText: (i)=> i.input })
43
+ * await safe.invoke({ input: "..." })
44
+ */
45
+ export function withEntropyGate(target, opts = {}) {
46
+ const {
47
+ blockOn = "BLOCK",
48
+ pickText, // (input) => string
49
+ onWarn, // (gateResult, input) => void
50
+ } = opts;
51
+
52
+ const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
53
+
54
+ // lazy import to avoid forcing langchain deps
55
+ let RunnableLambda;
56
+ async function getRunnableLambda() {
57
+ if (RunnableLambda) return RunnableLambda;
58
+ const mod = await import("@langchain/core/runnables");
59
+ RunnableLambda = mod.RunnableLambda;
60
+ return RunnableLambda;
61
+ }
62
+
63
+ return {
64
+ async invoke(input, config) {
65
+ const text = typeof pickText === "function"
66
+ ? String(pickText(input) ?? "")
67
+ : (typeof input === "string" ? input : String(input?.input ?? input?.text ?? ""));
68
+
69
+ const g = gate(text);
70
+
71
+ if (g?.action === "WARN" && typeof onWarn === "function") onWarn(g, input);
72
+
73
+ if (blockSet.has(g?.action)) {
74
+ throw entropyBlockedError(g, { status: 400 });
75
+ }
76
+
77
+ // If target is a Runnable with invoke()
78
+ if (target && typeof target.invoke === "function") {
79
+ return target.invoke(input, config);
80
+ }
81
+
82
+ // If target is a function
83
+ if (typeof target === "function") {
84
+ return target(input, config);
85
+ }
86
+
87
+ throw new Error("withEntropyGate: target must be a Runnable (invoke) or a function.");
88
+ },
89
+
90
+ // Optional: make it LCEL-friendly by exposing `asRunnable()`
91
+ async asRunnable() {
92
+ const RL = await getRunnableLambda();
93
+ return new RL({
94
+ func: async (input, config) => this.invoke(input, config),
95
+ });
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,44 @@
1
+ // integrations/vercel-ai-sdk.mjs
2
+ import { gate } from "llm-entropy-filter";
3
+
4
+ /**
5
+ * Pre-gate helper for Vercel AI SDK style flows.
6
+ *
7
+ * Typical usage:
8
+ * - Compute gate result
9
+ * - If BLOCK: return early
10
+ * - If WARN: optionally add metadata/logging and continue
11
+ */
12
+ export function entropyPreGate(input, opts = {}) {
13
+ const { blockOn = "BLOCK" } = opts;
14
+ const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
15
+
16
+ const result = gate(String(input ?? ""));
17
+
18
+ return {
19
+ gate: result,
20
+ shouldBlock: blockSet.has(result?.action),
21
+ shouldWarn: result?.action === "WARN",
22
+ shouldAllow: result?.action === "ALLOW",
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Helper to build a standard Response when blocked (Edge/Node compatible).
28
+ */
29
+ export function blockedResponse(gateResult, opts = {}) {
30
+ const { status = 400 } = opts;
31
+ return new Response(
32
+ JSON.stringify({
33
+ ok: false,
34
+ blocked: true,
35
+ gate: gateResult,
36
+ message:
37
+ "Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
38
+ }),
39
+ {
40
+ status,
41
+ headers: { "content-type": "application/json; charset=utf-8" },
42
+ }
43
+ );
44
+ }
package/package.json CHANGED
@@ -1,25 +1,9 @@
1
1
  {
2
2
  "name": "llm-entropy-filter",
3
- "version": "1.0.0",
4
- "description": "Fast entropy and intent gate for LLM inputs (ALLOW/WARN/BLOCK). Reduces hallucinations, cost and spam before calling an LLM.",
5
- "keywords": [
6
- "llm",
7
- "ai",
8
- "prompt-filter",
9
- "input-validation",
10
- "entropy",
11
- "spam-detection",
12
- "content-moderation",
13
- "heuristics",
14
- "openai",
15
- "guardrails"
16
- ],
3
+ "version": "1.1.0",
4
+ "description": "Deterministic entropy-based pre-gate for LLM pipelines. ALLOW / WARN / BLOCK high-entropy inputs before expensive model calls.",
17
5
  "license": "Apache-2.0",
18
6
  "type": "module",
19
- "sideEffects": false,
20
- "engines": {
21
- "node": ">=18"
22
- },
23
7
  "main": "./dist/index.cjs",
24
8
  "module": "./dist/index.js",
25
9
  "types": "./dist/index.d.ts",
@@ -28,37 +12,48 @@
28
12
  "types": "./dist/index.d.ts",
29
13
  "import": "./dist/index.js",
30
14
  "require": "./dist/index.cjs"
15
+ },
16
+ "./integrations/express": {
17
+ "import": "./integrations/express.mjs"
18
+ },
19
+ "./integrations/fastify": {
20
+ "import": "./integrations/fastify.mjs"
21
+ },
22
+ "./integrations/vercel-ai-sdk": {
23
+ "import": "./integrations/vercel-ai-sdk.mjs"
24
+ },
25
+ "./integrations/langchain": {
26
+ "import": "./integrations/langchain.mjs"
27
+ },
28
+ "./rulesets/default": {
29
+ "import": "./rulesets/default.js"
30
+ },
31
+ "./rulesets/strict": {
32
+ "import": "./rulesets/strict.js"
31
33
  }
32
34
  },
33
35
  "files": [
34
36
  "dist",
35
- "README.md"
37
+ "integrations",
38
+ "rulesets",
39
+ "LICENSE",
40
+ "README.md",
41
+ "CHANGELOG.md"
36
42
  ],
37
- "scripts": {
38
- "clean": "rm -rf dist node_modules package-lock.json",
39
- "clean:dist": "rm -rf dist",
40
- "build": "tsup",
41
- "demo": "npm run build && node demo/demo.mjs",
42
- "bench": "npm run build && node bench/benchmark.mjs",
43
- "serve": "npm run build && node demo/server.mjs",
44
- "prepublishOnly": "npm run clean:dist && npm run build"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/rosatisoft/llm-entropy-filter.git"
43
+ "sideEffects": false,
44
+ "engines": {
45
+ "node": ">=18"
49
46
  },
50
- "bugs": {
51
- "url": "https://github.com/rosatisoft/llm-entropy-filter/issues"
47
+ "scripts": {
48
+ "build": "tsup src/index.ts --format esm,cjs --dts",
49
+ "clean": "rimraf dist",
50
+ "prepublishOnly": "npm run clean && npm run build",
51
+ "bench:sms": "node bench/sms_benchmark.js",
52
+ "bench:report": "node bench/generate_report.js"
52
53
  },
53
- "homepage": "https://github.com/rosatisoft/llm-entropy-filter#readme",
54
54
  "devDependencies": {
55
- "@types/express": "^5.0.6",
56
- "@types/node": "^25.0.10",
57
- "autocannon": "^8.0.0",
58
- "minimist": "^1.2.8",
59
- "tsup": "^8.5.1",
60
- "typescript": "^5.9.3",
61
- "express": "^5.2.1",
62
- "openai": "^6.16.0"
55
+ "rimraf": "^5.0.10",
56
+ "tsup": "^8.0.1",
57
+ "typescript": "^5.4.0"
63
58
  }
64
59
  }
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "default",
3
+ "version": 1,
4
+ "description": "Balanced preset: safe defaults for general apps.",
5
+ "thresholds": { "warn": 0.45, "block": 0.65 },
6
+ "normalization": {
7
+ "lowercase": true,
8
+ "trim": true,
9
+ "collapse_whitespace": true,
10
+ "unicode_nfkc": true
11
+ },
12
+ "hard_triggers": [
13
+ {
14
+ "id": "shouting",
15
+ "type": "signal",
16
+ "weight": 0.18,
17
+ "notes": "Excess uppercase or repeated punctuation",
18
+ "patterns": ["[A-ZÁÉÍÓÚÑ]{6,}", "!!+", "\\?\\?+", "¡¡+", "…{3,}"]
19
+ },
20
+ {
21
+ "id": "urgency",
22
+ "type": "topic_hint",
23
+ "weight": 0.12,
24
+ "patterns": ["\\bnow\\b", "\\btoday\\b", "\\burgent\\b", "\\bya\\b", "\\bahora\\b", "\\bhoy\\b", "\\búltim[oa]s?\\b", "\\bsolo\\s+hoy\\b"]
25
+ },
26
+ {
27
+ "id": "money_signal",
28
+ "type": "topic_hint",
29
+ "weight": 0.12,
30
+ "patterns": ["\\$\\d+", "\\bUSD\\b", "\\bMXN\\b", "\\b%\\b", "\\b90%\\b", "\\bfree\\b", "\\bgratis\\b", "\\bpromo\\b", "\\bdiscount\\b", "\\boffer\\b", "\\boferta\\b"]
31
+ },
32
+ {
33
+ "id": "conspiracy_vague",
34
+ "type": "topic_hint",
35
+ "weight": 0.14,
36
+ "patterns": ["\\ball\\s+hide\\b", "\\bthey\\s+hide\\b", "\\beveryone\\s+knows\\b", "\\btodos\\s+lo\\s+esconden\\b", "\\bla\\s+verdad\\s+oculta\\b", "\\bnadie\\s+quiere\\s+que\\s+sepas\\b"]
37
+ }
38
+ ],
39
+ "topics": [
40
+ {
41
+ "id": "marketing_spam",
42
+ "weight": 0.35,
43
+ "signals": [
44
+ { "id": "cta", "weight": 0.18, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b", "\\borden(a|e)\\b"] },
45
+ { "id": "promo_terms", "weight": 0.15, "patterns": ["\\bfree\\b", "\\bgratis\\b", "\\bwin\\b", "\\bganaste\\b", "\\biphone\\b", "\\bpremio\\b"] },
46
+ { "id": "links", "weight": 0.12, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b"] }
47
+ ]
48
+ },
49
+ {
50
+ "id": "coercion",
51
+ "weight": 0.25,
52
+ "signals": [
53
+ { "id": "threat", "weight": 0.18, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bte\\s+voy\\s+a\\b", "\\bI\\s+will\\b"] },
54
+ { "id": "forced_tone", "weight": 0.12, "patterns": ["\\bdo\\s+it\\b", "\\bhazlo\\b", "\\bahora\\b"] }
55
+ ]
56
+ },
57
+ {
58
+ "id": "conspiracy",
59
+ "weight": 0.25,
60
+ "signals": [
61
+ { "id": "vague_all", "weight": 0.18, "patterns": ["\\btodos\\b", "\\beveryone\\b", "\\bthey\\b", "\\bel\\s+sistema\\b"] },
62
+ { "id": "hidden_truth", "weight": 0.14, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b", "\\bcover\\s*up\\b"] }
63
+ ]
64
+ },
65
+ {
66
+ "id": "broken_causality",
67
+ "weight": 0.15,
68
+ "signals": [
69
+ { "id": "contradiction_markers", "weight": 0.12, "patterns": ["\\bpero\\s+entonces\\b", "\\btherefore\\b.*\\bnot\\b", "\\bsi\\s+A\\s+entonces\\s+no\\s+A\\b"] }
70
+ ]
71
+ }
72
+ ]
73
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "public-api",
3
+ "version": 1,
4
+ "description": "Hardened preset for open/public APIs: reduces abuse and quota burn.",
5
+ "thresholds": { "warn": 0.40, "block": 0.60 },
6
+ "normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
7
+ "hard_triggers": [
8
+ { "id": "links", "type": "signal", "weight": 0.22, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b", "\\bt\\.co\\b"] },
9
+ { "id": "shouting", "type": "signal", "weight": 0.18, "patterns": ["[A-ZÁÉÍÓÚÑ]{5,}", "!!+", "\\?\\?+"] },
10
+ { "id": "money_signal", "type": "topic_hint", "weight": 0.16, "patterns": ["\\$\\d+", "\\b%\\b", "\\bfree\\b", "\\bgratis\\b"] }
11
+ ],
12
+ "topics": [
13
+ { "id": "marketing_spam", "weight": 0.40, "signals": [
14
+ { "id": "cta", "weight": 0.20, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b"] },
15
+ { "id": "promo", "weight": 0.18, "patterns": ["\\bfree\\b", "\\bgratis\\b", "\\bwin\\b", "\\bganaste\\b"] }
16
+ ]},
17
+ { "id": "coercion", "weight": 0.25, "signals": [
18
+ { "id": "threat", "weight": 0.18, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bI\\s+will\\b"] }
19
+ ]},
20
+ { "id": "conspiracy", "weight": 0.20, "signals": [
21
+ { "id": "hidden", "weight": 0.14, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b"] }
22
+ ]},
23
+ { "id": "incoherence", "weight": 0.15, "signals": [
24
+ { "id": "noise_markers", "weight": 0.10, "patterns": ["\\bqwerty\\b", "\\basdf\\b", "([!?.])\\1{4,}"] }
25
+ ]}
26
+ ]
27
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "default",
3
+ "version": 1,
4
+ "description": "Balanced preset",
5
+ "thresholds": { "warn": 0.45, "block": 0.65 },
6
+ "hard_triggers": [
7
+ { "id": "shouting", "type": "pattern", "weight": 0.20, "patterns": ["..."] }
8
+ ],
9
+ "topics": [
10
+ {
11
+ "id": "marketing_spam",
12
+ "weight": 0.25,
13
+ "signals": [
14
+ { "id": "money_signal", "weight": 0.10, "patterns": ["..."] }
15
+ ]
16
+ }
17
+ ],
18
+ "normalization": {
19
+ "lowercase": true,
20
+ "trim": true,
21
+ "collapse_whitespace": true,
22
+ "unicode_nfkc": true
23
+ }
24
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "strict",
3
+ "version": 1,
4
+ "description": "Aggressive preset for public endpoints and high-abuse environments.",
5
+ "thresholds": { "warn": 0.35, "block": 0.55 },
6
+ "normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
7
+ "hard_triggers": [
8
+ { "id": "shouting", "type": "signal", "weight": 0.22, "patterns": ["[A-ZÁÉÍÓÚÑ]{5,}", "!!+", "\\?\\?+", "¡¡+"] },
9
+ { "id": "money_signal", "type": "topic_hint", "weight": 0.18, "patterns": ["\\$\\d+", "\\b%\\b", "\\bfree\\b", "\\bgratis\\b"] },
10
+ { "id": "links", "type": "signal", "weight": 0.20, "patterns": ["https?://", "www\\.", "\\bbit\\.ly\\b"] },
11
+ { "id": "conspiracy_vague", "type": "topic_hint", "weight": 0.18, "patterns": ["\\btodos\\s+lo\\s+esconden\\b", "\\btruth\\s+is\\s+hidden\\b"] }
12
+ ],
13
+ "topics": [
14
+ { "id": "marketing_spam", "weight": 0.45, "signals": [
15
+ { "id": "cta", "weight": 0.22, "patterns": ["\\bclick\\b", "\\bclaim\\b", "\\bbuy\\b", "\\bcompra\\b"] },
16
+ { "id": "promo", "weight": 0.18, "patterns": ["\\bwin\\b", "\\bganaste\\b", "\\bgratis\\b", "\\bfree\\b"] }
17
+ ]},
18
+ { "id": "coercion", "weight": 0.30, "signals": [
19
+ { "id": "threat", "weight": 0.22, "patterns": ["\\bor\\s+else\\b", "\\bsi\\s+no\\b", "\\bI\\s+will\\b", "\\bte\\s+voy\\s+a\\b"] }
20
+ ]},
21
+ { "id": "conspiracy", "weight": 0.25, "signals": [
22
+ { "id": "hidden", "weight": 0.20, "patterns": ["\\bocult\\w+\\b", "\\bhidden\\b", "\\bsecret\\b"] }
23
+ ]}
24
+ ]
25
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "support",
3
+ "version": 1,
4
+ "description": "Customer-support preset: minimizes false positives for normal conversation.",
5
+ "thresholds": { "warn": 0.55, "block": 0.75 },
6
+ "normalization": { "lowercase": true, "trim": true, "collapse_whitespace": true, "unicode_nfkc": true },
7
+ "hard_triggers": [
8
+ { "id": "links", "type": "signal", "weight": 0.10, "patterns": ["https?://", "www\\."] }
9
+ ],
10
+ "topics": [
11
+ { "id": "marketing_spam", "weight": 0.25, "signals": [
12
+ { "id": "cta", "weight": 0.12, "patterns": ["\\bclick\\b", "\\bclaim\\b"] },
13
+ { "id": "promo", "weight": 0.10, "patterns": ["\\bfree\\b", "\\bgratis\\b"] }
14
+ ]},
15
+ { "id": "coercion", "weight": 0.20, "signals": [
16
+ { "id": "threat", "weight": 0.12, "patterns": ["\\bor\\s+else\\b", "\\bI\\s+will\\b"] }
17
+ ]},
18
+ { "id": "conspiracy", "weight": 0.15, "signals": [
19
+ { "id": "hidden", "weight": 0.10, "patterns": ["\\btruth\\s+hidden\\b", "\\bverdad\\s+oculta\\b"] }
20
+ ]}
21
+ ]
22
+ }