@shutovks/opencode-model-fallback 1.0.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-model-fallback contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @shutovks/opencode-model-fallback
2
+
3
+ OpenCode plugin that keeps a session alive when the current model fails with rate limits, quota exhaustion, overloads, or temporary provider errors.
4
+
5
+ It retries the last user message in the same session with the next configured fallback model.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add -g @shutovks/opencode-model-fallback
11
+ ```
12
+
13
+ Or use any package manager OpenCode can resolve from your `plugin` config.
14
+
15
+ ## Configure
16
+
17
+ Add the plugin to `opencode.json` or `.opencode/opencode.json`:
18
+
19
+ ```jsonc
20
+ {
21
+ "$schema": "https://opencode.ai/config.json",
22
+ "plugin": [
23
+ [
24
+ "@shutovks/opencode-model-fallback",
25
+ {
26
+ "enabled": true,
27
+ "fallback_models": [
28
+ "anthropic/claude-sonnet-4-5",
29
+ "openai/gpt-5.1-codex",
30
+ "google/gemini-2.5-pro"
31
+ ],
32
+ "unavailable_models": [],
33
+ "max_attempts": 3,
34
+ "cooldown_ms": 60000,
35
+ "notify": true
36
+ }
37
+ ]
38
+ ]
39
+ }
40
+ ```
41
+
42
+ Restart OpenCode after changing config.
43
+
44
+ ## Options
45
+
46
+ | Option | Default | Meaning |
47
+ | --- | --- | --- |
48
+ | `enabled` | `true` | Default behavior for new sessions. |
49
+ | `fallback_models` | `[]` | Ordered fallback model list, each as `provider/model`. |
50
+ | `unavailable_models` | `[]` | Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting `session.error`. |
51
+ | `retry_on_errors` | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger fallback. |
52
+ | `max_attempts` | `3` | Max fallback retries per session. |
53
+ | `cooldown_ms` | `60000` | How long to avoid a failed model. |
54
+ | `notify` | `true` | Show a toast when switching models. |
55
+
56
+ ## Session Control
57
+
58
+ The plugin registers one tool:
59
+
60
+ ```text
61
+ model_fallback_control(action: "enable" | "disable" | "status" | "reset")
62
+ ```
63
+
64
+ Use it from inside OpenCode to control fallback for the current session:
65
+
66
+ ```text
67
+ Use model_fallback_control to disable fallback in this session.
68
+ ```
69
+
70
+ Actions:
71
+
72
+ - `enable`: enable fallback for the current session.
73
+ - `disable`: disable fallback for the current session.
74
+ - `status`: return effective state, current model, attempts, and configured fallbacks.
75
+ - `reset`: clear session state and return to the config default.
76
+
77
+ Session overrides are in memory only. They disappear when the session is deleted or OpenCode restarts.
78
+
79
+ ## What Counts As Retryable
80
+
81
+ Fallback runs on configured status codes and common transient error text:
82
+
83
+ - rate limit / too many requests
84
+ - quota exceeded
85
+ - all credentials for model exhausted
86
+ - model unsupported
87
+ - service unavailable / overloaded / temporarily unavailable
88
+ - `429`, `503`, `529` in plain text errors
89
+
90
+ ## Limitations
91
+
92
+ OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.
93
+
94
+ It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ bun install
100
+ bun test
101
+ bun run typecheck
102
+ bun run build
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1 @@
1
+ export { ModelFallbackPlugin, default } from "./plugin";
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ // @bun
2
+ // src/plugin.ts
3
+ import { tool } from "@opencode-ai/plugin/tool";
4
+ var DEFAULT_CONFIG = {
5
+ enabled: true,
6
+ fallbackModels: [],
7
+ unavailableModels: [],
8
+ retryOnErrors: [429, 500, 502, 503, 504],
9
+ maxAttempts: 3,
10
+ cooldownMs: 60000,
11
+ notify: true
12
+ };
13
+ var RETRYABLE_PATTERNS = [
14
+ /rate.?limit/i,
15
+ /too.?many.?requests/i,
16
+ /quota.?exceeded/i,
17
+ /exceeded.*quota/i,
18
+ /usage\s*quota/i,
19
+ /all.*credentials.*for.*model/i,
20
+ /cool(?:ing)?.?down/i,
21
+ /model.{0,20}?not.{0,10}?supported/i,
22
+ /model_not_supported/i,
23
+ /service.?unavailable/i,
24
+ /overloaded/i,
25
+ /temporarily.?unavailable/i,
26
+ /try.?again/i,
27
+ /(?:^|\s)429(?:\s|$)/,
28
+ /(?:^|\s)503(?:\s|$)/,
29
+ /(?:^|\s)529(?:\s|$)/
30
+ ];
31
+ function isRecord(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value);
33
+ }
34
+ function stringField(record, key) {
35
+ const value = record?.[key];
36
+ return typeof value === "string" && value.length > 0 ? value : undefined;
37
+ }
38
+ function normalizeOptions(options = {}) {
39
+ const fallbackModels = Array.isArray(options.fallback_models) ? options.fallback_models : DEFAULT_CONFIG.fallbackModels;
40
+ const unavailableModels = Array.isArray(options.unavailable_models) ? options.unavailable_models : DEFAULT_CONFIG.unavailableModels;
41
+ return {
42
+ enabled: options.enabled ?? DEFAULT_CONFIG.enabled,
43
+ fallbackModels: fallbackModels.filter((model) => typeof model === "string" && parseModel(model) !== undefined),
44
+ unavailableModels: unavailableModels.filter((model) => typeof model === "string" && parseModel(model) !== undefined),
45
+ retryOnErrors: options.retry_on_errors ?? DEFAULT_CONFIG.retryOnErrors,
46
+ maxAttempts: options.max_attempts ?? DEFAULT_CONFIG.maxAttempts,
47
+ cooldownMs: options.cooldown_ms ?? DEFAULT_CONFIG.cooldownMs,
48
+ notify: options.notify ?? DEFAULT_CONFIG.notify
49
+ };
50
+ }
51
+ function parseModel(model) {
52
+ const [providerID, ...modelParts] = model.split("/");
53
+ if (!providerID || modelParts.length === 0)
54
+ return;
55
+ return { providerID, modelID: modelParts.join("/") };
56
+ }
57
+ function normalizeModel(model) {
58
+ if (typeof model === "string")
59
+ return model;
60
+ if (!isRecord(model))
61
+ return;
62
+ const providerID = stringField(model, "providerID");
63
+ const modelID = stringField(model, "modelID") ?? stringField(model, "id");
64
+ return providerID && modelID ? `${providerID}/${modelID}` : undefined;
65
+ }
66
+ function resolveSessionID(properties) {
67
+ const props = isRecord(properties) ? properties : undefined;
68
+ const info = isRecord(props?.info) ? props.info : undefined;
69
+ return stringField(props, "sessionID") ?? stringField(info, "sessionID") ?? stringField(info, "id");
70
+ }
71
+ function resolveEventModel(properties) {
72
+ const props = isRecord(properties) ? properties : undefined;
73
+ const info = isRecord(props?.info) ? props.info : undefined;
74
+ const providerID = stringField(props, "providerID");
75
+ const modelID = stringField(props, "modelID");
76
+ return normalizeModel(props?.model) ?? normalizeModel(info?.model) ?? (providerID && modelID ? `${providerID}/${modelID}` : undefined);
77
+ }
78
+ function statusCode(error) {
79
+ if (!isRecord(error))
80
+ return;
81
+ const direct = error.statusCode ?? error.status ?? error.code;
82
+ if (typeof direct === "number")
83
+ return direct;
84
+ if (typeof direct === "string" && /^\d+$/.test(direct))
85
+ return Number(direct);
86
+ const data = isRecord(error.data) ? error.data : undefined;
87
+ const dataStatus = data?.statusCode ?? data?.status ?? data?.code;
88
+ if (typeof dataStatus === "number")
89
+ return dataStatus;
90
+ if (typeof dataStatus === "string" && /^\d+$/.test(dataStatus))
91
+ return Number(dataStatus);
92
+ const response = isRecord(error.response) ? error.response : undefined;
93
+ const responseStatus = response?.status;
94
+ return typeof responseStatus === "number" ? responseStatus : undefined;
95
+ }
96
+ function errorText(error) {
97
+ if (typeof error === "string")
98
+ return error;
99
+ if (error instanceof Error)
100
+ return `${error.name} ${error.message}`;
101
+ try {
102
+ return JSON.stringify(error);
103
+ } catch {
104
+ return String(error);
105
+ }
106
+ }
107
+ function isRetryableError(error, retryOnErrors) {
108
+ const status = statusCode(error);
109
+ if (status !== undefined && retryOnErrors.includes(status))
110
+ return true;
111
+ const text = errorText(error);
112
+ return RETRYABLE_PATTERNS.some((pattern) => pattern.test(text));
113
+ }
114
+ function extractMessages(response) {
115
+ if (Array.isArray(response))
116
+ return response;
117
+ if (isRecord(response) && Array.isArray(response.data))
118
+ return response.data;
119
+ return [];
120
+ }
121
+ function extractParts(value) {
122
+ if (!Array.isArray(value))
123
+ return [];
124
+ return value.filter((part) => {
125
+ return isRecord(part) && typeof part.type === "string";
126
+ });
127
+ }
128
+ function getLastUserPayload(messagesResponse) {
129
+ const lastUserMessage = extractMessages(messagesResponse).filter((message) => isRecord(message) && isRecord(message.info) && message.info.role === "user").pop();
130
+ if (!isRecord(lastUserMessage))
131
+ return { parts: [] };
132
+ const info = isRecord(lastUserMessage.info) ? lastUserMessage.info : undefined;
133
+ const messageParts = extractParts(lastUserMessage.parts);
134
+ const infoID = stringField(info, "id");
135
+ const parts = messageParts.length > 0 ? messageParts : extractParts(info?.parts);
136
+ return {
137
+ parts,
138
+ ...infoID ? { messageID: infoID } : {}
139
+ };
140
+ }
141
+ function getState(states, sessionID, model, agent) {
142
+ let state = states.get(sessionID);
143
+ if (!state) {
144
+ state = {
145
+ originalModel: model,
146
+ currentModel: model,
147
+ agent,
148
+ attempts: 0,
149
+ failedUntil: new Map
150
+ };
151
+ states.set(sessionID, state);
152
+ return state;
153
+ }
154
+ if (agent)
155
+ state.agent = agent;
156
+ if (!state.originalModel && model)
157
+ state.originalModel = model;
158
+ if (!state.currentModel && model)
159
+ state.currentModel = model;
160
+ return state;
161
+ }
162
+ function isEnabled(config, state) {
163
+ return state?.enabledOverride ?? config.enabled;
164
+ }
165
+ function nextModel(config, state) {
166
+ const now = Date.now();
167
+ return config.fallbackModels.find((model) => {
168
+ if (model === state.currentModel)
169
+ return false;
170
+ return (state.failedUntil.get(model) ?? 0) <= now;
171
+ });
172
+ }
173
+ function firstFallbackFor(config, model) {
174
+ return config.fallbackModels.find((fallbackModel) => fallbackModel !== model);
175
+ }
176
+ function fallbackForUnavailable(config, model) {
177
+ if (!model || !config.unavailableModels.includes(model))
178
+ return;
179
+ return firstFallbackFor(config, model);
180
+ }
181
+ function resetState(state) {
182
+ state.currentModel = state.originalModel;
183
+ state.attempts = 0;
184
+ state.failedUntil.clear();
185
+ state.enabledOverride = undefined;
186
+ state.pendingModel = undefined;
187
+ state.awaitingModel = undefined;
188
+ }
189
+ function statusText(config, state) {
190
+ const enabled = isEnabled(config, state);
191
+ return JSON.stringify({
192
+ enabled,
193
+ defaultEnabled: config.enabled,
194
+ override: state?.enabledOverride ?? null,
195
+ currentModel: state?.currentModel ?? null,
196
+ originalModel: state?.originalModel ?? null,
197
+ attempts: state?.attempts ?? 0,
198
+ maxAttempts: config.maxAttempts,
199
+ fallbackModels: config.fallbackModels,
200
+ unavailableModels: config.unavailableModels
201
+ });
202
+ }
203
+ function createModelFallbackPlugin(input, options = {}) {
204
+ const config = normalizeOptions(options);
205
+ const states = new Map;
206
+ const inFlight = new Set;
207
+ async function showToast(message) {
208
+ if (!config.notify)
209
+ return;
210
+ await input.client.tui?.showToast?.({
211
+ body: {
212
+ title: "Model Fallback",
213
+ message,
214
+ variant: "warning",
215
+ duration: 5000
216
+ }
217
+ }).catch(() => {
218
+ return;
219
+ });
220
+ }
221
+ async function retryWithModel(sessionID, state, model) {
222
+ const parsed = parseModel(model);
223
+ if (!parsed)
224
+ return false;
225
+ const messages = await input.client.session.messages({
226
+ path: { id: sessionID },
227
+ query: { directory: input.directory }
228
+ });
229
+ const payload = getLastUserPayload(messages);
230
+ if (payload.parts.length === 0)
231
+ return false;
232
+ await input.client.session.promptAsync({
233
+ path: { id: sessionID },
234
+ query: { directory: input.directory },
235
+ body: {
236
+ model: parsed,
237
+ parts: payload.parts,
238
+ ...state.agent ? { agent: state.agent } : {},
239
+ ...payload.messageID ? { messageID: payload.messageID } : {}
240
+ }
241
+ });
242
+ return true;
243
+ }
244
+ return {
245
+ config: async (opencodeConfig) => {
246
+ if (!config.enabled)
247
+ return;
248
+ const rootFallback = fallbackForUnavailable(config, opencodeConfig.model);
249
+ if (rootFallback)
250
+ opencodeConfig.model = rootFallback;
251
+ for (const agent of Object.values(opencodeConfig.agent ?? {})) {
252
+ if (!agent)
253
+ continue;
254
+ const agentFallback = fallbackForUnavailable(config, agent.model);
255
+ if (agentFallback)
256
+ agent.model = agentFallback;
257
+ }
258
+ },
259
+ tool: {
260
+ model_fallback_control: tool({
261
+ description: "Enable, disable, inspect, or reset model fallback for the current OpenCode session.",
262
+ args: {
263
+ action: tool.schema.string().describe("Session fallback action: enable, disable, status, or reset")
264
+ },
265
+ execute: async (args, context) => {
266
+ const state = getState(states, context.sessionID, undefined, context.agent);
267
+ if (args.action === "enable") {
268
+ state.enabledOverride = true;
269
+ return statusText(config, state);
270
+ }
271
+ if (args.action === "disable") {
272
+ state.enabledOverride = false;
273
+ return statusText(config, state);
274
+ }
275
+ if (args.action === "reset") {
276
+ resetState(state);
277
+ return statusText(config, state);
278
+ }
279
+ if (args.action !== "status") {
280
+ return `Unknown action: ${args.action}. Use enable, disable, status, or reset.`;
281
+ }
282
+ return statusText(config, state);
283
+ }
284
+ })
285
+ },
286
+ event: async ({ event }) => {
287
+ const props = isRecord(event.properties) ? event.properties : {};
288
+ const sessionID = resolveSessionID(props);
289
+ if (!sessionID)
290
+ return;
291
+ if (event.type === "session.created") {
292
+ const info = isRecord(props.info) ? props.info : undefined;
293
+ getState(states, sessionID, resolveEventModel(props), stringField(info, "agent"));
294
+ return;
295
+ }
296
+ if (event.type === "session.deleted") {
297
+ states.delete(sessionID);
298
+ inFlight.delete(sessionID);
299
+ return;
300
+ }
301
+ if (event.type !== "session.error")
302
+ return;
303
+ if (inFlight.has(sessionID))
304
+ return;
305
+ const state = getState(states, sessionID, resolveEventModel(props), stringField(props, "agent"));
306
+ if (!isEnabled(config, state))
307
+ return;
308
+ if (config.fallbackModels.length === 0)
309
+ return;
310
+ if (!isRetryableError(props.error, config.retryOnErrors))
311
+ return;
312
+ if (state.attempts >= config.maxAttempts)
313
+ return;
314
+ const eventModel = resolveEventModel(props);
315
+ if (state.awaitingModel && eventModel && eventModel !== state.awaitingModel)
316
+ return;
317
+ state.awaitingModel = undefined;
318
+ const failedModel = eventModel ?? state.currentModel;
319
+ if (failedModel) {
320
+ state.currentModel = failedModel;
321
+ state.failedUntil.set(failedModel, Date.now() + config.cooldownMs);
322
+ }
323
+ const model = nextModel(config, state);
324
+ if (!model)
325
+ return;
326
+ inFlight.add(sessionID);
327
+ try {
328
+ const accepted = await retryWithModel(sessionID, state, model);
329
+ if (!accepted)
330
+ return;
331
+ state.attempts += 1;
332
+ state.currentModel = model;
333
+ state.pendingModel = model;
334
+ state.awaitingModel = model;
335
+ await showToast(`Switched to ${model}`);
336
+ } finally {
337
+ inFlight.delete(sessionID);
338
+ }
339
+ },
340
+ "chat.message": async (chatInput, output) => {
341
+ const requestedModel = normalizeModel(chatInput.model);
342
+ const state = states.get(chatInput.sessionID) ?? (requestedModel ? getState(states, chatInput.sessionID, requestedModel, chatInput.agent) : undefined);
343
+ if (!state || !isEnabled(config, state))
344
+ return;
345
+ if (requestedModel === state.pendingModel) {
346
+ state.pendingModel = undefined;
347
+ return;
348
+ }
349
+ if (requestedModel && config.unavailableModels.includes(requestedModel)) {
350
+ const fallbackModel = firstFallbackFor(config, requestedModel);
351
+ const parsed2 = fallbackModel ? parseModel(fallbackModel) : undefined;
352
+ if (!parsed2)
353
+ return;
354
+ state.originalModel = requestedModel;
355
+ state.currentModel = fallbackModel;
356
+ state.pendingModel = fallbackModel;
357
+ output.message.model = parsed2;
358
+ return;
359
+ }
360
+ if (!state.currentModel)
361
+ return;
362
+ if (requestedModel && requestedModel !== state.currentModel) {
363
+ state.originalModel = requestedModel;
364
+ state.currentModel = requestedModel;
365
+ state.attempts = 0;
366
+ state.failedUntil.clear();
367
+ state.pendingModel = undefined;
368
+ state.awaitingModel = undefined;
369
+ return;
370
+ }
371
+ if (state.currentModel === state.originalModel)
372
+ return;
373
+ const parsed = parseModel(state.currentModel);
374
+ if (parsed)
375
+ output.message.model = parsed;
376
+ }
377
+ };
378
+ }
379
+ var plugin = async (input, options) => createModelFallbackPlugin(input, options);
380
+ var ModelFallbackPlugin = plugin;
381
+ var plugin_default = plugin;
382
+ export {
383
+ plugin_default as default,
384
+ ModelFallbackPlugin
385
+ };
@@ -0,0 +1,129 @@
1
+ export type Options = {
2
+ enabled?: boolean;
3
+ fallback_models?: string[];
4
+ unavailable_models?: string[];
5
+ retry_on_errors?: number[];
6
+ max_attempts?: number;
7
+ cooldown_ms?: number;
8
+ notify?: boolean;
9
+ };
10
+ type Config = {
11
+ enabled: boolean;
12
+ fallbackModels: string[];
13
+ unavailableModels: string[];
14
+ retryOnErrors: number[];
15
+ maxAttempts: number;
16
+ cooldownMs: number;
17
+ notify: boolean;
18
+ };
19
+ type ModelRef = {
20
+ providerID: string;
21
+ modelID: string;
22
+ };
23
+ type PromptPart = {
24
+ type: string;
25
+ text?: string;
26
+ [key: string]: unknown;
27
+ };
28
+ type LastUserPayload = {
29
+ parts: PromptPart[];
30
+ messageID?: string;
31
+ };
32
+ type RuntimeEventInput = {
33
+ event: {
34
+ type: string;
35
+ properties?: unknown;
36
+ };
37
+ };
38
+ type ChatMessageInput = {
39
+ sessionID: string;
40
+ agent?: string;
41
+ model?: ModelRef;
42
+ };
43
+ type ChatMessageOutput = {
44
+ message: {
45
+ model?: ModelRef;
46
+ };
47
+ parts: PromptPart[];
48
+ };
49
+ type OpenCodeConfigInput = {
50
+ model?: string;
51
+ agent?: Record<string, {
52
+ model?: string;
53
+ } | undefined>;
54
+ };
55
+ type RuntimeInput = {
56
+ directory: string;
57
+ client: {
58
+ session: {
59
+ messages(input: unknown): Promise<unknown>;
60
+ promptAsync(input: unknown): Promise<unknown>;
61
+ };
62
+ tui?: {
63
+ showToast?(input: {
64
+ body: {
65
+ title: string;
66
+ message: string;
67
+ variant: "warning";
68
+ duration: number;
69
+ };
70
+ }): Promise<unknown>;
71
+ };
72
+ };
73
+ };
74
+ export declare function normalizeOptions(options?: Options): Config;
75
+ export declare function parseModel(model: string): ModelRef | undefined;
76
+ export declare function normalizeModel(model: unknown): string | undefined;
77
+ export declare function resolveSessionID(properties: unknown): string | undefined;
78
+ export declare function resolveEventModel(properties: unknown): string | undefined;
79
+ export declare function isRetryableError(error: unknown, retryOnErrors: number[]): boolean;
80
+ export declare function getLastUserPayload(messagesResponse: unknown): LastUserPayload;
81
+ export declare function createModelFallbackPlugin(input: RuntimeInput, options?: Options): {
82
+ config: (opencodeConfig: OpenCodeConfigInput) => Promise<void>;
83
+ tool: {
84
+ model_fallback_control: {
85
+ description: string;
86
+ args: {
87
+ action: import("zod").ZodString;
88
+ };
89
+ execute(args: {
90
+ action: string;
91
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
92
+ };
93
+ };
94
+ event: ({ event }: RuntimeEventInput) => Promise<void>;
95
+ "chat.message": (chatInput: ChatMessageInput, output: ChatMessageOutput) => Promise<void>;
96
+ };
97
+ declare const plugin: (input: import("@opencode-ai/plugin").PluginInput, options: import("@opencode-ai/plugin").PluginOptions | undefined) => Promise<{
98
+ config: (opencodeConfig: OpenCodeConfigInput) => Promise<void>;
99
+ tool: {
100
+ model_fallback_control: {
101
+ description: string;
102
+ args: {
103
+ action: import("zod").ZodString;
104
+ };
105
+ execute(args: {
106
+ action: string;
107
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
108
+ };
109
+ };
110
+ event: ({ event }: RuntimeEventInput) => Promise<void>;
111
+ "chat.message": (chatInput: ChatMessageInput, output: ChatMessageOutput) => Promise<void>;
112
+ }>;
113
+ export declare const ModelFallbackPlugin: (input: import("@opencode-ai/plugin").PluginInput, options: import("@opencode-ai/plugin").PluginOptions | undefined) => Promise<{
114
+ config: (opencodeConfig: OpenCodeConfigInput) => Promise<void>;
115
+ tool: {
116
+ model_fallback_control: {
117
+ description: string;
118
+ args: {
119
+ action: import("zod").ZodString;
120
+ };
121
+ execute(args: {
122
+ action: string;
123
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
124
+ };
125
+ };
126
+ event: ({ event }: RuntimeEventInput) => Promise<void>;
127
+ "chat.message": (chatInput: ChatMessageInput, output: ChatMessageOutput) => Promise<void>;
128
+ }>;
129
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@shutovks/opencode-model-fallback",
3
+ "version": "1.0.7",
4
+ "description": "OpenCode plugin that retries failed sessions with fallback models.",
5
+ "homepage": "https://github.com/ShutovKS/opencode-model-fallback#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ShutovKS/opencode-model-fallback/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ShutovKS/opencode-model-fallback.git"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "bun -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && bun build ./src/index.ts --target bun --format esm --outfile dist/index.js --external @opencode-ai/plugin --external @opencode-ai/plugin/tool --external zod && tsc -p tsconfig.build.json",
23
+ "test": "bun test",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "keywords": [
27
+ "opencode",
28
+ "opencode-plugin",
29
+ "model-fallback",
30
+ "fallback",
31
+ "retry"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "@opencode-ai/plugin": ">=1.15.0"
36
+ },
37
+ "devDependencies": {
38
+ "@opencode-ai/plugin": "^1.15.13",
39
+ "@types/bun": "latest",
40
+ "typescript": "^5.9.2"
41
+ }
42
+ }