@poolzin/pool-bot 2026.2.20 → 2026.2.21

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.
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Token Pool Manager
3
+ *
4
+ * Manages multiple API keys per provider with intelligent rotation,
5
+ * rate limit awareness, and scheduling strategies.
6
+ *
7
+ * @module provider/token-pool
8
+ */
9
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
10
+ import { RateLimits } from "./rate-limits.js";
11
+ export var TokenPool;
12
+ (function (TokenPool) {
13
+ const log = createSubsystemLogger("provider/token-pool");
14
+ // ============================================================================
15
+ // Internal State
16
+ // ============================================================================
17
+ /** Pools per provider */
18
+ const pools = new Map();
19
+ /** Maximum number of provider pools to prevent unbounded growth */
20
+ const MAX_POOLS = 50;
21
+ /** Default pool configuration */
22
+ const DEFAULT_CONFIG = {
23
+ scheduling: "priority",
24
+ maxWaitMs: 5000,
25
+ autoDisable: true,
26
+ autoDisableThreshold: 5,
27
+ };
28
+ /** Tier priority (lower is higher priority) */
29
+ const TIER_PRIORITY = {
30
+ enterprise: 0,
31
+ paid: 1,
32
+ free: 2,
33
+ };
34
+ // ============================================================================
35
+ // Helper Functions
36
+ // ============================================================================
37
+ /**
38
+ * Gets or creates a pool for a provider.
39
+ * Enforces MAX_POOLS limit by evicting least recently used pools.
40
+ */
41
+ function getOrCreatePool(providerID) {
42
+ let pool = pools.get(providerID);
43
+ if (!pool) {
44
+ if (pools.size >= MAX_POOLS) {
45
+ evictLeastUsedPool();
46
+ }
47
+ pool = {
48
+ providerID,
49
+ config: { ...DEFAULT_CONFIG },
50
+ tokens: new Map(),
51
+ currentIndex: 0,
52
+ failureCounts: new Map(),
53
+ };
54
+ pools.set(providerID, pool);
55
+ }
56
+ return pool;
57
+ }
58
+ /**
59
+ * Evicts the pool with the fewest total requests (least used).
60
+ */
61
+ function evictLeastUsedPool() {
62
+ let leastUsedID;
63
+ let leastUsedRequests = Infinity;
64
+ for (const [providerID, pool] of pools.entries()) {
65
+ const totalRequests = Array.from(pool.tokens.values()).reduce((sum, t) => sum + t.usage.totalRequests, 0);
66
+ if (totalRequests < leastUsedRequests) {
67
+ leastUsedRequests = totalRequests;
68
+ leastUsedID = providerID;
69
+ }
70
+ }
71
+ if (leastUsedID) {
72
+ pools.delete(leastUsedID);
73
+ RateLimits.clearProvider(leastUsedID);
74
+ log.info("pool-evicted", { providerID: leastUsedID, reason: "max-pools-reached" });
75
+ }
76
+ }
77
+ /**
78
+ * Creates a default token entry.
79
+ */
80
+ function createTokenEntry(id, key, tier = "paid", label) {
81
+ return {
82
+ id,
83
+ key,
84
+ tier,
85
+ enabled: true,
86
+ label,
87
+ usage: {
88
+ totalRequests: 0,
89
+ totalInputTokens: 0,
90
+ totalOutputTokens: 0,
91
+ requestsToday: 0,
92
+ },
93
+ addedAt: Date.now(),
94
+ };
95
+ }
96
+ /**
97
+ * Gets available (enabled and not rate-limited) tokens from a pool.
98
+ */
99
+ function getAvailableTokens(pool) {
100
+ const available = [];
101
+ for (const token of pool.tokens.values()) {
102
+ if (!token.enabled)
103
+ continue;
104
+ const limitCheck = RateLimits.isLimited(pool.providerID, token.id);
105
+ if (!limitCheck.isLimited) {
106
+ available.push(token);
107
+ }
108
+ }
109
+ return available;
110
+ }
111
+ /**
112
+ * Sorts tokens by scheduling strategy.
113
+ */
114
+ function sortByStrategy(tokens, mode) {
115
+ switch (mode) {
116
+ case "priority":
117
+ return [...tokens].sort((a, b) => {
118
+ // First by tier
119
+ const tierDiff = TIER_PRIORITY[a.tier] - TIER_PRIORITY[b.tier];
120
+ if (tierDiff !== 0)
121
+ return tierDiff;
122
+ // Then by least used
123
+ return a.usage.totalRequests - b.usage.totalRequests;
124
+ });
125
+ case "least-used":
126
+ return [...tokens].sort((a, b) => a.usage.totalRequests - b.usage.totalRequests);
127
+ case "random":
128
+ return [...tokens].sort(() => Math.random() - 0.5);
129
+ case "round-robin":
130
+ default:
131
+ return tokens;
132
+ }
133
+ }
134
+ /**
135
+ * Gets the current date string (YYYY-MM-DD).
136
+ */
137
+ function getDateString() {
138
+ return new Date().toISOString().split("T")[0];
139
+ }
140
+ /**
141
+ * Resets daily counters if needed.
142
+ */
143
+ function resetDailyCountersIfNeeded(token) {
144
+ const today = getDateString();
145
+ if (token.usage.requestsDate !== today) {
146
+ token.usage.requestsToday = 0;
147
+ token.usage.requestsDate = today;
148
+ }
149
+ }
150
+ // ============================================================================
151
+ // Public API
152
+ // ============================================================================
153
+ /**
154
+ * Adds a token to a provider's pool.
155
+ *
156
+ * @param providerID - Provider identifier
157
+ * @param id - Unique token identifier
158
+ * @param key - The API key
159
+ * @param options - Additional options
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * TokenPool.addToken("anthropic", "personal", "sk-ant-...", {
164
+ * tier: "paid",
165
+ * label: "Personal API Key"
166
+ * })
167
+ * ```
168
+ */
169
+ function addToken(providerID, id, key, options = {}) {
170
+ const pool = getOrCreatePool(providerID);
171
+ const entry = createTokenEntry(id, key, options.tier ?? "paid", options.label);
172
+ if (options.enabled !== undefined) {
173
+ entry.enabled = options.enabled;
174
+ }
175
+ pool.tokens.set(id, entry);
176
+ log.info("token-added", { providerID, tokenID: id, tier: entry.tier });
177
+ return entry;
178
+ }
179
+ TokenPool.addToken = addToken;
180
+ /**
181
+ * Removes a token from a provider's pool.
182
+ *
183
+ * @param providerID - Provider identifier
184
+ * @param tokenID - Token identifier to remove
185
+ * @returns Whether the token was found and removed
186
+ */
187
+ function removeToken(providerID, tokenID) {
188
+ const pool = pools.get(providerID);
189
+ if (!pool)
190
+ return false;
191
+ const removed = pool.tokens.delete(tokenID);
192
+ if (removed) {
193
+ pool.failureCounts.delete(tokenID);
194
+ RateLimits.clear(providerID, tokenID);
195
+ log.info("token-removed", { providerID, tokenID });
196
+ }
197
+ return removed;
198
+ }
199
+ TokenPool.removeToken = removeToken;
200
+ /**
201
+ * Configures a provider's token pool.
202
+ *
203
+ * @param providerID - Provider identifier
204
+ * @param poolConfig - Partial configuration to merge
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * TokenPool.configure("anthropic", {
209
+ * scheduling: "priority",
210
+ * maxWaitMs: 10000
211
+ * })
212
+ * ```
213
+ */
214
+ function configure(providerID, poolConfig) {
215
+ const pool = getOrCreatePool(providerID);
216
+ pool.config = { ...pool.config, ...poolConfig };
217
+ log.info("configured", { providerID, config: { ...pool.config } });
218
+ }
219
+ TokenPool.configure = configure;
220
+ /**
221
+ * Gets the current configuration for a provider.
222
+ *
223
+ * @param providerID - Provider identifier
224
+ * @returns Configuration or default if no pool exists
225
+ */
226
+ function getConfig(providerID) {
227
+ const pool = pools.get(providerID);
228
+ return pool?.config ?? { ...DEFAULT_CONFIG };
229
+ }
230
+ TokenPool.getConfig = getConfig;
231
+ /**
232
+ * Gets a token from the pool using the configured scheduling strategy.
233
+ * Automatically skips rate-limited tokens.
234
+ *
235
+ * @param providerID - Provider identifier
236
+ * @param options - Selection options
237
+ * @returns Token result or undefined if no tokens available
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * const result = await TokenPool.getToken("anthropic")
242
+ * if (result) {
243
+ * console.log("Using token:", result.token.id)
244
+ * // Make API call with result.token.key
245
+ * }
246
+ * ```
247
+ */
248
+ async function getToken(providerID, options = {}) {
249
+ const pool = pools.get(providerID);
250
+ if (!pool || pool.tokens.size === 0) {
251
+ return undefined;
252
+ }
253
+ const maxWaitMs = options.maxWaitMs ?? pool.config.maxWaitMs;
254
+ // Check preferred token first (for sticky sessions)
255
+ if (options.preferredTokenID) {
256
+ const preferred = pool.tokens.get(options.preferredTokenID);
257
+ if (preferred?.enabled) {
258
+ const limitCheck = RateLimits.isLimited(providerID, preferred.id);
259
+ if (!limitCheck.isLimited) {
260
+ return {
261
+ token: preferred,
262
+ waited: false,
263
+ waitedMs: 0,
264
+ onlyOption: pool.tokens.size === 1,
265
+ };
266
+ }
267
+ // Wait for preferred if requested and wait time is acceptable
268
+ if (options.waitForPreferred && limitCheck.waitTimeMs <= maxWaitMs) {
269
+ log.info("waiting-for-preferred", {
270
+ providerID,
271
+ tokenID: preferred.id,
272
+ waitMs: limitCheck.waitTimeMs,
273
+ });
274
+ await sleep(limitCheck.waitTimeMs);
275
+ return {
276
+ token: preferred,
277
+ waited: true,
278
+ waitedMs: limitCheck.waitTimeMs,
279
+ onlyOption: pool.tokens.size === 1,
280
+ };
281
+ }
282
+ }
283
+ }
284
+ // Get available tokens
285
+ const available = getAvailableTokens(pool);
286
+ if (available.length > 0) {
287
+ // Sort by strategy
288
+ const sorted = sortByStrategy(available, pool.config.scheduling);
289
+ // For round-robin, use index
290
+ let selected;
291
+ if (pool.config.scheduling === "round-robin") {
292
+ pool.currentIndex = pool.currentIndex % sorted.length;
293
+ selected = sorted[pool.currentIndex];
294
+ pool.currentIndex++;
295
+ }
296
+ else {
297
+ selected = sorted[0];
298
+ }
299
+ return {
300
+ token: selected,
301
+ waited: false,
302
+ waitedMs: 0,
303
+ onlyOption: available.length === 1,
304
+ };
305
+ }
306
+ // All tokens are rate-limited, find the one that resets soonest
307
+ const allTokens = Array.from(pool.tokens.values()).filter((t) => t.enabled);
308
+ if (allTokens.length === 0) {
309
+ return undefined;
310
+ }
311
+ let soonestToken;
312
+ let soonestWaitMs = Infinity;
313
+ for (const token of allTokens) {
314
+ const limitCheck = RateLimits.isLimited(providerID, token.id);
315
+ if (limitCheck.waitTimeMs < soonestWaitMs) {
316
+ soonestWaitMs = limitCheck.waitTimeMs;
317
+ soonestToken = token;
318
+ }
319
+ }
320
+ if (!soonestToken) {
321
+ return undefined;
322
+ }
323
+ // Wait if within acceptable range
324
+ if (soonestWaitMs <= maxWaitMs) {
325
+ log.info("waiting-for-soonest", {
326
+ providerID,
327
+ tokenID: soonestToken.id,
328
+ waitMs: soonestWaitMs,
329
+ });
330
+ await sleep(soonestWaitMs);
331
+ return {
332
+ token: soonestToken,
333
+ waited: true,
334
+ waitedMs: soonestWaitMs,
335
+ onlyOption: allTokens.length === 1,
336
+ };
337
+ }
338
+ // Return soonest token anyway (caller can decide to wait or fail)
339
+ return {
340
+ token: soonestToken,
341
+ waited: false,
342
+ waitedMs: 0,
343
+ onlyOption: allTokens.length === 1,
344
+ };
345
+ }
346
+ TokenPool.getToken = getToken;
347
+ /**
348
+ * Records a successful request with a token.
349
+ *
350
+ * @param providerID - Provider identifier
351
+ * @param tokenID - Token identifier
352
+ * @param usage - Token usage from the request
353
+ */
354
+ function recordSuccess(providerID, tokenID, usage) {
355
+ const pool = pools.get(providerID);
356
+ const token = pool?.tokens.get(tokenID);
357
+ if (!pool || !token)
358
+ return;
359
+ // Reset failure count on success
360
+ pool.failureCounts.set(tokenID, 0);
361
+ // Update usage stats
362
+ resetDailyCountersIfNeeded(token);
363
+ token.usage.totalRequests++;
364
+ token.usage.requestsToday++;
365
+ token.usage.lastUsedAt = Date.now();
366
+ if (usage?.inputTokens) {
367
+ token.usage.totalInputTokens += usage.inputTokens;
368
+ }
369
+ if (usage?.outputTokens) {
370
+ token.usage.totalOutputTokens += usage.outputTokens;
371
+ }
372
+ }
373
+ TokenPool.recordSuccess = recordSuccess;
374
+ /**
375
+ * Records a failed request with a token.
376
+ * May auto-disable the token if threshold is reached.
377
+ *
378
+ * @param providerID - Provider identifier
379
+ * @param tokenID - Token identifier
380
+ * @param statusCode - HTTP status code
381
+ * @param headers - Response headers (for rate limit parsing)
382
+ */
383
+ function recordFailure(providerID, tokenID, statusCode, headers) {
384
+ const pool = pools.get(providerID);
385
+ const token = pool?.tokens.get(tokenID);
386
+ if (!pool || !token)
387
+ return;
388
+ // Track rate limits
389
+ if (statusCode === RateLimits.RATE_LIMIT_STATUS_CODES.TOO_MANY_REQUESTS ||
390
+ statusCode === RateLimits.RATE_LIMIT_STATUS_CODES.SERVICE_UNAVAILABLE ||
391
+ statusCode === RateLimits.RATE_LIMIT_STATUS_CODES.SITE_OVERLOADED) {
392
+ if (headers) {
393
+ RateLimits.markLimitedFromResponse(providerID, statusCode, headers, tokenID);
394
+ }
395
+ else {
396
+ RateLimits.markLimited(providerID, statusCode, `HTTP ${statusCode}`, {
397
+ keyID: tokenID,
398
+ });
399
+ }
400
+ }
401
+ // Track consecutive failures
402
+ const failures = (pool.failureCounts.get(tokenID) ?? 0) + 1;
403
+ pool.failureCounts.set(tokenID, failures);
404
+ // Auto-disable if threshold reached
405
+ if (pool.config.autoDisable && failures >= pool.config.autoDisableThreshold) {
406
+ token.enabled = false;
407
+ log.warn("token-auto-disabled", {
408
+ providerID,
409
+ tokenID,
410
+ failures,
411
+ threshold: pool.config.autoDisableThreshold,
412
+ });
413
+ }
414
+ }
415
+ TokenPool.recordFailure = recordFailure;
416
+ /**
417
+ * Enables a previously disabled token.
418
+ *
419
+ * @param providerID - Provider identifier
420
+ * @param tokenID - Token identifier
421
+ */
422
+ function enableToken(providerID, tokenID) {
423
+ const pool = pools.get(providerID);
424
+ const token = pool?.tokens.get(tokenID);
425
+ if (!pool || !token)
426
+ return false;
427
+ token.enabled = true;
428
+ pool.failureCounts.set(tokenID, 0);
429
+ RateLimits.clear(providerID, tokenID);
430
+ log.info("token-enabled", { providerID, tokenID });
431
+ return true;
432
+ }
433
+ TokenPool.enableToken = enableToken;
434
+ /**
435
+ * Disables a token.
436
+ *
437
+ * @param providerID - Provider identifier
438
+ * @param tokenID - Token identifier
439
+ */
440
+ function disableToken(providerID, tokenID) {
441
+ const pool = pools.get(providerID);
442
+ const token = pool?.tokens.get(tokenID);
443
+ if (!token)
444
+ return false;
445
+ token.enabled = false;
446
+ log.info("token-disabled", { providerID, tokenID });
447
+ return true;
448
+ }
449
+ TokenPool.disableToken = disableToken;
450
+ /**
451
+ * Gets all tokens for a provider.
452
+ *
453
+ * @param providerID - Provider identifier
454
+ * @returns Array of token entries (without key values for security)
455
+ */
456
+ function getTokens(providerID) {
457
+ const pool = pools.get(providerID);
458
+ if (!pool)
459
+ return [];
460
+ return Array.from(pool.tokens.values()).map((t) => {
461
+ const { key: _key, ...rest } = t;
462
+ return rest;
463
+ });
464
+ }
465
+ TokenPool.getTokens = getTokens;
466
+ /**
467
+ * Gets a summary of a provider's pool.
468
+ *
469
+ * @param providerID - Provider identifier
470
+ */
471
+ function getPoolSummary(providerID) {
472
+ const pool = pools.get(providerID);
473
+ if (!pool)
474
+ return undefined;
475
+ const tokens = Array.from(pool.tokens.values());
476
+ const enabled = tokens.filter((t) => t.enabled);
477
+ const available = getAvailableTokens(pool);
478
+ const rateLimited = enabled.length - available.length;
479
+ return {
480
+ providerID,
481
+ config: pool.config,
482
+ totalTokens: tokens.length,
483
+ enabledTokens: enabled.length,
484
+ availableTokens: available.length,
485
+ rateLimitedTokens: rateLimited,
486
+ tokens: tokens.map((t) => {
487
+ const { key: _key, ...rest } = t;
488
+ return rest;
489
+ }),
490
+ };
491
+ }
492
+ TokenPool.getPoolSummary = getPoolSummary;
493
+ /**
494
+ * Gets summaries for all pools.
495
+ */
496
+ function getAllPoolSummaries() {
497
+ const result = {};
498
+ for (const providerID of pools.keys()) {
499
+ result[providerID] = getPoolSummary(providerID);
500
+ }
501
+ return result;
502
+ }
503
+ TokenPool.getAllPoolSummaries = getAllPoolSummaries;
504
+ /**
505
+ * Checks if a provider has a token pool configured.
506
+ *
507
+ * @param providerID - Provider identifier
508
+ */
509
+ function hasPool(providerID) {
510
+ const pool = pools.get(providerID);
511
+ return pool !== undefined && pool.tokens.size > 0;
512
+ }
513
+ TokenPool.hasPool = hasPool;
514
+ /**
515
+ * Clears all tokens from a provider's pool.
516
+ *
517
+ * @param providerID - Provider identifier
518
+ */
519
+ function clearPool(providerID) {
520
+ pools.delete(providerID);
521
+ RateLimits.clearProvider(providerID);
522
+ log.info("pool-cleared", { providerID });
523
+ }
524
+ TokenPool.clearPool = clearPool;
525
+ /**
526
+ * Clears all pools.
527
+ */
528
+ function clearAll() {
529
+ const count = pools.size;
530
+ pools.clear();
531
+ RateLimits.clearAll();
532
+ log.info("all-pools-cleared", { count });
533
+ }
534
+ TokenPool.clearAll = clearAll;
535
+ /**
536
+ * Simple sleep utility.
537
+ */
538
+ function sleep(ms) {
539
+ return new Promise((resolve) => setTimeout(resolve, ms));
540
+ }
541
+ })(TokenPool || (TokenPool = {}));
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.2.20",
3
- "commit": "d55929d079a2231e96e962d49e52c54efb661a80",
4
- "builtAt": "2026-02-18T03:33:36.817Z"
2
+ "version": "2026.2.21",
3
+ "commit": "c1b8db5b38e189213061393716632d810d42bafd",
4
+ "builtAt": "2026-02-18T06:05:30.087Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolzin/pool-bot",
3
- "version": "2026.2.20",
3
+ "version": "2026.2.21",
4
4
  "description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
5
5
  "keywords": [],
6
6
  "license": "MIT",