@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,449 @@
1
+ /**
2
+ * Request Monitor
3
+ *
4
+ * Provides observability into API requests for debugging, cost analysis, and analytics.
5
+ * Implements a ring buffer for memory-efficient storage with optional persistence.
6
+ *
7
+ * @module provider/request-monitor
8
+ */
9
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
10
+ export var RequestMonitor;
11
+ (function (RequestMonitor) {
12
+ const log = createSubsystemLogger("provider/request-monitor");
13
+ // ============================================================================
14
+ // Internal State
15
+ // ============================================================================
16
+ /** Ring buffer for request logs */
17
+ const logs = [];
18
+ /** Per-provider statistics */
19
+ const providerStats = new Map();
20
+ /** Overall statistics */
21
+ let globalStats = createEmptyStats();
22
+ /** Event listeners */
23
+ const listeners = new Set();
24
+ /** Monitor configuration */
25
+ let config = {
26
+ enabled: true,
27
+ maxLogs: 1000,
28
+ calculateCosts: true,
29
+ emitEvents: true,
30
+ };
31
+ /** Rolling window for RPM/TPM calculations (last 60 seconds) */
32
+ const rollingWindow = [];
33
+ // ============================================================================
34
+ // Helper Functions
35
+ // ============================================================================
36
+ /**
37
+ * Creates an empty stats object.
38
+ */
39
+ function createEmptyStats() {
40
+ return {
41
+ totalRequests: 0,
42
+ successCount: 0,
43
+ errorCount: 0,
44
+ rateLimitCount: 0,
45
+ totalInputTokens: 0,
46
+ totalOutputTokens: 0,
47
+ totalCacheReadTokens: 0,
48
+ totalCacheWriteTokens: 0,
49
+ avgLatencyMs: 0,
50
+ minLatencyMs: Infinity,
51
+ maxLatencyMs: 0,
52
+ p50LatencyMs: 0,
53
+ p95LatencyMs: 0,
54
+ p99LatencyMs: 0,
55
+ totalCostUSD: 0,
56
+ requestsPerMinute: 0,
57
+ tokensPerMinute: 0,
58
+ };
59
+ }
60
+ /**
61
+ * Generates a unique request ID.
62
+ */
63
+ function generateID() {
64
+ const timestamp = Date.now().toString(36);
65
+ const random = Math.random().toString(36).substring(2, 8);
66
+ return `req_${timestamp}_${random}`;
67
+ }
68
+ /**
69
+ * Updates statistics with a new request log.
70
+ */
71
+ function updateStats(stats, entry) {
72
+ stats.totalRequests++;
73
+ if (entry.status >= 200 && entry.status < 400) {
74
+ stats.successCount++;
75
+ }
76
+ else {
77
+ stats.errorCount++;
78
+ }
79
+ if (entry.status === 429) {
80
+ stats.rateLimitCount++;
81
+ }
82
+ stats.totalInputTokens += entry.inputTokens ?? 0;
83
+ stats.totalOutputTokens += entry.outputTokens ?? 0;
84
+ stats.totalCacheReadTokens += entry.cacheReadTokens ?? 0;
85
+ stats.totalCacheWriteTokens += entry.cacheWriteTokens ?? 0;
86
+ stats.totalCostUSD += entry.costUSD ?? 0;
87
+ // Update latency stats
88
+ stats.minLatencyMs = Math.min(stats.minLatencyMs, entry.latencyMs);
89
+ stats.maxLatencyMs = Math.max(stats.maxLatencyMs, entry.latencyMs);
90
+ // Rolling average for avgLatencyMs
91
+ stats.avgLatencyMs =
92
+ (stats.avgLatencyMs * (stats.totalRequests - 1) + entry.latencyMs) / stats.totalRequests;
93
+ // Update first/last timestamps
94
+ if (!stats.firstRequestAt) {
95
+ stats.firstRequestAt = entry.timestamp;
96
+ }
97
+ stats.lastRequestAt = entry.timestamp;
98
+ }
99
+ /**
100
+ * Calculates percentile latencies from the log buffer.
101
+ */
102
+ function calculatePercentiles(stats) {
103
+ if (logs.length === 0)
104
+ return;
105
+ const latencies = logs.map((l) => l.latencyMs).sort((a, b) => a - b);
106
+ const len = latencies.length;
107
+ stats.p50LatencyMs = latencies[Math.floor(len * 0.5)] ?? 0;
108
+ stats.p95LatencyMs = latencies[Math.floor(len * 0.95)] ?? 0;
109
+ stats.p99LatencyMs = latencies[Math.floor(len * 0.99)] ?? 0;
110
+ }
111
+ /**
112
+ * Updates rolling window metrics (RPM, TPM).
113
+ */
114
+ function updateRollingMetrics() {
115
+ const now = Date.now();
116
+ const oneMinuteAgo = now - 60_000;
117
+ // Remove old entries
118
+ while (rollingWindow.length > 0 && rollingWindow[0].timestamp < oneMinuteAgo) {
119
+ rollingWindow.shift();
120
+ }
121
+ // Calculate RPM and TPM
122
+ globalStats.requestsPerMinute = rollingWindow.length;
123
+ globalStats.tokensPerMinute = rollingWindow.reduce((sum, entry) => sum + entry.tokens, 0);
124
+ // Update per-provider stats
125
+ for (const [providerID, stats] of providerStats.entries()) {
126
+ const providerLogs = logs.filter((l) => l.providerID === providerID && l.timestamp >= oneMinuteAgo);
127
+ stats.requestsPerMinute = providerLogs.length;
128
+ stats.tokensPerMinute = providerLogs.reduce((sum, l) => sum + (l.inputTokens ?? 0) + (l.outputTokens ?? 0), 0);
129
+ }
130
+ }
131
+ // ============================================================================
132
+ // Public API
133
+ // ============================================================================
134
+ /**
135
+ * Configures the request monitor.
136
+ *
137
+ * @param newConfig - Partial configuration to merge
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * RequestMonitor.configure({
142
+ * maxLogs: 500,
143
+ * calculateCosts: false
144
+ * })
145
+ * ```
146
+ */
147
+ function configure(newConfig) {
148
+ config = { ...config, ...newConfig };
149
+ log.info("configured", { ...config });
150
+ // Trim logs if max reduced
151
+ while (logs.length > config.maxLogs) {
152
+ logs.shift();
153
+ }
154
+ }
155
+ RequestMonitor.configure = configure;
156
+ /**
157
+ * Gets the current configuration.
158
+ */
159
+ function getConfig() {
160
+ return { ...config };
161
+ }
162
+ RequestMonitor.getConfig = getConfig;
163
+ /**
164
+ * Logs a new API request.
165
+ *
166
+ * @param entry - Partial request log entry (id and timestamp auto-generated if missing)
167
+ * @returns The complete log entry
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const entry = RequestMonitor.logRequest({
172
+ * providerID: "anthropic",
173
+ * modelID: "claude-sonnet-4-20250514",
174
+ * method: "chat",
175
+ * status: 200,
176
+ * latencyMs: 1234,
177
+ * inputTokens: 500,
178
+ * outputTokens: 200,
179
+ * streaming: true
180
+ * })
181
+ * ```
182
+ */
183
+ function logRequest(entry) {
184
+ if (!config.enabled) {
185
+ return {
186
+ ...entry,
187
+ id: entry.id ?? generateID(),
188
+ timestamp: entry.timestamp ?? Date.now(),
189
+ };
190
+ }
191
+ const requestLog = {
192
+ ...entry,
193
+ id: entry.id ?? generateID(),
194
+ timestamp: entry.timestamp ?? Date.now(),
195
+ };
196
+ // Add to ring buffer (remove oldest if at capacity)
197
+ if (logs.length >= config.maxLogs) {
198
+ logs.shift();
199
+ }
200
+ logs.push(requestLog);
201
+ // Update stats
202
+ updateStats(globalStats, requestLog);
203
+ if (!providerStats.has(requestLog.providerID)) {
204
+ providerStats.set(requestLog.providerID, createEmptyStats());
205
+ }
206
+ updateStats(providerStats.get(requestLog.providerID), requestLog);
207
+ // Update rolling window
208
+ const totalTokens = (requestLog.inputTokens ?? 0) + (requestLog.outputTokens ?? 0);
209
+ rollingWindow.push({ timestamp: requestLog.timestamp, tokens: totalTokens });
210
+ updateRollingMetrics();
211
+ // Emit event
212
+ if (config.emitEvents) {
213
+ const event = { type: "request", log: requestLog };
214
+ for (const listener of listeners) {
215
+ try {
216
+ listener(event);
217
+ }
218
+ catch (e) {
219
+ log.error("event-listener-error", { error: String(e) });
220
+ }
221
+ }
222
+ }
223
+ log.debug("logged", {
224
+ id: requestLog.id,
225
+ provider: requestLog.providerID,
226
+ model: requestLog.modelID,
227
+ status: requestLog.status,
228
+ latency: requestLog.latencyMs,
229
+ });
230
+ return requestLog;
231
+ }
232
+ RequestMonitor.logRequest = logRequest;
233
+ /**
234
+ * Gets recent request logs.
235
+ *
236
+ * @param options - Filter options
237
+ * @returns Array of request logs (newest first)
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // Get last 10 logs
242
+ * const recent = RequestMonitor.getRecentLogs({ limit: 10 })
243
+ *
244
+ * // Get logs for a specific provider
245
+ * const anthropicLogs = RequestMonitor.getRecentLogs({
246
+ * providerID: "anthropic",
247
+ * limit: 50
248
+ * })
249
+ * ```
250
+ */
251
+ function getRecentLogs(options = {}) {
252
+ let result = [...logs];
253
+ // Apply filters
254
+ if (options.providerID) {
255
+ result = result.filter((l) => l.providerID === options.providerID);
256
+ }
257
+ if (options.modelID) {
258
+ result = result.filter((l) => l.modelID === options.modelID);
259
+ }
260
+ if (options.sessionID) {
261
+ result = result.filter((l) => l.sessionID === options.sessionID);
262
+ }
263
+ if (options.minStatus !== undefined) {
264
+ result = result.filter((l) => l.status >= options.minStatus);
265
+ }
266
+ if (options.maxStatus !== undefined) {
267
+ result = result.filter((l) => l.status <= options.maxStatus);
268
+ }
269
+ if (options.since !== undefined) {
270
+ result = result.filter((l) => l.timestamp >= options.since);
271
+ }
272
+ if (options.until !== undefined) {
273
+ result = result.filter((l) => l.timestamp <= options.until);
274
+ }
275
+ // Sort newest first and apply limit
276
+ result.reverse();
277
+ if (options.limit) {
278
+ result = result.slice(0, options.limit);
279
+ }
280
+ return result;
281
+ }
282
+ RequestMonitor.getRecentLogs = getRecentLogs;
283
+ /**
284
+ * Gets a specific request log by ID.
285
+ *
286
+ * @param id - Request ID
287
+ * @returns The log entry or undefined if not found
288
+ */
289
+ function getLog(id) {
290
+ return logs.find((l) => l.id === id);
291
+ }
292
+ RequestMonitor.getLog = getLog;
293
+ /**
294
+ * Gets statistics for a provider or overall.
295
+ *
296
+ * @param providerID - Optional provider ID (undefined for global stats)
297
+ * @returns Statistics object
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * // Get global stats
302
+ * const global = RequestMonitor.getStats()
303
+ *
304
+ * // Get provider-specific stats
305
+ * const anthropic = RequestMonitor.getStats("anthropic")
306
+ * ```
307
+ */
308
+ function getStats(providerID) {
309
+ // Calculate percentiles on demand
310
+ calculatePercentiles(globalStats);
311
+ for (const stats of providerStats.values()) {
312
+ calculatePercentiles(stats);
313
+ }
314
+ updateRollingMetrics();
315
+ if (providerID) {
316
+ return providerStats.get(providerID) ?? createEmptyStats();
317
+ }
318
+ return { ...globalStats };
319
+ }
320
+ RequestMonitor.getStats = getStats;
321
+ /**
322
+ * Gets statistics for all providers.
323
+ *
324
+ * @returns Map of provider ID to statistics
325
+ */
326
+ function getAllStats() {
327
+ updateRollingMetrics();
328
+ calculatePercentiles(globalStats);
329
+ const result = {};
330
+ for (const [providerID, stats] of providerStats.entries()) {
331
+ calculatePercentiles(stats);
332
+ result[providerID] = { ...stats };
333
+ }
334
+ return result;
335
+ }
336
+ RequestMonitor.getAllStats = getAllStats;
337
+ /**
338
+ * Gets a summary of all monitoring data.
339
+ *
340
+ * @returns Summary object suitable for API responses
341
+ */
342
+ function getSummary() {
343
+ return {
344
+ enabled: config.enabled,
345
+ logCount: logs.length,
346
+ global: getStats(),
347
+ providers: getAllStats(),
348
+ recentErrors: getRecentLogs({ minStatus: 400, limit: 10 }),
349
+ };
350
+ }
351
+ RequestMonitor.getSummary = getSummary;
352
+ /**
353
+ * Adds an event listener.
354
+ *
355
+ * @param listener - Callback function
356
+ * @returns Unsubscribe function
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * const unsubscribe = RequestMonitor.addListener((event) => {
361
+ * console.log("Request:", event.log.id)
362
+ * })
363
+ *
364
+ * // Later...
365
+ * unsubscribe()
366
+ * ```
367
+ */
368
+ function addListener(listener) {
369
+ listeners.add(listener);
370
+ return () => listeners.delete(listener);
371
+ }
372
+ RequestMonitor.addListener = addListener;
373
+ /**
374
+ * Exports logs to JSON format.
375
+ *
376
+ * @param options - Export options
377
+ * @returns JSON string
378
+ */
379
+ function exportJSON(options = {}) {
380
+ const data = {
381
+ exportedAt: new Date().toISOString(),
382
+ global: getStats(),
383
+ providers: getAllStats(),
384
+ logs: getRecentLogs({
385
+ providerID: options.providerID,
386
+ since: options.since,
387
+ until: options.until,
388
+ }),
389
+ };
390
+ return JSON.stringify(data, null, options.pretty ? 2 : 0);
391
+ }
392
+ RequestMonitor.exportJSON = exportJSON;
393
+ /**
394
+ * Clears all logs and resets statistics.
395
+ */
396
+ function clear() {
397
+ logs.length = 0;
398
+ providerStats.clear();
399
+ globalStats = createEmptyStats();
400
+ rollingWindow.length = 0;
401
+ log.info("cleared");
402
+ }
403
+ RequestMonitor.clear = clear;
404
+ /**
405
+ * Clears logs and stats for a specific provider.
406
+ *
407
+ * @param providerID - Provider ID to clear
408
+ */
409
+ function clearProvider(providerID) {
410
+ // Remove logs for this provider
411
+ for (let i = logs.length - 1; i >= 0; i--) {
412
+ if (logs[i].providerID === providerID) {
413
+ logs.splice(i, 1);
414
+ }
415
+ }
416
+ // Remove provider stats
417
+ providerStats.delete(providerID);
418
+ // Recalculate global stats
419
+ globalStats = createEmptyStats();
420
+ for (const l of logs) {
421
+ updateStats(globalStats, l);
422
+ }
423
+ log.info("cleared-provider", { providerID });
424
+ }
425
+ RequestMonitor.clearProvider = clearProvider;
426
+ /**
427
+ * Enables monitoring.
428
+ */
429
+ function enable() {
430
+ config.enabled = true;
431
+ log.info("enabled");
432
+ }
433
+ RequestMonitor.enable = enable;
434
+ /**
435
+ * Disables monitoring.
436
+ */
437
+ function disable() {
438
+ config.enabled = false;
439
+ log.info("disabled");
440
+ }
441
+ RequestMonitor.disable = disable;
442
+ /**
443
+ * Checks if monitoring is enabled.
444
+ */
445
+ function isEnabled() {
446
+ return config.enabled;
447
+ }
448
+ RequestMonitor.isEnabled = isEnabled;
449
+ })(RequestMonitor || (RequestMonitor = {}));