@renjfk/opencode-model-fallback 0.1.2 → 0.2.1

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/README.md CHANGED
@@ -55,7 +55,6 @@ If you want to set plugin options, use the tuple form:
55
55
  - `retry_on_errors`: retryable HTTP status codes. Defaults to `429`.
56
56
  - `retryable_error_patterns`: retryable error message patterns. Defaults to `["rate.?limit"]`.
57
57
  - `cooldown_seconds`: how long a failed original model remains on fallback. Defaults to `3600`.
58
- - `timeout_seconds`: abort and retry if a response is inactive for this long. Defaults to `30`.
59
58
  - `notify_on_fallback`: show fallback/recovery toasts. Defaults to `true`.
60
59
 
61
60
  ## How it works
@@ -227,7 +226,7 @@ at the local repo path, not the npm package name:
227
226
 
228
227
  ```json
229
228
  {
230
- "plugin": ["/Users/your-user/opencode-model-fallback"]
229
+ "plugin": ["/Users/your-user/opencode-model-fallback/index.js"]
231
230
  }
232
231
  ```
233
232
 
package/lib/options.js CHANGED
@@ -3,7 +3,6 @@ const DEFAULT_OPTIONS = {
3
3
  retry_on_errors: [429],
4
4
  retryable_error_patterns: ["rate.?limit"],
5
5
  cooldown_seconds: 3600,
6
- timeout_seconds: 30,
7
6
  notify_on_fallback: true,
8
7
  };
9
8
 
package/lib/router.js CHANGED
@@ -13,9 +13,9 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
13
13
  );
14
14
  const store = createStateStore();
15
15
  const retrying = new Set();
16
- const timers = new Map();
17
16
  const selfAbortAt = new Map();
18
17
  const activeOriginals = new Map();
18
+ const activeRequested = new Map();
19
19
  const activeTargets = new Map();
20
20
  let agentConfigs;
21
21
 
@@ -42,13 +42,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
42
42
  return cooldown ? fallback : original;
43
43
  }
44
44
 
45
- function timedOriginal(requested) {
46
- if (!hasMapping(requested)) return undefined;
47
- const cooldown = store.getModelCooldown(requested);
48
- if (cooldown) return undefined;
49
- return requested;
50
- }
51
-
52
45
  function shouldRoute(requested) {
53
46
  return hasMapping(requested) || !!fallbackToOriginal[requested];
54
47
  }
@@ -66,31 +59,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
66
59
  return !cooldown;
67
60
  }
68
61
 
69
- function clearTimer(sessionID) {
70
- const timer = timers.get(sessionID);
71
- if (timer) clearTimeout(timer);
72
- timers.delete(sessionID);
73
- }
74
-
75
- function scheduleTimeout(sessionID, original, agent) {
76
- clearTimer(sessionID);
77
- if (options.timeout_seconds <= 0 || !hasMapping(original)) return;
78
- timers.set(
79
- sessionID,
80
- setTimeout(async () => {
81
- timers.delete(sessionID);
82
- if (retrying.has(sessionID) || !timedOriginal(original)) return;
83
- retrying.add(sessionID);
84
- try {
85
- await abortCurrentSession(sessionID);
86
- await retryWithFallback(sessionID, original, agent, "timeout");
87
- } finally {
88
- retrying.delete(sessionID);
89
- }
90
- }, options.timeout_seconds * 1000),
91
- );
92
- }
93
-
94
62
  async function abortCurrentSession(sessionID) {
95
63
  const aborted = await abortSession(ctx.client, sessionID);
96
64
  if (aborted) selfAbortAt.set(sessionID, Date.now());
@@ -105,7 +73,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
105
73
  store.setModelCooldown(original, reason, failedAt, cooldownUntil);
106
74
  const parts = await getReplayParts(ctx.client, ctx.directory, sessionID);
107
75
  if (parts.length === 0) return;
108
- clearTimer(sessionID);
109
76
  try {
110
77
  await new Promise((resolve) => setTimeout(resolve, POST_ABORT_DELAY_MS));
111
78
  await ctx.client.session.promptAsync({
@@ -125,10 +92,13 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
125
92
  }
126
93
 
127
94
  async function toastRouteChange(sessionID, requested, target, original) {
95
+ const previousRequested = activeRequested.get(sessionID);
128
96
  const previous = activeTargets.get(sessionID);
97
+ activeRequested.set(sessionID, requested);
129
98
  activeTargets.set(sessionID, target);
130
99
  if (previous === target) return;
131
100
  if (!previous && requested === target) return;
101
+ if (requested === target && previousRequested === previous) return;
132
102
  const isFallback = target !== original;
133
103
  await toast(
134
104
  isFallback ? "Model Fallback" : "Model Recovered",
@@ -152,7 +122,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
152
122
  }
153
123
  if (!shouldFallbackFromError(failed, original)) return;
154
124
  retrying.add(sessionID);
155
- clearTimer(sessionID);
156
125
  try {
157
126
  await retryWithFallback(sessionID, original, agent, source);
158
127
  } finally {
@@ -180,7 +149,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
180
149
  }
181
150
  if (!shouldFallbackFromError(failed, original)) return;
182
151
  retrying.add(sessionID);
183
- clearTimer(sessionID);
184
152
  try {
185
153
  await abortCurrentSession(sessionID);
186
154
  await retryWithFallback(sessionID, original, agent, "session.status");
@@ -212,8 +180,6 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
212
180
  const model = modelObject(target);
213
181
  if (model && output.message) output.message.model = model;
214
182
  await toastRouteChange(sessionID, requested, target, original);
215
- if (hasMapping(target)) scheduleTimeout(sessionID, target, input.agent);
216
- else clearTimer(sessionID);
217
183
  },
218
184
 
219
185
  event: async ({ event }) => {
@@ -223,8 +189,8 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
223
189
  if (id) {
224
190
  retrying.delete(id);
225
191
  activeOriginals.delete(id);
192
+ activeRequested.delete(id);
226
193
  activeTargets.delete(id);
227
- clearTimer(id);
228
194
  }
229
195
  return;
230
196
  }
@@ -245,11 +211,8 @@ export function createMappedFallbackRouter(ctx, rawOptions) {
245
211
  if (event.type === "message.updated") {
246
212
  const info = props?.info;
247
213
  if (info?.role !== "assistant") return;
214
+ if (!info?.error) return;
248
215
  const sessionID = info?.sessionID;
249
- if (!info?.error) {
250
- if (sessionID) clearTimer(sessionID);
251
- return;
252
- }
253
216
  const model =
254
217
  info?.model ??
255
218
  (typeof info?.providerID === "string" && typeof info?.modelID === "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renjfk/opencode-model-fallback",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Mapped model fallback router for OpenCode. Routes retryable model failures to configured fallback models and recovers after cooldown.",
5
5
  "keywords": [
6
6
  "fallback",
@@ -39,6 +39,6 @@
39
39
  "devDependencies": {
40
40
  "oxfmt": "0.50.0",
41
41
  "oxlint": "1.65.0",
42
- "vitest": "latest"
42
+ "vitest": "4.1.6"
43
43
  }
44
44
  }