@azumag/opencode-rate-limit-fallback 1.21.0 → 1.21.2

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,377 @@
1
+ /**
2
+ * Metrics Manager - Handles metrics collection, aggregation, and reporting
3
+ */
4
+ import { RESET_INTERVAL_MS } from './types.js';
5
+ import { getModelKey } from '../utils/helpers.js';
6
+ /**
7
+ * Metrics Manager class for collecting and reporting metrics
8
+ */
9
+ export class MetricsManager {
10
+ metrics;
11
+ config;
12
+ logger;
13
+ resetTimer = null;
14
+ constructor(config, logger) {
15
+ this.config = config;
16
+ this.logger = logger;
17
+ this.metrics = {
18
+ rateLimits: new Map(),
19
+ fallbacks: {
20
+ total: 0,
21
+ successful: 0,
22
+ failed: 0,
23
+ averageDuration: 0,
24
+ byTargetModel: new Map(),
25
+ },
26
+ modelPerformance: new Map(),
27
+ startedAt: Date.now(),
28
+ generatedAt: Date.now(),
29
+ };
30
+ if (this.config.enabled) {
31
+ this.startResetTimer();
32
+ }
33
+ }
34
+ /**
35
+ * Start the automatic reset timer
36
+ */
37
+ startResetTimer() {
38
+ if (this.resetTimer) {
39
+ clearInterval(this.resetTimer);
40
+ }
41
+ const intervalMs = RESET_INTERVAL_MS[this.config.resetInterval];
42
+ this.resetTimer = setInterval(() => {
43
+ this.reset();
44
+ }, intervalMs);
45
+ }
46
+ /**
47
+ * Reset all metrics data
48
+ */
49
+ reset() {
50
+ this.metrics = {
51
+ rateLimits: new Map(),
52
+ fallbacks: {
53
+ total: 0,
54
+ successful: 0,
55
+ failed: 0,
56
+ averageDuration: 0,
57
+ byTargetModel: new Map(),
58
+ },
59
+ modelPerformance: new Map(),
60
+ startedAt: Date.now(),
61
+ generatedAt: Date.now(),
62
+ };
63
+ this.logger.debug("Metrics reset");
64
+ }
65
+ /**
66
+ * Record a rate limit event
67
+ */
68
+ recordRateLimit(providerID, modelID) {
69
+ if (!this.config.enabled)
70
+ return;
71
+ const key = getModelKey(providerID, modelID);
72
+ const now = Date.now();
73
+ const existing = this.metrics.rateLimits.get(key);
74
+ if (existing) {
75
+ const intervalMs = now - existing.lastOccurrence;
76
+ existing.count++;
77
+ existing.lastOccurrence = now;
78
+ existing.averageInterval = existing.averageInterval
79
+ ? (existing.averageInterval + intervalMs) / 2
80
+ : intervalMs;
81
+ this.metrics.rateLimits.set(key, existing);
82
+ }
83
+ else {
84
+ this.metrics.rateLimits.set(key, {
85
+ count: 1,
86
+ firstOccurrence: now,
87
+ lastOccurrence: now,
88
+ });
89
+ }
90
+ }
91
+ /**
92
+ * Record the start of a fallback operation
93
+ * @returns timestamp for tracking duration
94
+ */
95
+ recordFallbackStart() {
96
+ if (!this.config.enabled)
97
+ return 0;
98
+ return Date.now();
99
+ }
100
+ /**
101
+ * Record a successful fallback operation
102
+ */
103
+ recordFallbackSuccess(targetProviderID, targetModelID, startTime) {
104
+ if (!this.config.enabled)
105
+ return;
106
+ const duration = Date.now() - startTime;
107
+ const key = getModelKey(targetProviderID, targetModelID);
108
+ this.metrics.fallbacks.total++;
109
+ this.metrics.fallbacks.successful++;
110
+ // Update average duration
111
+ const totalDuration = this.metrics.fallbacks.averageDuration * (this.metrics.fallbacks.successful - 1);
112
+ this.metrics.fallbacks.averageDuration = (totalDuration + duration) / this.metrics.fallbacks.successful;
113
+ // Update target model metrics
114
+ const targetMetrics = this.metrics.fallbacks.byTargetModel.get(key) || {
115
+ usedAsFallback: 0,
116
+ successful: 0,
117
+ failed: 0,
118
+ };
119
+ targetMetrics.usedAsFallback++;
120
+ targetMetrics.successful++;
121
+ this.metrics.fallbacks.byTargetModel.set(key, targetMetrics);
122
+ }
123
+ /**
124
+ * Record a failed fallback operation
125
+ */
126
+ recordFallbackFailure() {
127
+ if (!this.config.enabled)
128
+ return;
129
+ this.metrics.fallbacks.total++;
130
+ this.metrics.fallbacks.failed++;
131
+ }
132
+ /**
133
+ * Record a model request
134
+ */
135
+ recordModelRequest(providerID, modelID) {
136
+ if (!this.config.enabled)
137
+ return;
138
+ const key = getModelKey(providerID, modelID);
139
+ const existing = this.metrics.modelPerformance.get(key) || {
140
+ requests: 0,
141
+ successes: 0,
142
+ failures: 0,
143
+ };
144
+ existing.requests++;
145
+ this.metrics.modelPerformance.set(key, existing);
146
+ }
147
+ /**
148
+ * Record a successful model request
149
+ */
150
+ recordModelSuccess(providerID, modelID, responseTime) {
151
+ if (!this.config.enabled)
152
+ return;
153
+ const key = getModelKey(providerID, modelID);
154
+ const existing = this.metrics.modelPerformance.get(key) || {
155
+ requests: 0,
156
+ successes: 0,
157
+ failures: 0,
158
+ };
159
+ existing.successes++;
160
+ // Update average response time
161
+ const totalTime = (existing.averageResponseTime || 0) * (existing.successes - 1);
162
+ existing.averageResponseTime = (totalTime + responseTime) / existing.successes;
163
+ this.metrics.modelPerformance.set(key, existing);
164
+ }
165
+ /**
166
+ * Record a failed model request
167
+ */
168
+ recordModelFailure(providerID, modelID) {
169
+ if (!this.config.enabled)
170
+ return;
171
+ const key = getModelKey(providerID, modelID);
172
+ const existing = this.metrics.modelPerformance.get(key) || {
173
+ requests: 0,
174
+ successes: 0,
175
+ failures: 0,
176
+ };
177
+ existing.failures++;
178
+ this.metrics.modelPerformance.set(key, existing);
179
+ }
180
+ /**
181
+ * Get a copy of the current metrics
182
+ */
183
+ getMetrics() {
184
+ this.metrics.generatedAt = Date.now();
185
+ return { ...this.metrics };
186
+ }
187
+ /**
188
+ * Export metrics in the specified format
189
+ */
190
+ export(format = "json") {
191
+ const metrics = this.getMetrics();
192
+ switch (format) {
193
+ case "pretty":
194
+ return this.exportPretty(metrics);
195
+ case "csv":
196
+ return this.exportCSV(metrics);
197
+ case "json":
198
+ default:
199
+ return JSON.stringify(this.toPlainObject(metrics), null, 2);
200
+ }
201
+ }
202
+ /**
203
+ * Convert metrics to a plain object (converts Maps to Objects)
204
+ */
205
+ toPlainObject(metrics) {
206
+ return {
207
+ rateLimits: Object.fromEntries(Array.from(metrics.rateLimits.entries()).map(([k, v]) => [k, v])),
208
+ fallbacks: {
209
+ ...metrics.fallbacks,
210
+ byTargetModel: Object.fromEntries(Array.from(metrics.fallbacks.byTargetModel.entries()).map(([k, v]) => [k, v])),
211
+ },
212
+ modelPerformance: Object.fromEntries(Array.from(metrics.modelPerformance.entries()).map(([k, v]) => [k, v])),
213
+ startedAt: metrics.startedAt,
214
+ generatedAt: metrics.generatedAt,
215
+ };
216
+ }
217
+ /**
218
+ * Export metrics in pretty-printed text format
219
+ */
220
+ exportPretty(metrics) {
221
+ const lines = [];
222
+ lines.push("=".repeat(60));
223
+ lines.push("Rate Limit Fallback Metrics");
224
+ lines.push("=".repeat(60));
225
+ lines.push(`Started: ${new Date(metrics.startedAt).toISOString()}`);
226
+ lines.push(`Generated: ${new Date(metrics.generatedAt).toISOString()}`);
227
+ lines.push("");
228
+ // Rate Limits
229
+ lines.push("Rate Limits:");
230
+ lines.push("-".repeat(40));
231
+ if (metrics.rateLimits.size === 0) {
232
+ lines.push(" No rate limits recorded");
233
+ }
234
+ else {
235
+ for (const [model, data] of metrics.rateLimits.entries()) {
236
+ lines.push(` ${model}:`);
237
+ lines.push(` Count: ${data.count}`);
238
+ lines.push(` First: ${new Date(data.firstOccurrence).toISOString()}`);
239
+ lines.push(` Last: ${new Date(data.lastOccurrence).toISOString()}`);
240
+ if (data.averageInterval) {
241
+ lines.push(` Avg Interval: ${(data.averageInterval / 1000).toFixed(2)}s`);
242
+ }
243
+ }
244
+ }
245
+ lines.push("");
246
+ // Fallbacks
247
+ lines.push("Fallbacks:");
248
+ lines.push("-".repeat(40));
249
+ lines.push(` Total: ${metrics.fallbacks.total}`);
250
+ lines.push(` Successful: ${metrics.fallbacks.successful}`);
251
+ lines.push(` Failed: ${metrics.fallbacks.failed}`);
252
+ if (metrics.fallbacks.averageDuration > 0) {
253
+ lines.push(` Avg Duration: ${(metrics.fallbacks.averageDuration / 1000).toFixed(2)}s`);
254
+ }
255
+ if (metrics.fallbacks.byTargetModel.size > 0) {
256
+ lines.push("");
257
+ lines.push(" By Target Model:");
258
+ for (const [model, data] of metrics.fallbacks.byTargetModel.entries()) {
259
+ lines.push(` ${model}:`);
260
+ lines.push(` Used: ${data.usedAsFallback}`);
261
+ lines.push(` Success: ${data.successful}`);
262
+ lines.push(` Failed: ${data.failed}`);
263
+ }
264
+ }
265
+ lines.push("");
266
+ // Model Performance
267
+ lines.push("Model Performance:");
268
+ lines.push("-".repeat(40));
269
+ if (metrics.modelPerformance.size === 0) {
270
+ lines.push(" No performance data recorded");
271
+ }
272
+ else {
273
+ for (const [model, data] of metrics.modelPerformance.entries()) {
274
+ lines.push(` ${model}:`);
275
+ lines.push(` Requests: ${data.requests}`);
276
+ lines.push(` Successes: ${data.successes}`);
277
+ lines.push(` Failures: ${data.failures}`);
278
+ if (data.averageResponseTime) {
279
+ lines.push(` Avg Response: ${(data.averageResponseTime / 1000).toFixed(2)}s`);
280
+ }
281
+ if (data.requests > 0) {
282
+ const successRate = ((data.successes / data.requests) * 100).toFixed(1);
283
+ lines.push(` Success Rate: ${successRate}%`);
284
+ }
285
+ }
286
+ }
287
+ return lines.join("\n");
288
+ }
289
+ /**
290
+ * Export metrics in CSV format
291
+ */
292
+ exportCSV(metrics) {
293
+ const lines = [];
294
+ // Rate Limits CSV
295
+ lines.push("=== RATE_LIMITS ===");
296
+ lines.push("model,count,first_occurrence,last_occurrence,avg_interval_ms");
297
+ for (const [model, data] of metrics.rateLimits.entries()) {
298
+ lines.push([
299
+ model,
300
+ data.count,
301
+ data.firstOccurrence,
302
+ data.lastOccurrence,
303
+ data.averageInterval || 0,
304
+ ].join(","));
305
+ }
306
+ lines.push("");
307
+ // Fallbacks Summary CSV
308
+ lines.push("=== FALLBACKS_SUMMARY ===");
309
+ lines.push(`total,successful,failed,avg_duration_ms`);
310
+ lines.push([
311
+ metrics.fallbacks.total,
312
+ metrics.fallbacks.successful,
313
+ metrics.fallbacks.failed,
314
+ metrics.fallbacks.averageDuration || 0,
315
+ ].join(","));
316
+ lines.push("");
317
+ // Fallbacks by Model CSV
318
+ lines.push("=== FALLBACKS_BY_MODEL ===");
319
+ lines.push("model,used_as_fallback,successful,failed");
320
+ for (const [model, data] of metrics.fallbacks.byTargetModel.entries()) {
321
+ lines.push([
322
+ model,
323
+ data.usedAsFallback,
324
+ data.successful,
325
+ data.failed,
326
+ ].join(","));
327
+ }
328
+ lines.push("");
329
+ // Model Performance CSV
330
+ lines.push("=== MODEL_PERFORMANCE ===");
331
+ lines.push("model,requests,successes,failures,avg_response_time_ms,success_rate");
332
+ for (const [model, data] of metrics.modelPerformance.entries()) {
333
+ const successRate = data.requests > 0 ? ((data.successes / data.requests) * 100).toFixed(1) : "0";
334
+ lines.push([
335
+ model,
336
+ data.requests,
337
+ data.successes,
338
+ data.failures,
339
+ data.averageResponseTime || 0,
340
+ successRate,
341
+ ].join(","));
342
+ }
343
+ return lines.join("\n");
344
+ }
345
+ /**
346
+ * Report metrics to configured outputs
347
+ */
348
+ async report() {
349
+ if (!this.config.enabled)
350
+ return;
351
+ const output = this.export(this.config.output.format);
352
+ // Console output
353
+ if (this.config.output.console) {
354
+ console.log(output);
355
+ }
356
+ // File output
357
+ if (this.config.output.file) {
358
+ try {
359
+ const { writeFileSync } = await import('fs');
360
+ writeFileSync(this.config.output.file, output, "utf-8");
361
+ this.logger.debug(`Metrics exported to ${this.config.output.file}`);
362
+ }
363
+ catch (error) {
364
+ this.logger.warn(`Failed to write metrics to file: ${this.config.output.file}`, { error });
365
+ }
366
+ }
367
+ }
368
+ /**
369
+ * Clean up resources
370
+ */
371
+ destroy() {
372
+ if (this.resetTimer) {
373
+ clearInterval(this.resetTimer);
374
+ this.resetTimer = null;
375
+ }
376
+ }
377
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Metrics-specific types for the MetricsManager
3
+ */
4
+ /**
5
+ * Reset interval options for metrics
6
+ */
7
+ export type ResetInterval = "hourly" | "daily" | "weekly";
8
+ /**
9
+ * Reset interval values in milliseconds
10
+ */
11
+ export declare const RESET_INTERVAL_MS: Record<ResetInterval, number>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Metrics-specific types for the MetricsManager
3
+ */
4
+ /**
5
+ * Reset interval values in milliseconds
6
+ */
7
+ export const RESET_INTERVAL_MS = {
8
+ hourly: 60 * 60 * 1000,
9
+ daily: 24 * 60 * 60 * 1000,
10
+ weekly: 7 * 24 * 60 * 60 * 1000,
11
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Subagent hierarchy and fallback propagation management
3
+ */
4
+ import type { SessionHierarchy, PluginConfig } from '../types/index.js';
5
+ /**
6
+ * Initialize subagent tracker with config
7
+ */
8
+ export declare function initSubagentTracker(config: PluginConfig): void;
9
+ /**
10
+ * Register a new subagent in the hierarchy
11
+ */
12
+ export declare function registerSubagent(sessionID: string, parentSessionID: string, config: PluginConfig): boolean;
13
+ /**
14
+ * Get root session ID for a session
15
+ */
16
+ export declare function getRootSession(sessionID: string): string | null;
17
+ /**
18
+ * Get hierarchy for a session
19
+ */
20
+ export declare function getHierarchy(sessionID: string): SessionHierarchy | null;
21
+ /**
22
+ * Get all session hierarchies (for cleanup)
23
+ */
24
+ export declare function getAllHierarchies(): Map<string, SessionHierarchy>;
25
+ /**
26
+ * Get session to root map (for cleanup)
27
+ */
28
+ export declare function getSessionToRootMap(): Map<string, string>;
29
+ /**
30
+ * Clean up stale hierarchies
31
+ */
32
+ export declare function cleanupStaleEntries(): void;
33
+ /**
34
+ * Clean up all hierarchies
35
+ */
36
+ export declare function clearAll(): void;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Subagent hierarchy and fallback propagation management
3
+ */
4
+ import { SESSION_ENTRY_TTL_MS } from '../types/index.js';
5
+ /**
6
+ * Session Hierarchy storage
7
+ */
8
+ const sessionHierarchies = new Map();
9
+ const sessionToRootMap = new Map();
10
+ let maxSubagentDepth = 10;
11
+ /**
12
+ * Initialize subagent tracker with config
13
+ */
14
+ export function initSubagentTracker(config) {
15
+ maxSubagentDepth = config.maxSubagentDepth ?? 10;
16
+ }
17
+ /**
18
+ * Register a new subagent in the hierarchy
19
+ */
20
+ export function registerSubagent(sessionID, parentSessionID, config) {
21
+ // Validate parent session exists
22
+ // Parent session must either be registered in sessionToRootMap or be a new root session
23
+ const parentRootSessionID = sessionToRootMap.get(parentSessionID);
24
+ // Determine root session - if parent doesn't exist, treat it as a new root
25
+ const rootSessionID = parentRootSessionID || parentSessionID;
26
+ // If parent is not a subagent but we're treating it as a root, create a hierarchy for it
27
+ // This allows sessions to become roots when their first subagent is registered
28
+ const hierarchy = getOrCreateHierarchy(rootSessionID, config);
29
+ const parentSubagent = hierarchy.subagents.get(parentSessionID);
30
+ const depth = parentSubagent ? parentSubagent.depth + 1 : 1;
31
+ // Enforce max depth
32
+ if (depth > maxSubagentDepth) {
33
+ return false;
34
+ }
35
+ const subagent = {
36
+ sessionID,
37
+ parentSessionID,
38
+ depth,
39
+ fallbackState: "none",
40
+ createdAt: Date.now(),
41
+ lastActivity: Date.now(),
42
+ };
43
+ hierarchy.subagents.set(sessionID, subagent);
44
+ sessionToRootMap.set(sessionID, rootSessionID);
45
+ hierarchy.lastActivity = Date.now();
46
+ return true;
47
+ }
48
+ /**
49
+ * Get or create hierarchy for a root session
50
+ */
51
+ function getOrCreateHierarchy(rootSessionID, config) {
52
+ let hierarchy = sessionHierarchies.get(rootSessionID);
53
+ if (!hierarchy) {
54
+ hierarchy = {
55
+ rootSessionID,
56
+ subagents: new Map(),
57
+ sharedFallbackState: "none",
58
+ sharedConfig: config,
59
+ createdAt: Date.now(),
60
+ lastActivity: Date.now(),
61
+ };
62
+ sessionHierarchies.set(rootSessionID, hierarchy);
63
+ sessionToRootMap.set(rootSessionID, rootSessionID);
64
+ }
65
+ return hierarchy;
66
+ }
67
+ /**
68
+ * Get root session ID for a session
69
+ */
70
+ export function getRootSession(sessionID) {
71
+ return sessionToRootMap.get(sessionID) || null;
72
+ }
73
+ /**
74
+ * Get hierarchy for a session
75
+ */
76
+ export function getHierarchy(sessionID) {
77
+ const rootSessionID = getRootSession(sessionID);
78
+ return rootSessionID ? sessionHierarchies.get(rootSessionID) || null : null;
79
+ }
80
+ /**
81
+ * Get all session hierarchies (for cleanup)
82
+ */
83
+ export function getAllHierarchies() {
84
+ return sessionHierarchies;
85
+ }
86
+ /**
87
+ * Get session to root map (for cleanup)
88
+ */
89
+ export function getSessionToRootMap() {
90
+ return sessionToRootMap;
91
+ }
92
+ /**
93
+ * Clean up stale hierarchies
94
+ */
95
+ export function cleanupStaleEntries() {
96
+ const now = Date.now();
97
+ for (const [rootSessionID, hierarchy] of sessionHierarchies.entries()) {
98
+ if (now - hierarchy.lastActivity > SESSION_ENTRY_TTL_MS) {
99
+ // Clean up all subagents in this hierarchy
100
+ for (const subagentID of hierarchy.subagents.keys()) {
101
+ sessionToRootMap.delete(subagentID);
102
+ }
103
+ sessionHierarchies.delete(rootSessionID);
104
+ sessionToRootMap.delete(rootSessionID);
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * Clean up all hierarchies
110
+ */
111
+ export function clearAll() {
112
+ sessionHierarchies.clear();
113
+ sessionToRootMap.clear();
114
+ }