@gnahz77/opencode-copilot-multi-auth 0.1.1 → 0.1.3
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 +9 -7
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +56 -12
- package/dist/constants.d.ts +9 -1
- package/dist/constants.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -4
- package/dist/models.d.ts +60 -1
- package/dist/models.js +10 -6
- package/dist/pool.js +4 -4
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +13 -0
- package/index.mjs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Add the plugin to your `opencode` config:
|
|
|
12
12
|
{
|
|
13
13
|
"$schema": "https://opencode.ai/config.json",
|
|
14
14
|
"plugin": [
|
|
15
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
15
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.3"
|
|
16
16
|
]
|
|
17
17
|
}
|
|
18
18
|
```
|
|
@@ -25,7 +25,7 @@ If you also want the TUI command support provided by this package, add the same
|
|
|
25
25
|
{
|
|
26
26
|
"$schema": "https://opencode.ai/tui.json",
|
|
27
27
|
"plugin": [
|
|
28
|
-
"@gnahz77/opencode-copilot-multi-auth@0.1.
|
|
28
|
+
"@gnahz77/opencode-copilot-multi-auth@0.1.3"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
```
|
|
@@ -157,11 +157,13 @@ The pool stores one object per account under `accounts` in a `version: 1` docume
|
|
|
157
157
|
| `id` | Stable human-friendly identifier stored with the account record. |
|
|
158
158
|
| `name` | Display name for the account. |
|
|
159
159
|
| `enabled` | Whether the account can participate in automatic routing. Disabled accounts stay stored but are ignored for winner selection. |
|
|
160
|
-
| `priority` |
|
|
161
|
-
| `allowlist` |
|
|
162
|
-
| `blocklist` |
|
|
160
|
+
| `priority` | Lower values win when multiple enabled accounts can serve the same raw model ID. |
|
|
161
|
+
| `allowlist` | Raw model IDs or `*` wildcard patterns this account is allowed to serve. If non-empty, the account can only serve models that match one of these entries. |
|
|
162
|
+
| `blocklist` | Raw model IDs or `*` wildcard patterns this account must never serve. If both `allowlist` and `blocklist` are non-empty, the plugin checks `allowlist` first and then applies `blocklist`. |
|
|
163
163
|
|
|
164
|
-
Automatic routing works on the raw Copilot model IDs that `opencode` already uses. The plugin filters eligible accounts by `enabled`, then checks `allowlist` first (when non-empty, the model must
|
|
164
|
+
Automatic routing works on the raw Copilot model IDs that `opencode` already uses. The plugin filters eligible accounts by `enabled`, then checks `allowlist` first (when non-empty, the model must match one of its exact entries or wildcard patterns), then applies `blocklist`, and finally picks exactly one winning account by lowest `priority` (with a stable key-based tie-breaker). The model ID itself is not rewritten, so account identity does not appear in model IDs.
|
|
165
|
+
|
|
166
|
+
Wildcard matching is case-sensitive and currently supports `*` for zero or more characters anywhere in the pattern. For example, `claude-*` matches `claude-sonnet-4.6` and `claude-opus-4.6`.
|
|
165
167
|
|
|
166
168
|
Example pool file:
|
|
167
169
|
|
|
@@ -175,7 +177,7 @@ Example pool file:
|
|
|
175
177
|
"name": "Work",
|
|
176
178
|
"enabled": true,
|
|
177
179
|
"priority": 100,
|
|
178
|
-
"allowlist": ["claude
|
|
180
|
+
"allowlist": ["claude-*"],
|
|
179
181
|
"blocklist": [],
|
|
180
182
|
"deployment": "github.com",
|
|
181
183
|
"domain": "github.com",
|
package/dist/auth.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare function getUrls(domain: string): {
|
|
|
3
3
|
DEVICE_CODE_URL: string;
|
|
4
4
|
ACCESS_TOKEN_URL: string;
|
|
5
5
|
COPILOT_ENTITLEMENT_URL: string;
|
|
6
|
+
COPILOT_TOKEN_URL: string;
|
|
6
7
|
};
|
|
7
8
|
export declare function lookupGitHubIdentity(accessToken: string, enterpriseUrl?: string): Promise<{
|
|
8
9
|
login: any;
|
|
@@ -10,7 +11,8 @@ export declare function lookupGitHubIdentity(accessToken: string, enterpriseUrl?
|
|
|
10
11
|
}>;
|
|
11
12
|
export declare function fetchEntitlement(info: AuthInput): Promise<any>;
|
|
12
13
|
export declare function getBaseURL(info: AuthInput): Promise<any>;
|
|
13
|
-
export declare function
|
|
14
|
+
export declare function getCopilotToken(info: AuthInput, forceRefresh?: boolean): Promise<any>;
|
|
15
|
+
export declare function buildHeaders(init: RequestInit | undefined, copilotToken: string, isVision: boolean, isAgent: boolean): {
|
|
14
16
|
[k: string]: string;
|
|
15
17
|
};
|
|
16
18
|
export declare function resolveSelectedPoolAccount(pool: AccountPool, accountKey?: string, requestedRawModelId?: string): PoolAccount | null;
|
package/dist/auth.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { API_VERSION } from "./constants.js";
|
|
1
|
+
import { API_VERSION, COPILOT_TOKEN_EXPIRY_BUFFER_MS, DEFAULT_COPILOT_BASE_URL, VSCODE_HEADERS, } from "./constants.js";
|
|
2
2
|
import { getSelectedAccountExpiredError, getSelectedAccountMissingError } from "./errors.js";
|
|
3
3
|
import { readPool, resolveWinnerAccount, writePool } from "./pool.js";
|
|
4
4
|
import { stripRoutingHeaders } from "./routing.js";
|
|
5
|
-
import { applyBaseURLToRequestInput, getConversationMetadata, isValidBaseURL, normalizeDomain, } from "./utils.js";
|
|
5
|
+
import { applyBaseURLToRequestInput, getConversationMetadata, isValidBaseURL, normalizeDomain, normalizeHeaderObject, } from "./utils.js";
|
|
6
6
|
export function getUrls(domain) {
|
|
7
7
|
const apiDomain = domain === "github.com" ? "api.github.com" : `api.${domain}`;
|
|
8
8
|
return {
|
|
9
9
|
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
10
10
|
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
11
11
|
COPILOT_ENTITLEMENT_URL: `https://${apiDomain}/copilot_internal/user`,
|
|
12
|
+
COPILOT_TOKEN_URL: `https://${apiDomain}/copilot_internal/v2/token`,
|
|
12
13
|
};
|
|
13
14
|
}
|
|
15
|
+
const cachedCopilotTokens = new Map();
|
|
16
|
+
function getCopilotTokenCacheKey(info) {
|
|
17
|
+
const domain = info.enterpriseUrl ? normalizeDomain(info.enterpriseUrl) : "github.com";
|
|
18
|
+
return `${domain}:${info.refresh}`;
|
|
19
|
+
}
|
|
20
|
+
function isReusableCopilotToken(token, expiresAt, now) {
|
|
21
|
+
return Boolean(token && expiresAt && expiresAt - COPILOT_TOKEN_EXPIRY_BUFFER_MS > now);
|
|
22
|
+
}
|
|
14
23
|
export async function lookupGitHubIdentity(accessToken, enterpriseUrl) {
|
|
15
24
|
const deployment = enterpriseUrl ? normalizeDomain(enterpriseUrl) : "github.com";
|
|
16
25
|
const apiDomain = deployment === "github.com" ? "api.github.com" : `api.${deployment}`;
|
|
@@ -42,7 +51,7 @@ export async function fetchEntitlement(info) {
|
|
|
42
51
|
headers: {
|
|
43
52
|
Accept: "application/json",
|
|
44
53
|
Authorization: `Bearer ${info.refresh}`,
|
|
45
|
-
|
|
54
|
+
...VSCODE_HEADERS,
|
|
46
55
|
},
|
|
47
56
|
});
|
|
48
57
|
if (!response.ok) {
|
|
@@ -54,7 +63,41 @@ export async function getBaseURL(info) {
|
|
|
54
63
|
if (info.baseUrl)
|
|
55
64
|
return info.baseUrl;
|
|
56
65
|
const entitlement = await fetchEntitlement(info);
|
|
57
|
-
return entitlement?.endpoints?.api;
|
|
66
|
+
return entitlement?.endpoints?.api ?? DEFAULT_COPILOT_BASE_URL;
|
|
67
|
+
}
|
|
68
|
+
export async function getCopilotToken(info, forceRefresh = false) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (!forceRefresh && isReusableCopilotToken(info.access, info.expires, now)) {
|
|
71
|
+
return info.access;
|
|
72
|
+
}
|
|
73
|
+
const cacheKey = getCopilotTokenCacheKey(info);
|
|
74
|
+
const cachedToken = cachedCopilotTokens.get(cacheKey);
|
|
75
|
+
if (!forceRefresh && cachedToken && isReusableCopilotToken(cachedToken.token, cachedToken.expiresAt, now)) {
|
|
76
|
+
return cachedToken.token;
|
|
77
|
+
}
|
|
78
|
+
const domain = info.enterpriseUrl ? normalizeDomain(info.enterpriseUrl) : "github.com";
|
|
79
|
+
const urls = getUrls(domain);
|
|
80
|
+
const response = await fetch(urls.COPILOT_TOKEN_URL, {
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: "application/json",
|
|
83
|
+
Authorization: `Bearer ${info.refresh}`,
|
|
84
|
+
...VSCODE_HEADERS,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`[opencode-copilot-cli-auth] Copilot token exchange failed: ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
const token = typeof data?.token === "string" ? data.token : "";
|
|
92
|
+
const expiresAt = typeof data?.expires_at === "number" ? data.expires_at * 1000 : 0;
|
|
93
|
+
if (!token || !expiresAt) {
|
|
94
|
+
throw new Error("[opencode-copilot-cli-auth] Copilot token exchange returned an invalid payload.");
|
|
95
|
+
}
|
|
96
|
+
cachedCopilotTokens.set(cacheKey, {
|
|
97
|
+
token,
|
|
98
|
+
expiresAt,
|
|
99
|
+
});
|
|
100
|
+
return token;
|
|
58
101
|
}
|
|
59
102
|
function getHeader(headers, name) {
|
|
60
103
|
if (!headers)
|
|
@@ -74,14 +117,13 @@ function getHeader(headers, name) {
|
|
|
74
117
|
}
|
|
75
118
|
return undefined;
|
|
76
119
|
}
|
|
77
|
-
export function buildHeaders(init,
|
|
120
|
+
export function buildHeaders(init, copilotToken, isVision, isAgent) {
|
|
78
121
|
const explicitInitiator = getHeader(init?.headers, "x-initiator");
|
|
79
122
|
const headers = {
|
|
80
|
-
...(init?.headers
|
|
81
|
-
Authorization: `Bearer ${
|
|
82
|
-
|
|
123
|
+
...normalizeHeaderObject(init?.headers),
|
|
124
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
125
|
+
...VSCODE_HEADERS,
|
|
83
126
|
"Openai-Intent": "conversation-agent",
|
|
84
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
85
127
|
"X-GitHub-Api-Version": API_VERSION,
|
|
86
128
|
"X-Initiator": explicitInitiator ?? (isAgent ? "agent" : "user"),
|
|
87
129
|
"X-Interaction-Id": crypto.randomUUID(),
|
|
@@ -93,6 +135,7 @@ export function buildHeaders(init, info, isVision, isAgent) {
|
|
|
93
135
|
}
|
|
94
136
|
delete headers["x-api-key"];
|
|
95
137
|
delete headers.authorization;
|
|
138
|
+
delete headers["authorization"];
|
|
96
139
|
delete headers["x-initiator"];
|
|
97
140
|
return stripRoutingHeaders(headers);
|
|
98
141
|
}
|
|
@@ -170,8 +213,9 @@ export async function refreshSelectedAccountBaseURL(accountKey) {
|
|
|
170
213
|
}
|
|
171
214
|
export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
172
215
|
const { isVision, isAgent } = getConversationMetadata(init);
|
|
173
|
-
const dispatch = async (account) => {
|
|
174
|
-
const
|
|
216
|
+
const dispatch = async (account, forceRefreshToken = false) => {
|
|
217
|
+
const copilotToken = await getCopilotToken(account.auth, forceRefreshToken);
|
|
218
|
+
const headers = buildHeaders(init, copilotToken, isVision, isAgent);
|
|
175
219
|
const requestInput = applyBaseURLToRequestInput(input, account.baseUrl);
|
|
176
220
|
return fetch(requestInput, {
|
|
177
221
|
...init,
|
|
@@ -187,7 +231,7 @@ export async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
|
187
231
|
return response;
|
|
188
232
|
}
|
|
189
233
|
const refreshedAccount = await refreshSelectedAccountBaseURL(activeAccount.key);
|
|
190
|
-
const retryResponse = await dispatch(refreshedAccount);
|
|
234
|
+
const retryResponse = await dispatch(refreshedAccount, true);
|
|
191
235
|
if ([401, 403].includes(retryResponse.status)) {
|
|
192
236
|
throw getSelectedAccountExpiredError(refreshedAccount.key);
|
|
193
237
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -2,8 +2,16 @@ export declare const ACCOUNT_POOL_SCHEMA_VERSION = 1;
|
|
|
2
2
|
export declare const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
3
3
|
export declare const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
4
4
|
export declare const INTERNAL_ROUTING_HEADERS: Set<string>;
|
|
5
|
-
export declare const CLIENT_ID = "
|
|
5
|
+
export declare const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
6
6
|
export declare const API_VERSION = "2025-05-01";
|
|
7
7
|
export declare const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
8
8
|
export declare const OAUTH_SCOPES = "read:user read:org repo gist";
|
|
9
|
+
export declare const DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
10
|
+
export declare const COPILOT_TOKEN_EXPIRY_BUFFER_MS: number;
|
|
11
|
+
export declare const VSCODE_HEADERS: {
|
|
12
|
+
readonly "User-Agent": "GitHubCopilotChat/0.38.0";
|
|
13
|
+
readonly "Editor-Version": "vscode/1.110.1";
|
|
14
|
+
readonly "Editor-Plugin-Version": "copilot-chat/0.38.0";
|
|
15
|
+
readonly "Copilot-Integration-Id": "vscode-chat";
|
|
16
|
+
};
|
|
9
17
|
export declare const RESPONSES_API_ALTERNATE_INPUT_TYPES: readonly ["file_search_call", "computer_call", "computer_call_output", "web_search_call", "function_call", "function_call_output", "image_generation_call", "code_interpreter_call", "local_shell_call", "local_shell_call_output", "mcp_list_tools", "mcp_approval_request", "mcp_approval_response", "mcp_call", "reasoning"];
|
package/dist/constants.js
CHANGED
|
@@ -5,10 +5,18 @@ export const INTERNAL_ROUTING_HEADERS = new Set([
|
|
|
5
5
|
ROUTING_ACCOUNT_KEY_HEADER,
|
|
6
6
|
ROUTING_SOURCE_HEADER,
|
|
7
7
|
]);
|
|
8
|
-
export const CLIENT_ID = "
|
|
8
|
+
export const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
9
9
|
export const API_VERSION = "2025-05-01";
|
|
10
10
|
export const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
11
11
|
export const OAUTH_SCOPES = "read:user read:org repo gist";
|
|
12
|
+
export const DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
13
|
+
export const COPILOT_TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
14
|
+
export const VSCODE_HEADERS = {
|
|
15
|
+
"User-Agent": "GitHubCopilotChat/0.38.0",
|
|
16
|
+
"Editor-Version": "vscode/1.110.1",
|
|
17
|
+
"Editor-Plugin-Version": "copilot-chat/0.38.0",
|
|
18
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
19
|
+
};
|
|
12
20
|
export const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
|
|
13
21
|
"file_search_call",
|
|
14
22
|
"computer_call",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { lookupGitHubIdentity } from "./auth.js";
|
|
3
3
|
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool } from "./pool.js";
|
|
4
|
+
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
4
5
|
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
6
|
+
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
5
7
|
export declare const CopilotAuthPlugin: Plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { CLIENT_ID, OAUTH_POLLING_SAFETY_MARGIN_MS, OAUTH_SCOPES, ROUTING_ACCOUNT_KEY_HEADER, ROUTING_SOURCE_HEADER, } from "./constants.js";
|
|
2
|
-
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
2
|
+
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getCopilotToken, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
3
3
|
import { buildPoolBackedModels, normalizeExistingModels, resolveProviderModels } from "./models.js";
|
|
4
4
|
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
5
|
-
import {
|
|
5
|
+
import { injectRoutingHeaders, stripRoutingHeaders } from "./routing.js";
|
|
6
|
+
import { applyBaseURLToRequestInput, getHeader, getConversationMetadata, getRequestedRawModelId, normalizeDomain, resolveClaudeThinkingBudget, } from "./utils.js";
|
|
6
7
|
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
8
|
+
export { injectRoutingHeaders, stripRoutingHeaders };
|
|
7
9
|
export const CopilotAuthPlugin = async (input) => {
|
|
8
10
|
return {
|
|
9
11
|
provider: {
|
|
@@ -67,8 +69,10 @@ export const CopilotAuthPlugin = async (input) => {
|
|
|
67
69
|
return fetch(inputRequest, init);
|
|
68
70
|
}
|
|
69
71
|
const { isVision, isAgent } = getConversationMetadata(init);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
const copilotToken = await getCopilotToken(auth);
|
|
73
|
+
const headers = buildHeaders(init, copilotToken, isVision, isAgent);
|
|
74
|
+
const requestInput = applyBaseURLToRequestInput(inputRequest, baseURL);
|
|
75
|
+
return fetch(requestInput, {
|
|
72
76
|
...init,
|
|
73
77
|
headers,
|
|
74
78
|
});
|
package/dist/models.d.ts
CHANGED
|
@@ -130,5 +130,64 @@ export declare function resolveProviderModels(existingModels: Record<string, SDK
|
|
|
130
130
|
};
|
|
131
131
|
}>;
|
|
132
132
|
export declare function buildPoolBackedModels(existingModels: Record<string, SDKModel> | undefined, pool: AccountPool): Promise<{
|
|
133
|
-
[k: string]:
|
|
133
|
+
[k: string]: {
|
|
134
|
+
cost: {
|
|
135
|
+
input: number;
|
|
136
|
+
output: number;
|
|
137
|
+
cache: {
|
|
138
|
+
read: number;
|
|
139
|
+
write: number;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
api: {
|
|
143
|
+
url: string;
|
|
144
|
+
npm: string;
|
|
145
|
+
id: string;
|
|
146
|
+
};
|
|
147
|
+
id: string;
|
|
148
|
+
providerID: string;
|
|
149
|
+
name: string;
|
|
150
|
+
family?: string;
|
|
151
|
+
capabilities: {
|
|
152
|
+
temperature: boolean;
|
|
153
|
+
reasoning: boolean;
|
|
154
|
+
attachment: boolean;
|
|
155
|
+
toolcall: boolean;
|
|
156
|
+
input: {
|
|
157
|
+
text: boolean;
|
|
158
|
+
audio: boolean;
|
|
159
|
+
image: boolean;
|
|
160
|
+
video: boolean;
|
|
161
|
+
pdf: boolean;
|
|
162
|
+
};
|
|
163
|
+
output: {
|
|
164
|
+
text: boolean;
|
|
165
|
+
audio: boolean;
|
|
166
|
+
image: boolean;
|
|
167
|
+
video: boolean;
|
|
168
|
+
pdf: boolean;
|
|
169
|
+
};
|
|
170
|
+
interleaved: boolean | {
|
|
171
|
+
field: "reasoning_content" | "reasoning_details";
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
limit: {
|
|
175
|
+
context: number;
|
|
176
|
+
input?: number;
|
|
177
|
+
output: number;
|
|
178
|
+
};
|
|
179
|
+
status: "alpha" | "beta" | "deprecated" | "active";
|
|
180
|
+
options: {
|
|
181
|
+
[key: string]: unknown;
|
|
182
|
+
};
|
|
183
|
+
headers: {
|
|
184
|
+
[key: string]: string;
|
|
185
|
+
};
|
|
186
|
+
release_date: string;
|
|
187
|
+
variants?: {
|
|
188
|
+
[key: string]: {
|
|
189
|
+
[key: string]: unknown;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
};
|
|
134
193
|
}>;
|
package/dist/models.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { getBaseURL } from "./auth.js";
|
|
1
|
+
import { getBaseURL, getCopilotToken } from "./auth.js";
|
|
2
|
+
import { API_VERSION, VSCODE_HEADERS } from "./constants.js";
|
|
2
3
|
import { resolveWinnerAccount } from "./pool.js";
|
|
3
4
|
import { getReleaseDate, isPickerModel, zeroCost } from "./utils.js";
|
|
4
5
|
export async function fetchModels(info, baseURL) {
|
|
6
|
+
const copilotToken = await getCopilotToken(info);
|
|
5
7
|
const response = await fetch(`${baseURL}/models`, {
|
|
6
8
|
headers: {
|
|
7
|
-
Authorization: `Bearer ${
|
|
8
|
-
|
|
9
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
10
|
+
...VSCODE_HEADERS,
|
|
9
11
|
"Openai-Intent": "model-access",
|
|
10
|
-
"
|
|
11
|
-
"X-GitHub-Api-Version": "2025-05-01",
|
|
12
|
+
"X-GitHub-Api-Version": API_VERSION,
|
|
12
13
|
"X-Interaction-Type": "model-access",
|
|
13
14
|
"X-Request-Id": crypto.randomUUID(),
|
|
14
15
|
},
|
|
@@ -115,7 +116,7 @@ export async function buildPoolBackedModels(existingModels, pool) {
|
|
|
115
116
|
const enabledAccounts = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
116
117
|
.filter((account) => account?.enabled !== false);
|
|
117
118
|
if (enabledAccounts.length === 0) {
|
|
118
|
-
return
|
|
119
|
+
return normalizeExistingModels(existingModels);
|
|
119
120
|
}
|
|
120
121
|
const candidatesByModel = new Map();
|
|
121
122
|
for (const account of enabledAccounts) {
|
|
@@ -138,6 +139,9 @@ export async function buildPoolBackedModels(existingModels, pool) {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
const existingById = new Map(Object.values(existingModels ?? {}).map((model) => [model?.api?.id ?? model?.id, model]));
|
|
142
|
+
if (candidatesByModel.size === 0) {
|
|
143
|
+
return normalizeExistingModels(existingModels);
|
|
144
|
+
}
|
|
141
145
|
return Object.fromEntries([...candidatesByModel.entries()]
|
|
142
146
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
143
147
|
.map(([rawModelId, { liveModel, baseURL }]) => [
|
package/dist/pool.js
CHANGED
|
@@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSy
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import { ACCOUNT_POOL_SCHEMA_VERSION } from "./constants.js";
|
|
5
|
-
import { normalizeDomain, normalizeIdSource, normalizeList, normalizePriority, preserveStringOrDefault } from "./utils.js";
|
|
5
|
+
import { matchesAnyModelIdPattern, normalizeDomain, normalizeIdSource, normalizeList, normalizePriority, preserveStringOrDefault, } from "./utils.js";
|
|
6
6
|
export function getPoolPath() {
|
|
7
7
|
return `${homedir()}/.local/share/opencode/copilot-auth.json`;
|
|
8
8
|
}
|
|
@@ -137,17 +137,17 @@ export function upsertAccount(pool, accountData) {
|
|
|
137
137
|
export function resolveWinnerAccount(rawModelId, pool) {
|
|
138
138
|
const canAccountServeModel = (account) => {
|
|
139
139
|
const allowlist = normalizeList(account?.allowlist);
|
|
140
|
-
if (allowlist.length > 0 && !allowlist
|
|
140
|
+
if (allowlist.length > 0 && !matchesAnyModelIdPattern(allowlist, rawModelId)) {
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
143
|
const blocklist = normalizeList(account?.blocklist);
|
|
144
|
-
return !blocklist
|
|
144
|
+
return !matchesAnyModelIdPattern(blocklist, rawModelId);
|
|
145
145
|
};
|
|
146
146
|
const candidates = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
147
147
|
.filter((account) => account?.enabled !== false)
|
|
148
148
|
.filter(canAccountServeModel)
|
|
149
149
|
.sort((left, right) => {
|
|
150
|
-
const priorityDelta = normalizePriority(
|
|
150
|
+
const priorityDelta = normalizePriority(left?.priority) - normalizePriority(right?.priority);
|
|
151
151
|
if (priorityDelta !== 0) {
|
|
152
152
|
return priorityDelta;
|
|
153
153
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface ProviderModel {
|
|
|
111
111
|
export type HeaderObject = Headers | Array<[string, string]> | Record<string, string> | undefined;
|
|
112
112
|
export interface AuthInput {
|
|
113
113
|
refresh: string;
|
|
114
|
+
access?: string;
|
|
115
|
+
expires?: number;
|
|
114
116
|
enterpriseUrl?: string | null;
|
|
115
117
|
baseUrl?: string | null;
|
|
116
118
|
type?: string;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ConversationMetadata, HeaderObject } from "./types.js";
|
|
2
2
|
export declare function normalizeHeaderObject(headers: HeaderObject): Record<string, string>;
|
|
3
3
|
export declare function normalizeList(value: unknown): string[];
|
|
4
|
+
export declare function matchesModelIdPattern(pattern: unknown, rawModelId: string): boolean;
|
|
5
|
+
export declare function matchesAnyModelIdPattern(patterns: unknown, rawModelId: string): boolean;
|
|
4
6
|
export declare function normalizePriority(value: unknown): number;
|
|
5
7
|
export declare function normalizeDomain(urlOrDomain: unknown): string;
|
|
6
8
|
export declare function normalizeIdSource(value: unknown): string;
|
package/dist/utils.js
CHANGED
|
@@ -28,6 +28,19 @@ export function normalizeHeaderObject(headers) {
|
|
|
28
28
|
export function normalizeList(value) {
|
|
29
29
|
return Array.isArray(value) ? value : [];
|
|
30
30
|
}
|
|
31
|
+
function escapeRegex(value) {
|
|
32
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
33
|
+
}
|
|
34
|
+
export function matchesModelIdPattern(pattern, rawModelId) {
|
|
35
|
+
if (typeof pattern !== "string") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const matcher = new RegExp(`^${escapeRegex(pattern).replaceAll("*", ".*")}$`);
|
|
39
|
+
return matcher.test(rawModelId);
|
|
40
|
+
}
|
|
41
|
+
export function matchesAnyModelIdPattern(patterns, rawModelId) {
|
|
42
|
+
return normalizeList(patterns).some((pattern) => matchesModelIdPattern(pattern, rawModelId));
|
|
43
|
+
}
|
|
31
44
|
export function normalizePriority(value) {
|
|
32
45
|
return Number.isInteger(value) ? value : 0;
|
|
33
46
|
}
|
package/index.mjs
CHANGED