@azumag/opencode-rate-limit-fallback 1.0.12 → 1.0.14

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 +24 -9
  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,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
- 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
- // Abort first to cancel the retry loop, then promptAsync immediately
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,6 +324,13 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
324
  catch (abortErr) {
317
325
  logToFile(`abort failed (non-critical): ${abortErr}`);
318
326
  }
327
+ // Wait for abort to fully complete before sending new prompt.
328
+ // Toast alone (~50ms) is insufficient — the abort flag may still be set,
329
+ // causing the new prompt to be immediately interrupted.
330
+ await new Promise(resolve => setTimeout(resolve, 500));
331
+ toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info").catch(() => { });
332
+ // Use promptAsync — returns immediately, compatible with both TUI and headless modes.
333
+ // prompt (sync) causes race condition: abort flag not yet cleared → prompt interrupted.
319
334
  await client.session.promptAsync({
320
335
  path: { id: sessionID },
321
336
  body: promptBody,
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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
  }