@azumag/opencode-rate-limit-fallback 1.0.20 → 1.0.22

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 (3) hide show
  1. package/README.md +6 -8
  2. package/dist/index.js +20 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,16 +89,14 @@ If no configuration is provided, the following models are used:
89
89
 
90
90
  ## How It Works
91
91
 
92
- 1. **Detection**: The plugin listens for rate limit errors via:
93
- - `session.error` events
94
- - `message.updated` events with errors
95
- - `session.status` events with `type: "retry"`
92
+ 1. **Detection**: The plugin listens for rate limit errors via:
93
+ - `session.error` events
94
+ - `message.updated` events with errors
95
+ - `session.status` events with `type: "retry"`
96
96
 
97
- 2. **Abort**: When a rate limit is detected, the current session is aborted to stop OpenCode's internal retry mechanism.
97
+ 2. **Fallback**: The plugin selects the next available model from the fallback list and resends the last user message using the `promptAsync` API.
98
98
 
99
- 3. **Fallback**: The plugin selects the next available model from the fallback list and resends the last user message.
100
-
101
- 4. **Cooldown**: Rate-limited models are tracked and skipped for the configured cooldown period.
99
+ 3. **Cooldown**: Rate-limited models are tracked and skipped for the configured cooldown period.
102
100
 
103
101
  ## License
104
102
 
package/dist/index.js CHANGED
@@ -316,29 +316,28 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
316
  parts: parts,
317
317
  model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
318
318
  };
319
- // promptAsync → abort: send fallback prompt first, then abort the old request
320
- // This prevents the abort from killing the new fallback prompt.
321
- //
322
- // For session.error events, the request is already in error state, so we
323
- // only send promptAsync (skipAbort = true). For retry status, we send both.
324
- //
325
319
  // promptAsync: HTTP POST /session/{id}/prompt_async → 204 (SDK sdk.gen.js).
320
+ //
321
+ // For session.error and retry status events, we skip abort (skipAbort = true)
322
+ // because the request is already in an error or delayed state. Calling abort
323
+ // in these cases can trigger interrupted events that cancel the new fallback
324
+ // prompt.
326
325
  const t0 = Date.now();
327
- await client.session.promptAsync({
328
- path: { id: sessionID },
329
- body: promptBody,
330
- });
331
- const t1 = Date.now();
332
326
  if (!skipAbort) {
333
327
  try {
334
328
  await client.session.abort({ path: { id: sessionID } });
335
- logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms)`);
329
+ logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t0}ms)`);
336
330
  }
337
331
  catch (abortErr) {
338
- logToFile(`abort failed (${Date.now() - t1}ms): ${abortErr}`);
332
+ logToFile(`abort failed (${Date.now() - t0}ms): ${abortErr}`);
339
333
  }
340
334
  }
341
- logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
335
+ const t1 = Date.now();
336
+ await client.session.promptAsync({
337
+ path: { id: sessionID },
338
+ body: promptBody,
339
+ });
340
+ logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
342
341
  // Toast is best-effort notification. The toast() function (line ~185) has
343
342
  // built-in fallback: showToast failure → app.log. After promptAsync the
344
343
  // server may already be disposing, so both showToast and app.log could fail.
@@ -458,23 +457,10 @@ export const RateLimitFallback = async ({ client, directory }) => {
458
457
  catch {
459
458
  console.log("[rate-limit-fallback] session.status:", status);
460
459
  }
461
- if (status?.type === "interrupted") {
462
- // Handle interrupted session - resend prompt with fallback model (skip abort)
463
- logToFile("Session interrupted, attempting to resend prompt");
464
- try {
465
- await client.app.log({
466
- body: {
467
- service: "rate-limit-fallback",
468
- level: "info",
469
- message: "Session interrupted, attempting to resend prompt",
470
- },
471
- });
472
- }
473
- catch {
474
- console.log("[rate-limit-fallback] Session interrupted, attempting to resend prompt");
475
- }
476
- await handleRateLimitFallback(props.sessionID, "", "", true); // skipAbort = true
477
- }
460
+ // Note: interrupted status is ignored here. Since we no longer call abort
461
+ // for retry status events, interrupted events should not be triggered by
462
+ // our fallback logic. If they occur (e.g., from user action), we let them
463
+ // be handled by the system.
478
464
  if (status?.type === "retry" && status?.message) {
479
465
  const message = status.message.toLowerCase();
480
466
  const isRateLimitRetry = message.includes("usage limit") ||
@@ -509,7 +495,9 @@ export const RateLimitFallback = async ({ client, directory }) => {
509
495
  catch {
510
496
  console.log("[rate-limit-fallback] Attempting fallback for rate limit retry");
511
497
  }
512
- await handleRateLimitFallback(props.sessionID, "", "");
498
+ // skipAbort = true for retry status to avoid triggering interrupted events
499
+ // that could cancel the new fallback prompt
500
+ await handleRateLimitFallback(props.sessionID, "", "", true);
513
501
  }
514
502
  }
515
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",