@lacspace/keypool 1.0.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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @lacspace/keypool
2
+
3
+ **Turn five free keys into one big quota.** Provider-agnostic API-key rotation and rate-limit accounting: pool N keys per provider, track RPM/RPD/TPM/TPD per key (and per model — Groq's token-per-day is per model), round-robin among healthy keys, cool down on `429` using `retry-after`, and quarantine invalid keys. `pick()` hands you a usable key or `null`; `report()` feeds the outcome back. Persist state through a tiny adapter so every process shares one pool. Zero dependencies, isomorphic.
4
+
5
+ ```bash
6
+ npm i @lacspace/keypool
7
+ ```
8
+
9
+ ```ts
10
+ import { createKeypool, kvStore } from "@lacspace/keypool";
11
+
12
+ const pool = createKeypool({
13
+ keys: [
14
+ { id: "gem-1", provider: "gemini", secret: process.env.GEMINI_1! },
15
+ { id: "gem-2", provider: "gemini", secret: process.env.GEMINI_2! },
16
+ { id: "gem-3", provider: "gemini", secret: process.env.GEMINI_3! },
17
+ ],
18
+ providerLimits: { gemini: { rpm: 15, rpd: 1500, tpm: 1_000_000 } },
19
+ store: kvStore(myMongoKv), // shared across PM2 workers
20
+ });
21
+
22
+ const picked = await pool.pick("gemini", "gemini-2.0-flash", estTokens);
23
+ if (!picked) throw new Error("all keys cooling down / quota spent");
24
+ try {
25
+ const res = await callGemini(picked.secret, prompt);
26
+ await pool.report(picked.id, { ok: true, tokens: res.usage.total, model: "gemini-2.0-flash" });
27
+ } catch (e) {
28
+ await pool.report(picked.id, { ok: false, status: e.status, headers: e.headers });
29
+ }
30
+ ```
31
+
32
+ - **Windowed accounting** — RPM/RPD/TPM/TPD roll automatically; `pick(provider, model, estTokens)` only returns a key that can afford the call.
33
+ - **Per-model buckets** — `modelLimits["groq:llama-3.3-70b"]` are tracked separately, because that's how the provider counts.
34
+ - **429 aware** — cools a key down for `retry-after` (seconds or an HTTP date), or a default window; recovers automatically.
35
+ - **Invalid quarantine** — a `401/403` sidelines a key until you `reinstate(id, newSecret)`.
36
+ - **Priority + round-robin** — prefer some keys, otherwise spread load least-recently-used first, deterministically.
37
+ - **Shared state** — pass a `KeypoolStore` (Mongo/Redis/KV) so multiple processes don't each burn the same quota; `kvStore(kv)` wraps any `{get,set}`.
38
+
39
+ Secrets live only in memory on the `KeySpec`; the persisted state is counters and health, never the key. Exports `createKeypool`, `kvStore`, and the state types.
40
+
41
+ ## Licence
42
+ [Lacspace Free Licence v1.0](https://developer.lacspace.com/licenses/lacspace-free-1.0) — free for personal and commercial use.
package/dist/index.cjs ADDED
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var MIN = 6e4;
5
+ var DAY = 864e5;
6
+ function emptyBucket(now) {
7
+ return { minStart: now, minReq: 0, minTok: 0, dayStart: now, dayReq: 0, dayTok: 0 };
8
+ }
9
+ function rollBucket(b, now) {
10
+ if (now - b.minStart >= MIN) {
11
+ b.minStart = now;
12
+ b.minReq = 0;
13
+ b.minTok = 0;
14
+ }
15
+ if (now - b.dayStart >= DAY) {
16
+ b.dayStart = now;
17
+ b.dayReq = 0;
18
+ b.dayTok = 0;
19
+ }
20
+ }
21
+ function headerGet(headers, name) {
22
+ if (!headers) return null;
23
+ if (typeof headers.get === "function") return headers.get(name);
24
+ const h = headers;
25
+ const found = h[name] ?? h[name.toLowerCase()] ?? h[name.toUpperCase()];
26
+ return found === void 0 || found === null ? null : String(found);
27
+ }
28
+ function createKeypool(options) {
29
+ const now = options.now ?? (() => Date.now());
30
+ const cooldownMs = options.defaultCooldownMs ?? MIN;
31
+ const store = options.store;
32
+ const specs = /* @__PURE__ */ new Map();
33
+ for (const k of options.keys) specs.set(k.id, k);
34
+ let state = {};
35
+ let loaded = false;
36
+ const ensureLoaded = async () => {
37
+ if (loaded) return;
38
+ const fromStore = store ? await store.load() : null;
39
+ for (const spec of options.keys) {
40
+ state[spec.id] = fromStore?.[spec.id] ?? {
41
+ id: spec.id,
42
+ provider: spec.provider,
43
+ cooldownUntil: 0,
44
+ invalid: false,
45
+ buckets: {},
46
+ lastUsed: 0
47
+ };
48
+ }
49
+ loaded = true;
50
+ };
51
+ const persist = async () => {
52
+ if (store) await store.save(state);
53
+ };
54
+ const limitsFor = (spec, model) => ({
55
+ ...options.providerLimits?.[spec.provider] ?? {},
56
+ ...options.modelLimits?.[`${spec.provider}:${model}`] ?? {},
57
+ ...spec.limits ?? {}
58
+ });
59
+ const bucketOf = (ks, model, t) => {
60
+ let b = ks.buckets[model];
61
+ if (!b) {
62
+ b = emptyBucket(t);
63
+ ks.buckets[model] = b;
64
+ }
65
+ rollBucket(b, t);
66
+ return b;
67
+ };
68
+ const canAfford = (spec, ks, model, estTokens, t) => {
69
+ if (ks.invalid) return false;
70
+ if (ks.cooldownUntil > t) return false;
71
+ const lim = limitsFor(spec, model);
72
+ const b = bucketOf(ks, model, t);
73
+ if (lim.rpm !== void 0 && b.minReq + 1 > lim.rpm) return false;
74
+ if (lim.rpd !== void 0 && b.dayReq + 1 > lim.rpd) return false;
75
+ if (lim.tpm !== void 0 && b.minTok + estTokens > lim.tpm) return false;
76
+ if (lim.tpd !== void 0 && b.dayTok + estTokens > lim.tpd) return false;
77
+ return true;
78
+ };
79
+ const candidates = (provider, model, estTokens, t) => {
80
+ const out = [];
81
+ for (const spec of options.keys) {
82
+ if (spec.provider !== provider) continue;
83
+ const ks = state[spec.id];
84
+ if (canAfford(spec, ks, model, estTokens, t)) out.push({ spec, ks });
85
+ }
86
+ out.sort(
87
+ (a, b) => (b.spec.priority ?? 0) - (a.spec.priority ?? 0) || a.ks.lastUsed - b.ks.lastUsed || a.spec.id.localeCompare(b.spec.id)
88
+ );
89
+ return out;
90
+ };
91
+ return {
92
+ async pick(provider, model = "", estTokens = 0) {
93
+ await ensureLoaded();
94
+ const t = now();
95
+ const cand = candidates(provider, model, estTokens, t)[0];
96
+ if (!cand) return null;
97
+ const b = bucketOf(cand.ks, model, t);
98
+ b.minReq += 1;
99
+ b.dayReq += 1;
100
+ b.minTok += estTokens;
101
+ b.dayTok += estTokens;
102
+ cand.ks.lastUsed = t;
103
+ await persist();
104
+ return { id: cand.spec.id, secret: cand.spec.secret, provider, model };
105
+ },
106
+ async report(id, result) {
107
+ await ensureLoaded();
108
+ const ks = state[id];
109
+ if (!ks) return;
110
+ const t = now();
111
+ const model = result.model ?? Object.keys(ks.buckets)[0] ?? "";
112
+ const b = bucketOf(ks, model, t);
113
+ if (result.tokens !== void 0) {
114
+ b.minTok += result.tokens;
115
+ b.dayTok += result.tokens;
116
+ }
117
+ const retryAfter = headerGet(result.headers, "retry-after");
118
+ const status = result.status ?? (result.ok ? 200 : 0);
119
+ if (status === 401 || status === 403) {
120
+ ks.invalid = true;
121
+ } else if (status === 429) {
122
+ let waitMs = cooldownMs;
123
+ if (retryAfter) {
124
+ const n = Number(retryAfter);
125
+ waitMs = Number.isFinite(n) ? n * 1e3 : Math.max(0, new Date(retryAfter).getTime() - t) || cooldownMs;
126
+ }
127
+ ks.cooldownUntil = t + waitMs;
128
+ } else if (result.ok) {
129
+ if (ks.cooldownUntil <= t) ks.cooldownUntil = 0;
130
+ }
131
+ await persist();
132
+ },
133
+ async note(id, model, tokens) {
134
+ await ensureLoaded();
135
+ const ks = state[id];
136
+ if (!ks) return;
137
+ const b = bucketOf(ks, model, now());
138
+ b.minTok += tokens;
139
+ b.dayTok += tokens;
140
+ await persist();
141
+ },
142
+ async reinstate(id, secret) {
143
+ await ensureLoaded();
144
+ const ks = state[id];
145
+ if (!ks) return;
146
+ ks.invalid = false;
147
+ ks.cooldownUntil = 0;
148
+ if (secret) {
149
+ const spec = specs.get(id);
150
+ if (spec) spec.secret = secret;
151
+ }
152
+ await persist();
153
+ },
154
+ async states() {
155
+ await ensureLoaded();
156
+ return JSON.parse(JSON.stringify(state));
157
+ },
158
+ async available(provider, model = "", estTokens = 0) {
159
+ await ensureLoaded();
160
+ return candidates(provider, model, estTokens, now()).length;
161
+ }
162
+ };
163
+ }
164
+ function kvStore(kv, key = "keypool:state") {
165
+ return {
166
+ async load() {
167
+ const raw = await kv.get(key);
168
+ return raw ? JSON.parse(raw) : null;
169
+ },
170
+ async save(states) {
171
+ await kv.set(key, JSON.stringify(states));
172
+ }
173
+ };
174
+ }
175
+
176
+ exports.createKeypool = createKeypool;
177
+ exports.kvStore = kvStore;
178
+ //# sourceMappingURL=index.cjs.map
179
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { BucketState, KeypoolStore, KeySpec, KeyState, Limits, ReportInput } from \"./types.js\";\n\nexport type { BucketState, KeypoolStore, KeySpec, KeyState, Limits, ReportInput } from \"./types.js\";\n\nexport interface KeypoolOptions {\n keys: KeySpec[];\n /** Default limits per provider, applied to every key unless the key overrides. */\n providerLimits?: Record<string, Limits>;\n /** Per-model limit overrides, e.g. { \"groq:llama-3.3-70b\": { tpd: 500000 } }. Key format `provider:model`. */\n modelLimits?: Record<string, Limits>;\n /** Cooldown (ms) applied on a 429 when no retry-after header is present. Default 60000. */\n defaultCooldownMs?: number;\n /** Persistence adapter for cross-process sharing. Default: in-memory only. */\n store?: KeypoolStore;\n /** Injectable clock (ms). */\n now?: () => number;\n}\n\nexport interface Pick {\n id: string;\n secret: string;\n provider: string;\n model: string;\n}\n\nexport interface Keypool {\n /**\n * Pick the best healthy key for (provider, model) that can afford ~estTokens,\n * or null if every key is cooling down / quota-exhausted / invalid.\n */\n pick(provider: string, model?: string, estTokens?: number): Promise<Pick | null>;\n /** Report the outcome of a call so counters, cooldowns and quarantine update. */\n report(id: string, result: ReportInput): Promise<void>;\n /** Reserve usage yourself (when you don't call report), e.g. optimistic accounting. */\n note(id: string, model: string, tokens: number): Promise<void>;\n /** Clear a key's invalid quarantine (after you rotated the secret). */\n reinstate(id: string, secret?: string): Promise<void>;\n /** Snapshot of every key's state. */\n states(): Promise<Record<string, KeyState>>;\n /** How many keys are pickable right now for a provider/model. */\n available(provider: string, model?: string, estTokens?: number): Promise<number>;\n}\n\nconst MIN = 60_000;\nconst DAY = 86_400_000;\n\nfunction emptyBucket(now: number): BucketState {\n return { minStart: now, minReq: 0, minTok: 0, dayStart: now, dayReq: 0, dayTok: 0 };\n}\n\nfunction rollBucket(b: BucketState, now: number): void {\n if (now - b.minStart >= MIN) {\n b.minStart = now;\n b.minReq = 0;\n b.minTok = 0;\n }\n if (now - b.dayStart >= DAY) {\n b.dayStart = now;\n b.dayReq = 0;\n b.dayTok = 0;\n }\n}\n\nfunction headerGet(headers: ReportInput[\"headers\"], name: string): string | null {\n if (!headers) return null;\n if (typeof (headers as { get?: unknown }).get === \"function\") return (headers as { get(n: string): string | null }).get(name);\n const h = headers as Record<string, string | number | null | undefined>;\n const found = h[name] ?? h[name.toLowerCase()] ?? h[name.toUpperCase()];\n return found === undefined || found === null ? null : String(found);\n}\n\nexport function createKeypool(options: KeypoolOptions): Keypool {\n const now = options.now ?? (() => Date.now());\n const cooldownMs = options.defaultCooldownMs ?? MIN;\n const store = options.store;\n const specs = new Map<string, KeySpec>();\n for (const k of options.keys) specs.set(k.id, k);\n\n // Live state, hydrated from the store if present.\n let state: Record<string, KeyState> = {};\n let loaded = false;\n\n const ensureLoaded = async () => {\n if (loaded) return;\n const fromStore = store ? await store.load() : null;\n for (const spec of options.keys) {\n state[spec.id] = fromStore?.[spec.id] ?? {\n id: spec.id,\n provider: spec.provider,\n cooldownUntil: 0,\n invalid: false,\n buckets: {},\n lastUsed: 0,\n };\n }\n loaded = true;\n };\n\n const persist = async () => {\n if (store) await store.save(state);\n };\n\n const limitsFor = (spec: KeySpec, model: string): Limits => ({\n ...(options.providerLimits?.[spec.provider] ?? {}),\n ...(options.modelLimits?.[`${spec.provider}:${model}`] ?? {}),\n ...(spec.limits ?? {}),\n });\n\n const bucketOf = (ks: KeyState, model: string, t: number): BucketState => {\n let b = ks.buckets[model];\n if (!b) {\n b = emptyBucket(t);\n ks.buckets[model] = b;\n }\n rollBucket(b, t);\n return b;\n };\n\n const canAfford = (spec: KeySpec, ks: KeyState, model: string, estTokens: number, t: number): boolean => {\n if (ks.invalid) return false;\n if (ks.cooldownUntil > t) return false;\n const lim = limitsFor(spec, model);\n const b = bucketOf(ks, model, t);\n if (lim.rpm !== undefined && b.minReq + 1 > lim.rpm) return false;\n if (lim.rpd !== undefined && b.dayReq + 1 > lim.rpd) return false;\n if (lim.tpm !== undefined && b.minTok + estTokens > lim.tpm) return false;\n if (lim.tpd !== undefined && b.dayTok + estTokens > lim.tpd) return false;\n return true;\n };\n\n const candidates = (provider: string, model: string, estTokens: number, t: number): { spec: KeySpec; ks: KeyState }[] => {\n const out: { spec: KeySpec; ks: KeyState }[] = [];\n for (const spec of options.keys) {\n if (spec.provider !== provider) continue;\n const ks = state[spec.id]!;\n if (canAfford(spec, ks, model, estTokens, t)) out.push({ spec, ks });\n }\n // Highest priority, then least-recently-used (round-robin), then id for stability.\n out.sort(\n (a, b) =>\n (b.spec.priority ?? 0) - (a.spec.priority ?? 0) ||\n a.ks.lastUsed - b.ks.lastUsed ||\n a.spec.id.localeCompare(b.spec.id),\n );\n return out;\n };\n\n return {\n async pick(provider, model = \"\", estTokens = 0) {\n await ensureLoaded();\n const t = now();\n const cand = candidates(provider, model, estTokens, t)[0];\n if (!cand) return null;\n // Optimistically reserve one request + estimated tokens so concurrent picks spread out.\n const b = bucketOf(cand.ks, model, t);\n b.minReq += 1;\n b.dayReq += 1;\n b.minTok += estTokens;\n b.dayTok += estTokens;\n cand.ks.lastUsed = t;\n await persist();\n return { id: cand.spec.id, secret: cand.spec.secret, provider, model };\n },\n\n async report(id, result) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n const t = now();\n const model = result.model ?? Object.keys(ks.buckets)[0] ?? \"\";\n const b = bucketOf(ks, model, t);\n\n // Reconcile token count if the caller reserved an estimate at pick().\n if (result.tokens !== undefined) {\n b.minTok += result.tokens; // add ACTUAL on top of the estimate reserved at pick; callers can pass a delta if they prefer\n b.dayTok += result.tokens;\n }\n\n // Absorb provider-reported remaining/limits when present.\n const retryAfter = headerGet(result.headers, \"retry-after\");\n const status = result.status ?? (result.ok ? 200 : 0);\n\n if (status === 401 || status === 403) {\n ks.invalid = true;\n } else if (status === 429) {\n let waitMs = cooldownMs;\n if (retryAfter) {\n const n = Number(retryAfter);\n waitMs = Number.isFinite(n) ? n * 1000 : Math.max(0, new Date(retryAfter).getTime() - t) || cooldownMs;\n }\n ks.cooldownUntil = t + waitMs;\n } else if (result.ok) {\n // clear any stale cooldown on a clean success\n if (ks.cooldownUntil <= t) ks.cooldownUntil = 0;\n }\n await persist();\n },\n\n async note(id, model, tokens) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n const b = bucketOf(ks, model, now());\n b.minTok += tokens;\n b.dayTok += tokens;\n await persist();\n },\n\n async reinstate(id, secret) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n ks.invalid = false;\n ks.cooldownUntil = 0;\n if (secret) {\n const spec = specs.get(id);\n if (spec) spec.secret = secret;\n }\n await persist();\n },\n\n async states() {\n await ensureLoaded();\n return JSON.parse(JSON.stringify(state));\n },\n\n async available(provider, model = \"\", estTokens = 0) {\n await ensureLoaded();\n return candidates(provider, model, estTokens, now()).length;\n },\n };\n}\n\n/** A KeypoolStore backed by any {get,set} KV (JSON string values). Handy over Mongo/Redis. */\nexport function kvStore(kv: { get(k: string): Promise<string | null> | string | null; set(k: string, v: string): Promise<void> | void }, key = \"keypool:state\"): KeypoolStore {\n return {\n async load() {\n const raw = await kv.get(key);\n return raw ? (JSON.parse(raw) as Record<string, KeyState>) : null;\n },\n async save(states) {\n await kv.set(key, JSON.stringify(states));\n },\n };\n}\n"]}
@@ -0,0 +1,108 @@
1
+ /** Per-window limits for a provider or a specific model. Any field omitted = unlimited. */
2
+ interface Limits {
3
+ /** Requests per minute. */
4
+ rpm?: number;
5
+ /** Requests per day. */
6
+ rpd?: number;
7
+ /** Tokens per minute. */
8
+ tpm?: number;
9
+ /** Tokens per day. */
10
+ tpd?: number;
11
+ }
12
+ interface KeySpec {
13
+ /** Unique id for this key (never the secret itself — store a label or hash). */
14
+ id: string;
15
+ provider: string;
16
+ /** The secret. Held in memory only; never persisted by the built-in memory adapter's snapshot of counters. */
17
+ secret: string;
18
+ /** Optional per-key limit overrides (else provider/model defaults apply). */
19
+ limits?: Limits;
20
+ /** Priority — higher is preferred among equally-healthy keys. Default 0. */
21
+ priority?: number;
22
+ }
23
+ /** Rolling counters + health for one key, per model bucket. */
24
+ interface BucketState {
25
+ /** minute window start (epoch ms) and counts */
26
+ minStart: number;
27
+ minReq: number;
28
+ minTok: number;
29
+ /** day window start (epoch ms) and counts */
30
+ dayStart: number;
31
+ dayReq: number;
32
+ dayTok: number;
33
+ }
34
+ interface KeyState {
35
+ id: string;
36
+ provider: string;
37
+ /** epoch ms until which the key is cooling down (0 = healthy). */
38
+ cooldownUntil: number;
39
+ /** true if quarantined (invalid key) — never picked until cleared. */
40
+ invalid: boolean;
41
+ /** per-model buckets, keyed by model id ("" = provider-wide bucket). */
42
+ buckets: Record<string, BucketState>;
43
+ /** round-robin tiebreaker cursor. */
44
+ lastUsed: number;
45
+ }
46
+ /** A persistence adapter so multiple processes share pool state. All async-friendly. */
47
+ interface KeypoolStore {
48
+ load(): Record<string, KeyState> | null | Promise<Record<string, KeyState> | null>;
49
+ save(states: Record<string, KeyState>): void | Promise<void>;
50
+ }
51
+ interface ReportInput {
52
+ ok: boolean;
53
+ /** HTTP status of the response, if any. */
54
+ status?: number;
55
+ /** Tokens actually consumed by the call (input+output). */
56
+ tokens?: number;
57
+ /** Raw response headers (or a subset). `retry-after` and rate-limit remaining headers are read when present. */
58
+ headers?: Record<string, string | number | null | undefined> | {
59
+ get(name: string): string | null;
60
+ };
61
+ /** Model used (defaults to the model passed to pick). */
62
+ model?: string;
63
+ }
64
+
65
+ interface KeypoolOptions {
66
+ keys: KeySpec[];
67
+ /** Default limits per provider, applied to every key unless the key overrides. */
68
+ providerLimits?: Record<string, Limits>;
69
+ /** Per-model limit overrides, e.g. { "groq:llama-3.3-70b": { tpd: 500000 } }. Key format `provider:model`. */
70
+ modelLimits?: Record<string, Limits>;
71
+ /** Cooldown (ms) applied on a 429 when no retry-after header is present. Default 60000. */
72
+ defaultCooldownMs?: number;
73
+ /** Persistence adapter for cross-process sharing. Default: in-memory only. */
74
+ store?: KeypoolStore;
75
+ /** Injectable clock (ms). */
76
+ now?: () => number;
77
+ }
78
+ interface Pick {
79
+ id: string;
80
+ secret: string;
81
+ provider: string;
82
+ model: string;
83
+ }
84
+ interface Keypool {
85
+ /**
86
+ * Pick the best healthy key for (provider, model) that can afford ~estTokens,
87
+ * or null if every key is cooling down / quota-exhausted / invalid.
88
+ */
89
+ pick(provider: string, model?: string, estTokens?: number): Promise<Pick | null>;
90
+ /** Report the outcome of a call so counters, cooldowns and quarantine update. */
91
+ report(id: string, result: ReportInput): Promise<void>;
92
+ /** Reserve usage yourself (when you don't call report), e.g. optimistic accounting. */
93
+ note(id: string, model: string, tokens: number): Promise<void>;
94
+ /** Clear a key's invalid quarantine (after you rotated the secret). */
95
+ reinstate(id: string, secret?: string): Promise<void>;
96
+ /** Snapshot of every key's state. */
97
+ states(): Promise<Record<string, KeyState>>;
98
+ /** How many keys are pickable right now for a provider/model. */
99
+ available(provider: string, model?: string, estTokens?: number): Promise<number>;
100
+ }
101
+ declare function createKeypool(options: KeypoolOptions): Keypool;
102
+ /** A KeypoolStore backed by any {get,set} KV (JSON string values). Handy over Mongo/Redis. */
103
+ declare function kvStore(kv: {
104
+ get(k: string): Promise<string | null> | string | null;
105
+ set(k: string, v: string): Promise<void> | void;
106
+ }, key?: string): KeypoolStore;
107
+
108
+ export { type BucketState, type KeySpec, type KeyState, type Keypool, type KeypoolOptions, type KeypoolStore, type Limits, type Pick, type ReportInput, createKeypool, kvStore };
@@ -0,0 +1,108 @@
1
+ /** Per-window limits for a provider or a specific model. Any field omitted = unlimited. */
2
+ interface Limits {
3
+ /** Requests per minute. */
4
+ rpm?: number;
5
+ /** Requests per day. */
6
+ rpd?: number;
7
+ /** Tokens per minute. */
8
+ tpm?: number;
9
+ /** Tokens per day. */
10
+ tpd?: number;
11
+ }
12
+ interface KeySpec {
13
+ /** Unique id for this key (never the secret itself — store a label or hash). */
14
+ id: string;
15
+ provider: string;
16
+ /** The secret. Held in memory only; never persisted by the built-in memory adapter's snapshot of counters. */
17
+ secret: string;
18
+ /** Optional per-key limit overrides (else provider/model defaults apply). */
19
+ limits?: Limits;
20
+ /** Priority — higher is preferred among equally-healthy keys. Default 0. */
21
+ priority?: number;
22
+ }
23
+ /** Rolling counters + health for one key, per model bucket. */
24
+ interface BucketState {
25
+ /** minute window start (epoch ms) and counts */
26
+ minStart: number;
27
+ minReq: number;
28
+ minTok: number;
29
+ /** day window start (epoch ms) and counts */
30
+ dayStart: number;
31
+ dayReq: number;
32
+ dayTok: number;
33
+ }
34
+ interface KeyState {
35
+ id: string;
36
+ provider: string;
37
+ /** epoch ms until which the key is cooling down (0 = healthy). */
38
+ cooldownUntil: number;
39
+ /** true if quarantined (invalid key) — never picked until cleared. */
40
+ invalid: boolean;
41
+ /** per-model buckets, keyed by model id ("" = provider-wide bucket). */
42
+ buckets: Record<string, BucketState>;
43
+ /** round-robin tiebreaker cursor. */
44
+ lastUsed: number;
45
+ }
46
+ /** A persistence adapter so multiple processes share pool state. All async-friendly. */
47
+ interface KeypoolStore {
48
+ load(): Record<string, KeyState> | null | Promise<Record<string, KeyState> | null>;
49
+ save(states: Record<string, KeyState>): void | Promise<void>;
50
+ }
51
+ interface ReportInput {
52
+ ok: boolean;
53
+ /** HTTP status of the response, if any. */
54
+ status?: number;
55
+ /** Tokens actually consumed by the call (input+output). */
56
+ tokens?: number;
57
+ /** Raw response headers (or a subset). `retry-after` and rate-limit remaining headers are read when present. */
58
+ headers?: Record<string, string | number | null | undefined> | {
59
+ get(name: string): string | null;
60
+ };
61
+ /** Model used (defaults to the model passed to pick). */
62
+ model?: string;
63
+ }
64
+
65
+ interface KeypoolOptions {
66
+ keys: KeySpec[];
67
+ /** Default limits per provider, applied to every key unless the key overrides. */
68
+ providerLimits?: Record<string, Limits>;
69
+ /** Per-model limit overrides, e.g. { "groq:llama-3.3-70b": { tpd: 500000 } }. Key format `provider:model`. */
70
+ modelLimits?: Record<string, Limits>;
71
+ /** Cooldown (ms) applied on a 429 when no retry-after header is present. Default 60000. */
72
+ defaultCooldownMs?: number;
73
+ /** Persistence adapter for cross-process sharing. Default: in-memory only. */
74
+ store?: KeypoolStore;
75
+ /** Injectable clock (ms). */
76
+ now?: () => number;
77
+ }
78
+ interface Pick {
79
+ id: string;
80
+ secret: string;
81
+ provider: string;
82
+ model: string;
83
+ }
84
+ interface Keypool {
85
+ /**
86
+ * Pick the best healthy key for (provider, model) that can afford ~estTokens,
87
+ * or null if every key is cooling down / quota-exhausted / invalid.
88
+ */
89
+ pick(provider: string, model?: string, estTokens?: number): Promise<Pick | null>;
90
+ /** Report the outcome of a call so counters, cooldowns and quarantine update. */
91
+ report(id: string, result: ReportInput): Promise<void>;
92
+ /** Reserve usage yourself (when you don't call report), e.g. optimistic accounting. */
93
+ note(id: string, model: string, tokens: number): Promise<void>;
94
+ /** Clear a key's invalid quarantine (after you rotated the secret). */
95
+ reinstate(id: string, secret?: string): Promise<void>;
96
+ /** Snapshot of every key's state. */
97
+ states(): Promise<Record<string, KeyState>>;
98
+ /** How many keys are pickable right now for a provider/model. */
99
+ available(provider: string, model?: string, estTokens?: number): Promise<number>;
100
+ }
101
+ declare function createKeypool(options: KeypoolOptions): Keypool;
102
+ /** A KeypoolStore backed by any {get,set} KV (JSON string values). Handy over Mongo/Redis. */
103
+ declare function kvStore(kv: {
104
+ get(k: string): Promise<string | null> | string | null;
105
+ set(k: string, v: string): Promise<void> | void;
106
+ }, key?: string): KeypoolStore;
107
+
108
+ export { type BucketState, type KeySpec, type KeyState, type Keypool, type KeypoolOptions, type KeypoolStore, type Limits, type Pick, type ReportInput, createKeypool, kvStore };
package/dist/index.js ADDED
@@ -0,0 +1,176 @@
1
+ // src/index.ts
2
+ var MIN = 6e4;
3
+ var DAY = 864e5;
4
+ function emptyBucket(now) {
5
+ return { minStart: now, minReq: 0, minTok: 0, dayStart: now, dayReq: 0, dayTok: 0 };
6
+ }
7
+ function rollBucket(b, now) {
8
+ if (now - b.minStart >= MIN) {
9
+ b.minStart = now;
10
+ b.minReq = 0;
11
+ b.minTok = 0;
12
+ }
13
+ if (now - b.dayStart >= DAY) {
14
+ b.dayStart = now;
15
+ b.dayReq = 0;
16
+ b.dayTok = 0;
17
+ }
18
+ }
19
+ function headerGet(headers, name) {
20
+ if (!headers) return null;
21
+ if (typeof headers.get === "function") return headers.get(name);
22
+ const h = headers;
23
+ const found = h[name] ?? h[name.toLowerCase()] ?? h[name.toUpperCase()];
24
+ return found === void 0 || found === null ? null : String(found);
25
+ }
26
+ function createKeypool(options) {
27
+ const now = options.now ?? (() => Date.now());
28
+ const cooldownMs = options.defaultCooldownMs ?? MIN;
29
+ const store = options.store;
30
+ const specs = /* @__PURE__ */ new Map();
31
+ for (const k of options.keys) specs.set(k.id, k);
32
+ let state = {};
33
+ let loaded = false;
34
+ const ensureLoaded = async () => {
35
+ if (loaded) return;
36
+ const fromStore = store ? await store.load() : null;
37
+ for (const spec of options.keys) {
38
+ state[spec.id] = fromStore?.[spec.id] ?? {
39
+ id: spec.id,
40
+ provider: spec.provider,
41
+ cooldownUntil: 0,
42
+ invalid: false,
43
+ buckets: {},
44
+ lastUsed: 0
45
+ };
46
+ }
47
+ loaded = true;
48
+ };
49
+ const persist = async () => {
50
+ if (store) await store.save(state);
51
+ };
52
+ const limitsFor = (spec, model) => ({
53
+ ...options.providerLimits?.[spec.provider] ?? {},
54
+ ...options.modelLimits?.[`${spec.provider}:${model}`] ?? {},
55
+ ...spec.limits ?? {}
56
+ });
57
+ const bucketOf = (ks, model, t) => {
58
+ let b = ks.buckets[model];
59
+ if (!b) {
60
+ b = emptyBucket(t);
61
+ ks.buckets[model] = b;
62
+ }
63
+ rollBucket(b, t);
64
+ return b;
65
+ };
66
+ const canAfford = (spec, ks, model, estTokens, t) => {
67
+ if (ks.invalid) return false;
68
+ if (ks.cooldownUntil > t) return false;
69
+ const lim = limitsFor(spec, model);
70
+ const b = bucketOf(ks, model, t);
71
+ if (lim.rpm !== void 0 && b.minReq + 1 > lim.rpm) return false;
72
+ if (lim.rpd !== void 0 && b.dayReq + 1 > lim.rpd) return false;
73
+ if (lim.tpm !== void 0 && b.minTok + estTokens > lim.tpm) return false;
74
+ if (lim.tpd !== void 0 && b.dayTok + estTokens > lim.tpd) return false;
75
+ return true;
76
+ };
77
+ const candidates = (provider, model, estTokens, t) => {
78
+ const out = [];
79
+ for (const spec of options.keys) {
80
+ if (spec.provider !== provider) continue;
81
+ const ks = state[spec.id];
82
+ if (canAfford(spec, ks, model, estTokens, t)) out.push({ spec, ks });
83
+ }
84
+ out.sort(
85
+ (a, b) => (b.spec.priority ?? 0) - (a.spec.priority ?? 0) || a.ks.lastUsed - b.ks.lastUsed || a.spec.id.localeCompare(b.spec.id)
86
+ );
87
+ return out;
88
+ };
89
+ return {
90
+ async pick(provider, model = "", estTokens = 0) {
91
+ await ensureLoaded();
92
+ const t = now();
93
+ const cand = candidates(provider, model, estTokens, t)[0];
94
+ if (!cand) return null;
95
+ const b = bucketOf(cand.ks, model, t);
96
+ b.minReq += 1;
97
+ b.dayReq += 1;
98
+ b.minTok += estTokens;
99
+ b.dayTok += estTokens;
100
+ cand.ks.lastUsed = t;
101
+ await persist();
102
+ return { id: cand.spec.id, secret: cand.spec.secret, provider, model };
103
+ },
104
+ async report(id, result) {
105
+ await ensureLoaded();
106
+ const ks = state[id];
107
+ if (!ks) return;
108
+ const t = now();
109
+ const model = result.model ?? Object.keys(ks.buckets)[0] ?? "";
110
+ const b = bucketOf(ks, model, t);
111
+ if (result.tokens !== void 0) {
112
+ b.minTok += result.tokens;
113
+ b.dayTok += result.tokens;
114
+ }
115
+ const retryAfter = headerGet(result.headers, "retry-after");
116
+ const status = result.status ?? (result.ok ? 200 : 0);
117
+ if (status === 401 || status === 403) {
118
+ ks.invalid = true;
119
+ } else if (status === 429) {
120
+ let waitMs = cooldownMs;
121
+ if (retryAfter) {
122
+ const n = Number(retryAfter);
123
+ waitMs = Number.isFinite(n) ? n * 1e3 : Math.max(0, new Date(retryAfter).getTime() - t) || cooldownMs;
124
+ }
125
+ ks.cooldownUntil = t + waitMs;
126
+ } else if (result.ok) {
127
+ if (ks.cooldownUntil <= t) ks.cooldownUntil = 0;
128
+ }
129
+ await persist();
130
+ },
131
+ async note(id, model, tokens) {
132
+ await ensureLoaded();
133
+ const ks = state[id];
134
+ if (!ks) return;
135
+ const b = bucketOf(ks, model, now());
136
+ b.minTok += tokens;
137
+ b.dayTok += tokens;
138
+ await persist();
139
+ },
140
+ async reinstate(id, secret) {
141
+ await ensureLoaded();
142
+ const ks = state[id];
143
+ if (!ks) return;
144
+ ks.invalid = false;
145
+ ks.cooldownUntil = 0;
146
+ if (secret) {
147
+ const spec = specs.get(id);
148
+ if (spec) spec.secret = secret;
149
+ }
150
+ await persist();
151
+ },
152
+ async states() {
153
+ await ensureLoaded();
154
+ return JSON.parse(JSON.stringify(state));
155
+ },
156
+ async available(provider, model = "", estTokens = 0) {
157
+ await ensureLoaded();
158
+ return candidates(provider, model, estTokens, now()).length;
159
+ }
160
+ };
161
+ }
162
+ function kvStore(kv, key = "keypool:state") {
163
+ return {
164
+ async load() {
165
+ const raw = await kv.get(key);
166
+ return raw ? JSON.parse(raw) : null;
167
+ },
168
+ async save(states) {
169
+ await kv.set(key, JSON.stringify(states));
170
+ }
171
+ };
172
+ }
173
+
174
+ export { createKeypool, kvStore };
175
+ //# sourceMappingURL=index.js.map
176
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { BucketState, KeypoolStore, KeySpec, KeyState, Limits, ReportInput } from \"./types.js\";\n\nexport type { BucketState, KeypoolStore, KeySpec, KeyState, Limits, ReportInput } from \"./types.js\";\n\nexport interface KeypoolOptions {\n keys: KeySpec[];\n /** Default limits per provider, applied to every key unless the key overrides. */\n providerLimits?: Record<string, Limits>;\n /** Per-model limit overrides, e.g. { \"groq:llama-3.3-70b\": { tpd: 500000 } }. Key format `provider:model`. */\n modelLimits?: Record<string, Limits>;\n /** Cooldown (ms) applied on a 429 when no retry-after header is present. Default 60000. */\n defaultCooldownMs?: number;\n /** Persistence adapter for cross-process sharing. Default: in-memory only. */\n store?: KeypoolStore;\n /** Injectable clock (ms). */\n now?: () => number;\n}\n\nexport interface Pick {\n id: string;\n secret: string;\n provider: string;\n model: string;\n}\n\nexport interface Keypool {\n /**\n * Pick the best healthy key for (provider, model) that can afford ~estTokens,\n * or null if every key is cooling down / quota-exhausted / invalid.\n */\n pick(provider: string, model?: string, estTokens?: number): Promise<Pick | null>;\n /** Report the outcome of a call so counters, cooldowns and quarantine update. */\n report(id: string, result: ReportInput): Promise<void>;\n /** Reserve usage yourself (when you don't call report), e.g. optimistic accounting. */\n note(id: string, model: string, tokens: number): Promise<void>;\n /** Clear a key's invalid quarantine (after you rotated the secret). */\n reinstate(id: string, secret?: string): Promise<void>;\n /** Snapshot of every key's state. */\n states(): Promise<Record<string, KeyState>>;\n /** How many keys are pickable right now for a provider/model. */\n available(provider: string, model?: string, estTokens?: number): Promise<number>;\n}\n\nconst MIN = 60_000;\nconst DAY = 86_400_000;\n\nfunction emptyBucket(now: number): BucketState {\n return { minStart: now, minReq: 0, minTok: 0, dayStart: now, dayReq: 0, dayTok: 0 };\n}\n\nfunction rollBucket(b: BucketState, now: number): void {\n if (now - b.minStart >= MIN) {\n b.minStart = now;\n b.minReq = 0;\n b.minTok = 0;\n }\n if (now - b.dayStart >= DAY) {\n b.dayStart = now;\n b.dayReq = 0;\n b.dayTok = 0;\n }\n}\n\nfunction headerGet(headers: ReportInput[\"headers\"], name: string): string | null {\n if (!headers) return null;\n if (typeof (headers as { get?: unknown }).get === \"function\") return (headers as { get(n: string): string | null }).get(name);\n const h = headers as Record<string, string | number | null | undefined>;\n const found = h[name] ?? h[name.toLowerCase()] ?? h[name.toUpperCase()];\n return found === undefined || found === null ? null : String(found);\n}\n\nexport function createKeypool(options: KeypoolOptions): Keypool {\n const now = options.now ?? (() => Date.now());\n const cooldownMs = options.defaultCooldownMs ?? MIN;\n const store = options.store;\n const specs = new Map<string, KeySpec>();\n for (const k of options.keys) specs.set(k.id, k);\n\n // Live state, hydrated from the store if present.\n let state: Record<string, KeyState> = {};\n let loaded = false;\n\n const ensureLoaded = async () => {\n if (loaded) return;\n const fromStore = store ? await store.load() : null;\n for (const spec of options.keys) {\n state[spec.id] = fromStore?.[spec.id] ?? {\n id: spec.id,\n provider: spec.provider,\n cooldownUntil: 0,\n invalid: false,\n buckets: {},\n lastUsed: 0,\n };\n }\n loaded = true;\n };\n\n const persist = async () => {\n if (store) await store.save(state);\n };\n\n const limitsFor = (spec: KeySpec, model: string): Limits => ({\n ...(options.providerLimits?.[spec.provider] ?? {}),\n ...(options.modelLimits?.[`${spec.provider}:${model}`] ?? {}),\n ...(spec.limits ?? {}),\n });\n\n const bucketOf = (ks: KeyState, model: string, t: number): BucketState => {\n let b = ks.buckets[model];\n if (!b) {\n b = emptyBucket(t);\n ks.buckets[model] = b;\n }\n rollBucket(b, t);\n return b;\n };\n\n const canAfford = (spec: KeySpec, ks: KeyState, model: string, estTokens: number, t: number): boolean => {\n if (ks.invalid) return false;\n if (ks.cooldownUntil > t) return false;\n const lim = limitsFor(spec, model);\n const b = bucketOf(ks, model, t);\n if (lim.rpm !== undefined && b.minReq + 1 > lim.rpm) return false;\n if (lim.rpd !== undefined && b.dayReq + 1 > lim.rpd) return false;\n if (lim.tpm !== undefined && b.minTok + estTokens > lim.tpm) return false;\n if (lim.tpd !== undefined && b.dayTok + estTokens > lim.tpd) return false;\n return true;\n };\n\n const candidates = (provider: string, model: string, estTokens: number, t: number): { spec: KeySpec; ks: KeyState }[] => {\n const out: { spec: KeySpec; ks: KeyState }[] = [];\n for (const spec of options.keys) {\n if (spec.provider !== provider) continue;\n const ks = state[spec.id]!;\n if (canAfford(spec, ks, model, estTokens, t)) out.push({ spec, ks });\n }\n // Highest priority, then least-recently-used (round-robin), then id for stability.\n out.sort(\n (a, b) =>\n (b.spec.priority ?? 0) - (a.spec.priority ?? 0) ||\n a.ks.lastUsed - b.ks.lastUsed ||\n a.spec.id.localeCompare(b.spec.id),\n );\n return out;\n };\n\n return {\n async pick(provider, model = \"\", estTokens = 0) {\n await ensureLoaded();\n const t = now();\n const cand = candidates(provider, model, estTokens, t)[0];\n if (!cand) return null;\n // Optimistically reserve one request + estimated tokens so concurrent picks spread out.\n const b = bucketOf(cand.ks, model, t);\n b.minReq += 1;\n b.dayReq += 1;\n b.minTok += estTokens;\n b.dayTok += estTokens;\n cand.ks.lastUsed = t;\n await persist();\n return { id: cand.spec.id, secret: cand.spec.secret, provider, model };\n },\n\n async report(id, result) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n const t = now();\n const model = result.model ?? Object.keys(ks.buckets)[0] ?? \"\";\n const b = bucketOf(ks, model, t);\n\n // Reconcile token count if the caller reserved an estimate at pick().\n if (result.tokens !== undefined) {\n b.minTok += result.tokens; // add ACTUAL on top of the estimate reserved at pick; callers can pass a delta if they prefer\n b.dayTok += result.tokens;\n }\n\n // Absorb provider-reported remaining/limits when present.\n const retryAfter = headerGet(result.headers, \"retry-after\");\n const status = result.status ?? (result.ok ? 200 : 0);\n\n if (status === 401 || status === 403) {\n ks.invalid = true;\n } else if (status === 429) {\n let waitMs = cooldownMs;\n if (retryAfter) {\n const n = Number(retryAfter);\n waitMs = Number.isFinite(n) ? n * 1000 : Math.max(0, new Date(retryAfter).getTime() - t) || cooldownMs;\n }\n ks.cooldownUntil = t + waitMs;\n } else if (result.ok) {\n // clear any stale cooldown on a clean success\n if (ks.cooldownUntil <= t) ks.cooldownUntil = 0;\n }\n await persist();\n },\n\n async note(id, model, tokens) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n const b = bucketOf(ks, model, now());\n b.minTok += tokens;\n b.dayTok += tokens;\n await persist();\n },\n\n async reinstate(id, secret) {\n await ensureLoaded();\n const ks = state[id];\n if (!ks) return;\n ks.invalid = false;\n ks.cooldownUntil = 0;\n if (secret) {\n const spec = specs.get(id);\n if (spec) spec.secret = secret;\n }\n await persist();\n },\n\n async states() {\n await ensureLoaded();\n return JSON.parse(JSON.stringify(state));\n },\n\n async available(provider, model = \"\", estTokens = 0) {\n await ensureLoaded();\n return candidates(provider, model, estTokens, now()).length;\n },\n };\n}\n\n/** A KeypoolStore backed by any {get,set} KV (JSON string values). Handy over Mongo/Redis. */\nexport function kvStore(kv: { get(k: string): Promise<string | null> | string | null; set(k: string, v: string): Promise<void> | void }, key = \"keypool:state\"): KeypoolStore {\n return {\n async load() {\n const raw = await kv.get(key);\n return raw ? (JSON.parse(raw) as Record<string, KeyState>) : null;\n },\n async save(states) {\n await kv.set(key, JSON.stringify(states));\n },\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@lacspace/keypool",
3
+ "version": "1.0.0",
4
+ "description": "Provider-agnostic API-key rotation and rate-limit accounting — pool N free keys per provider, track RPM/RPD/TPM/TPD windows from response headers and 429 retry-after, round-robin among healthy keys, cool down on 429, quarantine invalid keys, and keep per-model buckets (Groq TPD is per model). pick(provider, model, estTokens) → key | null; report(key, result). Persist state via a tiny adapter so multiple processes share the pool. Zero-dependency, isomorphic, typed.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
12
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "sideEffects": false,
17
+ "scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
18
+ "keywords": ["api-key", "key-rotation", "rate-limit", "keypool", "round-robin", "429", "retry-after", "quota", "rpm", "tpm", "gemini", "groq", "openai", "free-tier", "zero-dependency", "isomorphic", "typescript"],
19
+ "author": "Lacspace <contact@lacspace.com>",
20
+ "license": "SEE LICENSE IN LICENSE",
21
+ "homepage": "https://developer.lacspace.com/packages/keypool",
22
+ "repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "keypool" },
23
+ "bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
24
+ "engines": { "node": ">=18" },
25
+ "publishConfig": { "access": "public" }
26
+ }