@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.
@@ -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
- ### Via npm
17
+ Add the plugin to your `opencode.json`:
18
18
 
19
- ```bash
20
- npm install @azumag/opencode-rate-limit-fallback
19
+ ```json
20
+ {
21
+ "plugins": ["@azumag/opencode-rate-limit-fallback"]
22
+ }
21
23
  ```
22
24
 
23
- Then copy to the plugins directory:
25
+ OpenCode will automatically install the plugin on startup.
24
26
 
25
- ```bash
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: "Switching to fallback model...",
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
- const nextModel = findNextAvailableModel(currentProviderID || "", currentModelID || "", state.attemptedModels);
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 silently
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
- if (message.includes("usage limit") || message.includes("rate limit")) {
257
- if (status.attempt === 1) {
258
- await handleRateLimitFallback(props.sessionID, "", "");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "main": "index.ts",
6
6
  "type": "module",