@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.
@@ -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 client.tui.showToast({
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
- if (lastModel) {
222
- const lastKey = getModelKey(lastModel.providerID, lastModel.modelID);
223
- const isLastModelCurrent = currentProviderID === lastModel.providerID && currentModelID === lastModel.modelID;
224
-
225
- if (!isLastModelCurrent && !isModelRateLimited(lastModel.providerID, lastModel.modelID)) {
226
- // Use the last model for one more try
227
- nextModel = lastModel;
228
- await client.tui.showToast({
229
- body: {
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 client.tui.showToast({
251
- body: {
252
- title: "No Fallback Available",
253
- message: config.fallbackMode === "stop"
254
- ? "All fallback models exhausted"
255
- : "All models are rate limited",
256
- variant: "error",
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 client.tui.showToast({
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 client.tui.showToast({
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -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
- }