@azumag/opencode-rate-limit-fallback 1.21.2 → 1.24.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.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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
subagentTracker.cleanupStaleEntries();
|
|
102
77
|
fallbackHandler.cleanupStaleEntries();
|
|
103
78
|
}, CLEANUP_INTERVAL_MS);
|
|
104
79
|
return {
|
|
@@ -146,14 +121,14 @@ 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
|
|
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
|
-
|
|
131
|
+
subagentTracker.clearAll();
|
|
157
132
|
metricsManager.destroy();
|
|
158
133
|
fallbackHandler.destroy();
|
|
159
134
|
},
|
|
@@ -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
|
|
29
|
-
constructor(config: PluginConfig, client: OpenCodeClient, logger: Logger, metricsManager: MetricsManager,
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
constructor(config, client, logger, metricsManager,
|
|
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.
|
|
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
|
|
136
|
-
const rootSessionID = this.
|
|
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.
|
|
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
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
6
|
+
* SubagentTracker class for managing session hierarchies
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
sessionToRootMap.set(
|
|
43
|
+
hierarchy.subagents.set(sessionID, subagent);
|
|
44
|
+
this.sessionToRootMap.set(sessionID, rootSessionID);
|
|
45
|
+
hierarchy.lastActivity = Date.now();
|
|
46
|
+
return true;
|
|
64
47
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Clean up all hierarchies
|
|
116
|
+
*/
|
|
117
|
+
clearAll() {
|
|
118
|
+
this.sessionHierarchies.clear();
|
|
119
|
+
this.sessionToRootMap.clear();
|
|
120
|
+
}
|
|
114
121
|
}
|
package/package.json
CHANGED