@azumag/opencode-rate-limit-fallback 1.0.11 → 1.0.13
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 +35 -19
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, appendFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
3
|
// Debug log at module level
|
|
4
4
|
console.log("[rate-limit-fallback] Module loaded, version:", process.env.npm_package_version || "unknown");
|
|
5
5
|
const DEFAULT_FALLBACK_MODELS = [
|
|
@@ -74,6 +74,11 @@ function isRateLimitError(error) {
|
|
|
74
74
|
export const RateLimitFallback = async ({ client, directory }) => {
|
|
75
75
|
const config = loadConfig(directory);
|
|
76
76
|
const logFilePath = join(process.env.HOME || "", ".opencode", "rate-limit-fallback-debug.log");
|
|
77
|
+
// Ensure log directory exists
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(logFilePath), { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
77
82
|
// Write to file for debugging
|
|
78
83
|
const logToFile = (message) => {
|
|
79
84
|
try {
|
|
@@ -227,12 +232,16 @@ export const RateLimitFallback = async ({ client, directory }) => {
|
|
|
227
232
|
}
|
|
228
233
|
// Fetch messages BEFORE abort — session must still be alive
|
|
229
234
|
const messagesResult = await client.session.messages({ path: { id: sessionID } });
|
|
230
|
-
if (!messagesResult.data)
|
|
235
|
+
if (!messagesResult.data) {
|
|
236
|
+
fallbackInProgress.delete(sessionID);
|
|
231
237
|
return;
|
|
238
|
+
}
|
|
232
239
|
const messages = messagesResult.data;
|
|
233
240
|
const lastUserMessage = [...messages].reverse().find(m => m.info.role === "user");
|
|
234
|
-
if (!lastUserMessage)
|
|
241
|
+
if (!lastUserMessage) {
|
|
242
|
+
fallbackInProgress.delete(sessionID);
|
|
235
243
|
return;
|
|
244
|
+
}
|
|
236
245
|
toast("Rate Limit Detected", `Switching from ${currentModelID || 'current model'}...`, "warning").catch(() => { });
|
|
237
246
|
const stateKey = `${sessionID}:${lastUserMessage.info.id}`;
|
|
238
247
|
let state = retryState.get(stateKey);
|
|
@@ -297,37 +306,44 @@ export const RateLimitFallback = async ({ client, directory }) => {
|
|
|
297
306
|
return null;
|
|
298
307
|
})
|
|
299
308
|
.filter(Boolean);
|
|
300
|
-
if (parts.length === 0)
|
|
309
|
+
if (parts.length === 0) {
|
|
310
|
+
fallbackInProgress.delete(sessionID);
|
|
301
311
|
return;
|
|
302
|
-
|
|
312
|
+
}
|
|
303
313
|
// Track the new model for this session
|
|
304
314
|
currentSessionModel.set(sessionID, { providerID: nextModel.providerID, modelID: nextModel.modelID });
|
|
305
315
|
const promptBody = {
|
|
306
316
|
parts: parts,
|
|
307
317
|
model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
|
|
308
318
|
};
|
|
309
|
-
//
|
|
319
|
+
// Abort to cancel the retry loop
|
|
310
320
|
try {
|
|
311
|
-
await client.session.
|
|
321
|
+
await client.session.abort({ path: { id: sessionID } });
|
|
322
|
+
logToFile(`abort succeeded for session ${sessionID}`);
|
|
323
|
+
}
|
|
324
|
+
catch (abortErr) {
|
|
325
|
+
logToFile(`abort failed (non-critical): ${abortErr}`);
|
|
326
|
+
}
|
|
327
|
+
// await toast AFTER abort — provides natural delay (~50ms) for abort flag to clear
|
|
328
|
+
// before prompting. Without this delay, the new prompt gets immediately aborted.
|
|
329
|
+
await toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info");
|
|
330
|
+
// Try prompt (sync) first — reliably triggers generation in TUI mode.
|
|
331
|
+
// If it fails (e.g. run mode where server shuts down after abort),
|
|
332
|
+
// fall back to promptAsync which fires before server can exit.
|
|
333
|
+
try {
|
|
334
|
+
await client.session.prompt({
|
|
312
335
|
path: { id: sessionID },
|
|
313
336
|
body: promptBody,
|
|
314
337
|
});
|
|
315
|
-
logToFile(`
|
|
338
|
+
logToFile(`prompt completed successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
|
|
316
339
|
}
|
|
317
|
-
catch (
|
|
318
|
-
|
|
319
|
-
logToFile(`promptAsync failed (${promptAsyncErr}), falling back to abort + promptAsync`);
|
|
320
|
-
try {
|
|
321
|
-
await client.session.abort({ path: { id: sessionID } });
|
|
322
|
-
}
|
|
323
|
-
catch (abortErr) {
|
|
324
|
-
logToFile(`abort also failed: ${abortErr}`);
|
|
325
|
-
}
|
|
340
|
+
catch (promptErr) {
|
|
341
|
+
logToFile(`prompt failed (${promptErr}), falling back to promptAsync`);
|
|
326
342
|
await client.session.promptAsync({
|
|
327
343
|
path: { id: sessionID },
|
|
328
344
|
body: promptBody,
|
|
329
345
|
});
|
|
330
|
-
logToFile(`
|
|
346
|
+
logToFile(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
|
|
331
347
|
}
|
|
332
348
|
toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success").catch(() => { });
|
|
333
349
|
retryState.delete(stateKey);
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azumag/opencode-rate-limit-fallback",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "OpenCode plugin that automatically switches to fallback models when rate limited",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
+
"test": "vitest",
|
|
9
10
|
"prepublishOnly": "npm run build"
|
|
10
11
|
},
|
|
11
12
|
"keywords": [
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@tsconfig/node22": "^22.0.5",
|
|
42
43
|
"@types/node": "^25.2.2",
|
|
43
|
-
"typescript": "^5.9.3"
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
44
46
|
}
|
|
45
47
|
}
|