@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.
- package/dist/index.js +188 -16
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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