@azumag/opencode-rate-limit-fallback 1.0.9 → 1.0.11

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 +188 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync, appendFileSync } from "fs";
2
2
  import { join } from "path";
3
+ // Debug log at module level
4
+ console.log("[rate-limit-fallback] Module loaded, version:", process.env.npm_package_version || "unknown");
3
5
  const DEFAULT_FALLBACK_MODELS = [
4
6
  { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
5
7
  { providerID: "google", modelID: "gemini-2.5-pro" },
@@ -71,7 +73,44 @@ function isRateLimitError(error) {
71
73
  }
72
74
  export const RateLimitFallback = async ({ client, directory }) => {
73
75
  const config = loadConfig(directory);
76
+ const logFilePath = join(process.env.HOME || "", ".opencode", "rate-limit-fallback-debug.log");
77
+ // Write to file for debugging
78
+ const logToFile = (message) => {
79
+ try {
80
+ appendFileSync(logFilePath, `[${new Date().toISOString()}] ${message}\n`);
81
+ }
82
+ catch { }
83
+ };
84
+ logToFile(`Plugin loaded, config: ${JSON.stringify(config)}`);
85
+ console.log("[rate-limit-fallback] Plugin loaded, config:", config);
86
+ // Use client.app.log for better logging in both run and TUI modes
87
+ try {
88
+ await client.app.log({
89
+ body: {
90
+ service: "rate-limit-fallback",
91
+ level: "info",
92
+ message: `Plugin loaded, config: ${JSON.stringify(config)}`,
93
+ },
94
+ });
95
+ }
96
+ catch (e) {
97
+ logToFile(`client.app.log failed: ${e}`);
98
+ console.log("[rate-limit-fallback] Plugin loaded, config:", config);
99
+ }
74
100
  if (!config.enabled) {
101
+ logToFile("Plugin disabled, returning empty object");
102
+ try {
103
+ await client.app.log({
104
+ body: {
105
+ service: "rate-limit-fallback",
106
+ level: "info",
107
+ message: "Plugin disabled, returning empty object",
108
+ },
109
+ });
110
+ }
111
+ catch {
112
+ console.log("[rate-limit-fallback] Plugin disabled, returning empty object");
113
+ }
75
114
  return {};
76
115
  }
77
116
  const rateLimitedModels = new Map();
@@ -186,8 +225,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
186
225
  currentModelID = tracked.modelID;
187
226
  }
188
227
  }
189
- await client.session.abort({ path: { id: sessionID } });
190
- await toast("Rate Limit Detected", `Switching from ${currentModelID || 'current model'}...`, "warning");
228
+ // Fetch messages BEFORE abort session must still be alive
191
229
  const messagesResult = await client.session.messages({ path: { id: sessionID } });
192
230
  if (!messagesResult.data)
193
231
  return;
@@ -195,6 +233,7 @@ export const RateLimitFallback = async ({ client, directory }) => {
195
233
  const lastUserMessage = [...messages].reverse().find(m => m.info.role === "user");
196
234
  if (!lastUserMessage)
197
235
  return;
236
+ toast("Rate Limit Detected", `Switching from ${currentModelID || 'current model'}...`, "warning").catch(() => { });
198
237
  const stateKey = `${sessionID}:${lastUserMessage.info.id}`;
199
238
  let state = retryState.get(stateKey);
200
239
  if (!state || Date.now() - state.lastAttemptTime > 30000) {
@@ -220,12 +259,11 @@ export const RateLimitFallback = async ({ client, directory }) => {
220
259
  // Try the last model in the list once, then reset on next prompt
221
260
  const lastModel = config.fallbackModels[config.fallbackModels.length - 1];
222
261
  if (lastModel) {
223
- const lastKey = getModelKey(lastModel.providerID, lastModel.modelID);
224
262
  const isLastModelCurrent = currentProviderID === lastModel.providerID && currentModelID === lastModel.modelID;
225
263
  if (!isLastModelCurrent && !isModelRateLimited(lastModel.providerID, lastModel.modelID)) {
226
264
  // Use the last model for one more try
227
265
  nextModel = lastModel;
228
- await toast("Last Resort", `Trying ${lastModel.modelID} one more time...`, "warning");
266
+ toast("Last Resort", `Trying ${lastModel.modelID} one more time...`, "warning").catch(() => { });
229
267
  }
230
268
  else {
231
269
  // Last model also failed, reset for next prompt
@@ -240,9 +278,9 @@ export const RateLimitFallback = async ({ client, directory }) => {
240
278
  // "stop" mode: nextModel remains null, will show error below
241
279
  }
242
280
  if (!nextModel) {
243
- await toast("No Fallback Available", config.fallbackMode === "stop"
281
+ toast("No Fallback Available", config.fallbackMode === "stop"
244
282
  ? "All fallback models exhausted"
245
- : "All models are rate limited", "error");
283
+ : "All models are rate limited", "error").catch(() => { });
246
284
  retryState.delete(stateKey);
247
285
  fallbackInProgress.delete(sessionID);
248
286
  return;
@@ -261,51 +299,185 @@ export const RateLimitFallback = async ({ client, directory }) => {
261
299
  .filter(Boolean);
262
300
  if (parts.length === 0)
263
301
  return;
264
- await toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info");
302
+ toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info").catch(() => { });
265
303
  // Track the new model for this session
266
304
  currentSessionModel.set(sessionID, { providerID: nextModel.providerID, modelID: nextModel.modelID });
267
- await client.session.prompt({
268
- path: { id: sessionID },
269
- body: {
270
- parts: parts,
271
- model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
272
- },
273
- });
274
- await toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success");
305
+ const promptBody = {
306
+ parts: parts,
307
+ model: { providerID: nextModel.providerID, modelID: nextModel.modelID },
308
+ };
309
+ // Try promptAsync first (no abort needed — works in both TUI and run modes)
310
+ try {
311
+ await client.session.promptAsync({
312
+ path: { id: sessionID },
313
+ body: promptBody,
314
+ });
315
+ logToFile(`promptAsync sent successfully for session ${sessionID} with model ${nextModel.providerID}/${nextModel.modelID}`);
316
+ }
317
+ catch (promptAsyncErr) {
318
+ // promptAsync failed — fall back to abort + promptAsync
319
+ logToFile(`promptAsync failed (${promptAsyncErr}), falling back to abort + promptAsync`);
320
+ try {
321
+ await client.session.abort({ path: { id: sessionID } });
322
+ }
323
+ catch (abortErr) {
324
+ logToFile(`abort also failed: ${abortErr}`);
325
+ }
326
+ await client.session.promptAsync({
327
+ path: { id: sessionID },
328
+ body: promptBody,
329
+ });
330
+ logToFile(`abort + promptAsync sent successfully for session ${sessionID}`);
331
+ }
332
+ toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success").catch(() => { });
275
333
  retryState.delete(stateKey);
276
334
  // Clear fallback flag to allow next fallback if needed
277
335
  fallbackInProgress.delete(sessionID);
278
336
  }
279
337
  catch (err) {
338
+ logToFile(`handleRateLimitFallback error: ${err}`);
280
339
  // Fallback failed, clear the flag
281
340
  fallbackInProgress.delete(sessionID);
282
341
  }
283
342
  }
284
343
  return {
285
344
  event: async ({ event }) => {
345
+ // Log events to debug plugin execution
346
+ logToFile(`Event received: ${event.type}`);
347
+ try {
348
+ await client.app.log({
349
+ body: {
350
+ service: "rate-limit-fallback",
351
+ level: "debug",
352
+ message: `Event received: ${event.type}`,
353
+ },
354
+ });
355
+ }
356
+ catch (e) {
357
+ logToFile(`client.app.log failed for event: ${e}`);
358
+ console.log("[rate-limit-fallback] Event received:", event.type);
359
+ }
286
360
  if (event.type === "session.error") {
287
361
  const { sessionID, error } = event.properties;
362
+ logToFile(`session.error: sessionID=${sessionID}, error=${JSON.stringify(error)}`);
363
+ try {
364
+ await client.app.log({
365
+ body: {
366
+ service: "rate-limit-fallback",
367
+ level: "debug",
368
+ message: `session.error: sessionID=${sessionID}, error=${JSON.stringify(error)}`,
369
+ },
370
+ });
371
+ }
372
+ catch {
373
+ console.log("[rate-limit-fallback] session.error:", sessionID, error);
374
+ }
288
375
  if (sessionID && error && isRateLimitError(error)) {
376
+ logToFile("Rate limit error detected, attempting fallback");
377
+ try {
378
+ await client.app.log({
379
+ body: {
380
+ service: "rate-limit-fallback",
381
+ level: "info",
382
+ message: "Rate limit error detected, attempting fallback",
383
+ },
384
+ });
385
+ }
386
+ catch {
387
+ console.log("[rate-limit-fallback] Rate limit error detected, attempting fallback");
388
+ }
289
389
  await handleRateLimitFallback(sessionID, "", "");
290
390
  }
291
391
  }
292
392
  if (event.type === "message.updated") {
293
393
  const info = event.properties?.info;
394
+ logToFile(`message.updated: ${JSON.stringify(info)}`);
395
+ try {
396
+ await client.app.log({
397
+ body: {
398
+ service: "rate-limit-fallback",
399
+ level: "debug",
400
+ message: `message.updated: ${JSON.stringify(info)}`,
401
+ },
402
+ });
403
+ }
404
+ catch {
405
+ console.log("[rate-limit-fallback] message.updated:", info);
406
+ }
407
+ // Track assistant message model info for later use in fallback
408
+ if (info?.role === "assistant" && info?.sessionID && info?.providerID && info?.modelID) {
409
+ currentSessionModel.set(info.sessionID, {
410
+ providerID: info.providerID,
411
+ modelID: info.modelID,
412
+ });
413
+ }
294
414
  if (info?.error && isRateLimitError(info.error)) {
415
+ logToFile("Rate limit error in message, attempting fallback");
416
+ try {
417
+ await client.app.log({
418
+ body: {
419
+ service: "rate-limit-fallback",
420
+ level: "info",
421
+ message: "Rate limit error in message, attempting fallback",
422
+ },
423
+ });
424
+ }
425
+ catch {
426
+ console.log("[rate-limit-fallback] Rate limit error in message, attempting fallback");
427
+ }
295
428
  await handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "");
296
429
  }
297
430
  }
298
431
  if (event.type === "session.status") {
299
432
  const props = event.properties;
300
433
  const status = props?.status;
434
+ logToFile(`session.status: ${JSON.stringify(status)}`);
435
+ try {
436
+ await client.app.log({
437
+ body: {
438
+ service: "rate-limit-fallback",
439
+ level: "debug",
440
+ message: `session.status: ${JSON.stringify(status)}`,
441
+ },
442
+ });
443
+ }
444
+ catch {
445
+ console.log("[rate-limit-fallback] session.status:", status);
446
+ }
301
447
  if (status?.type === "retry" && status?.message) {
302
448
  const message = status.message.toLowerCase();
303
449
  const isRateLimitRetry = message.includes("usage limit") ||
304
450
  message.includes("rate limit") ||
305
451
  message.includes("high concurrency") ||
306
452
  message.includes("reduce concurrency");
453
+ logToFile(`Is rate limit retry: ${isRateLimitRetry}, message: ${message}`);
454
+ try {
455
+ await client.app.log({
456
+ body: {
457
+ service: "rate-limit-fallback",
458
+ level: "debug",
459
+ message: `Is rate limit retry: ${isRateLimitRetry}, message: ${message}`,
460
+ },
461
+ });
462
+ }
463
+ catch {
464
+ console.log("[rate-limit-fallback] Is rate limit retry:", isRateLimitRetry, "message:", message);
465
+ }
307
466
  if (isRateLimitRetry) {
308
467
  // Try fallback on any attempt, handleRateLimitFallback will manage state
468
+ logToFile("Attempting fallback for rate limit retry");
469
+ try {
470
+ await client.app.log({
471
+ body: {
472
+ service: "rate-limit-fallback",
473
+ level: "info",
474
+ message: "Attempting fallback for rate limit retry",
475
+ },
476
+ });
477
+ }
478
+ catch {
479
+ console.log("[rate-limit-fallback] Attempting fallback for rate limit retry");
480
+ }
309
481
  await handleRateLimitFallback(props.sessionID, "", "");
310
482
  }
311
483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",