opencode-copilot-failover 0.1.0

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) 2025-2026 pl4fun
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,65 @@
1
+ # opencode-copilot-failover
2
+
3
+ Automatic provider failover plugin for opencode — switches to GitHub Copilot when primary providers fail.
4
+
5
+ ## How it works
6
+
7
+ 1. Listens for `session.error` events after all built-in retries are exhausted
8
+ 2. Detects retryable errors (429, 500, 502, 503, 529, unknown errors)
9
+ 3. Maps the failed model to its GitHub Copilot equivalent
10
+ 4. Re-prompts the session via the copilot provider
11
+ 5. Shows a toast notification in the TUI
12
+
13
+ ## Installation
14
+
15
+ Add to your `opencode.json`:
16
+
17
+ ```json
18
+ {
19
+ "plugin": ["opencode-copilot-failover"]
20
+ }
21
+ ```
22
+
23
+ Or alongside other plugins:
24
+
25
+ ```json
26
+ {
27
+ "plugin": ["oh-my-opencode@latest", "opencode-openai-codex-auth", "opencode-copilot-failover"]
28
+ }
29
+ ```
30
+
31
+ ## Supported Models
32
+
33
+ | Source Model | Copilot Model |
34
+ | --- | --- |
35
+ | `claude-opus-4-6` | `claude-opus-4.6` |
36
+ | `claude-opus-4-5` | `claude-opus-4.5` |
37
+ | `claude-sonnet-4-5` | `claude-sonnet-4.5` |
38
+ | `claude-sonnet-4` | `claude-sonnet-4` |
39
+ | `claude-haiku-4-5` | `claude-haiku-4.5` |
40
+ | `gpt-5.3-codex` | `gpt-5.3-codex` |
41
+ | `gpt-5.2-codex` | `gpt-5.2-codex` |
42
+ | `gpt-5.2` | `gpt-5.2` |
43
+ | `gpt-5.1-codex-max` | `gpt-5.1-codex-max` |
44
+ | `gpt-5.1-codex` | `gpt-5.1-codex` |
45
+ | `gpt-5.1-codex-mini` | `gpt-5.1-codex-mini` |
46
+ | `gpt-5.1` | `gpt-5.1` |
47
+ | `gpt-4.1` | `gpt-4.1` |
48
+ | `gpt-5` | `gpt-5` |
49
+ | `gpt-5-mini` | `gpt-5-mini` |
50
+ | `gemini-2.5-pro` | `gemini-2.5-pro` |
51
+ | `gemini-3-flash` | `gemini-3-flash` |
52
+ | `gemini-3-pro` | `gemini-3-pro` |
53
+
54
+ ## Behavior
55
+
56
+ - **Zero-config** — works out of the box
57
+ - **Per-request failover** — always tries the primary provider first
58
+ - **Prevents infinite loops** — won't failover if already on copilot
59
+ - **Prevents duplicate failovers** — won't retry the same message twice
60
+
61
+ ## Limitations
62
+
63
+ - GitHub Copilot must be authenticated in opencode
64
+ - Not all models are available on copilot (unmapped models are skipped)
65
+ - If copilot also fails, no further failover occurs
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const plugin: Plugin;
3
+ export default plugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAIlD,QAAA,MAAM,MAAM,EAAE,MAUb,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { CopilotFailoverEngine } from "./lib/failover-engine.js";
2
+ const plugin = async (ctx) => {
3
+ const engine = new CopilotFailoverEngine(ctx.client);
4
+ return {
5
+ event: async ({ event }) => {
6
+ if (event.type === "session.error") {
7
+ await engine.handleSessionError(event);
8
+ }
9
+ },
10
+ };
11
+ };
12
+ export default plugin;
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,MAAM,MAAM,GAAW,KAAK,EAAE,GAAG,EAAE,EAAE;IACnC,MAAM,MAAM,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAErD,OAAO;QACL,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACzB,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;gBACnC,MAAM,MAAM,CAAC,kBAAkB,CAAC,KAA0B,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Plugin identifier for logging and toast notifications */
2
+ export declare const PLUGIN_NAME: "opencode-copilot-failover";
3
+ /** The target failover provider ID */
4
+ export declare const COPILOT_PROVIDER_ID: "github-copilot";
5
+ /** Providers eligible for failover (any non-copilot provider) */
6
+ export declare const FAILOVER_PROVIDERS: readonly ["anthropic", "openai"];
7
+ /** HTTP status codes that trigger failover */
8
+ export declare const RETRYABLE_STATUS_CODES: readonly [429, 500, 502, 503, 529];
9
+ /** Log prefix for console messages */
10
+ export declare const LOG_PREFIX: "[copilot-failover]";
11
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../lib/constants.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,eAAO,MAAM,WAAW,EAAG,2BAAoC,CAAC;AAEhE,sCAAsC;AACtC,eAAO,MAAM,mBAAmB,EAAG,gBAAyB,CAAC;AAE7D,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,kCAAmC,CAAC;AAEnE,8CAA8C;AAC9C,eAAO,MAAM,sBAAsB,oCAAqC,CAAC;AAEzE,sCAAsC;AACtC,eAAO,MAAM,UAAU,EAAG,oBAA6B,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Plugin identifier for logging and toast notifications */
2
+ export const PLUGIN_NAME = "opencode-copilot-failover";
3
+ /** The target failover provider ID */
4
+ export const COPILOT_PROVIDER_ID = "github-copilot";
5
+ /** Providers eligible for failover (any non-copilot provider) */
6
+ export const FAILOVER_PROVIDERS = ["anthropic", "openai"];
7
+ /** HTTP status codes that trigger failover */
8
+ export const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 529];
9
+ /** Log prefix for console messages */
10
+ export const LOG_PREFIX = "[copilot-failover]";
11
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../lib/constants.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG,2BAAoC,CAAC;AAEhE,sCAAsC;AACtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAyB,CAAC;AAE7D,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAU,CAAC;AAEnE,8CAA8C;AAC9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAEzE,sCAAsC;AACtC,MAAM,CAAC,MAAM,UAAU,GAAG,oBAA6B,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { ApiError, EventSessionError, MessageAbortedError, MessageOutputLengthError, ProviderAuthError, UnknownError } from "@opencode-ai/sdk";
2
+ export type FailoverTrigger = {
3
+ reason: string;
4
+ statusCode?: number;
5
+ provider: string;
6
+ model: string;
7
+ };
8
+ type SessionError = ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError;
9
+ export declare function isRetryableProviderError(error: SessionError): boolean;
10
+ export declare function extractFailoverTrigger(event: EventSessionError): FailoverTrigger | null;
11
+ export {};
12
+ //# sourceMappingURL=error-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-detector.d.ts","sourceRoot":"","sources":["../../lib/error-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,iBAAiB,EACjB,YAAY,EACb,MAAM,kBAAkB,CAAC;AAI1B,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,YAAY,GACb,iBAAiB,GACjB,YAAY,GACZ,wBAAwB,GACxB,mBAAmB,GACnB,QAAQ,CAAC;AA2Bb,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAoCrE;AAYD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,iBAAiB,GACvB,eAAe,GAAG,IAAI,CAiBxB"}
@@ -0,0 +1,97 @@
1
+ import { RETRYABLE_STATUS_CODES } from "./constants.js";
2
+ const RETRYABLE_TEXT_SIGNALS = [
3
+ "rate limit",
4
+ "too many requests",
5
+ "quota",
6
+ "usage limit",
7
+ "billing limit",
8
+ "capacity",
9
+ "overloaded",
10
+ "service unavailable",
11
+ "temporarily unavailable",
12
+ "timeout",
13
+ "timed out",
14
+ "deadline exceeded",
15
+ ];
16
+ const USER_ABORT_TEXT_SIGNALS = [
17
+ "user cancelled",
18
+ "user canceled",
19
+ "cancelled by user",
20
+ "canceled by user",
21
+ "aborted by user",
22
+ "ctrl+c",
23
+ "ctrl-c",
24
+ ];
25
+ export function isRetryableProviderError(error) {
26
+ switch (error.name) {
27
+ case "APIError": {
28
+ if (error.data.isRetryable)
29
+ return true;
30
+ if (error.data.statusCode === 408)
31
+ return true;
32
+ if (error.data.statusCode !== undefined &&
33
+ RETRYABLE_STATUS_CODES.includes(error.data.statusCode)) {
34
+ return true;
35
+ }
36
+ if (hasRetryableTextSignal(error.data.message, error.data.responseBody)) {
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ case "ProviderAuthError":
42
+ return false;
43
+ case "MessageAbortedError":
44
+ return !isLikelyUserAbort(error.data.message);
45
+ case "MessageOutputLengthError":
46
+ return false;
47
+ case "UnknownError":
48
+ return true;
49
+ default: {
50
+ const _exhaustive = error;
51
+ return _exhaustive;
52
+ }
53
+ }
54
+ }
55
+ function hasRetryableTextSignal(message, responseBody) {
56
+ const haystack = `${message}\n${responseBody ?? ""}`.toLowerCase();
57
+ return RETRYABLE_TEXT_SIGNALS.some((signal) => haystack.includes(signal));
58
+ }
59
+ function isLikelyUserAbort(message) {
60
+ const normalized = message.toLowerCase();
61
+ return USER_ABORT_TEXT_SIGNALS.some((signal) => normalized.includes(signal));
62
+ }
63
+ export function extractFailoverTrigger(event) {
64
+ const error = event.properties.error;
65
+ if (!error)
66
+ return null;
67
+ if (!isRetryableProviderError(error))
68
+ return null;
69
+ const trigger = {
70
+ reason: buildReason(error),
71
+ provider: "",
72
+ model: "",
73
+ };
74
+ if (error.name === "APIError" && error.data.statusCode !== undefined) {
75
+ trigger.statusCode = error.data.statusCode;
76
+ }
77
+ return trigger;
78
+ }
79
+ function buildReason(error) {
80
+ switch (error.name) {
81
+ case "APIError":
82
+ return error.data.statusCode
83
+ ? `API error ${error.data.statusCode}: ${error.data.message}`
84
+ : `API error: ${error.data.message}`;
85
+ case "UnknownError":
86
+ return `Unknown error: ${error.data.message}`;
87
+ case "ProviderAuthError":
88
+ case "MessageAbortedError":
89
+ case "MessageOutputLengthError":
90
+ return error.name;
91
+ default: {
92
+ const _exhaustive = error;
93
+ return _exhaustive;
94
+ }
95
+ }
96
+ }
97
+ //# sourceMappingURL=error-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-detector.js","sourceRoot":"","sources":["../../lib/error-detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { EventSessionError, OpencodeClient } from "@opencode-ai/sdk";
2
+ /**
3
+ * Detects provider errors, maps models to github-copilot equivalents,
4
+ * and re-prompts sessions via the SDK client. Must NEVER throw.
5
+ */
6
+ export declare class CopilotFailoverEngine {
7
+ private readonly client;
8
+ private readonly dedup;
9
+ private cachedCopilotModels;
10
+ constructor(client: OpencodeClient);
11
+ /**
12
+ * Discover which models are available in the github-copilot provider.
13
+ * Result is cached for the lifetime of the engine instance.
14
+ */
15
+ private discoverCopilotModels;
16
+ handleSessionError(event: EventSessionError): Promise<void>;
17
+ private processError;
18
+ private emitFailoverToast;
19
+ private emitToast;
20
+ /**
21
+ * Convert output Part[] (session.messages response) to input PartInput[]
22
+ * (session.prompt request). Only TextPart and FilePart map to user-input
23
+ * equivalents; assistant-side parts (tool, reasoning, step, etc.) are skipped.
24
+ */
25
+ private convertPartsToInput;
26
+ private isDuplicate;
27
+ private recordDedup;
28
+ }
29
+ //# sourceMappingURL=failover-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failover-engine.d.ts","sourceRoot":"","sources":["../../lib/failover-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,iBAAiB,EAOjB,cAAc,EACd,MAAM,kBAAkB,CAAC;AAe1B;;;GAGG;AACH,qBAAa,qBAAqB;IAIrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,mBAAmB,CAA4B;gBAE1B,MAAM,EAAE,cAAc;IAEnD;;;OAGG;YACW,qBAAqB;IAuB7B,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;YAYnD,YAAY;YAiFZ,iBAAiB;YAajB,SAAS;IAgCvB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,WAAW;CAQnB"}
@@ -0,0 +1,199 @@
1
+ import { COPILOT_PROVIDER_ID } from "./constants.js";
2
+ import { extractFailoverTrigger } from "./error-detector.js";
3
+ import { resolveWithFallback } from "./model-mapper.js";
4
+ /**
5
+ * Detects provider errors, maps models to github-copilot equivalents,
6
+ * and re-prompts sessions via the SDK client. Must NEVER throw.
7
+ */
8
+ export class CopilotFailoverEngine {
9
+ client;
10
+ dedup = new Map();
11
+ cachedCopilotModels = null;
12
+ constructor(client) {
13
+ this.client = client;
14
+ }
15
+ /**
16
+ * Discover which models are available in the github-copilot provider.
17
+ * Result is cached for the lifetime of the engine instance.
18
+ */
19
+ async discoverCopilotModels() {
20
+ if (this.cachedCopilotModels)
21
+ return this.cachedCopilotModels;
22
+ try {
23
+ const result = await this.client.config.providers();
24
+ const providers = result.data?.providers;
25
+ if (!providers)
26
+ return null;
27
+ const copilotProvider = providers.find((p) => p.id === COPILOT_PROVIDER_ID);
28
+ if (!copilotProvider?.models)
29
+ return null;
30
+ this.cachedCopilotModels = new Set(Object.keys(copilotProvider.models));
31
+ return this.cachedCopilotModels;
32
+ }
33
+ catch {
34
+ // If the providers API fails, proceed without availability filtering
35
+ return null;
36
+ }
37
+ }
38
+ async handleSessionError(event) {
39
+ try {
40
+ await this.processError(event);
41
+ }
42
+ catch (err) {
43
+ await this.emitToast({
44
+ title: "Copilot Failover Error",
45
+ message: err instanceof Error ? err.message : String(err),
46
+ variant: "error",
47
+ }).catch(() => { });
48
+ }
49
+ }
50
+ async processError(event) {
51
+ const error = event.properties.error;
52
+ if (!error)
53
+ return;
54
+ const sessionID = event.properties.sessionID;
55
+ if (!sessionID)
56
+ return;
57
+ const trigger = extractFailoverTrigger(event);
58
+ if (!trigger)
59
+ return;
60
+ const messagesResult = await this.client.session.messages({
61
+ path: { id: sessionID },
62
+ });
63
+ const messages = messagesResult.data;
64
+ if (!messages || messages.length === 0)
65
+ return;
66
+ const failedEntry = [...messages]
67
+ .reverse()
68
+ .find((m) => m.info.role === "assistant" && m.info.error);
69
+ if (!failedEntry)
70
+ return;
71
+ const failedAssistant = failedEntry.info;
72
+ const originalProvider = failedAssistant.providerID;
73
+ const originalModel = failedAssistant.modelID;
74
+ if (originalProvider === COPILOT_PROVIDER_ID) {
75
+ await this.emitToast({
76
+ title: "Copilot Failover Failed",
77
+ message: `${COPILOT_PROVIDER_ID}/${originalModel} also errored — no further fallback available`,
78
+ variant: "error",
79
+ });
80
+ return;
81
+ }
82
+ const parentID = failedAssistant.parentID;
83
+ if (this.isDuplicate(sessionID, parentID))
84
+ return;
85
+ const availableModels = await this.discoverCopilotModels();
86
+ const copilotModelID = resolveWithFallback(originalModel, availableModels);
87
+ if (!copilotModelID) {
88
+ await this.emitToast({
89
+ title: "Copilot Failover Unavailable",
90
+ message: `No copilot mapping for ${originalProvider}/${originalModel}`,
91
+ variant: "error",
92
+ });
93
+ return;
94
+ }
95
+ const userEntry = messages.find((m) => m.info.id === parentID);
96
+ if (!userEntry)
97
+ return;
98
+ const inputParts = this.convertPartsToInput(userEntry.parts);
99
+ if (inputParts.length === 0)
100
+ return;
101
+ const userInfo = userEntry.info;
102
+ await this.client.session.promptAsync({
103
+ path: { id: sessionID },
104
+ body: {
105
+ parts: inputParts,
106
+ model: {
107
+ providerID: COPILOT_PROVIDER_ID,
108
+ modelID: copilotModelID,
109
+ },
110
+ agent: userInfo.agent,
111
+ system: userInfo.system,
112
+ tools: userInfo.tools,
113
+ },
114
+ });
115
+ await this.emitFailoverToast(originalProvider, originalModel, copilotModelID, trigger.reason);
116
+ this.recordDedup(sessionID, parentID);
117
+ }
118
+ async emitFailoverToast(fromProvider, fromModel, toModel, reason) {
119
+ await this.emitToast({
120
+ title: "Provider switched to GitHub Copilot",
121
+ message: `${fromProvider}/${fromModel} → ${COPILOT_PROVIDER_ID}/${toModel} (${reason})`,
122
+ variant: "warning",
123
+ });
124
+ }
125
+ async emitToast(payload) {
126
+ const toastEvent = {
127
+ type: "tui.toast.show",
128
+ properties: {
129
+ title: payload.title,
130
+ message: payload.message,
131
+ variant: payload.variant,
132
+ duration: payload.duration,
133
+ },
134
+ };
135
+ try {
136
+ await this.client.tui.publish({ body: toastEvent });
137
+ return;
138
+ }
139
+ catch {
140
+ // publish unavailable, fall through to showToast
141
+ }
142
+ try {
143
+ await this.client.tui.showToast({
144
+ body: {
145
+ title: payload.title,
146
+ message: payload.message,
147
+ variant: payload.variant,
148
+ duration: payload.duration,
149
+ },
150
+ });
151
+ }
152
+ catch {
153
+ // both toast methods failed — nothing we can do without polluting TUI
154
+ }
155
+ }
156
+ /**
157
+ * Convert output Part[] (session.messages response) to input PartInput[]
158
+ * (session.prompt request). Only TextPart and FilePart map to user-input
159
+ * equivalents; assistant-side parts (tool, reasoning, step, etc.) are skipped.
160
+ */
161
+ convertPartsToInput(parts) {
162
+ const result = [];
163
+ for (const part of parts) {
164
+ switch (part.type) {
165
+ case "text": {
166
+ result.push({
167
+ type: "text",
168
+ text: part.text,
169
+ });
170
+ break;
171
+ }
172
+ case "file": {
173
+ result.push({
174
+ type: "file",
175
+ mime: part.mime,
176
+ url: part.url,
177
+ ...(part.filename ? { filename: part.filename } : {}),
178
+ ...(part.source ? { source: part.source } : {}),
179
+ });
180
+ break;
181
+ }
182
+ }
183
+ }
184
+ return result;
185
+ }
186
+ isDuplicate(sessionID, parentID) {
187
+ const sessionSet = this.dedup.get(sessionID);
188
+ return sessionSet !== undefined && sessionSet.has(parentID);
189
+ }
190
+ recordDedup(sessionID, parentID) {
191
+ let sessionSet = this.dedup.get(sessionID);
192
+ if (!sessionSet) {
193
+ sessionSet = new Set();
194
+ this.dedup.set(sessionID, sessionSet);
195
+ }
196
+ sessionSet.add(parentID);
197
+ }
198
+ }
199
+ //# sourceMappingURL=failover-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failover-engine.js","sourceRoot":"","sources":["../../lib/failover-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Model Mapper — maps source provider model IDs to github-copilot model IDs
3
+ * and provides a ranked fallback chain when the exact model isn't available.
4
+ *
5
+ * GitHub Copilot uses DOTS in version numbers where Anthropic uses DASHES.
6
+ * e.g. anthropic: "claude-opus-4-6" -> copilot: "claude-opus-4.6"
7
+ *
8
+ * Verified against `opencode models github-copilot` output (2026-02-10).
9
+ */
10
+ /**
11
+ * Maps source provider model IDs to their github-copilot equivalents.
12
+ *
13
+ * Key: model ID as used by the source provider (anthropic, openai, google, etc.)
14
+ * Value: model ID as accepted by the github-copilot API
15
+ */
16
+ export declare const COPILOT_MODEL_MAP: Record<string, string>;
17
+ /**
18
+ * Fallback chain — ordered list of model IDs to try when the original
19
+ * model isn't available in copilot. Walked top-to-bottom, first match wins.
20
+ *
21
+ * Order per user spec:
22
+ * 1. claude-opus (latest)
23
+ * 2. openai codex (latest)
24
+ * 3. claude-sonnet (latest)
25
+ * 4. kimi-2.5
26
+ * 5. any remaining
27
+ */
28
+ export declare const FALLBACK_CHAIN: readonly string[];
29
+ export declare function parseModelString(fullModelID: string): {
30
+ providerID: string;
31
+ modelID: string;
32
+ };
33
+ /**
34
+ * Map a source-provider model ID to its github-copilot equivalent.
35
+ * Returns `null` if no direct mapping exists (use `resolveWithFallback` for chain).
36
+ */
37
+ export declare function mapModelToCopilot(modelID: string): string | null;
38
+ /**
39
+ * Resolve a model to a copilot equivalent, falling back through the
40
+ * ranked chain if no direct mapping exists.
41
+ *
42
+ * @param modelID - The original model ID (bare or fully-qualified)
43
+ * @param availableModels - Set of model IDs currently available in copilot.
44
+ * If provided, both direct mapping and fallback are validated against it.
45
+ * If null/undefined, the hardcoded map and chain are trusted as-is.
46
+ */
47
+ export declare function resolveWithFallback(modelID: string, availableModels?: ReadonlySet<string> | null): string | null;
48
+ export declare function isModelCopilotAvailable(modelID: string): boolean;
49
+ //# sourceMappingURL=model-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-mapper.d.ts","sourceRoot":"","sources":["../../lib/model-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoCpD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAa3C,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CAChB,CASA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAehE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,MAAM,EACf,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,GAC1C,MAAM,GAAG,IAAI,CAuBf;AAUD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Model Mapper — maps source provider model IDs to github-copilot model IDs
3
+ * and provides a ranked fallback chain when the exact model isn't available.
4
+ *
5
+ * GitHub Copilot uses DOTS in version numbers where Anthropic uses DASHES.
6
+ * e.g. anthropic: "claude-opus-4-6" -> copilot: "claude-opus-4.6"
7
+ *
8
+ * Verified against `opencode models github-copilot` output (2026-02-10).
9
+ */
10
+ /**
11
+ * Maps source provider model IDs to their github-copilot equivalents.
12
+ *
13
+ * Key: model ID as used by the source provider (anthropic, openai, google, etc.)
14
+ * Value: model ID as accepted by the github-copilot API
15
+ */
16
+ export const COPILOT_MODEL_MAP = {
17
+ // Anthropic
18
+ "claude-opus-4-6": "claude-opus-4.6",
19
+ "claude-opus-4.6": "claude-opus-4.6",
20
+ "claude-opus-4-5": "claude-opus-4.5",
21
+ "claude-opus-4.5": "claude-opus-4.5",
22
+ "claude-opus-4-1": "claude-opus-41",
23
+ "claude-opus-4.1": "claude-opus-41",
24
+ "claude-opus-41": "claude-opus-41",
25
+ "claude-sonnet-4-5": "claude-sonnet-4.5",
26
+ "claude-sonnet-4.5": "claude-sonnet-4.5",
27
+ "claude-sonnet-4": "claude-sonnet-4",
28
+ "claude-haiku-4-5": "claude-haiku-4.5",
29
+ "claude-haiku-4.5": "claude-haiku-4.5",
30
+ // OpenAI
31
+ "gpt-5.2-codex": "gpt-5.2-codex",
32
+ "gpt-5.2": "gpt-5.2",
33
+ "gpt-5.1-codex-max": "gpt-5.1-codex-max",
34
+ "gpt-5.1-codex": "gpt-5.1-codex",
35
+ "gpt-5.1-codex-mini": "gpt-5.1-codex-mini",
36
+ "gpt-5.1": "gpt-5.1",
37
+ "gpt-5": "gpt-5",
38
+ "gpt-5-mini": "gpt-5-mini",
39
+ "gpt-4.1": "gpt-4.1",
40
+ "gpt-4o": "gpt-4o",
41
+ // Google
42
+ "gemini-2.5-pro": "gemini-2.5-pro",
43
+ "gemini-3-flash": "gemini-3-flash-preview",
44
+ "gemini-3-flash-preview": "gemini-3-flash-preview",
45
+ "gemini-3-pro": "gemini-3-pro-preview",
46
+ "gemini-3-pro-preview": "gemini-3-pro-preview",
47
+ // xAI
48
+ "grok-code-fast-1": "grok-code-fast-1",
49
+ };
50
+ /**
51
+ * Fallback chain — ordered list of model IDs to try when the original
52
+ * model isn't available in copilot. Walked top-to-bottom, first match wins.
53
+ *
54
+ * Order per user spec:
55
+ * 1. claude-opus (latest)
56
+ * 2. openai codex (latest)
57
+ * 3. claude-sonnet (latest)
58
+ * 4. kimi-2.5
59
+ * 5. any remaining
60
+ */
61
+ export const FALLBACK_CHAIN = [
62
+ "claude-opus-4.6",
63
+ "claude-opus-4.5",
64
+ "gpt-5.3-codex",
65
+ "gpt-5.2-codex",
66
+ "claude-sonnet-4.5",
67
+ "kimi-2.5",
68
+ "gpt-5.2",
69
+ "gpt-5-mini",
70
+ "gemini-3-pro-preview",
71
+ "gemini-3-flash-preview",
72
+ "grok-code-fast-1",
73
+ "claude-haiku-4.5",
74
+ ];
75
+ export function parseModelString(fullModelID) {
76
+ const slashIndex = fullModelID.indexOf("/");
77
+ if (slashIndex === -1) {
78
+ return { providerID: "", modelID: fullModelID };
79
+ }
80
+ return {
81
+ providerID: fullModelID.slice(0, slashIndex),
82
+ modelID: fullModelID.slice(slashIndex + 1),
83
+ };
84
+ }
85
+ /**
86
+ * Map a source-provider model ID to its github-copilot equivalent.
87
+ * Returns `null` if no direct mapping exists (use `resolveWithFallback` for chain).
88
+ */
89
+ export function mapModelToCopilot(modelID) {
90
+ const { modelID: bare } = parseModelString(modelID);
91
+ if (COPILOT_MODEL_MAP[bare] !== undefined) {
92
+ return COPILOT_MODEL_MAP[bare];
93
+ }
94
+ const lower = bare.toLowerCase();
95
+ for (const key of Object.keys(COPILOT_MODEL_MAP)) {
96
+ if (key.toLowerCase() === lower) {
97
+ return COPILOT_MODEL_MAP[key];
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Resolve a model to a copilot equivalent, falling back through the
104
+ * ranked chain if no direct mapping exists.
105
+ *
106
+ * @param modelID - The original model ID (bare or fully-qualified)
107
+ * @param availableModels - Set of model IDs currently available in copilot.
108
+ * If provided, both direct mapping and fallback are validated against it.
109
+ * If null/undefined, the hardcoded map and chain are trusted as-is.
110
+ */
111
+ export function resolveWithFallback(modelID, availableModels) {
112
+ const { modelID: bare } = parseModelString(modelID);
113
+ // 1. Try the original model ID as-is against copilot's available models
114
+ if (isAvailable(bare, availableModels)) {
115
+ return bare;
116
+ }
117
+ // 2. Try the mapped name (e.g. claude-opus-4-6 -> claude-opus-4.6)
118
+ const mappedMatch = mapModelToCopilot(modelID);
119
+ if (mappedMatch && mappedMatch !== bare && isAvailable(mappedMatch, availableModels)) {
120
+ return mappedMatch;
121
+ }
122
+ // 3. Walk the fallback chain
123
+ for (const candidate of FALLBACK_CHAIN) {
124
+ if (candidate === bare || candidate === mappedMatch)
125
+ continue;
126
+ if (isAvailable(candidate, availableModels)) {
127
+ return candidate;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ function isAvailable(modelID, availableModels) {
133
+ if (!availableModels)
134
+ return true;
135
+ return availableModels.has(modelID);
136
+ }
137
+ export function isModelCopilotAvailable(modelID) {
138
+ return mapModelToCopilot(modelID) !== null;
139
+ }
140
+ //# sourceMappingURL=model-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-mapper.js","sourceRoot":"","sources":["../../lib/model-mapper.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "opencode-copilot-failover",
3
+ "version": "0.1.0",
4
+ "description": "Auto-failover from anthropic/openai providers to github-copilot when primary providers return retryable errors",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "type": "module",
8
+ "license": "MIT",
9
+ "author": "pl4fun",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/pl4fun/opencode-copilot-failover.git"
13
+ },
14
+ "homepage": "https://github.com/pl4fun/opencode-copilot-failover#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/pl4fun/opencode-copilot-failover/issues"
17
+ },
18
+ "keywords": [
19
+ "opencode",
20
+ "plugin",
21
+ "copilot",
22
+ "failover",
23
+ "github-copilot",
24
+ "provider",
25
+ "auto-switch"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "files": [
31
+ "dist/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "prepublishOnly": "npm run build && npm run test",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --noEmit"
40
+ },
41
+ "peerDependencies": {
42
+ "@opencode-ai/plugin": "^1.1.19"
43
+ },
44
+ "devDependencies": {
45
+ "@opencode-ai/plugin": "^1.1.19",
46
+ "@opencode-ai/sdk": "^1.1.19",
47
+ "@types/node": "^22",
48
+ "typescript": "^5.8",
49
+ "vitest": "latest"
50
+ }
51
+ }