@azumag/opencode-rate-limit-fallback 1.0.1 → 1.0.3
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/.claude/settings.local.json +3 -1
- package/README.md +7 -11
- package/index.ts +49 -7
- package/package.json +1 -1
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"Bash(npm token create:*)",
|
|
11
11
|
"Bash(npm view:*)",
|
|
12
12
|
"Bash(git add:*)",
|
|
13
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nPublish as scoped package @azumag/opencode-rate-limit-fallback\n\n- Rename package to @azumag/opencode-rate-limit-fallback\n- Add npm installation instructions to README\n- Add npm version badge\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
|
13
|
+
"Bash(git commit -m \"$\\(cat <<''EOF''\nPublish as scoped package @azumag/opencode-rate-limit-fallback\n\n- Rename package to @azumag/opencode-rate-limit-fallback\n- Add npm installation instructions to README\n- Add npm version badge\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
14
|
+
"Bash(git push:*)",
|
|
15
|
+
"Bash(git commit:*)"
|
|
14
16
|
]
|
|
15
17
|
}
|
|
16
18
|
}
|
package/README.md
CHANGED
|
@@ -14,19 +14,17 @@ OpenCode plugin that automatically switches to fallback models when rate limited
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Add the plugin to your `opencode.json`:
|
|
18
18
|
|
|
19
|
-
```
|
|
20
|
-
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"plugins": ["@azumag/opencode-rate-limit-fallback"]
|
|
22
|
+
}
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
OpenCode will automatically install the plugin on startup.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
cp node_modules/@azumag/opencode-rate-limit-fallback/index.ts ~/.config/opencode/plugins/rate-limit-fallback.ts
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Manual Installation
|
|
27
|
+
### Manual Installation (Alternative)
|
|
30
28
|
|
|
31
29
|
Copy `index.ts` to your OpenCode plugins directory:
|
|
32
30
|
|
|
@@ -36,8 +34,6 @@ curl -o ~/.config/opencode/plugins/rate-limit-fallback.ts \
|
|
|
36
34
|
https://raw.githubusercontent.com/azumag/opencode-rate-limit-fallback/main/index.ts
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
Dependencies (`@opencode-ai/plugin`) will be automatically installed by OpenCode on startup.
|
|
40
|
-
|
|
41
37
|
Restart OpenCode to load the plugin.
|
|
42
38
|
|
|
43
39
|
## Configuration
|
package/index.ts
CHANGED
|
@@ -76,6 +76,9 @@ function isRateLimitError(error: any): boolean {
|
|
|
76
76
|
"quota exceeded",
|
|
77
77
|
"resource exhausted",
|
|
78
78
|
"usage limit",
|
|
79
|
+
"high concurrency usage of this api",
|
|
80
|
+
"high concurrency",
|
|
81
|
+
"reduce concurrency",
|
|
79
82
|
"429",
|
|
80
83
|
];
|
|
81
84
|
|
|
@@ -96,6 +99,8 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
96
99
|
|
|
97
100
|
const rateLimitedModels = new Map<string, number>();
|
|
98
101
|
const retryState = new Map<string, { attemptedModels: Set<string>; lastAttemptTime: number }>();
|
|
102
|
+
const currentSessionModel = new Map<string, { providerID: string; modelID: string }>();
|
|
103
|
+
const fallbackInProgress = new Map<string, number>(); // sessionID -> timestamp
|
|
99
104
|
|
|
100
105
|
function isModelRateLimited(providerID: string, modelID: string): boolean {
|
|
101
106
|
const key = getModelKey(providerID, modelID);
|
|
@@ -139,12 +144,28 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
139
144
|
|
|
140
145
|
async function handleRateLimitFallback(sessionID: string, currentProviderID: string, currentModelID: string) {
|
|
141
146
|
try {
|
|
147
|
+
// Prevent duplicate fallback processing within 5 seconds
|
|
148
|
+
const lastFallback = fallbackInProgress.get(sessionID);
|
|
149
|
+
if (lastFallback && Date.now() - lastFallback < 5000) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
fallbackInProgress.set(sessionID, Date.now());
|
|
153
|
+
|
|
154
|
+
// If no model info provided, try to get from tracked session model
|
|
155
|
+
if (!currentProviderID || !currentModelID) {
|
|
156
|
+
const tracked = currentSessionModel.get(sessionID);
|
|
157
|
+
if (tracked) {
|
|
158
|
+
currentProviderID = tracked.providerID;
|
|
159
|
+
currentModelID = tracked.modelID;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
await client.session.abort({ path: { id: sessionID } });
|
|
143
164
|
|
|
144
165
|
await client.tui.showToast({
|
|
145
166
|
body: {
|
|
146
167
|
title: "Rate Limit Detected",
|
|
147
|
-
message:
|
|
168
|
+
message: `Switching from ${currentModelID || 'current model'}...`,
|
|
148
169
|
variant: "warning",
|
|
149
170
|
duration: 3000,
|
|
150
171
|
},
|
|
@@ -170,7 +191,17 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
170
191
|
state.attemptedModels.add(getModelKey(currentProviderID, currentModelID));
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
|
|
194
|
+
let nextModel = findNextAvailableModel(currentProviderID || "", currentModelID || "", state.attemptedModels);
|
|
195
|
+
|
|
196
|
+
// If no model found and we've attempted models, reset and try again from the beginning
|
|
197
|
+
if (!nextModel && state.attemptedModels.size > 0) {
|
|
198
|
+
state.attemptedModels.clear();
|
|
199
|
+
// Keep the current model marked as attempted to avoid immediate retry
|
|
200
|
+
if (currentProviderID && currentModelID) {
|
|
201
|
+
state.attemptedModels.add(getModelKey(currentProviderID, currentModelID));
|
|
202
|
+
}
|
|
203
|
+
nextModel = findNextAvailableModel("", "", state.attemptedModels);
|
|
204
|
+
}
|
|
174
205
|
|
|
175
206
|
if (!nextModel) {
|
|
176
207
|
await client.tui.showToast({
|
|
@@ -208,6 +239,9 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
208
239
|
},
|
|
209
240
|
});
|
|
210
241
|
|
|
242
|
+
// Track the new model for this session
|
|
243
|
+
currentSessionModel.set(sessionID, { providerID: nextModel.providerID, modelID: nextModel.modelID });
|
|
244
|
+
|
|
211
245
|
await client.session.prompt({
|
|
212
246
|
path: { id: sessionID },
|
|
213
247
|
body: {
|
|
@@ -226,8 +260,11 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
226
260
|
});
|
|
227
261
|
|
|
228
262
|
retryState.delete(stateKey);
|
|
263
|
+
// Clear fallback flag to allow next fallback if needed
|
|
264
|
+
fallbackInProgress.delete(sessionID);
|
|
229
265
|
} catch (err) {
|
|
230
|
-
// Fallback failed
|
|
266
|
+
// Fallback failed, clear the flag
|
|
267
|
+
fallbackInProgress.delete(sessionID);
|
|
231
268
|
}
|
|
232
269
|
}
|
|
233
270
|
|
|
@@ -253,10 +290,15 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
253
290
|
|
|
254
291
|
if (status?.type === "retry" && status?.message) {
|
|
255
292
|
const message = status.message.toLowerCase();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
const isRateLimitRetry =
|
|
294
|
+
message.includes("usage limit") ||
|
|
295
|
+
message.includes("rate limit") ||
|
|
296
|
+
message.includes("high concurrency") ||
|
|
297
|
+
message.includes("reduce concurrency");
|
|
298
|
+
|
|
299
|
+
if (isRateLimitRetry) {
|
|
300
|
+
// Try fallback on any attempt, handleRateLimitFallback will manage state
|
|
301
|
+
await handleRateLimitFallback(props.sessionID, "", "");
|
|
260
302
|
}
|
|
261
303
|
}
|
|
262
304
|
}
|