@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.
Files changed (2) hide show
  1. package/dist/index.js +38 -14
  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,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 client.session.promptAsync({
320
- path: { id: sessionID },
321
- body: promptBody,
322
- });
323
- logToFile(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
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.12",
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
  }