@azumag/opencode-rate-limit-fallback 1.0.12 → 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 +38 -14
- 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,18 +306,17 @@ 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
|
-
// Abort
|
|
310
|
-
// The abort→promptAsync gap is minimal, so even in headless mode
|
|
311
|
-
// the server won't shut down before promptAsync fires
|
|
319
|
+
// Abort to cancel the retry loop
|
|
312
320
|
try {
|
|
313
321
|
await client.session.abort({ path: { id: sessionID } });
|
|
314
322
|
logToFile(`abort succeeded for session ${sessionID}`);
|
|
@@ -316,11 +324,27 @@ export const RateLimitFallback = async ({ client, directory }) => {
|
|
|
316
324
|
catch (abortErr) {
|
|
317
325
|
logToFile(`abort failed (non-critical): ${abortErr}`);
|
|
318
326
|
}
|
|
319
|
-
await
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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({
|
|
335
|
+
path: { id: sessionID },
|
|
336
|
+
body: promptBody,
|
|
337
|
+
});
|
|
338
|
+
logToFile(`prompt completed successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
|
|
339
|
+
}
|
|
340
|
+
catch (promptErr) {
|
|
341
|
+
logToFile(`prompt failed (${promptErr}), falling back to promptAsync`);
|
|
342
|
+
await client.session.promptAsync({
|
|
343
|
+
path: { id: sessionID },
|
|
344
|
+
body: promptBody,
|
|
345
|
+
});
|
|
346
|
+
logToFile(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
|
|
347
|
+
}
|
|
324
348
|
toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success").catch(() => { });
|
|
325
349
|
retryState.delete(stateKey);
|
|
326
350
|
// Clear fallback flag to allow next fallback if needed
|
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
|
}
|