@azumag/opencode-rate-limit-fallback 1.0.4 → 1.0.6
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/.github/workflows/npm-publish.yml +21 -0
- package/index.ts +66 -51
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -18
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: npm publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: '20'
|
|
16
|
+
registry-url: 'https://registry.npmjs.org'
|
|
17
|
+
|
|
18
|
+
- run: npm install
|
|
19
|
+
- run: npm publish
|
|
20
|
+
env:
|
|
21
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/index.ts
CHANGED
|
@@ -115,6 +115,38 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
115
115
|
const currentSessionModel = new Map<string, { providerID: string; modelID: string }>();
|
|
116
116
|
const fallbackInProgress = new Map<string, number>(); // sessionID -> timestamp
|
|
117
117
|
|
|
118
|
+
async function logOrToast(message: string, variant: "info" | "success" | "warning" | "error" = "info") {
|
|
119
|
+
try {
|
|
120
|
+
await client.tui.showToast({
|
|
121
|
+
body: { message, variant },
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
await client.app.log({
|
|
125
|
+
body: {
|
|
126
|
+
service: "rate-limit-fallback",
|
|
127
|
+
level: variant,
|
|
128
|
+
message,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function toast(title: string, message: string, variant: "info" | "success" | "warning" | "error" = "info") {
|
|
135
|
+
try {
|
|
136
|
+
await client.tui.showToast({
|
|
137
|
+
body: { title, message, variant },
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
await client.app.log({
|
|
141
|
+
body: {
|
|
142
|
+
service: "rate-limit-fallback",
|
|
143
|
+
level: variant,
|
|
144
|
+
message: `${title}: ${message}`,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
function isModelRateLimited(providerID: string, modelID: string): boolean {
|
|
119
151
|
const key = getModelKey(providerID, modelID);
|
|
120
152
|
const limitedAt = rateLimitedModels.get(key);
|
|
@@ -134,8 +166,21 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
134
166
|
function findNextAvailableModel(currentProviderID: string, currentModelID: string, attemptedModels: Set<string>): FallbackModel | null {
|
|
135
167
|
const currentKey = getModelKey(currentProviderID, currentModelID);
|
|
136
168
|
let startIndex = config.fallbackModels.findIndex(m => getModelKey(m.providerID, m.modelID) === currentKey);
|
|
137
|
-
if (startIndex === -1) startIndex = -1;
|
|
138
169
|
|
|
170
|
+
// If current model is not in the fallback list, search from the beginning
|
|
171
|
+
if (startIndex === -1) {
|
|
172
|
+
// Only search through all models once (first loop handles this)
|
|
173
|
+
for (let i = 0; i < config.fallbackModels.length; i++) {
|
|
174
|
+
const model = config.fallbackModels[i];
|
|
175
|
+
const key = getModelKey(model.providerID, model.modelID);
|
|
176
|
+
if (!attemptedModels.has(key) && !isModelRateLimited(model.providerID, model.modelID)) {
|
|
177
|
+
return model;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Search for the next model after current position
|
|
139
184
|
for (let i = startIndex + 1; i < config.fallbackModels.length; i++) {
|
|
140
185
|
const model = config.fallbackModels[i];
|
|
141
186
|
const key = getModelKey(model.providerID, model.modelID);
|
|
@@ -144,6 +189,7 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
144
189
|
}
|
|
145
190
|
}
|
|
146
191
|
|
|
192
|
+
// Search from the beginning to current position (wrap around)
|
|
147
193
|
for (let i = 0; i <= startIndex && i < config.fallbackModels.length; i++) {
|
|
148
194
|
const model = config.fallbackModels[i];
|
|
149
195
|
const key = getModelKey(model.providerID, model.modelID);
|
|
@@ -175,14 +221,7 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
175
221
|
|
|
176
222
|
await client.session.abort({ path: { id: sessionID } });
|
|
177
223
|
|
|
178
|
-
await
|
|
179
|
-
body: {
|
|
180
|
-
title: "Rate Limit Detected",
|
|
181
|
-
message: `Switching from ${currentModelID || 'current model'}...`,
|
|
182
|
-
variant: "warning",
|
|
183
|
-
duration: 3000,
|
|
184
|
-
},
|
|
185
|
-
});
|
|
224
|
+
await toast("Rate Limit Detected", `Switching from ${currentModelID || 'current model'}...`, "warning");
|
|
186
225
|
|
|
187
226
|
const messagesResult = await client.session.messages({ path: { id: sessionID } });
|
|
188
227
|
if (!messagesResult.data) return;
|
|
@@ -218,22 +257,15 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
218
257
|
} else if (config.fallbackMode === "retry-last") {
|
|
219
258
|
// Try the last model in the list once, then reset on next prompt
|
|
220
259
|
const lastModel = config.fallbackModels[config.fallbackModels.length - 1];
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
title: "Last Resort",
|
|
231
|
-
message: `Trying ${lastModel.modelID} one more time...`,
|
|
232
|
-
variant: "warning",
|
|
233
|
-
duration: 3000,
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
} else {
|
|
260
|
+
if (lastModel) {
|
|
261
|
+
const lastKey = getModelKey(lastModel.providerID, lastModel.modelID);
|
|
262
|
+
const isLastModelCurrent = currentProviderID === lastModel.providerID && currentModelID === lastModel.modelID;
|
|
263
|
+
|
|
264
|
+
if (!isLastModelCurrent && !isModelRateLimited(lastModel.providerID, lastModel.modelID)) {
|
|
265
|
+
// Use the last model for one more try
|
|
266
|
+
nextModel = lastModel;
|
|
267
|
+
await toast("Last Resort", `Trying ${lastModel.modelID} one more time...`, "warning");
|
|
268
|
+
} else {
|
|
237
269
|
// Last model also failed, reset for next prompt
|
|
238
270
|
state.attemptedModels.clear();
|
|
239
271
|
if (currentProviderID && currentModelID) {
|
|
@@ -247,16 +279,13 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
247
279
|
}
|
|
248
280
|
|
|
249
281
|
if (!nextModel) {
|
|
250
|
-
await
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
duration: 5000,
|
|
258
|
-
},
|
|
259
|
-
});
|
|
282
|
+
await toast(
|
|
283
|
+
"No Fallback Available",
|
|
284
|
+
config.fallbackMode === "stop"
|
|
285
|
+
? "All fallback models exhausted"
|
|
286
|
+
: "All models are rate limited",
|
|
287
|
+
"error"
|
|
288
|
+
);
|
|
260
289
|
retryState.delete(stateKey);
|
|
261
290
|
fallbackInProgress.delete(sessionID);
|
|
262
291
|
return;
|
|
@@ -276,14 +305,7 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
276
305
|
|
|
277
306
|
if (parts.length === 0) return;
|
|
278
307
|
|
|
279
|
-
await
|
|
280
|
-
body: {
|
|
281
|
-
title: "Retrying",
|
|
282
|
-
message: `Using ${nextModel.providerID}/${nextModel.modelID}`,
|
|
283
|
-
variant: "info",
|
|
284
|
-
duration: 3000,
|
|
285
|
-
},
|
|
286
|
-
});
|
|
308
|
+
await toast("Retrying", `Using ${nextModel.providerID}/${nextModel.modelID}`, "info");
|
|
287
309
|
|
|
288
310
|
// Track the new model for this session
|
|
289
311
|
currentSessionModel.set(sessionID, { providerID: nextModel.providerID, modelID: nextModel.modelID });
|
|
@@ -296,14 +318,7 @@ export const RateLimitFallback: Plugin = async ({ client, directory }) => {
|
|
|
296
318
|
},
|
|
297
319
|
});
|
|
298
320
|
|
|
299
|
-
await
|
|
300
|
-
body: {
|
|
301
|
-
title: "Fallback Successful",
|
|
302
|
-
message: `Now using ${nextModel.modelID}`,
|
|
303
|
-
variant: "success",
|
|
304
|
-
duration: 3000,
|
|
305
|
-
},
|
|
306
|
-
});
|
|
321
|
+
await toast("Fallback Successful", `Now using ${nextModel.modelID}`, "success");
|
|
307
322
|
|
|
308
323
|
retryState.delete(stateKey);
|
|
309
324
|
// Clear fallback flag to allow next fallback if needed
|
package/package.json
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm publish:*)",
|
|
5
|
-
"Bash(npm whoami:*)",
|
|
6
|
-
"Bash(npm config:*)",
|
|
7
|
-
"WebSearch",
|
|
8
|
-
"WebFetch(domain:github.blog)",
|
|
9
|
-
"Bash(npm login:*)",
|
|
10
|
-
"Bash(npm token create:*)",
|
|
11
|
-
"Bash(npm view:*)",
|
|
12
|
-
"Bash(git add:*)",
|
|
13
|
-
"Bash(git commit -m \"$\\(cat <<''EOF''\nPublish as scoped package @azumag/opencode-rate-limit-fallback\n\n- Rename package to @azumag/opencode-rate-limit-fallback\n- Add npm installation instructions to README\n- Add npm version badge\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
14
|
-
"Bash(git push:*)",
|
|
15
|
-
"Bash(git commit:*)"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|