@gnahz77/opencode-copilot-multi-auth 0.1.0 → 0.1.2
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 +56 -7
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +195 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +28 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +309 -0
- package/dist/models.d.ts +193 -0
- package/dist/models.js +150 -0
- package/dist/pool.d.ts +8 -0
- package/dist/pool.js +157 -0
- package/dist/routing.d.ts +8 -0
- package/dist/routing.js +12 -0
- package/dist/tui.d.ts +5 -0
- package/dist/tui.js +44 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +11 -0
- package/dist/usage.js +113 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +216 -0
- package/index.mjs +12 -1211
- package/package.json +28 -3
package/index.mjs
CHANGED
|
@@ -1,1211 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const ROUTING_ACCOUNT_KEY_HEADER = "x-opencode-copilot-account-key";
|
|
14
|
-
const ROUTING_SOURCE_HEADER = "x-opencode-copilot-route-source";
|
|
15
|
-
const INTERNAL_ROUTING_HEADERS = new Set([
|
|
16
|
-
ROUTING_ACCOUNT_KEY_HEADER,
|
|
17
|
-
ROUTING_SOURCE_HEADER,
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
export function getPoolPath() {
|
|
21
|
-
return `${homedir()}/.local/share/opencode/copilot-auth.json`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function validatePoolSchema(pool, context) {
|
|
25
|
-
if (
|
|
26
|
-
!pool
|
|
27
|
-
|| typeof pool !== "object"
|
|
28
|
-
|| pool.version !== ACCOUNT_POOL_SCHEMA_VERSION
|
|
29
|
-
|| !Array.isArray(pool.accounts)
|
|
30
|
-
) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`[opencode-copilot-cli-auth] Invalid ${context}: expected { version: ${ACCOUNT_POOL_SCHEMA_VERSION}, accounts: [] } schema.`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return pool;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function readPool() {
|
|
40
|
-
const poolPath = getPoolPath();
|
|
41
|
-
|
|
42
|
-
if (!existsSync(poolPath)) {
|
|
43
|
-
const defaultPool = {
|
|
44
|
-
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
45
|
-
accounts: [],
|
|
46
|
-
};
|
|
47
|
-
writePool(defaultPool);
|
|
48
|
-
return defaultPool;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const raw = readFileSync(poolPath, "utf8");
|
|
52
|
-
|
|
53
|
-
let parsed;
|
|
54
|
-
try {
|
|
55
|
-
parsed = JSON.parse(raw);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`[opencode-copilot-cli-auth] Malformed JSON in account pool file at ${poolPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return validatePoolSchema(parsed, `account pool file at ${poolPath}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function writePool(pool) {
|
|
66
|
-
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
67
|
-
const poolPath = getPoolPath();
|
|
68
|
-
const dirPath = dirname(poolPath);
|
|
69
|
-
const tmpPath = `${poolPath}.tmp`;
|
|
70
|
-
|
|
71
|
-
mkdirSync(dirPath, { recursive: true });
|
|
72
|
-
writeFileSync(tmpPath, `${JSON.stringify(validatedPool, null, 2)}\n`, "utf8");
|
|
73
|
-
renameSync(tmpPath, poolPath);
|
|
74
|
-
chmodSync(poolPath, 0o600);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeHeaderObject(headers) {
|
|
78
|
-
if (!headers) {
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
83
|
-
return Object.fromEntries(headers.entries());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (Array.isArray(headers)) {
|
|
87
|
-
return Object.fromEntries(headers);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { ...headers };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function normalizeList(value) {
|
|
94
|
-
return Array.isArray(value) ? value : [];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function normalizePriority(value) {
|
|
98
|
-
return Number.isInteger(value) ? value : 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function normalizeDomain(urlOrDomain) {
|
|
102
|
-
if (!urlOrDomain || typeof urlOrDomain !== "string") {
|
|
103
|
-
return "github.com";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const value = urlOrDomain.trim();
|
|
107
|
-
if (!value) {
|
|
108
|
-
return "github.com";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const parsed = value.includes("://")
|
|
113
|
-
? new URL(value)
|
|
114
|
-
: new URL(`https://${value}`);
|
|
115
|
-
return parsed.hostname.toLowerCase();
|
|
116
|
-
} catch { // intentional: fall back to regex-based normalization if URL parsing fails
|
|
117
|
-
return value
|
|
118
|
-
.replace(/^https?:\/\//i, "")
|
|
119
|
-
.replace(/\/.*$/, "")
|
|
120
|
-
.replace(/\/+$/, "")
|
|
121
|
-
.toLowerCase();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeIdSource(value) {
|
|
126
|
-
return String(value ?? "")
|
|
127
|
-
.toLowerCase()
|
|
128
|
-
.replace(/[^a-z0-9-]/g, "-")
|
|
129
|
-
.replace(/-+/g, "-")
|
|
130
|
-
.replace(/^-+|-+$/g, "");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function preserveStringOrDefault(value, fallback) {
|
|
134
|
-
if (typeof value === "string" && value.trim()) {
|
|
135
|
-
return value;
|
|
136
|
-
}
|
|
137
|
-
return fallback;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function deriveDefaultAccountId(accounts, key, identity) {
|
|
141
|
-
const userIdText = String(identity.userId);
|
|
142
|
-
const baseId = normalizeIdSource(identity.login || userIdText) || userIdText;
|
|
143
|
-
const idTakenByDifferentKey = accounts.some(
|
|
144
|
-
(account) => account?.id === baseId && account?.key !== key,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
if (!idTakenByDifferentKey) {
|
|
148
|
-
return baseId;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return `${baseId}-${userIdText.slice(-6)}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function deriveAccountKey(deployment, userId) {
|
|
155
|
-
return `${deployment}:${userId}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function lookupGitHubIdentity(accessToken, enterpriseUrl) {
|
|
159
|
-
const deployment = enterpriseUrl ? normalizeDomain(enterpriseUrl) : "github.com";
|
|
160
|
-
const apiDomain = deployment === "github.com" ? "api.github.com" : `api.${deployment}`;
|
|
161
|
-
const identityUrl = `https://${apiDomain}/user`;
|
|
162
|
-
|
|
163
|
-
const response = await fetch(identityUrl, {
|
|
164
|
-
method: "GET",
|
|
165
|
-
headers: {
|
|
166
|
-
Accept: "application/json",
|
|
167
|
-
Authorization: `Bearer ${accessToken}`,
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`[opencode-copilot-cli-auth] Failed to lookup GitHub identity from ${identityUrl}: ${response.status} ${response.statusText}`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const payload = await response.json();
|
|
178
|
-
const userId = Number(payload?.id);
|
|
179
|
-
if (!Number.isFinite(userId)) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
"[opencode-copilot-cli-auth] Failed to lookup GitHub identity: response did not include a numeric user id.",
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
login: typeof payload?.login === "string" ? payload.login : "",
|
|
187
|
-
userId,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function upsertAccount(pool, accountData) {
|
|
192
|
-
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
193
|
-
const now = new Date().toISOString();
|
|
194
|
-
const {
|
|
195
|
-
key,
|
|
196
|
-
deployment,
|
|
197
|
-
domain,
|
|
198
|
-
identity,
|
|
199
|
-
enterpriseUrl,
|
|
200
|
-
baseUrl,
|
|
201
|
-
auth,
|
|
202
|
-
authResult,
|
|
203
|
-
} = accountData ?? {};
|
|
204
|
-
|
|
205
|
-
if (!key || typeof key !== "string") {
|
|
206
|
-
throw new Error("[opencode-copilot-cli-auth] Cannot upsert account: missing key.");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const userId = Number(identity?.userId);
|
|
210
|
-
if (!Number.isFinite(userId)) {
|
|
211
|
-
throw new Error("[opencode-copilot-cli-auth] Cannot upsert account: missing numeric identity.userId.");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const login = typeof identity?.login === "string" ? identity.login : "";
|
|
215
|
-
const normalizedDeployment = normalizeDomain(deployment || domain || enterpriseUrl || "github.com");
|
|
216
|
-
const normalizedDomain = normalizeDomain(domain || normalizedDeployment);
|
|
217
|
-
const normalizedEnterpriseUrl =
|
|
218
|
-
normalizedDeployment === "github.com"
|
|
219
|
-
? null
|
|
220
|
-
: normalizeDomain(enterpriseUrl || normalizedDeployment);
|
|
221
|
-
const normalizedIdentity = {
|
|
222
|
-
login,
|
|
223
|
-
userId,
|
|
224
|
-
};
|
|
225
|
-
const defaultId = deriveDefaultAccountId(validatedPool.accounts, key, normalizedIdentity);
|
|
226
|
-
const defaultName = login || String(userId);
|
|
227
|
-
const mergedAuth = auth ?? authResult ?? {};
|
|
228
|
-
const nextBaseUrl = baseUrl ?? authResult?.baseUrl ?? null;
|
|
229
|
-
|
|
230
|
-
const existingIndex = validatedPool.accounts.findIndex((account) => account?.key === key);
|
|
231
|
-
if (existingIndex === -1) {
|
|
232
|
-
return {
|
|
233
|
-
...validatedPool,
|
|
234
|
-
accounts: [
|
|
235
|
-
...validatedPool.accounts,
|
|
236
|
-
{
|
|
237
|
-
key,
|
|
238
|
-
id: defaultId,
|
|
239
|
-
name: defaultName,
|
|
240
|
-
enabled: true,
|
|
241
|
-
priority: 0,
|
|
242
|
-
deployment: normalizedDeployment,
|
|
243
|
-
domain: normalizedDomain,
|
|
244
|
-
identity: normalizedIdentity,
|
|
245
|
-
enterpriseUrl: normalizedEnterpriseUrl,
|
|
246
|
-
baseUrl: nextBaseUrl,
|
|
247
|
-
allowlist: [],
|
|
248
|
-
blocklist: [],
|
|
249
|
-
auth: mergedAuth,
|
|
250
|
-
createdAt: now,
|
|
251
|
-
updatedAt: now,
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const existing = validatedPool.accounts[existingIndex];
|
|
258
|
-
const updatedAccount = {
|
|
259
|
-
...existing,
|
|
260
|
-
key,
|
|
261
|
-
deployment: normalizedDeployment,
|
|
262
|
-
domain: normalizedDomain,
|
|
263
|
-
identity: normalizedIdentity,
|
|
264
|
-
enterpriseUrl: normalizedEnterpriseUrl,
|
|
265
|
-
auth: mergedAuth,
|
|
266
|
-
baseUrl: nextBaseUrl ?? existing.baseUrl ?? null,
|
|
267
|
-
id: preserveStringOrDefault(existing.id, defaultId),
|
|
268
|
-
name: preserveStringOrDefault(existing.name, defaultName),
|
|
269
|
-
enabled: typeof existing.enabled === "boolean" ? existing.enabled : true,
|
|
270
|
-
priority: Number.isFinite(existing.priority) ? existing.priority : 0,
|
|
271
|
-
allowlist: Array.isArray(existing.allowlist) ? existing.allowlist : [],
|
|
272
|
-
blocklist: Array.isArray(existing.blocklist) ? existing.blocklist : [],
|
|
273
|
-
createdAt: existing.createdAt ?? now,
|
|
274
|
-
updatedAt: now,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const nextAccounts = [...validatedPool.accounts];
|
|
278
|
-
nextAccounts[existingIndex] = updatedAccount;
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
...validatedPool,
|
|
282
|
-
accounts: nextAccounts,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function resolveWinnerAccount(rawModelId, pool) {
|
|
287
|
-
const candidates = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
288
|
-
.filter((account) => account?.enabled !== false)
|
|
289
|
-
.filter((account) => {
|
|
290
|
-
const allowlist = normalizeList(account?.allowlist);
|
|
291
|
-
return allowlist.length === 0 || allowlist.includes(rawModelId);
|
|
292
|
-
})
|
|
293
|
-
.filter((account) => !normalizeList(account?.blocklist).includes(rawModelId))
|
|
294
|
-
.sort((left, right) => {
|
|
295
|
-
const priorityDelta = normalizePriority(right?.priority) - normalizePriority(left?.priority);
|
|
296
|
-
if (priorityDelta !== 0) {
|
|
297
|
-
return priorityDelta;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return String(left?.key ?? "").localeCompare(String(right?.key ?? ""));
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
return candidates[0] ?? null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function injectRoutingHeaders(headers, accountKey) {
|
|
307
|
-
return {
|
|
308
|
-
...normalizeHeaderObject(headers),
|
|
309
|
-
[ROUTING_ACCOUNT_KEY_HEADER]: accountKey,
|
|
310
|
-
[ROUTING_SOURCE_HEADER]: "model-resolution",
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function stripRoutingHeaders(headers) {
|
|
315
|
-
return Object.fromEntries(
|
|
316
|
-
Object.entries(normalizeHeaderObject(headers)).filter(
|
|
317
|
-
([key]) => !INTERNAL_ROUTING_HEADERS.has(key.toLowerCase()),
|
|
318
|
-
),
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @type {import("@opencode-ai/plugin").Plugin}
|
|
324
|
-
*/
|
|
325
|
-
export async function CopilotAuthPlugin(input = {}) {
|
|
326
|
-
const CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
|
|
327
|
-
const API_VERSION = "2025-05-01";
|
|
328
|
-
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
329
|
-
const OAUTH_SCOPES = "read:user read:org repo gist";
|
|
330
|
-
const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
|
|
331
|
-
"file_search_call",
|
|
332
|
-
"computer_call",
|
|
333
|
-
"computer_call_output",
|
|
334
|
-
"web_search_call",
|
|
335
|
-
"function_call",
|
|
336
|
-
"function_call_output",
|
|
337
|
-
"image_generation_call",
|
|
338
|
-
"code_interpreter_call",
|
|
339
|
-
"local_shell_call",
|
|
340
|
-
"local_shell_call_output",
|
|
341
|
-
"mcp_list_tools",
|
|
342
|
-
"mcp_approval_request",
|
|
343
|
-
"mcp_approval_response",
|
|
344
|
-
"mcp_call",
|
|
345
|
-
"reasoning",
|
|
346
|
-
];
|
|
347
|
-
|
|
348
|
-
function getUrls(domain) {
|
|
349
|
-
const apiDomain = domain === "github.com" ? "api.github.com" : `api.${domain}`;
|
|
350
|
-
return {
|
|
351
|
-
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
352
|
-
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
353
|
-
COPILOT_ENTITLEMENT_URL: `https://${apiDomain}/copilot_internal/user`,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function fetchEntitlement(info) {
|
|
358
|
-
const domain = info.enterpriseUrl ? normalizeDomain(info.enterpriseUrl) : "github.com";
|
|
359
|
-
const urls = getUrls(domain);
|
|
360
|
-
|
|
361
|
-
const response = await fetch(urls.COPILOT_ENTITLEMENT_URL, {
|
|
362
|
-
headers: {
|
|
363
|
-
Accept: "application/json",
|
|
364
|
-
Authorization: `Bearer ${info.refresh}`,
|
|
365
|
-
"User-Agent": "GithubCopilot/1.155.0",
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (!response.ok) {
|
|
370
|
-
throw new Error(`[opencode-copilot-cli-auth] Entitlement fetch failed: ${response.status}`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return response.json();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function getBaseURL(info) {
|
|
377
|
-
if (info.baseUrl) return info.baseUrl;
|
|
378
|
-
const entitlement = await fetchEntitlement(info);
|
|
379
|
-
return entitlement?.endpoints?.api;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function fetchModels(info, baseURL) {
|
|
383
|
-
const response = await fetch(`${baseURL}/models`, {
|
|
384
|
-
headers: {
|
|
385
|
-
Authorization: `Bearer ${info.refresh}`,
|
|
386
|
-
"Copilot-Integration-Id": "copilot-developer-cli",
|
|
387
|
-
"Openai-Intent": "model-access",
|
|
388
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
389
|
-
"X-GitHub-Api-Version": API_VERSION,
|
|
390
|
-
"X-Interaction-Type": "model-access",
|
|
391
|
-
"X-Request-Id": crypto.randomUUID(),
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
if (!response.ok) {
|
|
396
|
-
throw new Error(`[opencode-copilot-cli-auth] Model fetch failed: ${response.status}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const data = await response.json();
|
|
400
|
-
return Array.isArray(data?.data) ? data.data : [];
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function zeroCost() {
|
|
404
|
-
return {
|
|
405
|
-
input: 0,
|
|
406
|
-
output: 0,
|
|
407
|
-
cache: {
|
|
408
|
-
read: 0,
|
|
409
|
-
write: 0,
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function isLiveChatModel(model) {
|
|
415
|
-
return model?.capabilities?.type === "chat";
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function isPickerModel(model) {
|
|
419
|
-
return isLiveChatModel(model) && model?.model_picker_enabled !== false;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function getReleaseDate(id, version, fallback = "") {
|
|
423
|
-
if (typeof version === "string" && version.startsWith(`${id}-`)) {
|
|
424
|
-
return version.slice(id.length + 1);
|
|
425
|
-
}
|
|
426
|
-
return version || fallback;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function createProviderModel(existing, live, baseURL) {
|
|
430
|
-
const limits = live.capabilities?.limits ?? {};
|
|
431
|
-
const supports = live.capabilities?.supports ?? {};
|
|
432
|
-
const vision = !!supports.vision || !!limits.vision;
|
|
433
|
-
const reasoning =
|
|
434
|
-
existing?.capabilities?.reasoning
|
|
435
|
-
?? (
|
|
436
|
-
!!supports.adaptive_thinking
|
|
437
|
-
|| typeof supports.max_thinking_budget === "number"
|
|
438
|
-
|| Array.isArray(supports.reasoning_effort)
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
...structuredClone(existing ?? {}),
|
|
443
|
-
id: live.id,
|
|
444
|
-
api: {
|
|
445
|
-
...(existing?.api ?? {}),
|
|
446
|
-
id: live.id,
|
|
447
|
-
url: baseURL,
|
|
448
|
-
npm: "@ai-sdk/github-copilot",
|
|
449
|
-
},
|
|
450
|
-
name: live.name ?? existing?.name ?? live.id,
|
|
451
|
-
family: live.capabilities?.family ?? existing?.family ?? "",
|
|
452
|
-
cost: zeroCost(),
|
|
453
|
-
limit: {
|
|
454
|
-
context:
|
|
455
|
-
limits.max_context_window_tokens
|
|
456
|
-
?? existing?.limit?.context
|
|
457
|
-
?? 0,
|
|
458
|
-
input:
|
|
459
|
-
limits.max_prompt_tokens
|
|
460
|
-
?? existing?.limit?.input
|
|
461
|
-
?? limits.max_context_window_tokens,
|
|
462
|
-
output:
|
|
463
|
-
limits.max_output_tokens
|
|
464
|
-
?? limits.max_non_streaming_output_tokens
|
|
465
|
-
?? existing?.limit?.output
|
|
466
|
-
?? 0,
|
|
467
|
-
},
|
|
468
|
-
capabilities: {
|
|
469
|
-
temperature: existing?.capabilities?.temperature ?? true,
|
|
470
|
-
reasoning,
|
|
471
|
-
attachment: existing?.capabilities?.attachment ?? vision,
|
|
472
|
-
toolcall: !!supports.tool_calls,
|
|
473
|
-
input: {
|
|
474
|
-
text: existing?.capabilities?.input?.text ?? true,
|
|
475
|
-
audio: existing?.capabilities?.input?.audio ?? false,
|
|
476
|
-
image: existing?.capabilities?.input?.image ?? vision,
|
|
477
|
-
video: existing?.capabilities?.input?.video ?? false,
|
|
478
|
-
pdf: existing?.capabilities?.input?.pdf ?? false,
|
|
479
|
-
},
|
|
480
|
-
output: {
|
|
481
|
-
text: existing?.capabilities?.output?.text ?? true,
|
|
482
|
-
audio: existing?.capabilities?.output?.audio ?? false,
|
|
483
|
-
image: existing?.capabilities?.output?.image ?? false,
|
|
484
|
-
video: existing?.capabilities?.output?.video ?? false,
|
|
485
|
-
pdf: existing?.capabilities?.output?.pdf ?? false,
|
|
486
|
-
},
|
|
487
|
-
interleaved: existing?.capabilities?.interleaved ?? false,
|
|
488
|
-
},
|
|
489
|
-
options: existing?.options ?? {},
|
|
490
|
-
headers: existing?.headers ?? {},
|
|
491
|
-
release_date: getReleaseDate(live.id, live.version, existing?.release_date ?? ""),
|
|
492
|
-
variants: existing?.variants ?? {},
|
|
493
|
-
status: "active",
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function buildProviderModels(existingModels, liveModels, baseURL) {
|
|
498
|
-
const existingById = new Map(
|
|
499
|
-
Object.values(existingModels ?? {}).map((model) => [model?.api?.id ?? model?.id, model]),
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
return Object.fromEntries(
|
|
503
|
-
liveModels
|
|
504
|
-
.filter(isPickerModel)
|
|
505
|
-
.map((model) => [
|
|
506
|
-
model.id,
|
|
507
|
-
createProviderModel(existingById.get(model.id), model, baseURL),
|
|
508
|
-
]),
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function normalizeExistingModels(existingModels, baseURL) {
|
|
513
|
-
return Object.fromEntries(
|
|
514
|
-
Object.entries(existingModels ?? {}).map(([id, model]) => [
|
|
515
|
-
id,
|
|
516
|
-
{
|
|
517
|
-
...structuredClone(model),
|
|
518
|
-
cost: zeroCost(),
|
|
519
|
-
api: {
|
|
520
|
-
...model.api,
|
|
521
|
-
url: baseURL ?? model.api?.url,
|
|
522
|
-
npm: "@ai-sdk/github-copilot",
|
|
523
|
-
},
|
|
524
|
-
},
|
|
525
|
-
]),
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async function resolveProviderModels(existingModels, auth) {
|
|
530
|
-
const baseURL = auth ? await getBaseURL(auth) : undefined;
|
|
531
|
-
if (!auth || auth.type !== "oauth" || !baseURL) {
|
|
532
|
-
return normalizeExistingModels(existingModels, baseURL);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const liveModels = await fetchModels(auth, baseURL);
|
|
536
|
-
return buildProviderModels(existingModels, liveModels, baseURL);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async function buildPoolBackedModels(existingModels, pool) {
|
|
540
|
-
const enabledAccounts = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
541
|
-
.filter((account) => account?.enabled !== false);
|
|
542
|
-
|
|
543
|
-
if (enabledAccounts.length === 0) {
|
|
544
|
-
return {};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const candidatesByModel = new Map();
|
|
548
|
-
|
|
549
|
-
for (const account of enabledAccounts) {
|
|
550
|
-
try {
|
|
551
|
-
const baseURL = account.baseUrl ?? await getBaseURL(account.auth);
|
|
552
|
-
const liveModels = await fetchModels(account.auth, baseURL);
|
|
553
|
-
|
|
554
|
-
for (const liveModel of liveModels.filter(isPickerModel)) {
|
|
555
|
-
const winner = resolveWinnerAccount(liveModel.id, pool);
|
|
556
|
-
if (winner?.key === account.key) {
|
|
557
|
-
candidatesByModel.set(liveModel.id, {
|
|
558
|
-
account,
|
|
559
|
-
liveModel,
|
|
560
|
-
baseURL,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch (error) {
|
|
565
|
-
console.warn(
|
|
566
|
-
`[opencode-copilot-cli-auth] Skipping account ${account?.key ?? "unknown"} during model sync:`,
|
|
567
|
-
error?.message ?? error,
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const existingById = new Map(
|
|
573
|
-
Object.values(existingModels ?? {}).map((model) => [model?.api?.id ?? model?.id, model]),
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
return Object.fromEntries(
|
|
577
|
-
[...candidatesByModel.entries()]
|
|
578
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
579
|
-
.map(([rawModelId, { liveModel, baseURL }]) => [
|
|
580
|
-
rawModelId,
|
|
581
|
-
createProviderModel(existingById.get(rawModelId), liveModel, baseURL),
|
|
582
|
-
]),
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function getHeader(headers, name) {
|
|
587
|
-
if (!headers) return undefined;
|
|
588
|
-
const target = name.toLowerCase();
|
|
589
|
-
|
|
590
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
591
|
-
return headers.get(name) ?? headers.get(target) ?? undefined;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (Array.isArray(headers)) {
|
|
595
|
-
const found = headers.find(([key]) => String(key).toLowerCase() === target);
|
|
596
|
-
return found?.[1];
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
600
|
-
if (key.toLowerCase() === target) {
|
|
601
|
-
return value;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return undefined;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function getConversationMetadata(init) {
|
|
609
|
-
try {
|
|
610
|
-
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body;
|
|
611
|
-
|
|
612
|
-
if (body?.messages) {
|
|
613
|
-
const lastMessage = body.messages[body.messages.length - 1];
|
|
614
|
-
return {
|
|
615
|
-
isVision: body.messages.some(
|
|
616
|
-
(message) =>
|
|
617
|
-
Array.isArray(message.content) &&
|
|
618
|
-
message.content.some((part) => part.type === "image_url"),
|
|
619
|
-
),
|
|
620
|
-
isAgent:
|
|
621
|
-
lastMessage?.role &&
|
|
622
|
-
["tool", "assistant"].includes(lastMessage.role),
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (body?.input) {
|
|
627
|
-
const lastInput = body.input[body.input.length - 1];
|
|
628
|
-
const isAssistant = lastInput?.role === "assistant";
|
|
629
|
-
const hasAgentType = lastInput?.type
|
|
630
|
-
? RESPONSES_API_ALTERNATE_INPUT_TYPES.includes(lastInput.type)
|
|
631
|
-
: false;
|
|
632
|
-
|
|
633
|
-
return {
|
|
634
|
-
isVision:
|
|
635
|
-
Array.isArray(lastInput?.content) &&
|
|
636
|
-
lastInput.content.some((part) => part.type === "input_image"),
|
|
637
|
-
isAgent: isAssistant || hasAgentType,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
} catch {} // intentional: return safe defaults on any parse/inspection error
|
|
641
|
-
|
|
642
|
-
return {
|
|
643
|
-
isVision: false,
|
|
644
|
-
isAgent: false,
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function getRequestedRawModelId(init) {
|
|
649
|
-
try {
|
|
650
|
-
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body;
|
|
651
|
-
return typeof body?.model === "string" && body.model.trim() ? body.model.trim() : undefined;
|
|
652
|
-
} catch { // intentional: return undefined on body parse error
|
|
653
|
-
return undefined;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function applyBaseURLToRequestInput(input, baseURL) {
|
|
658
|
-
if (!baseURL) {
|
|
659
|
-
return input;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
try {
|
|
663
|
-
const original =
|
|
664
|
-
typeof input === "string" || input instanceof URL
|
|
665
|
-
? new URL(String(input))
|
|
666
|
-
: typeof Request !== "undefined" && input instanceof Request
|
|
667
|
-
? new URL(input.url)
|
|
668
|
-
: null;
|
|
669
|
-
|
|
670
|
-
if (!original) {
|
|
671
|
-
return input;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const nextBase = new URL(baseURL);
|
|
675
|
-
const nextUrl = new URL(`${original.pathname}${original.search}${original.hash}`, nextBase);
|
|
676
|
-
|
|
677
|
-
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
678
|
-
return new Request(nextUrl.toString(), input);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
return nextUrl.toString();
|
|
682
|
-
} catch { // intentional: return original input on URL rewrite error
|
|
683
|
-
return input;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function buildHeaders(init, info, isVision, isAgent) {
|
|
688
|
-
const explicitInitiator = getHeader(init?.headers, "x-initiator");
|
|
689
|
-
const headers = {
|
|
690
|
-
...(init?.headers ?? {}),
|
|
691
|
-
Authorization: `Bearer ${info.refresh}`,
|
|
692
|
-
"Copilot-Integration-Id": "copilot-developer-cli",
|
|
693
|
-
"Openai-Intent": "conversation-agent",
|
|
694
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
695
|
-
"X-GitHub-Api-Version": API_VERSION,
|
|
696
|
-
"X-Initiator": explicitInitiator ?? (isAgent ? "agent" : "user"),
|
|
697
|
-
"X-Interaction-Id": crypto.randomUUID(),
|
|
698
|
-
"X-Interaction-Type": "conversation-agent",
|
|
699
|
-
"X-Request-Id": crypto.randomUUID(),
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
if (isVision) {
|
|
703
|
-
headers["Copilot-Vision-Request"] = "true";
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
delete headers["x-api-key"];
|
|
707
|
-
delete headers["authorization"];
|
|
708
|
-
delete headers["x-initiator"];
|
|
709
|
-
|
|
710
|
-
return stripRoutingHeaders(headers);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function getSelectedAccountMissingError() {
|
|
714
|
-
return new Error(
|
|
715
|
-
"[opencode-copilot-cli-auth] Selected account is disabled or not found; re-login required",
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function getSelectedAccountExpiredError(accountKey) {
|
|
720
|
-
return new Error(
|
|
721
|
-
`[opencode-copilot-cli-auth] Account auth expired for ${accountKey}; re-login required`,
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function isValidBaseURL(value) {
|
|
726
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
727
|
-
return false;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
new URL(value);
|
|
732
|
-
return true;
|
|
733
|
-
} catch { // intentional: invalid URL means value is not a valid URL
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function resolveSelectedPoolAccount(pool, accountKey, requestedRawModelId) {
|
|
739
|
-
const accounts = Array.isArray(pool?.accounts) ? pool.accounts : [];
|
|
740
|
-
|
|
741
|
-
if (accounts.length === 0) {
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
let selectedAccount = null;
|
|
746
|
-
|
|
747
|
-
if (accountKey) {
|
|
748
|
-
const headerAccount = accounts.find((account) => account?.key === accountKey);
|
|
749
|
-
if (!headerAccount || headerAccount.enabled === false) {
|
|
750
|
-
throw getSelectedAccountMissingError();
|
|
751
|
-
}
|
|
752
|
-
selectedAccount = headerAccount;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (requestedRawModelId) {
|
|
756
|
-
const winner = resolveWinnerAccount(requestedRawModelId, pool);
|
|
757
|
-
if (!winner) {
|
|
758
|
-
throw new Error(
|
|
759
|
-
`[opencode-copilot-cli-auth] No eligible account found for model ${requestedRawModelId}; re-login required`,
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (selectedAccount && winner.key !== selectedAccount.key) {
|
|
764
|
-
throw new Error(
|
|
765
|
-
`[opencode-copilot-cli-auth] Selected account cannot serve model ${requestedRawModelId}; re-login required`,
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
selectedAccount = winner;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (!selectedAccount) {
|
|
773
|
-
throw new Error("[opencode-copilot-cli-auth] No eligible account found for routed request; re-login required");
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
selectedAccount.auth?.type !== "oauth"
|
|
778
|
-
|| typeof selectedAccount.auth.refresh !== "string"
|
|
779
|
-
|| !selectedAccount.auth.refresh.trim()
|
|
780
|
-
) {
|
|
781
|
-
throw getSelectedAccountExpiredError(selectedAccount.key ?? "unknown");
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return selectedAccount;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
async function refreshSelectedAccountBaseURL(accountKey) {
|
|
788
|
-
const currentPool = readPool();
|
|
789
|
-
const accountIndex = currentPool.accounts.findIndex((account) => account?.key === accountKey);
|
|
790
|
-
const currentAccount = accountIndex >= 0 ? currentPool.accounts[accountIndex] : null;
|
|
791
|
-
|
|
792
|
-
if (!currentAccount || currentAccount.enabled === false) {
|
|
793
|
-
throw getSelectedAccountMissingError();
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
currentAccount.auth?.type !== "oauth"
|
|
798
|
-
|| typeof currentAccount.auth.refresh !== "string"
|
|
799
|
-
|| !currentAccount.auth.refresh.trim()
|
|
800
|
-
) {
|
|
801
|
-
throw getSelectedAccountExpiredError(currentAccount.key ?? accountKey ?? "unknown");
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
let entitlement;
|
|
805
|
-
try {
|
|
806
|
-
entitlement = await fetchEntitlement({
|
|
807
|
-
refresh: currentAccount.auth.refresh,
|
|
808
|
-
enterpriseUrl: currentAccount.enterpriseUrl ?? null,
|
|
809
|
-
});
|
|
810
|
-
} catch { // intentional: entitlement fetch failure means account is expired/invalid; propagate as account error
|
|
811
|
-
throw getSelectedAccountExpiredError(currentAccount.key);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const nextBaseURL = entitlement?.endpoints?.api;
|
|
815
|
-
if (!isValidBaseURL(nextBaseURL)) {
|
|
816
|
-
throw getSelectedAccountExpiredError(currentAccount.key);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const updatedPool = {
|
|
820
|
-
...currentPool,
|
|
821
|
-
accounts: currentPool.accounts.map((account, index) =>
|
|
822
|
-
index === accountIndex
|
|
823
|
-
? {
|
|
824
|
-
...account,
|
|
825
|
-
baseUrl: nextBaseURL,
|
|
826
|
-
updatedAt: new Date().toISOString(),
|
|
827
|
-
}
|
|
828
|
-
: account
|
|
829
|
-
),
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
writePool(updatedPool);
|
|
833
|
-
return updatedPool.accounts[accountIndex];
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
async function fetchWithSelectedAccount(input, init, selectedAccount) {
|
|
837
|
-
const { isVision, isAgent } = getConversationMetadata(init);
|
|
838
|
-
|
|
839
|
-
const dispatch = async (account) => {
|
|
840
|
-
const headers = buildHeaders(init, account.auth, isVision, isAgent);
|
|
841
|
-
const requestInput = applyBaseURLToRequestInput(input, account.baseUrl);
|
|
842
|
-
|
|
843
|
-
return fetch(requestInput, {
|
|
844
|
-
...init,
|
|
845
|
-
headers,
|
|
846
|
-
});
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
let activeAccount = selectedAccount;
|
|
850
|
-
if (!isValidBaseURL(activeAccount.baseUrl)) {
|
|
851
|
-
activeAccount = await refreshSelectedAccountBaseURL(activeAccount.key);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const response = await dispatch(activeAccount);
|
|
855
|
-
if (![401, 403].includes(response.status)) {
|
|
856
|
-
return response;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const refreshedAccount = await refreshSelectedAccountBaseURL(activeAccount.key);
|
|
860
|
-
const retryResponse = await dispatch(refreshedAccount);
|
|
861
|
-
if ([401, 403].includes(retryResponse.status)) {
|
|
862
|
-
throw getSelectedAccountExpiredError(refreshedAccount.key);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return retryResponse;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function resolveClaudeThinkingBudget(model, variant) {
|
|
869
|
-
if (!model?.id?.includes("claude")) return undefined;
|
|
870
|
-
return variant === "thinking" ? 16000 : undefined;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return {
|
|
874
|
-
provider: {
|
|
875
|
-
id: "github-copilot",
|
|
876
|
-
models: async (provider, ctx) => {
|
|
877
|
-
try {
|
|
878
|
-
const pool = readPool();
|
|
879
|
-
if (pool.accounts.length > 0) {
|
|
880
|
-
return await buildPoolBackedModels(provider.models, pool);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return await resolveProviderModels(provider.models, ctx.auth);
|
|
884
|
-
} catch (error) {
|
|
885
|
-
console.warn("[opencode-copilot-cli-auth] Failed to sync live Copilot models.", error?.message ?? error);
|
|
886
|
-
return normalizeExistingModels(provider.models);
|
|
887
|
-
}
|
|
888
|
-
},
|
|
889
|
-
},
|
|
890
|
-
auth: {
|
|
891
|
-
provider: "github-copilot",
|
|
892
|
-
loader: async (getAuth) => {
|
|
893
|
-
const info = await getAuth();
|
|
894
|
-
let poolFirstEnabled;
|
|
895
|
-
|
|
896
|
-
try {
|
|
897
|
-
const currentPool = readPool();
|
|
898
|
-
poolFirstEnabled = currentPool.accounts.find((account) => account?.enabled !== false);
|
|
899
|
-
if (currentPool.accounts.length === 0 && info && info.type === "oauth") {
|
|
900
|
-
// Best-effort migration bridge for legacy single-account auth.
|
|
901
|
-
// We intentionally avoid identity lookup here because loader runs on hot paths,
|
|
902
|
-
// and we do not have a stable userId without making a network request.
|
|
903
|
-
// The next successful OAuth authorize callback performs canonical persistence.
|
|
904
|
-
}
|
|
905
|
-
} catch {} // intentional: pool read/parse errors fall back to legacy singleton auth
|
|
906
|
-
|
|
907
|
-
const baseSource =
|
|
908
|
-
poolFirstEnabled?.auth?.type === "oauth"
|
|
909
|
-
? poolFirstEnabled.auth
|
|
910
|
-
: info && info.type === "oauth"
|
|
911
|
-
? info
|
|
912
|
-
: null;
|
|
913
|
-
if (!baseSource) return {};
|
|
914
|
-
|
|
915
|
-
const baseURL = poolFirstEnabled?.baseUrl ?? await getBaseURL(baseSource);
|
|
916
|
-
|
|
917
|
-
return {
|
|
918
|
-
...(baseURL && { baseURL }),
|
|
919
|
-
apiKey: "",
|
|
920
|
-
async fetch(input, init) {
|
|
921
|
-
const pool = readPool();
|
|
922
|
-
const accountKey = getHeader(init?.headers, ROUTING_ACCOUNT_KEY_HEADER);
|
|
923
|
-
const requestedRawModelId = getRequestedRawModelId(init);
|
|
924
|
-
|
|
925
|
-
if (pool.accounts.length > 0) {
|
|
926
|
-
const selectedAccount = resolveSelectedPoolAccount(pool, accountKey, requestedRawModelId);
|
|
927
|
-
if (selectedAccount?.auth?.type === "oauth") {
|
|
928
|
-
return fetchWithSelectedAccount(input, init, selectedAccount);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const auth = await getAuth();
|
|
933
|
-
if (!auth || auth.type !== "oauth") {
|
|
934
|
-
return fetch(input, init);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const { isVision, isAgent } = getConversationMetadata(init);
|
|
938
|
-
const headers = buildHeaders(init, auth, isVision, isAgent);
|
|
939
|
-
|
|
940
|
-
return fetch(input, {
|
|
941
|
-
...init,
|
|
942
|
-
headers,
|
|
943
|
-
});
|
|
944
|
-
},
|
|
945
|
-
};
|
|
946
|
-
},
|
|
947
|
-
methods: [
|
|
948
|
-
{
|
|
949
|
-
type: "oauth",
|
|
950
|
-
label: "Login with GitHub Copilot CLI",
|
|
951
|
-
prompts: [
|
|
952
|
-
{
|
|
953
|
-
type: "select",
|
|
954
|
-
key: "deploymentType",
|
|
955
|
-
message: "Select GitHub deployment type",
|
|
956
|
-
options: [
|
|
957
|
-
{
|
|
958
|
-
label: "GitHub.com (Add)",
|
|
959
|
-
value: "github.com",
|
|
960
|
-
hint: "Public",
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
label: "GitHub Enterprise (Add)",
|
|
964
|
-
value: "enterprise",
|
|
965
|
-
hint: "Data residency or self-hosted",
|
|
966
|
-
},
|
|
967
|
-
],
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
type: "text",
|
|
971
|
-
key: "enterpriseUrl",
|
|
972
|
-
message: "Enter your GitHub Enterprise URL or domain",
|
|
973
|
-
placeholder: "github.com or https://github.com (default: github.com)",
|
|
974
|
-
condition: (inputs) => inputs.deploymentType === "enterprise",
|
|
975
|
-
validate: (value) => {
|
|
976
|
-
if (!value || !String(value).trim()) {
|
|
977
|
-
return undefined;
|
|
978
|
-
}
|
|
979
|
-
try {
|
|
980
|
-
const url = value.includes("://")
|
|
981
|
-
? new URL(value)
|
|
982
|
-
: new URL(`https://${value}`);
|
|
983
|
-
if (!url.hostname) {
|
|
984
|
-
return "Please enter a valid URL or domain";
|
|
985
|
-
}
|
|
986
|
-
return undefined;
|
|
987
|
-
} catch { // intentional: invalid URL input returns a user-facing validation error message
|
|
988
|
-
return "Please enter a valid URL (e.g., company.ghe.com or https://company.ghe.com)";
|
|
989
|
-
}
|
|
990
|
-
},
|
|
991
|
-
},
|
|
992
|
-
],
|
|
993
|
-
async authorize(inputs = {}) {
|
|
994
|
-
const deploymentType = inputs.deploymentType || "github.com";
|
|
995
|
-
|
|
996
|
-
let domain = "github.com";
|
|
997
|
-
let actualProvider = "github-copilot";
|
|
998
|
-
|
|
999
|
-
if (deploymentType === "enterprise") {
|
|
1000
|
-
domain = normalizeDomain(inputs.enterpriseUrl);
|
|
1001
|
-
actualProvider = "github-copilot-enterprise";
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const urls = getUrls(domain);
|
|
1005
|
-
|
|
1006
|
-
const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
|
|
1007
|
-
method: "POST",
|
|
1008
|
-
headers: {
|
|
1009
|
-
Accept: "application/json",
|
|
1010
|
-
"Content-Type": "application/json",
|
|
1011
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
1012
|
-
},
|
|
1013
|
-
body: JSON.stringify({
|
|
1014
|
-
client_id: CLIENT_ID,
|
|
1015
|
-
scope: OAUTH_SCOPES,
|
|
1016
|
-
}),
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
if (!deviceResponse.ok) {
|
|
1020
|
-
throw new Error("[opencode-copilot-cli-auth] Failed to initiate device authorization");
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const deviceData = await deviceResponse.json();
|
|
1024
|
-
|
|
1025
|
-
return {
|
|
1026
|
-
url: deviceData.verification_uri,
|
|
1027
|
-
instructions: `Enter code: ${deviceData.user_code}`,
|
|
1028
|
-
method: "auto",
|
|
1029
|
-
callback: async () => {
|
|
1030
|
-
while (true) {
|
|
1031
|
-
const response = await fetch(urls.ACCESS_TOKEN_URL, {
|
|
1032
|
-
method: "POST",
|
|
1033
|
-
headers: {
|
|
1034
|
-
Accept: "application/json",
|
|
1035
|
-
"Content-Type": "application/json",
|
|
1036
|
-
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
1037
|
-
},
|
|
1038
|
-
body: JSON.stringify({
|
|
1039
|
-
client_id: CLIENT_ID,
|
|
1040
|
-
device_code: deviceData.device_code,
|
|
1041
|
-
grant_type:
|
|
1042
|
-
"urn:ietf:params:oauth:grant-type:device_code",
|
|
1043
|
-
}),
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
if (!response.ok) return { type: "failed" };
|
|
1047
|
-
|
|
1048
|
-
const data = await response.json();
|
|
1049
|
-
|
|
1050
|
-
if (data.access_token) {
|
|
1051
|
-
const entitlement = await fetchEntitlement({
|
|
1052
|
-
refresh: data.access_token,
|
|
1053
|
-
enterpriseUrl:
|
|
1054
|
-
actualProvider === "github-copilot-enterprise"
|
|
1055
|
-
? domain
|
|
1056
|
-
: undefined,
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
const result = {
|
|
1060
|
-
type: "success",
|
|
1061
|
-
refresh: data.access_token,
|
|
1062
|
-
access: data.access_token,
|
|
1063
|
-
expires: 0,
|
|
1064
|
-
baseUrl: entitlement?.endpoints?.api,
|
|
1065
|
-
};
|
|
1066
|
-
|
|
1067
|
-
if (actualProvider === "github-copilot-enterprise") {
|
|
1068
|
-
result.provider = "github-copilot-enterprise";
|
|
1069
|
-
result.enterpriseUrl = domain;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
try {
|
|
1073
|
-
const identity = await lookupGitHubIdentity(
|
|
1074
|
-
data.access_token,
|
|
1075
|
-
actualProvider === "github-copilot-enterprise" ? domain : undefined,
|
|
1076
|
-
);
|
|
1077
|
-
const deployment = actualProvider === "github-copilot-enterprise" ? domain : "github.com";
|
|
1078
|
-
const key = deriveAccountKey(deployment, identity.userId);
|
|
1079
|
-
const pool = readPool();
|
|
1080
|
-
const updatedPool = upsertAccount(pool, {
|
|
1081
|
-
key,
|
|
1082
|
-
deployment,
|
|
1083
|
-
domain: deployment,
|
|
1084
|
-
identity,
|
|
1085
|
-
enterpriseUrl: actualProvider === "github-copilot-enterprise" ? domain : null,
|
|
1086
|
-
baseUrl: result.baseUrl,
|
|
1087
|
-
auth: {
|
|
1088
|
-
type: "oauth",
|
|
1089
|
-
refresh: data.access_token,
|
|
1090
|
-
access: data.access_token,
|
|
1091
|
-
expires: 0,
|
|
1092
|
-
baseUrl: result.baseUrl ?? null,
|
|
1093
|
-
...(result.provider && { provider: result.provider }),
|
|
1094
|
-
...(result.enterpriseUrl && { enterpriseUrl: result.enterpriseUrl }),
|
|
1095
|
-
},
|
|
1096
|
-
});
|
|
1097
|
-
writePool(updatedPool);
|
|
1098
|
-
} catch (persistError) {
|
|
1099
|
-
console.warn(
|
|
1100
|
-
"[opencode-copilot-cli-auth] Failed to persist account to pool:",
|
|
1101
|
-
persistError?.message ?? persistError,
|
|
1102
|
-
);
|
|
1103
|
-
// Non-fatal: continue with the login flow.
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return result;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (data.error === "authorization_pending") {
|
|
1110
|
-
await new Promise((resolve) =>
|
|
1111
|
-
setTimeout(
|
|
1112
|
-
resolve,
|
|
1113
|
-
deviceData.interval * 1000
|
|
1114
|
-
+ OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
1115
|
-
),
|
|
1116
|
-
);
|
|
1117
|
-
continue;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (data.error === "slow_down") {
|
|
1121
|
-
const nextInterval =
|
|
1122
|
-
(typeof data.interval === "number" && data.interval > 0 ?
|
|
1123
|
-
data.interval
|
|
1124
|
-
: deviceData.interval + 5) * 1000;
|
|
1125
|
-
await new Promise((resolve) =>
|
|
1126
|
-
setTimeout(
|
|
1127
|
-
resolve,
|
|
1128
|
-
nextInterval + OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
1129
|
-
),
|
|
1130
|
-
);
|
|
1131
|
-
continue;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
if (data.error) return { type: "failed" };
|
|
1135
|
-
|
|
1136
|
-
await new Promise((resolve) =>
|
|
1137
|
-
setTimeout(
|
|
1138
|
-
resolve,
|
|
1139
|
-
deviceData.interval * 1000
|
|
1140
|
-
+ OAUTH_POLLING_SAFETY_MARGIN_MS,
|
|
1141
|
-
),
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
},
|
|
1145
|
-
};
|
|
1146
|
-
},
|
|
1147
|
-
},
|
|
1148
|
-
],
|
|
1149
|
-
},
|
|
1150
|
-
"chat.params": async (input, output) => {
|
|
1151
|
-
if (input.model.providerID !== "github-copilot") return;
|
|
1152
|
-
if (input.model.api?.npm !== "@ai-sdk/github-copilot") return;
|
|
1153
|
-
if (!input.model.id.includes("claude")) return;
|
|
1154
|
-
|
|
1155
|
-
const thinkingBudget = resolveClaudeThinkingBudget(input.model, input.message.variant);
|
|
1156
|
-
if (thinkingBudget === undefined) return;
|
|
1157
|
-
|
|
1158
|
-
output.options.thinking_budget = thinkingBudget;
|
|
1159
|
-
},
|
|
1160
|
-
"chat.headers": async (incoming, output) => {
|
|
1161
|
-
if (!incoming.model.providerID.includes("github-copilot")) return;
|
|
1162
|
-
|
|
1163
|
-
const sdk = input.client;
|
|
1164
|
-
if (sdk?.session?.message && sdk?.session?.get) {
|
|
1165
|
-
const parts = await sdk.session
|
|
1166
|
-
.message({
|
|
1167
|
-
path: {
|
|
1168
|
-
id: incoming.message.sessionID,
|
|
1169
|
-
messageID: incoming.message.id,
|
|
1170
|
-
},
|
|
1171
|
-
query: {
|
|
1172
|
-
directory: input.directory,
|
|
1173
|
-
},
|
|
1174
|
-
throwOnError: true,
|
|
1175
|
-
})
|
|
1176
|
-
.catch(() => undefined);
|
|
1177
|
-
|
|
1178
|
-
if (parts?.data?.parts?.some((part) => part.type === "compaction")) {
|
|
1179
|
-
output.headers["x-initiator"] = "agent";
|
|
1180
|
-
} else {
|
|
1181
|
-
const session = await sdk.session
|
|
1182
|
-
.get({
|
|
1183
|
-
path: {
|
|
1184
|
-
id: incoming.sessionID,
|
|
1185
|
-
},
|
|
1186
|
-
query: {
|
|
1187
|
-
directory: input.directory,
|
|
1188
|
-
},
|
|
1189
|
-
throwOnError: true,
|
|
1190
|
-
})
|
|
1191
|
-
.catch(() => undefined);
|
|
1192
|
-
|
|
1193
|
-
if (session?.data?.parentID) {
|
|
1194
|
-
output.headers["x-initiator"] = "agent";
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
try {
|
|
1200
|
-
const pool = readPool();
|
|
1201
|
-
if (pool.accounts.length > 0) {
|
|
1202
|
-
const winner = resolveWinnerAccount(incoming.model.id, pool);
|
|
1203
|
-
if (winner) {
|
|
1204
|
-
output.headers[ROUTING_ACCOUNT_KEY_HEADER] = winner.key;
|
|
1205
|
-
output.headers[ROUTING_SOURCE_HEADER] = "model-resolution";
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
} catch {} // intentional: routing header injection is best-effort; missing header falls back to request-time model resolution
|
|
1209
|
-
},
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
CopilotAuthPlugin,
|
|
3
|
+
deriveAccountKey,
|
|
4
|
+
getPoolPath,
|
|
5
|
+
injectRoutingHeaders,
|
|
6
|
+
lookupGitHubIdentity,
|
|
7
|
+
readPool,
|
|
8
|
+
resolveWinnerAccount,
|
|
9
|
+
stripRoutingHeaders,
|
|
10
|
+
upsertAccount,
|
|
11
|
+
writePool,
|
|
12
|
+
} from "./dist/index.js";
|