@azumag/opencode-rate-limit-fallback 1.0.18 → 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 +33 -22
  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,35 +316,29 @@ export const RateLimitFallback = async ({ client, directory }) => {
316
316
  parts: parts,
317
317
  model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
318
318
  };
319
- // CRITICAL PATH: promptAsync BEFORE abort.
319
+ // promptAsync → abort: send fallback prompt first, then abort the old request
320
+ // This prevents the abort from killing the new fallback prompt.
320
321
  //
321
- // In headless mode (opencode run), abort promptAsync fails because:
322
- // 1. abort triggers server dispose sequence
323
- // 2. promptAsync is accepted and message created
324
- // 3. Server starts processing (busy) but dispose interrupts it (idle)
325
- // 4. server.instance.disposed — all within ~6ms
326
- //
327
- // By sending promptAsync FIRST, the server knows there is pending work
328
- // before abort triggers the dispose check. When abort cancels the retry
329
- // loop and the session goes idle, the server should process the queued
330
- // prompt instead of disposing.
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.
331
324
  //
332
325
  // promptAsync: HTTP POST /session/{id}/prompt_async → 204 (SDK sdk.gen.js).
333
- // prompt (sync): blocks until generation completes — do NOT use.
334
326
  const t0 = Date.now();
335
327
  await client.session.promptAsync({
336
328
  path: { id: sessionID },
337
329
  body: promptBody,
338
330
  });
339
- logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
340
331
  const t1 = Date.now();
341
- try {
342
- await client.session.abort({ path: { id: sessionID } });
343
- logToFile(`abort succeeded for session ${sessionID} (${Date.now() - t1}ms, total ${Date.now() - t0}ms)`);
344
- }
345
- catch (abortErr) {
346
- logToFile(`abort failed (${Date.now() - t1}ms): ${abortErr}`);
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
+ }
347
340
  }
341
+ logToFile(`promptAsync completed for session ${sessionID} (${Date.now() - t0}ms) with model ${nextModel.providerID}/${nextModel.modelID}`);
348
342
  // Toast is best-effort notification. The toast() function (line ~185) has
349
343
  // built-in fallback: showToast failure → app.log. After promptAsync the
350
344
  // server may already be disposing, so both showToast and app.log could fail.
@@ -406,7 +400,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
406
400
  catch {
407
401
  console.log("[rate-limit-fallback] Rate limit error detected, attempting fallback");
408
402
  }
409
- await handleRateLimitFallback(sessionID, "", "");
403
+ await handleRateLimitFallback(sessionID, "", "", true); // skipAbort = true (already in error state)
410
404
  }
411
405
  }
412
406
  if (event.type === "message.updated") {
@@ -445,7 +439,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
445
439
  catch {
446
440
  console.log("[rate-limit-fallback] Rate limit error in message, attempting fallback");
447
441
  }
448
- await handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "");
442
+ await handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "", true); // skipAbort = true (already in error state)
449
443
  }
450
444
  }
451
445
  if (event.type === "session.status") {
@@ -464,6 +458,23 @@ export const RateLimitFallback = async ({ client, directory }) => {
464
458
  catch {
465
459
  console.log("[rate-limit-fallback] session.status:", status);
466
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
+ }
467
478
  if (status?.type === "retry" && status?.message) {
468
479
  const message = status.message.toLowerCase();
469
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.18",
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",