@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.
Files changed (2) hide show
  1. package/dist/index.js +35 -19
  2. 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
- toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info").catch(() => { });
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
- // Try promptAsync first (no abort needed — works in both TUI and run modes)
319
+ // Abort to cancel the retry loop
310
320
  try {
311
- await client.session.promptAsync({
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(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
338
+ logToFile(`prompt completed successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
316
339
  }
317
- catch (promptAsyncErr) {
318
- // promptAsync failed fall back to abort + promptAsync
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(`abort + promptAsync sent successfully for session ${sessionID}`);
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.11",
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
  }