@azumag/opencode-rate-limit-fallback 1.21.1 → 1.23.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/dist/index.d.ts CHANGED
@@ -7,5 +7,4 @@ import type { Plugin } from "@opencode-ai/plugin";
7
7
  export declare const RateLimitFallback: Plugin;
8
8
  export default RateLimitFallback;
9
9
  export type { PluginConfig, MetricsConfig, FallbackModel, FallbackMode } from "./src/types/index.js";
10
- export { MetricsManager } from "./src/metrics/MetricsManager.js";
11
- export { createLogger, type LogConfig, type Logger } from "./logger.js";
10
+ export type { LogConfig, Logger } from "./logger.js";
package/dist/index.js CHANGED
@@ -3,14 +3,12 @@
3
3
  *
4
4
  * This plugin automatically switches to fallback models when rate limited
5
5
  */
6
- import { existsSync, readFileSync } from "fs";
7
- import { join } from "path";
8
6
  import { createLogger } from "./logger.js";
9
7
  import { MetricsManager } from "./src/metrics/MetricsManager.js";
10
8
  import { FallbackHandler } from "./src/fallback/FallbackHandler.js";
11
9
  import { loadConfig } from "./src/utils/config.js";
12
10
  import { isRateLimitError } from "./src/utils/errorDetection.js";
13
- import { initSubagentTracker, registerSubagent, getRootSession, getHierarchy, cleanupStaleEntries as clearHierarchyEntries, clearAll as clearAllHierarchies, } from "./src/session/SubagentTracker.js";
11
+ import { SubagentTracker } from "./src/session/SubagentTracker.js";
14
12
  import { CLEANUP_INTERVAL_MS } from "./src/types/index.js";
15
13
  // ============================================================================
16
14
  // Event Type Guards
@@ -66,39 +64,16 @@ export const RateLimitFallback = async ({ client, directory }) => {
66
64
  };
67
65
  // Create logger instance
68
66
  const logger = createLogger(logConfig, "RateLimitFallback");
69
- // Log config load errors (if any) after logger is initialized
70
- const homedir = process.env.HOME || "";
71
- const configPaths = [
72
- join(directory, ".opencode", "rate-limit-fallback.json"),
73
- join(directory, "rate-limit-fallback.json"),
74
- join(homedir, ".opencode", "rate-limit-fallback.json"),
75
- join(homedir, ".config", "opencode", "rate-limit-fallback.json"),
76
- ];
77
- for (const configPath of configPaths) {
78
- if (existsSync(configPath)) {
79
- try {
80
- readFileSync(configPath, "utf-8");
81
- }
82
- catch (error) {
83
- logger.error(`Failed to load config from ${configPath}`, { error });
84
- }
85
- }
86
- }
87
67
  if (!config.enabled) {
88
68
  return {};
89
69
  }
90
70
  // Initialize components
91
- initSubagentTracker(config);
71
+ const subagentTracker = new SubagentTracker(config);
92
72
  const metricsManager = new MetricsManager(config.metrics ?? { enabled: false, output: { console: true, format: "pretty" }, resetInterval: "daily" }, logger);
93
- // Create hierarchy resolver to avoid circular dependency
94
- const hierarchyResolver = {
95
- getRootSession: getRootSession,
96
- getHierarchy: getHierarchy,
97
- };
98
- const fallbackHandler = new FallbackHandler(config, client, logger, metricsManager, hierarchyResolver);
73
+ const fallbackHandler = new FallbackHandler(config, client, logger, metricsManager, subagentTracker);
99
74
  // Cleanup stale entries periodically
100
75
  const cleanupInterval = setInterval(() => {
101
- clearHierarchyEntries();
76
+ subagentTracker.cleanupStaleEntries();
102
77
  fallbackHandler.cleanupStaleEntries();
103
78
  }, CLEANUP_INTERVAL_MS);
104
79
  return {
@@ -146,19 +121,17 @@ export const RateLimitFallback = async ({ client, directory }) => {
146
121
  if (isSubagentSessionCreatedEvent(rawEvent)) {
147
122
  const { sessionID, parentSessionID } = rawEvent.properties;
148
123
  if (config.enableSubagentFallback !== false) {
149
- registerSubagent(sessionID, parentSessionID, config);
124
+ subagentTracker.registerSubagent(sessionID, parentSessionID);
150
125
  }
151
126
  }
152
127
  },
153
128
  // Cleanup function to prevent memory leaks
154
129
  cleanup: () => {
155
130
  clearInterval(cleanupInterval);
156
- clearAllHierarchies();
131
+ subagentTracker.clearAll();
157
132
  metricsManager.destroy();
158
133
  fallbackHandler.destroy();
159
134
  },
160
135
  };
161
136
  };
162
137
  export default RateLimitFallback;
163
- export { MetricsManager } from "./src/metrics/MetricsManager.js";
164
- export { createLogger } from "./logger.js";
@@ -4,13 +4,7 @@
4
4
  import type { Logger } from '../../logger.js';
5
5
  import type { FallbackModel, PluginConfig, OpenCodeClient, MessagePart, SessionHierarchy } from '../types/index.js';
6
6
  import { MetricsManager } from '../metrics/MetricsManager.js';
7
- /**
8
- * Hierarchy resolver functions
9
- */
10
- export type HierarchyResolver = {
11
- getRootSession: (sessionID: string) => string | null;
12
- getHierarchy: (sessionID: string) => SessionHierarchy | null;
13
- };
7
+ import type { SubagentTracker } from '../session/SubagentTracker.js';
14
8
  /**
15
9
  * Fallback Handler class for orchestrating the fallback retry flow
16
10
  */
@@ -25,8 +19,8 @@ export declare class FallbackHandler {
25
19
  private fallbackInProgress;
26
20
  private fallbackMessages;
27
21
  private metricsManager;
28
- private hierarchyResolver;
29
- constructor(config: PluginConfig, client: OpenCodeClient, logger: Logger, metricsManager: MetricsManager, hierarchyResolver: HierarchyResolver);
22
+ private subagentTracker;
23
+ constructor(config: PluginConfig, client: OpenCodeClient, logger: Logger, metricsManager: MetricsManager, subagentTracker: SubagentTracker);
30
24
  /**
31
25
  * Check and mark fallback in progress for deduplication
32
26
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Fallback orchestration logic
3
3
  */
4
+ import { SESSION_ENTRY_TTL_MS } from '../types/index.js';
4
5
  import { ModelSelector } from './ModelSelector.js';
5
6
  import { extractMessageParts, convertPartsToSDKFormat, safeShowToast, getStateKey, getModelKey, DEDUP_WINDOW_MS, STATE_TIMEOUT_MS } from '../utils/helpers.js';
6
7
  /**
@@ -18,15 +19,15 @@ export class FallbackHandler {
18
19
  fallbackMessages;
19
20
  // Metrics manager reference
20
21
  metricsManager;
21
- // Hierarchy resolver
22
- hierarchyResolver;
23
- constructor(config, client, logger, metricsManager, hierarchyResolver) {
22
+ // Subagent tracker reference
23
+ subagentTracker;
24
+ constructor(config, client, logger, metricsManager, subagentTracker) {
24
25
  this.config = config;
25
26
  this.client = client;
26
27
  this.logger = logger;
27
28
  this.modelSelector = new ModelSelector(config, client);
28
29
  this.metricsManager = metricsManager;
29
- this.hierarchyResolver = hierarchyResolver;
30
+ this.subagentTracker = subagentTracker;
30
31
  this.currentSessionModel = new Map();
31
32
  this.modelRequestStartTimes = new Map();
32
33
  this.retryState = new Map();
@@ -132,10 +133,10 @@ export class FallbackHandler {
132
133
  */
133
134
  async handleRateLimitFallback(sessionID, currentProviderID, currentModelID) {
134
135
  try {
135
- // Get root session and hierarchy using resolver
136
- const rootSessionID = this.hierarchyResolver.getRootSession(sessionID);
136
+ // Get root session and hierarchy using subagent tracker
137
+ const rootSessionID = this.subagentTracker.getRootSession(sessionID);
137
138
  const targetSessionID = rootSessionID || sessionID;
138
- const hierarchy = this.hierarchyResolver.getHierarchy(sessionID);
139
+ const hierarchy = this.subagentTracker.getHierarchy(sessionID);
139
140
  // If no model info provided, try to get from tracked session model
140
141
  if (!currentProviderID || !currentModelID) {
141
142
  const tracked = this.currentSessionModel.get(targetSessionID);
@@ -308,7 +309,6 @@ export class FallbackHandler {
308
309
  * Clean up stale entries
309
310
  */
310
311
  cleanupStaleEntries() {
311
- const { STATE_TIMEOUT_MS, SESSION_ENTRY_TTL_MS } = require('../types/index.js');
312
312
  const now = Date.now();
313
313
  for (const [sessionID, entry] of this.currentSessionModel.entries()) {
314
314
  if (now - entry.lastUpdated > SESSION_ENTRY_TTL_MS) {
@@ -3,34 +3,35 @@
3
3
  */
4
4
  import type { SessionHierarchy, PluginConfig } from '../types/index.js';
5
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;
6
+ * SubagentTracker class for managing session hierarchies
7
+ */
8
+ export declare class SubagentTracker {
9
+ private sessionHierarchies;
10
+ private sessionToRootMap;
11
+ private maxSubagentDepth;
12
+ constructor(config: PluginConfig);
13
+ /**
14
+ * Register a new subagent in the hierarchy
15
+ */
16
+ registerSubagent(sessionID: string, parentSessionID: string): boolean;
17
+ /**
18
+ * Get root session ID for a session
19
+ */
20
+ getRootSession(sessionID: string): string | null;
21
+ /**
22
+ * Get hierarchy for a session
23
+ */
24
+ getHierarchy(sessionID: string): SessionHierarchy | null;
25
+ /**
26
+ * Get or create hierarchy for a root session
27
+ */
28
+ private getOrCreateHierarchy;
29
+ /**
30
+ * Clean up stale hierarchies
31
+ */
32
+ cleanupStaleEntries(): void;
33
+ /**
34
+ * Clean up all hierarchies
35
+ */
36
+ clearAll(): void;
37
+ }
@@ -3,112 +3,119 @@
3
3
  */
4
4
  import { SESSION_ENTRY_TTL_MS } from '../types/index.js';
5
5
  /**
6
- * Session Hierarchy storage
6
+ * SubagentTracker class for managing session hierarchies
7
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;
8
+ export class SubagentTracker {
9
+ sessionHierarchies;
10
+ sessionToRootMap;
11
+ maxSubagentDepth;
12
+ constructor(config) {
13
+ this.sessionHierarchies = new Map();
14
+ this.sessionToRootMap = new Map();
15
+ this.maxSubagentDepth = config.maxSubagentDepth ?? 10;
34
16
  }
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,
17
+ /**
18
+ * Register a new subagent in the hierarchy
19
+ */
20
+ registerSubagent(sessionID, parentSessionID) {
21
+ // Validate parent session exists
22
+ // Parent session must either be registered in sessionToRootMap or be a new root session
23
+ const parentRootSessionID = this.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 = this.getOrCreateHierarchy(rootSessionID);
29
+ const parentSubagent = hierarchy.subagents.get(parentSessionID);
30
+ const depth = parentSubagent ? parentSubagent.depth + 1 : 1;
31
+ // Enforce max depth
32
+ if (depth > this.maxSubagentDepth) {
33
+ return false;
34
+ }
35
+ const subagent = {
36
+ sessionID,
37
+ parentSessionID,
38
+ depth,
39
+ fallbackState: "none",
59
40
  createdAt: Date.now(),
60
41
  lastActivity: Date.now(),
61
42
  };
62
- sessionHierarchies.set(rootSessionID, hierarchy);
63
- sessionToRootMap.set(rootSessionID, rootSessionID);
43
+ hierarchy.subagents.set(sessionID, subagent);
44
+ this.sessionToRootMap.set(sessionID, rootSessionID);
45
+ hierarchy.lastActivity = Date.now();
46
+ return true;
64
47
  }
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);
48
+ /**
49
+ * Get root session ID for a session
50
+ */
51
+ getRootSession(sessionID) {
52
+ return this.sessionToRootMap.get(sessionID) || null;
53
+ }
54
+ /**
55
+ * Get hierarchy for a session
56
+ */
57
+ getHierarchy(sessionID) {
58
+ const rootSessionID = this.getRootSession(sessionID);
59
+ return rootSessionID && this.sessionHierarchies.has(rootSessionID) ? this.sessionHierarchies.get(rootSessionID) : null;
60
+ }
61
+ /**
62
+ * Get or create hierarchy for a root session
63
+ */
64
+ getOrCreateHierarchy(rootSessionID) {
65
+ let hierarchy = this.sessionHierarchies.get(rootSessionID);
66
+ if (!hierarchy) {
67
+ hierarchy = {
68
+ rootSessionID,
69
+ subagents: new Map(),
70
+ sharedFallbackState: "none",
71
+ sharedConfig: {
72
+ fallbackModels: [],
73
+ cooldownMs: 60 * 1000,
74
+ enabled: true,
75
+ fallbackMode: "cycle",
76
+ log: {
77
+ level: "warn",
78
+ format: "simple",
79
+ enableTimestamp: true,
80
+ },
81
+ metrics: {
82
+ enabled: false,
83
+ output: {
84
+ console: true,
85
+ format: "pretty",
86
+ },
87
+ resetInterval: "daily",
88
+ },
89
+ },
90
+ createdAt: Date.now(),
91
+ lastActivity: Date.now(),
92
+ };
93
+ this.sessionHierarchies.set(rootSessionID, hierarchy);
94
+ this.sessionToRootMap.set(rootSessionID, rootSessionID);
95
+ }
96
+ return hierarchy;
97
+ }
98
+ /**
99
+ * Clean up stale hierarchies
100
+ */
101
+ cleanupStaleEntries() {
102
+ const now = Date.now();
103
+ for (const [rootSessionID, hierarchy] of this.sessionHierarchies.entries()) {
104
+ if (now - hierarchy.lastActivity > SESSION_ENTRY_TTL_MS) {
105
+ // Clean up all subagents in this hierarchy
106
+ for (const subagentID of hierarchy.subagents.keys()) {
107
+ this.sessionToRootMap.delete(subagentID);
108
+ }
109
+ this.sessionHierarchies.delete(rootSessionID);
110
+ this.sessionToRootMap.delete(rootSessionID);
102
111
  }
103
- sessionHierarchies.delete(rootSessionID);
104
- sessionToRootMap.delete(rootSessionID);
105
112
  }
106
113
  }
107
- }
108
- /**
109
- * Clean up all hierarchies
110
- */
111
- export function clearAll() {
112
- sessionHierarchies.clear();
113
- sessionToRootMap.clear();
114
+ /**
115
+ * Clean up all hierarchies
116
+ */
117
+ clearAll() {
118
+ this.sessionHierarchies.clear();
119
+ this.sessionToRootMap.clear();
120
+ }
114
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.21.1",
3
+ "version": "1.23.0",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",