@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 +1 -2
- package/lib/options.js +0 -1
- package/lib/router.js +6 -43
- package/package.json +2 -2
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
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
|
|
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": "
|
|
42
|
+
"vitest": "4.1.6"
|
|
43
43
|
}
|
|
44
44
|
}
|