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

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 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -214,7 +214,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
214
214
  }
215
215
  return null;
216
216
  }
217
- async function handleRateLimitFallback(sessionID, currentProviderID, currentModelID) {
217
+ async function handleRateLimitFallback(sessionID, currentProviderID, currentModelID, skipAbort = false) {
218
218
  try {
219
219
  // Prevent duplicate fallback processing within 5 seconds
220
220
  const lastFallback = fallbackInProgress.get(sessionID);
@@ -316,28 +316,29 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
316
  parts: parts,
317
317
  model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
318
318
  };
319
- // abortpromptAsync: works in TUI, fails in headless (server disposes)
320
- // promptAsync abort: fails in TUI (abort kills the new prompt too)
319
+ // promptAsyncabort: send fallback prompt first, then abort the old request
320
+ // This prevents the abort from killing the new fallback prompt.
321
321
  //
322
- // Use abort promptAsync as the primary path (TUI confirmed working).
323
- // Headless mode (opencode run) is a known limitation the server
324
- // dispose sequence interrupts the new prompt before it can complete.
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.
325
324
  //
326
325
  // promptAsync: HTTP POST /session/{id}/prompt_async → 204 (SDK sdk.gen.js).
327
326
  const t0 = Date.now();
328
- try {
329
- await client.session.abort({ path: { id: sessionID } });
330
- logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t0}ms)`);
331
- }
332
- catch (abortErr) {
333
- logToFile(`abort failed (${Date.now() - t0}ms): ${abortErr}`);
334
- }
335
- const t1 = Date.now();
336
327
  await client.session.promptAsync({
337
328
  path: { id: sessionID },
338
329
  body: promptBody,
339
330
  });
340
- logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
331
+ const t1 = Date.now();
332
+ if (!skipAbort) {
333
+ try {
334
+ await client.session.abort({ path: { id: sessionID } });
335
+ logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms)`);
336
+ }
337
+ catch (abortErr) {
338
+ logToFile(`abort failed (${Date.now() - t1}ms): ${abortErr}`);
339
+ }
340
+ }
341
+ logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
341
342
  // Toast is best-effort notification. The toast() function (line ~185) has
342
343
  // built-in fallback: showToast failure → app.log. After promptAsync the
343
344
  // server may already be disposing, so both showToast and app.log could fail.
@@ -399,7 +400,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
399
400
  catch {
400
401
  console.log("[rate-limit-fallback] Rate limit error detected, attempting fallback");
401
402
  }
402
- await handleRateLimitFallback(sessionID, "", "");
403
+ await handleRateLimitFallback(sessionID, "", "", true); // skipAbort = true (already in error state)
403
404
  }
404
405
  }
405
406
  if (event.type === "message.updated") {
@@ -438,7 +439,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
438
439
  catch {
439
440
  console.log("[rate-limit-fallback] Rate limit error in message, attempting fallback");
440
441
  }
441
- await handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "");
442
+ await handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "", true); // skipAbort = true (already in error state)
442
443
  }
443
444
  }
444
445
  if (event.type === "session.status") {
@@ -457,6 +458,23 @@ export const RateLimitFallback = async ({ client, directory }) => {
457
458
  catch {
458
459
  console.log("[rate-limit-fallback] session.status:", status);
459
460
  }
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
478
  if (status?.type === "retry" && status?.message) {
461
479
  const message = status.message.toLowerCase();
462
480
  const isRateLimitRetry = message.includes("usage limit") ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",