throttleai 0.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/adapters/express.cjs +75 -0
  4. package/dist/adapters/express.cjs.map +1 -0
  5. package/dist/adapters/express.d.cts +79 -0
  6. package/dist/adapters/express.d.ts +79 -0
  7. package/dist/adapters/express.js +50 -0
  8. package/dist/adapters/express.js.map +1 -0
  9. package/dist/adapters/fetch.cjs +99 -0
  10. package/dist/adapters/fetch.cjs.map +1 -0
  11. package/dist/adapters/fetch.d.cts +69 -0
  12. package/dist/adapters/fetch.d.ts +69 -0
  13. package/dist/adapters/fetch.js +68 -0
  14. package/dist/adapters/fetch.js.map +1 -0
  15. package/dist/adapters/hono.cjs +74 -0
  16. package/dist/adapters/hono.cjs.map +1 -0
  17. package/dist/adapters/hono.d.cts +73 -0
  18. package/dist/adapters/hono.d.ts +73 -0
  19. package/dist/adapters/hono.js +49 -0
  20. package/dist/adapters/hono.js.map +1 -0
  21. package/dist/adapters/openai.cjs +103 -0
  22. package/dist/adapters/openai.cjs.map +1 -0
  23. package/dist/adapters/openai.d.cts +102 -0
  24. package/dist/adapters/openai.d.ts +102 -0
  25. package/dist/adapters/openai.js +70 -0
  26. package/dist/adapters/openai.js.map +1 -0
  27. package/dist/adapters/tools.cjs +80 -0
  28. package/dist/adapters/tools.cjs.map +1 -0
  29. package/dist/adapters/tools.d.cts +56 -0
  30. package/dist/adapters/tools.d.ts +56 -0
  31. package/dist/adapters/tools.js +49 -0
  32. package/dist/adapters/tools.js.map +1 -0
  33. package/dist/chunk-YHOXYRXL.js +11 -0
  34. package/dist/chunk-YHOXYRXL.js.map +1 -0
  35. package/dist/governor-MVaCesqM.d.cts +206 -0
  36. package/dist/governor-MVaCesqM.d.ts +206 -0
  37. package/dist/index.cjs +1163 -0
  38. package/dist/index.cjs.map +1 -0
  39. package/dist/index.d.cts +213 -0
  40. package/dist/index.d.ts +213 -0
  41. package/dist/index.js +1128 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types-BkfBESR2.d.ts +47 -0
  44. package/dist/types-DOUI5hr7.d.cts +47 -0
  45. package/package.json +114 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mcp-tool-shop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="ThrottleAI" width="400">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/throttleai"><img src="https://img.shields.io/npm/v/throttleai" alt="npm"></a>
7
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <em>A token-based lease governor for AI calls — small enough to embed anywhere, strict enough to prevent stampedes.</em>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ## 60-second quickstart
17
+
18
+ ```bash
19
+ pnpm add throttleai
20
+ ```
21
+
22
+ ```ts
23
+ import { createGovernor, withLease, presets } from "throttleai";
24
+
25
+ const gov = createGovernor(presets.balanced());
26
+
27
+ const result = await withLease(
28
+ gov,
29
+ { actorId: "user-1", action: "chat" },
30
+ async () => await callMyModel(),
31
+ );
32
+
33
+ if (result.granted) {
34
+ console.log(result.result);
35
+ } else {
36
+ console.log("Throttled:", result.decision.recommendation);
37
+ }
38
+ ```
39
+
40
+ That's it. The governor enforces concurrency, rate limits, and fairness. Leases auto-expire if you forget to release.
41
+
42
+ ## Why
43
+
44
+ AI applications hit rate limits, blow budgets, and create stampedes. ThrottleAI sits between your code and the model call, enforcing:
45
+
46
+ - **Concurrency** — cap in-flight calls with weighted slots and interactive reserve
47
+ - **Rate** — requests/min and tokens/min with rolling windows
48
+ - **Fairness** — no single actor monopolizes capacity
49
+ - **Leases** — acquire before, release after, auto-expire on timeout
50
+ - **Observability** — `snapshot()`, `onEvent`, and `formatEvent()` for debugging
51
+
52
+ Zero dependencies. Node.js 18+. Tree-shakeable.
53
+
54
+ ## Presets
55
+
56
+ ```ts
57
+ import { presets } from "throttleai";
58
+
59
+ // Single user, CLI tools — 1 call at a time, 10 req/min
60
+ createGovernor(presets.quiet());
61
+
62
+ // SaaS backend — 5 concurrent (2 interactive reserve), 60 req/min, fairness
63
+ createGovernor(presets.balanced());
64
+
65
+ // Batch processing — 20 concurrent, 300 req/min, fairness + adaptive tuning
66
+ createGovernor(presets.aggressive());
67
+
68
+ // Override any field
69
+ createGovernor({ ...presets.balanced(), leaseTtlMs: 30_000 });
70
+ ```
71
+
72
+ ## Common patterns
73
+
74
+ ### Server endpoint: 429 vs queue
75
+
76
+ ```ts
77
+ // Option A: immediate deny with 429
78
+ const result = await withLease(gov, request, fn);
79
+ // result.granted === false → respond with 429
80
+
81
+ // Option B: wait with bounded retries
82
+ const result = await withLease(gov, request, fn, {
83
+ strategy: "wait-then-deny",
84
+ maxAttempts: 3,
85
+ maxWaitMs: 5_000,
86
+ });
87
+ ```
88
+
89
+ ### UI interactive vs background
90
+
91
+ ```ts
92
+ // User-facing chat gets priority
93
+ gov.acquire({ actorId: "user", action: "chat", priority: "interactive" });
94
+
95
+ // Background embedding can wait
96
+ gov.acquire({ actorId: "pipeline", action: "embed", priority: "background" });
97
+ ```
98
+
99
+ With `interactiveReserve: 2`, background tasks are blocked when only 2 slots remain, keeping those for interactive requests.
100
+
101
+ ### Streaming calls
102
+
103
+ ```ts
104
+ const decision = gov.acquire({ actorId: "user", action: "stream" });
105
+ if (!decision.granted) return;
106
+
107
+ try {
108
+ const stream = await openai.chat.completions.create({ stream: true, ... });
109
+ for await (const chunk of stream) {
110
+ // process chunk
111
+ }
112
+ gov.release(decision.leaseId, { outcome: "success" });
113
+ } catch (err) {
114
+ gov.release(decision.leaseId, { outcome: "error" });
115
+ throw err;
116
+ }
117
+ ```
118
+
119
+ Acquire once, release once — the lease holds for the entire stream duration.
120
+
121
+ ### Observability: see why it throttles
122
+
123
+ ```ts
124
+ import { createGovernor, formatEvent, formatSnapshot } from "throttleai";
125
+
126
+ const gov = createGovernor({
127
+ ...presets.balanced(),
128
+ onEvent: (e) => console.log(formatEvent(e)),
129
+ // [deny] actor=user-1 action=chat reason=concurrency retryAfterMs=500 — All 5 slots in use...
130
+ });
131
+
132
+ // Point-in-time view
133
+ console.log(formatSnapshot(gov.snapshot()));
134
+ // concurrency=3/5 rate=12/60 leases=3
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ ```ts
140
+ createGovernor({
141
+ // Concurrency (optional)
142
+ concurrency: {
143
+ maxInFlight: 5, // max simultaneous weight
144
+ interactiveReserve: 1, // slots reserved for interactive priority
145
+ },
146
+
147
+ // Rate limiting (optional)
148
+ rate: {
149
+ requestsPerMinute: 60, // request-rate cap
150
+ tokensPerMinute: 100_000, // token-rate cap
151
+ windowMs: 60_000, // rolling window (default 60s)
152
+ },
153
+
154
+ // Advanced (optional)
155
+ fairness: true, // prevent actor monopolization
156
+ adaptive: true, // auto-tune concurrency from deny rate + latency
157
+ strict: true, // throw on double release / unknown ID (dev mode)
158
+
159
+ // Lease settings
160
+ leaseTtlMs: 60_000, // auto-expire (default 60s)
161
+ reaperIntervalMs: 5_000, // sweep interval (default 5s)
162
+
163
+ // Observability
164
+ onEvent: (e) => { /* acquire, deny, release, expire, warn */ },
165
+ });
166
+ ```
167
+
168
+ ## API
169
+
170
+ ### `createGovernor(config): Governor`
171
+
172
+ Factory function. Returns a `Governor` instance.
173
+
174
+ ### `governor.acquire(request): AcquireDecision`
175
+
176
+ Request a lease. Returns:
177
+
178
+ ```ts
179
+ // Granted
180
+ { granted: true, leaseId: string, expiresAt: number }
181
+
182
+ // Denied
183
+ { granted: false, reason, retryAfterMs, recommendation, limitsHint? }
184
+ ```
185
+
186
+ Deny reasons: `"concurrency"` | `"rate"` | `"budget"` | `"policy"`
187
+
188
+ ### `governor.release(leaseId, report?): void`
189
+
190
+ Release a lease. Always call this — even on errors.
191
+
192
+ ### `withLease(governor, request, fn, options?)`
193
+
194
+ Execute `fn` under a lease with automatic release.
195
+
196
+ ```ts
197
+ withLease(gov, request, fn, {
198
+ strategy: "deny", // default — fail immediately
199
+ strategy: "wait", // retry with backoff until maxWaitMs
200
+ strategy: "wait-then-deny", // retry up to maxAttempts
201
+ maxWaitMs: 10_000, // max total wait (default 10s)
202
+ maxAttempts: 3, // for "wait-then-deny" (default 3)
203
+ initialBackoffMs: 250, // starting backoff (default 250ms)
204
+ });
205
+ ```
206
+
207
+ ### `governor.snapshot(): GovernorSnapshot`
208
+
209
+ Point-in-time state: concurrency, rate, tokens, last deny.
210
+
211
+ ### `formatEvent(event): string` / `formatSnapshot(snap): string`
212
+
213
+ One-line human-readable formatters.
214
+
215
+ ### Status getters
216
+
217
+ ```ts
218
+ gov.activeLeases // active lease count
219
+ gov.concurrencyActive // in-flight weight
220
+ gov.concurrencyAvailable // remaining capacity
221
+ gov.rateCount // requests in current window
222
+ gov.tokenRateCount // tokens in current window
223
+ ```
224
+
225
+ ### `governor.dispose(): void`
226
+
227
+ Stop the TTL reaper. Call on shutdown.
228
+
229
+ ## Adapters
230
+
231
+ Tree-shakeable wrappers — import only what you use. No runtime deps.
232
+
233
+ ### fetch
234
+
235
+ ```ts
236
+ import { wrapFetch } from "throttleai/adapters/fetch";
237
+ const throttledFetch = wrapFetch(fetch, { governor: gov });
238
+ const r = await throttledFetch("https://api.example.com/v1/chat");
239
+ if (r.ok) console.log(r.response.status);
240
+ ```
241
+
242
+ ### OpenAI-compatible
243
+
244
+ ```ts
245
+ import { wrapChatCompletions } from "throttleai/adapters/openai";
246
+ const chat = wrapChatCompletions(openai.chat.completions.create, { governor: gov });
247
+ const r = await chat({ model: "gpt-4", messages });
248
+ if (r.ok) console.log(r.result.choices[0].message.content);
249
+ ```
250
+
251
+ ### Tool call
252
+
253
+ ```ts
254
+ import { wrapTool } from "throttleai/adapters/tools";
255
+ const embed = wrapTool(myEmbedFn, { governor: gov, toolId: "embed", costWeight: 2 });
256
+ const r = await embed("hello");
257
+ if (r.ok) console.log(r.result);
258
+ ```
259
+
260
+ ### Express
261
+
262
+ ```ts
263
+ import { throttleMiddleware } from "throttleai/adapters/express";
264
+ app.use("/ai", throttleMiddleware({ governor: gov }));
265
+ // 429 + Retry-After header + JSON body on deny
266
+ ```
267
+
268
+ ### Hono
269
+
270
+ ```ts
271
+ import { throttle } from "throttleai/adapters/hono";
272
+ app.use("/ai/*", throttle({ governor: gov }));
273
+ // 429 JSON on deny, leaseId stored on context
274
+ ```
275
+
276
+ All adapters return `{ ok: true, result, latencyMs }` on grant, `{ ok: false, decision }` on deny.
277
+
278
+ ## Tuning guide
279
+
280
+ | You see this | Adjust this |
281
+ |---|---|
282
+ | `reason: "concurrency"` | Increase `maxInFlight` or decrease call duration |
283
+ | `reason: "rate"` | Increase `requestsPerMinute` / `tokensPerMinute` |
284
+ | `reason: "policy"` (fairness) | Lower `softCapRatio` or increase `maxInFlight` |
285
+ | High `retryAfterMs` | Reduce `leaseTtlMs` so expired leases free faster |
286
+ | Background tasks starved | Increase `maxInFlight` or reduce `interactiveReserve` |
287
+ | Interactive latency high | Increase `interactiveReserve` |
288
+ | Adaptive shrinks too fast | Lower `alpha` or raise `targetDenyRate` |
289
+
290
+ Use `snapshot()` and `formatSnapshot()` to observe state in production.
291
+
292
+ ## Examples
293
+
294
+ See [`examples/`](examples/) for runnable demos:
295
+
296
+ - **[node-basic.ts](examples/node-basic.ts)** — burst simulation with snapshot printing
297
+ - **[express-middleware.ts](examples/express-middleware.ts)** — 429 + retry-after endpoint
298
+ - **[cookbook-adapters.ts](examples/cookbook-adapters.ts)** — all five adapters in action
299
+ - **[cookbook-burst-snapshot.ts](examples/cookbook-burst-snapshot.ts)** — burst load with governor snapshots
300
+ - **[cookbook-interactive-reserve.ts](examples/cookbook-interactive-reserve.ts)** — interactive vs background priority
301
+
302
+ ```bash
303
+ npx tsx examples/node-basic.ts
304
+ ```
305
+
306
+ ## License
307
+
308
+ MIT
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/express.ts
21
+ var express_exports = {};
22
+ __export(express_exports, {
23
+ throttleMiddleware: () => throttleMiddleware
24
+ });
25
+ module.exports = __toCommonJS(express_exports);
26
+ function throttleMiddleware(options) {
27
+ const {
28
+ governor,
29
+ getActorId,
30
+ getAction,
31
+ getPriority,
32
+ getEstimate,
33
+ onDeny
34
+ } = options;
35
+ return (req, res, next) => {
36
+ const actorId = getActorId ? getActorId(req) : asString(req.headers["x-actor-id"]) ?? req.ip ?? "anonymous";
37
+ const request = {
38
+ actorId,
39
+ action: getAction ? getAction(req) : req.path,
40
+ priority: getPriority ? getPriority(req) : "interactive",
41
+ estimate: getEstimate ? getEstimate(req) : void 0
42
+ };
43
+ const decision = governor.acquire(request);
44
+ if (!decision.granted) {
45
+ if (onDeny) {
46
+ onDeny(req, res, decision);
47
+ return;
48
+ }
49
+ res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1e3)));
50
+ res.status(429).json({
51
+ error: "Too many requests",
52
+ reason: decision.reason,
53
+ retryAfterMs: decision.retryAfterMs,
54
+ recommendation: decision.recommendation
55
+ });
56
+ return;
57
+ }
58
+ const leaseId = decision.leaseId;
59
+ res.on("finish", () => {
60
+ governor.release(leaseId, {
61
+ outcome: (res.statusCode ?? 200) < 400 ? "success" : "error"
62
+ });
63
+ });
64
+ next();
65
+ };
66
+ }
67
+ function asString(value) {
68
+ if (Array.isArray(value)) return value[0];
69
+ return value;
70
+ }
71
+ // Annotate the CommonJS export names for ESM import in node:
72
+ 0 && (module.exports = {
73
+ throttleMiddleware
74
+ });
75
+ //# sourceMappingURL=express.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/express.ts"],"sourcesContent":["/**\n * ThrottleAI Express adapter — drop-in middleware.\n *\n * No dependency on Express — this exports a plain function that\n * returns `(req, res, next)`. You already have Express installed.\n *\n * @module throttleai/adapters/express\n */\n\nexport type {\n AdapterGovernor,\n AdapterOptions,\n} from \"./types.js\";\n\nimport type { AcquireRequest, AcquireDecision, Priority, TokenEstimate } from \"../types.js\";\nimport type { AdapterGovernor } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Types — use minimal shapes so we don't need @types/express\n// ---------------------------------------------------------------------------\n\n/** Minimal Express-compatible request shape. */\nexport interface ExpressLikeRequest {\n path: string;\n method: string;\n ip?: string;\n headers: Record<string, string | string[] | undefined>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Minimal Express-compatible response shape. */\nexport interface ExpressLikeResponse {\n status(code: number): this;\n json(body: unknown): void;\n setHeader(name: string, value: string | number): void;\n on(event: string, listener: () => void): void;\n statusCode?: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Options for the Express throttle middleware. */\nexport interface ThrottleMiddlewareOptions {\n /** Governor instance. */\n governor: AdapterGovernor;\n /**\n * Derive the actor ID from the request (default: x-actor-id header or req.ip).\n */\n getActorId?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the action from the request (default: req.path).\n */\n getAction?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the priority from the request (default: interactive).\n */\n getPriority?: (req: ExpressLikeRequest) => Priority;\n /**\n * Derive a token estimate from the request (optional).\n */\n getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;\n /**\n * Custom handler for denied requests (default: 429 JSON response).\n */\n onDeny?: (\n req: ExpressLikeRequest,\n res: ExpressLikeResponse,\n decision: AcquireDecision & { granted: false },\n ) => void;\n}\n\n// ---------------------------------------------------------------------------\n// throttleMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Create an Express middleware that throttles requests via the governor.\n *\n * ```ts\n * import express from \"express\";\n * import { createGovernor, presets } from \"throttleai\";\n * import { throttleMiddleware } from \"throttleai/adapters/express\";\n *\n * const gov = createGovernor(presets.balanced());\n * const app = express();\n *\n * app.use(\"/ai\", throttleMiddleware({ governor: gov }));\n *\n * app.post(\"/ai/chat\", (req, res) => {\n * // This only runs if the governor granted a lease\n * res.json({ message: \"ok\" });\n * });\n * ```\n */\nexport function throttleMiddleware(\n options: ThrottleMiddlewareOptions,\n): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void {\n const {\n governor,\n getActorId,\n getAction,\n getPriority,\n getEstimate,\n onDeny,\n } = options;\n\n return (req, res, next) => {\n const actorId = getActorId\n ? getActorId(req)\n : (asString(req.headers[\"x-actor-id\"]) ?? req.ip ?? \"anonymous\");\n\n const request: AcquireRequest = {\n actorId,\n action: getAction ? getAction(req) : req.path,\n priority: getPriority ? getPriority(req) : \"interactive\",\n estimate: getEstimate ? getEstimate(req) : undefined,\n };\n\n const decision = governor.acquire(request);\n\n if (!decision.granted) {\n if (onDeny) {\n onDeny(req, res, decision as AcquireDecision & { granted: false });\n return;\n }\n\n // Default: 429 JSON response\n res.setHeader(\"Retry-After\", String(Math.ceil(decision.retryAfterMs / 1000)));\n res.status(429).json({\n error: \"Too many requests\",\n reason: decision.reason,\n retryAfterMs: decision.retryAfterMs,\n recommendation: decision.recommendation,\n });\n return;\n }\n\n // Release lease when response finishes\n const leaseId = decision.leaseId;\n res.on(\"finish\", () => {\n governor.release(leaseId, {\n outcome: (res.statusCode ?? 200) < 400 ? \"success\" : \"error\",\n });\n });\n\n next();\n };\n}\n\n/** Safely convert a header value to string. */\nfunction asString(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0];\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+FO,SAAS,mBACd,SAC+E;AAC/E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SAAO,CAAC,KAAK,KAAK,SAAS;AACzB,UAAM,UAAU,aACZ,WAAW,GAAG,IACb,SAAS,IAAI,QAAQ,YAAY,CAAC,KAAK,IAAI,MAAM;AAEtD,UAAM,UAA0B;AAAA,MAC9B;AAAA,MACA,QAAQ,YAAY,UAAU,GAAG,IAAI,IAAI;AAAA,MACzC,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,MAC3C,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,IAC7C;AAEA,UAAM,WAAW,SAAS,QAAQ,OAAO;AAEzC,QAAI,CAAC,SAAS,SAAS;AACrB,UAAI,QAAQ;AACV,eAAO,KAAK,KAAK,QAAgD;AACjE;AAAA,MACF;AAGA,UAAI,UAAU,eAAe,OAAO,KAAK,KAAK,SAAS,eAAe,GAAI,CAAC,CAAC;AAC5E,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,QAAQ,SAAS;AAAA,QACjB,cAAc,SAAS;AAAA,QACvB,gBAAgB,SAAS;AAAA,MAC3B,CAAC;AACD;AAAA,IACF;AAGA,UAAM,UAAU,SAAS;AACzB,QAAI,GAAG,UAAU,MAAM;AACrB,eAAS,QAAQ,SAAS;AAAA,QACxB,UAAU,IAAI,cAAc,OAAO,MAAM,YAAY;AAAA,MACvD,CAAC;AAAA,IACH,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAGA,SAAS,SAAS,OAA0D;AAC1E,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;","names":[]}
@@ -0,0 +1,79 @@
1
+ import { A as AdapterGovernor } from '../types-DOUI5hr7.cjs';
2
+ export { a as AdapterOptions } from '../types-DOUI5hr7.cjs';
3
+ import { P as Priority, T as TokenEstimate, A as AcquireDecision } from '../governor-MVaCesqM.cjs';
4
+
5
+ /**
6
+ * ThrottleAI Express adapter — drop-in middleware.
7
+ *
8
+ * No dependency on Express — this exports a plain function that
9
+ * returns `(req, res, next)`. You already have Express installed.
10
+ *
11
+ * @module throttleai/adapters/express
12
+ */
13
+
14
+ /** Minimal Express-compatible request shape. */
15
+ interface ExpressLikeRequest {
16
+ path: string;
17
+ method: string;
18
+ ip?: string;
19
+ headers: Record<string, string | string[] | undefined>;
20
+ [key: string]: any;
21
+ }
22
+ /** Minimal Express-compatible response shape. */
23
+ interface ExpressLikeResponse {
24
+ status(code: number): this;
25
+ json(body: unknown): void;
26
+ setHeader(name: string, value: string | number): void;
27
+ on(event: string, listener: () => void): void;
28
+ statusCode?: number;
29
+ [key: string]: any;
30
+ }
31
+ /** Options for the Express throttle middleware. */
32
+ interface ThrottleMiddlewareOptions {
33
+ /** Governor instance. */
34
+ governor: AdapterGovernor;
35
+ /**
36
+ * Derive the actor ID from the request (default: x-actor-id header or req.ip).
37
+ */
38
+ getActorId?: (req: ExpressLikeRequest) => string;
39
+ /**
40
+ * Derive the action from the request (default: req.path).
41
+ */
42
+ getAction?: (req: ExpressLikeRequest) => string;
43
+ /**
44
+ * Derive the priority from the request (default: interactive).
45
+ */
46
+ getPriority?: (req: ExpressLikeRequest) => Priority;
47
+ /**
48
+ * Derive a token estimate from the request (optional).
49
+ */
50
+ getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;
51
+ /**
52
+ * Custom handler for denied requests (default: 429 JSON response).
53
+ */
54
+ onDeny?: (req: ExpressLikeRequest, res: ExpressLikeResponse, decision: AcquireDecision & {
55
+ granted: false;
56
+ }) => void;
57
+ }
58
+ /**
59
+ * Create an Express middleware that throttles requests via the governor.
60
+ *
61
+ * ```ts
62
+ * import express from "express";
63
+ * import { createGovernor, presets } from "throttleai";
64
+ * import { throttleMiddleware } from "throttleai/adapters/express";
65
+ *
66
+ * const gov = createGovernor(presets.balanced());
67
+ * const app = express();
68
+ *
69
+ * app.use("/ai", throttleMiddleware({ governor: gov }));
70
+ *
71
+ * app.post("/ai/chat", (req, res) => {
72
+ * // This only runs if the governor granted a lease
73
+ * res.json({ message: "ok" });
74
+ * });
75
+ * ```
76
+ */
77
+ declare function throttleMiddleware(options: ThrottleMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
78
+
79
+ export { AdapterGovernor, type ExpressLikeRequest, type ExpressLikeResponse, type ThrottleMiddlewareOptions, throttleMiddleware };
@@ -0,0 +1,79 @@
1
+ import { A as AdapterGovernor } from '../types-BkfBESR2.js';
2
+ export { a as AdapterOptions } from '../types-BkfBESR2.js';
3
+ import { P as Priority, T as TokenEstimate, A as AcquireDecision } from '../governor-MVaCesqM.js';
4
+
5
+ /**
6
+ * ThrottleAI Express adapter — drop-in middleware.
7
+ *
8
+ * No dependency on Express — this exports a plain function that
9
+ * returns `(req, res, next)`. You already have Express installed.
10
+ *
11
+ * @module throttleai/adapters/express
12
+ */
13
+
14
+ /** Minimal Express-compatible request shape. */
15
+ interface ExpressLikeRequest {
16
+ path: string;
17
+ method: string;
18
+ ip?: string;
19
+ headers: Record<string, string | string[] | undefined>;
20
+ [key: string]: any;
21
+ }
22
+ /** Minimal Express-compatible response shape. */
23
+ interface ExpressLikeResponse {
24
+ status(code: number): this;
25
+ json(body: unknown): void;
26
+ setHeader(name: string, value: string | number): void;
27
+ on(event: string, listener: () => void): void;
28
+ statusCode?: number;
29
+ [key: string]: any;
30
+ }
31
+ /** Options for the Express throttle middleware. */
32
+ interface ThrottleMiddlewareOptions {
33
+ /** Governor instance. */
34
+ governor: AdapterGovernor;
35
+ /**
36
+ * Derive the actor ID from the request (default: x-actor-id header or req.ip).
37
+ */
38
+ getActorId?: (req: ExpressLikeRequest) => string;
39
+ /**
40
+ * Derive the action from the request (default: req.path).
41
+ */
42
+ getAction?: (req: ExpressLikeRequest) => string;
43
+ /**
44
+ * Derive the priority from the request (default: interactive).
45
+ */
46
+ getPriority?: (req: ExpressLikeRequest) => Priority;
47
+ /**
48
+ * Derive a token estimate from the request (optional).
49
+ */
50
+ getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;
51
+ /**
52
+ * Custom handler for denied requests (default: 429 JSON response).
53
+ */
54
+ onDeny?: (req: ExpressLikeRequest, res: ExpressLikeResponse, decision: AcquireDecision & {
55
+ granted: false;
56
+ }) => void;
57
+ }
58
+ /**
59
+ * Create an Express middleware that throttles requests via the governor.
60
+ *
61
+ * ```ts
62
+ * import express from "express";
63
+ * import { createGovernor, presets } from "throttleai";
64
+ * import { throttleMiddleware } from "throttleai/adapters/express";
65
+ *
66
+ * const gov = createGovernor(presets.balanced());
67
+ * const app = express();
68
+ *
69
+ * app.use("/ai", throttleMiddleware({ governor: gov }));
70
+ *
71
+ * app.post("/ai/chat", (req, res) => {
72
+ * // This only runs if the governor granted a lease
73
+ * res.json({ message: "ok" });
74
+ * });
75
+ * ```
76
+ */
77
+ declare function throttleMiddleware(options: ThrottleMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
78
+
79
+ export { AdapterGovernor, type ExpressLikeRequest, type ExpressLikeResponse, type ThrottleMiddlewareOptions, throttleMiddleware };
@@ -0,0 +1,50 @@
1
+ // src/adapters/express.ts
2
+ function throttleMiddleware(options) {
3
+ const {
4
+ governor,
5
+ getActorId,
6
+ getAction,
7
+ getPriority,
8
+ getEstimate,
9
+ onDeny
10
+ } = options;
11
+ return (req, res, next) => {
12
+ const actorId = getActorId ? getActorId(req) : asString(req.headers["x-actor-id"]) ?? req.ip ?? "anonymous";
13
+ const request = {
14
+ actorId,
15
+ action: getAction ? getAction(req) : req.path,
16
+ priority: getPriority ? getPriority(req) : "interactive",
17
+ estimate: getEstimate ? getEstimate(req) : void 0
18
+ };
19
+ const decision = governor.acquire(request);
20
+ if (!decision.granted) {
21
+ if (onDeny) {
22
+ onDeny(req, res, decision);
23
+ return;
24
+ }
25
+ res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1e3)));
26
+ res.status(429).json({
27
+ error: "Too many requests",
28
+ reason: decision.reason,
29
+ retryAfterMs: decision.retryAfterMs,
30
+ recommendation: decision.recommendation
31
+ });
32
+ return;
33
+ }
34
+ const leaseId = decision.leaseId;
35
+ res.on("finish", () => {
36
+ governor.release(leaseId, {
37
+ outcome: (res.statusCode ?? 200) < 400 ? "success" : "error"
38
+ });
39
+ });
40
+ next();
41
+ };
42
+ }
43
+ function asString(value) {
44
+ if (Array.isArray(value)) return value[0];
45
+ return value;
46
+ }
47
+ export {
48
+ throttleMiddleware
49
+ };
50
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/express.ts"],"sourcesContent":["/**\n * ThrottleAI Express adapter — drop-in middleware.\n *\n * No dependency on Express — this exports a plain function that\n * returns `(req, res, next)`. You already have Express installed.\n *\n * @module throttleai/adapters/express\n */\n\nexport type {\n AdapterGovernor,\n AdapterOptions,\n} from \"./types.js\";\n\nimport type { AcquireRequest, AcquireDecision, Priority, TokenEstimate } from \"../types.js\";\nimport type { AdapterGovernor } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Types — use minimal shapes so we don't need @types/express\n// ---------------------------------------------------------------------------\n\n/** Minimal Express-compatible request shape. */\nexport interface ExpressLikeRequest {\n path: string;\n method: string;\n ip?: string;\n headers: Record<string, string | string[] | undefined>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Minimal Express-compatible response shape. */\nexport interface ExpressLikeResponse {\n status(code: number): this;\n json(body: unknown): void;\n setHeader(name: string, value: string | number): void;\n on(event: string, listener: () => void): void;\n statusCode?: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Options for the Express throttle middleware. */\nexport interface ThrottleMiddlewareOptions {\n /** Governor instance. */\n governor: AdapterGovernor;\n /**\n * Derive the actor ID from the request (default: x-actor-id header or req.ip).\n */\n getActorId?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the action from the request (default: req.path).\n */\n getAction?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the priority from the request (default: interactive).\n */\n getPriority?: (req: ExpressLikeRequest) => Priority;\n /**\n * Derive a token estimate from the request (optional).\n */\n getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;\n /**\n * Custom handler for denied requests (default: 429 JSON response).\n */\n onDeny?: (\n req: ExpressLikeRequest,\n res: ExpressLikeResponse,\n decision: AcquireDecision & { granted: false },\n ) => void;\n}\n\n// ---------------------------------------------------------------------------\n// throttleMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Create an Express middleware that throttles requests via the governor.\n *\n * ```ts\n * import express from \"express\";\n * import { createGovernor, presets } from \"throttleai\";\n * import { throttleMiddleware } from \"throttleai/adapters/express\";\n *\n * const gov = createGovernor(presets.balanced());\n * const app = express();\n *\n * app.use(\"/ai\", throttleMiddleware({ governor: gov }));\n *\n * app.post(\"/ai/chat\", (req, res) => {\n * // This only runs if the governor granted a lease\n * res.json({ message: \"ok\" });\n * });\n * ```\n */\nexport function throttleMiddleware(\n options: ThrottleMiddlewareOptions,\n): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void {\n const {\n governor,\n getActorId,\n getAction,\n getPriority,\n getEstimate,\n onDeny,\n } = options;\n\n return (req, res, next) => {\n const actorId = getActorId\n ? getActorId(req)\n : (asString(req.headers[\"x-actor-id\"]) ?? req.ip ?? \"anonymous\");\n\n const request: AcquireRequest = {\n actorId,\n action: getAction ? getAction(req) : req.path,\n priority: getPriority ? getPriority(req) : \"interactive\",\n estimate: getEstimate ? getEstimate(req) : undefined,\n };\n\n const decision = governor.acquire(request);\n\n if (!decision.granted) {\n if (onDeny) {\n onDeny(req, res, decision as AcquireDecision & { granted: false });\n return;\n }\n\n // Default: 429 JSON response\n res.setHeader(\"Retry-After\", String(Math.ceil(decision.retryAfterMs / 1000)));\n res.status(429).json({\n error: \"Too many requests\",\n reason: decision.reason,\n retryAfterMs: decision.retryAfterMs,\n recommendation: decision.recommendation,\n });\n return;\n }\n\n // Release lease when response finishes\n const leaseId = decision.leaseId;\n res.on(\"finish\", () => {\n governor.release(leaseId, {\n outcome: (res.statusCode ?? 200) < 400 ? \"success\" : \"error\",\n });\n });\n\n next();\n };\n}\n\n/** Safely convert a header value to string. */\nfunction asString(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0];\n return value;\n}\n"],"mappings":";AA+FO,SAAS,mBACd,SAC+E;AAC/E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SAAO,CAAC,KAAK,KAAK,SAAS;AACzB,UAAM,UAAU,aACZ,WAAW,GAAG,IACb,SAAS,IAAI,QAAQ,YAAY,CAAC,KAAK,IAAI,MAAM;AAEtD,UAAM,UAA0B;AAAA,MAC9B;AAAA,MACA,QAAQ,YAAY,UAAU,GAAG,IAAI,IAAI;AAAA,MACzC,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,MAC3C,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,IAC7C;AAEA,UAAM,WAAW,SAAS,QAAQ,OAAO;AAEzC,QAAI,CAAC,SAAS,SAAS;AACrB,UAAI,QAAQ;AACV,eAAO,KAAK,KAAK,QAAgD;AACjE;AAAA,MACF;AAGA,UAAI,UAAU,eAAe,OAAO,KAAK,KAAK,SAAS,eAAe,GAAI,CAAC,CAAC;AAC5E,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,QAAQ,SAAS;AAAA,QACjB,cAAc,SAAS;AAAA,QACvB,gBAAgB,SAAS;AAAA,MAC3B,CAAC;AACD;AAAA,IACF;AAGA,UAAM,UAAU,SAAS;AACzB,QAAI,GAAG,UAAU,MAAM;AACrB,eAAS,QAAQ,SAAS;AAAA,QACxB,UAAU,IAAI,cAAc,OAAO,MAAM,YAAY;AAAA,MACvD,CAAC;AAAA,IACH,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAGA,SAAS,SAAS,OAA0D;AAC1E,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;","names":[]}