@nick3/copilot-api 1.5.6 → 1.6.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.
Files changed (32) hide show
  1. package/README.md +41 -31
  2. package/README.zh-CN.md +953 -0
  3. package/dist/{account-AacnHem5.js → account-B2tSWtVS.js} +12 -4
  4. package/dist/account-B2tSWtVS.js.map +1 -0
  5. package/dist/accounts-manager-CAyZJSn8.js +2932 -0
  6. package/dist/accounts-manager-CAyZJSn8.js.map +1 -0
  7. package/dist/admin/assets/index-BESw8Vvd.css +1 -0
  8. package/dist/admin/assets/index-Ddo9RHg-.js +101 -0
  9. package/dist/admin/index.html +2 -2
  10. package/dist/{auth-B7x3wjry.js → auth-BXCeDjRG.js} +3 -3
  11. package/dist/{auth-B7x3wjry.js.map → auth-BXCeDjRG.js.map} +1 -1
  12. package/dist/{check-usage-B1cbDEOI.js → check-usage-CQxXYfUx.js} +3 -3
  13. package/dist/check-usage-CQxXYfUx.js.map +1 -0
  14. package/dist/{get-copilot-token-cha9rQwA.js → get-copilot-token-p17sJyPU.js} +2 -2
  15. package/dist/{get-copilot-token-cha9rQwA.js.map → get-copilot-token-p17sJyPU.js.map} +1 -1
  16. package/dist/main.js +3 -3
  17. package/dist/{poll-access-token-DFooFWhY.js → poll-access-token-Bc6VwWab.js} +105 -40
  18. package/dist/poll-access-token-Bc6VwWab.js.map +1 -0
  19. package/dist/{server-DVpkQrk2.js → server-CFijvv3C.js} +969 -945
  20. package/dist/server-CFijvv3C.js.map +1 -0
  21. package/dist/{start-fPbCDj4c.js → start-DQlnH71A.js} +7 -6
  22. package/dist/start-DQlnH71A.js.map +1 -0
  23. package/package.json +1 -1
  24. package/dist/account-AacnHem5.js.map +0 -1
  25. package/dist/accounts-manager-BE-Dq5Wn.js +0 -1494
  26. package/dist/accounts-manager-BE-Dq5Wn.js.map +0 -1
  27. package/dist/admin/assets/index-CdoHTemy.css +0 -1
  28. package/dist/admin/assets/index-wcoGQpIM.js +0 -66
  29. package/dist/check-usage-B1cbDEOI.js.map +0 -1
  30. package/dist/poll-access-token-DFooFWhY.js.map +0 -1
  31. package/dist/server-DVpkQrk2.js.map +0 -1
  32. package/dist/start-fPbCDj4c.js.map +0 -1
@@ -1,1494 +0,0 @@
1
- import { _ as HTTPError, g as getCopilotUsage, m as getGitHubUser, p as getModels } from "./poll-access-token-DFooFWhY.js";
2
- import { _ as buildIdentityKey, c as listAccountsFromRegistry, d as readLegacyToken, i as ensureAccountClientIdentity, l as loadAccountToken, m as saveAccountToken, o as hasLegacyToken, r as addAccountToRegistry, s as hasRegistry, v as createAccountSessionId, y as getCurrentIdentityEnvironment } from "./account-AacnHem5.js";
3
- import { t as PATHS } from "./paths-DGlr310R.js";
4
- import { t as getCopilotToken } from "./get-copilot-token-cha9rQwA.js";
5
- import consola from "consola";
6
- import fs from "node:fs";
7
-
8
- //#region src/lib/config.ts
9
- const PROVIDER_TYPE_ANTHROPIC = "anthropic";
10
- const gpt5ExplorationPrompt = `## Exploration and reading files
11
- - **Think first.** Before any tool call, decide ALL files/resources you will need.
12
- - **Batch everything.** If you need multiple files (even from different places), read them together.
13
- - **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.
14
- - **Only make sequential calls if you truly cannot know the next file without seeing a result first.**
15
- - **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`;
16
- const gpt5CommentaryPrompt = `# Working with the user
17
-
18
- You interact with the user through a terminal. You have 2 ways of communicating with the users:
19
- - Share intermediary updates in \`commentary\` channel.
20
- - After you have completed all your work, send a message to the \`final\` channel.
21
-
22
- ## Intermediary updates
23
-
24
- - Intermediary updates go to the \`commentary\` channel.
25
- - User updates are short updates while you are working, they are NOT final answers.
26
- - You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.
27
- - Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.
28
- - You provide user updates frequently, every 20s.
29
- - Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such as "Got it -" or "Understood -" etc.
30
- - When exploring, e.g. searching, reading files, you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.
31
- - After you have sufficient context, and the work is substantial, you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).
32
- - Before performing file edits of any kind, you provide updates explaining what edits you are making.
33
- - As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.
34
- - Tone of your updates MUST match your personality.`;
35
- const defaultConfig = {
36
- auth: { apiKeys: [] },
37
- providers: {},
38
- extraPrompts: {
39
- "gpt-5-mini": gpt5ExplorationPrompt,
40
- "gpt-5.3-codex": gpt5CommentaryPrompt,
41
- "gpt-5.4-mini": gpt5CommentaryPrompt,
42
- "gpt-5.4": gpt5CommentaryPrompt
43
- },
44
- smallModel: "gpt-5-mini",
45
- accountAffinity: true,
46
- responsesApiContextManagementModels: [],
47
- modelReasoningEfforts: {
48
- "gpt-5-mini": "low",
49
- "gpt-5.3-codex": "xhigh",
50
- "gpt-5.4-mini": "xhigh",
51
- "gpt-5.4": "xhigh"
52
- },
53
- allowOriginalModelNamesForAliases: false,
54
- useFunctionApplyPatch: true,
55
- forceAgent: false,
56
- compactUseSmallModel: true,
57
- messageStartInputTokensFallback: false,
58
- modelRefreshIntervalHours: 24,
59
- useMessagesApi: true,
60
- useResponsesApiWebSearch: true
61
- };
62
- let cachedConfig = null;
63
- function isPlainObject(value) {
64
- return typeof value === "object" && value !== null && !Array.isArray(value);
65
- }
66
- function normalizeAuthApiKeys(value) {
67
- if (!Array.isArray(value)) return [];
68
- return [...new Set(value.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0))];
69
- }
70
- function normalizeModelRefreshIntervalHours(value) {
71
- if (typeof value !== "number") return void 0;
72
- if (!Number.isFinite(value)) return void 0;
73
- if (value < 0) return void 0;
74
- return value;
75
- }
76
- function ensureConfigFile() {
77
- try {
78
- fs.accessSync(PATHS.CONFIG_PATH, fs.constants.R_OK);
79
- return;
80
- } catch {}
81
- try {
82
- fs.mkdirSync(PATHS.APP_DIR, { recursive: true });
83
- fs.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
84
- try {
85
- fs.chmodSync(PATHS.CONFIG_PATH, 384);
86
- } catch {}
87
- } catch {}
88
- }
89
- function readConfigFromDisk() {
90
- ensureConfigFile();
91
- try {
92
- const raw = fs.readFileSync(PATHS.CONFIG_PATH, "utf8");
93
- if (!raw.trim()) {
94
- fs.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
95
- return defaultConfig;
96
- }
97
- return JSON.parse(raw);
98
- } catch (error) {
99
- consola.error("Failed to read config file, using default config", error);
100
- return defaultConfig;
101
- }
102
- }
103
- function mergeDefaultConfig(config) {
104
- const extraPrompts = config.extraPrompts ?? {};
105
- const defaultExtraPrompts = defaultConfig.extraPrompts ?? {};
106
- const modelReasoningEfforts = config.modelReasoningEfforts ?? {};
107
- const defaultModelReasoningEfforts = defaultConfig.modelReasoningEfforts ?? {};
108
- const hasForceAgent = typeof config.forceAgent === "boolean";
109
- const defaultForceAgent = defaultConfig.forceAgent ?? false;
110
- const missingExtraPromptModels = Object.keys(defaultExtraPrompts).filter((model) => !Object.hasOwn(extraPrompts, model));
111
- const missingReasoningEffortModels = Object.keys(defaultModelReasoningEfforts).filter((model) => !Object.hasOwn(modelReasoningEfforts, model));
112
- const hasExtraPromptChanges = missingExtraPromptModels.length > 0;
113
- const hasReasoningEffortChanges = missingReasoningEffortModels.length > 0;
114
- if (!hasExtraPromptChanges && !hasReasoningEffortChanges && !!hasForceAgent) return {
115
- mergedConfig: config,
116
- changed: false
117
- };
118
- return {
119
- mergedConfig: {
120
- ...config,
121
- extraPrompts: {
122
- ...defaultExtraPrompts,
123
- ...extraPrompts
124
- },
125
- modelReasoningEfforts: {
126
- ...defaultModelReasoningEfforts,
127
- ...modelReasoningEfforts
128
- },
129
- forceAgent: hasForceAgent ? config.forceAgent : defaultForceAgent
130
- },
131
- changed: true
132
- };
133
- }
134
- function mergeDefaultAuth(config) {
135
- const authConfig = isPlainObject(config.auth) ? config.auth : void 0;
136
- const nextAuth = { apiKeys: normalizeAuthApiKeys(Array.isArray(authConfig?.apiKeys) ? authConfig.apiKeys : void 0) };
137
- if (authConfig && JSON.stringify(authConfig) === JSON.stringify(nextAuth)) return {
138
- mergedConfig: config,
139
- changed: false
140
- };
141
- return {
142
- mergedConfig: {
143
- ...config,
144
- auth: nextAuth
145
- },
146
- changed: true
147
- };
148
- }
149
- function mergeDefaultAccountAffinity(config) {
150
- const raw = config;
151
- const hasOld = typeof raw.freeModelLoadBalancing === "boolean";
152
- const hasNew = typeof config.accountAffinity === "boolean";
153
- if (hasOld) {
154
- const next = { ...config };
155
- if (!hasNew) next.accountAffinity = raw.freeModelLoadBalancing;
156
- delete next.freeModelLoadBalancing;
157
- return {
158
- mergedConfig: next,
159
- changed: true
160
- };
161
- }
162
- if (hasNew) return {
163
- mergedConfig: config,
164
- changed: false
165
- };
166
- return {
167
- mergedConfig: {
168
- ...config,
169
- accountAffinity: defaultConfig.accountAffinity ?? true
170
- },
171
- changed: true
172
- };
173
- }
174
- function mergeDefaultModelRefreshInterval(config) {
175
- if (normalizeModelRefreshIntervalHours(config.modelRefreshIntervalHours) !== void 0) return {
176
- mergedConfig: config,
177
- changed: false
178
- };
179
- return {
180
- mergedConfig: {
181
- ...config,
182
- modelRefreshIntervalHours: defaultConfig.modelRefreshIntervalHours ?? 24
183
- },
184
- changed: true
185
- };
186
- }
187
- function applyConfigMerges(config, mergeFns) {
188
- return mergeFns.reduce((acc, mergeFn) => {
189
- const result = mergeFn(acc.mergedConfig);
190
- return {
191
- mergedConfig: result.mergedConfig,
192
- changed: acc.changed || result.changed
193
- };
194
- }, {
195
- mergedConfig: config,
196
- changed: false
197
- });
198
- }
199
- function mergeConfigWithDefaults() {
200
- const { mergedConfig, changed } = applyConfigMerges(readConfigFromDisk(), [
201
- mergeDefaultAuth,
202
- mergeDefaultConfig,
203
- mergeDefaultAccountAffinity,
204
- mergeDefaultModelRefreshInterval
205
- ]);
206
- if (changed) try {
207
- fs.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
208
- } catch (writeError) {
209
- consola.warn("Failed to write merged config defaults", writeError);
210
- }
211
- cachedConfig = mergedConfig;
212
- return mergedConfig;
213
- }
214
- function getConfig() {
215
- cachedConfig ??= readConfigFromDisk();
216
- return cachedConfig;
217
- }
218
- function normalizeAliasKey(value) {
219
- const trimmed = value.trim().toLowerCase();
220
- return trimmed.length > 0 ? trimmed : null;
221
- }
222
- function normalizeAliasTarget(value) {
223
- const trimmed = value.trim();
224
- return trimmed.length > 0 ? trimmed : null;
225
- }
226
- function normalizeAliasSpec(value) {
227
- if (typeof value === "string") {
228
- const normalizedTarget$1 = normalizeAliasTarget(value);
229
- return normalizedTarget$1 ? { target: normalizedTarget$1 } : null;
230
- }
231
- if (!value || typeof value !== "object") return null;
232
- const targetValue = value.target;
233
- if (typeof targetValue !== "string") return null;
234
- const normalizedTarget = normalizeAliasTarget(targetValue);
235
- if (!normalizedTarget) return null;
236
- const allowOriginalValue = value.allowOriginal;
237
- return {
238
- target: normalizedTarget,
239
- allowOriginal: typeof allowOriginalValue === "boolean" ? allowOriginalValue : void 0
240
- };
241
- }
242
- function getModelAliasesInfo() {
243
- const raw = getConfig().modelAliases ?? {};
244
- const normalized = {};
245
- for (const [alias, rawSpec] of Object.entries(raw)) {
246
- const normalizedAlias = normalizeAliasKey(alias);
247
- const normalizedSpec = normalizeAliasSpec(rawSpec);
248
- if (!normalizedAlias || !normalizedSpec) continue;
249
- if (!Object.hasOwn(normalized, normalizedAlias)) normalized[normalizedAlias] = normalizedSpec;
250
- }
251
- return normalized;
252
- }
253
- function getModelAliases() {
254
- const info = getModelAliasesInfo();
255
- const normalized = {};
256
- for (const [alias, spec] of Object.entries(info)) normalized[alias] = spec.target;
257
- return normalized;
258
- }
259
- function resolveModelAlias(modelId) {
260
- const normalized = normalizeAliasKey(modelId);
261
- if (!normalized) return modelId;
262
- return getModelAliases()[normalized] ?? modelId;
263
- }
264
- function isOriginalModelNameAllowedForAliases() {
265
- return getConfig().allowOriginalModelNamesForAliases ?? false;
266
- }
267
- function getAliasTargetSet() {
268
- const aliases = getModelAliasesInfo();
269
- const allowOriginalDefault = isOriginalModelNameAllowedForAliases();
270
- const targetAllowMap = /* @__PURE__ */ new Map();
271
- for (const { target, allowOriginal } of Object.values(aliases)) {
272
- const normalizedTarget = target.toLowerCase();
273
- const effectiveAllow = allowOriginal ?? allowOriginalDefault;
274
- const currentAllow = targetAllowMap.get(normalizedTarget);
275
- if (currentAllow === true) continue;
276
- if (effectiveAllow) targetAllowMap.set(normalizedTarget, true);
277
- else if (currentAllow === void 0) targetAllowMap.set(normalizedTarget, false);
278
- }
279
- const blockedTargets = /* @__PURE__ */ new Set();
280
- for (const [target, allowed] of targetAllowMap.entries()) if (!allowed) blockedTargets.add(target);
281
- return blockedTargets;
282
- }
283
- function isOriginalModelNameAllowedForTarget(modelId) {
284
- const normalized = normalizeAliasKey(modelId);
285
- if (!normalized) return true;
286
- return !getAliasTargetSet().has(normalized);
287
- }
288
- function getPreferredAliasForTarget(modelId) {
289
- return getAliasKeysForTarget(modelId, getModelAliases())[0] ?? null;
290
- }
291
- function getAliasKeysForTarget(target, aliases) {
292
- const normalizedTarget = target.toLowerCase();
293
- return Object.entries(aliases).filter(([, model]) => model.toLowerCase() === normalizedTarget).map(([alias]) => alias).sort();
294
- }
295
- function getAliasFallbackValue(record, modelId, aliases) {
296
- if (!record) return void 0;
297
- const aliasKeys = getAliasKeysForTarget(modelId, aliases);
298
- if (aliasKeys.length === 0) return void 0;
299
- const recordByAlias = /* @__PURE__ */ new Map();
300
- for (const [key, value] of Object.entries(record)) {
301
- const normalized = normalizeAliasKey(key);
302
- if (normalized) recordByAlias.set(normalized, value);
303
- }
304
- for (const alias of aliasKeys) {
305
- const value = recordByAlias.get(alias);
306
- if (value !== void 0) return value;
307
- }
308
- }
309
- function getExtraPromptForModel(model) {
310
- const config = getConfig();
311
- const direct = config.extraPrompts?.[model];
312
- if (direct !== void 0) return direct;
313
- const aliases = getModelAliases();
314
- return getAliasFallbackValue(config.extraPrompts, model, aliases) ?? "";
315
- }
316
- function getSmallModel() {
317
- const model = getConfig().smallModel ?? "gpt-5-mini";
318
- if (isOriginalModelNameAllowedForTarget(model)) return model;
319
- return getPreferredAliasForTarget(model) ?? model;
320
- }
321
- function isAccountAffinityEnabled() {
322
- return getConfig().accountAffinity ?? true;
323
- }
324
- function getModelRefreshIntervalHours() {
325
- return normalizeModelRefreshIntervalHours(getConfig().modelRefreshIntervalHours) ?? defaultConfig.modelRefreshIntervalHours ?? 24;
326
- }
327
- function getModelRefreshIntervalMs() {
328
- const hours = getModelRefreshIntervalHours();
329
- if (!Number.isFinite(hours) || hours <= 0) return 0;
330
- return hours * 60 * 60 * 1e3;
331
- }
332
- function isMessageStartInputTokensFallbackEnabled() {
333
- return getConfig().messageStartInputTokensFallback ?? false;
334
- }
335
- function shouldCompactUseSmallModel() {
336
- return getConfig().compactUseSmallModel ?? true;
337
- }
338
- function getResponsesApiContextManagementModels() {
339
- return getConfig().responsesApiContextManagementModels ?? defaultConfig.responsesApiContextManagementModels ?? [];
340
- }
341
- function isResponsesApiContextManagementModel(model) {
342
- return getResponsesApiContextManagementModels().includes(model);
343
- }
344
- function getReasoningEffortForModel(model) {
345
- const config = getConfig();
346
- const direct = config.modelReasoningEfforts?.[model];
347
- if (direct !== void 0) return direct;
348
- const aliases = getModelAliases();
349
- return getAliasFallbackValue(config.modelReasoningEfforts, model, aliases) ?? "high";
350
- }
351
- function isForceAgentEnabled() {
352
- return getConfig().forceAgent ?? false;
353
- }
354
- function normalizeProviderBaseUrl(url) {
355
- return url.trim().replace(/\/+$/u, "");
356
- }
357
- function resolveProviderAuthType(providerName, authType) {
358
- if (authType === void 0 || authType === "x-api-key") return "x-api-key";
359
- if (authType === "authorization") return authType;
360
- consola.warn(`Provider ${providerName} has invalid authType '${authType}', ignoring provider`);
361
- return null;
362
- }
363
- function getProviderConfig(name) {
364
- const providerName = name.trim();
365
- if (!providerName) return null;
366
- const provider = getConfig().providers?.[providerName];
367
- if (!provider) return null;
368
- if (provider.enabled === false) return null;
369
- if ((provider.type ?? PROVIDER_TYPE_ANTHROPIC) !== PROVIDER_TYPE_ANTHROPIC) {
370
- consola.warn(`Provider ${providerName} is ignored because only anthropic type is supported`);
371
- return null;
372
- }
373
- const baseUrl = normalizeProviderBaseUrl(provider.baseUrl ?? "");
374
- const apiKey = (provider.apiKey ?? "").trim();
375
- const authType = resolveProviderAuthType(providerName, provider.authType);
376
- if (!authType) return null;
377
- if (!baseUrl || !apiKey) {
378
- consola.warn(`Provider ${providerName} is enabled but missing baseUrl or apiKey`);
379
- return null;
380
- }
381
- return {
382
- name: providerName,
383
- type: PROVIDER_TYPE_ANTHROPIC,
384
- baseUrl,
385
- apiKey,
386
- authType,
387
- models: provider.models,
388
- adjustInputTokens: provider.adjustInputTokens
389
- };
390
- }
391
- function isMessagesApiEnabled() {
392
- return getConfig().useMessagesApi ?? true;
393
- }
394
- function getAnthropicApiKey() {
395
- return getConfig().anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? void 0;
396
- }
397
- function isResponsesApiWebSearchEnabled() {
398
- return getConfig().useResponsesApiWebSearch ?? true;
399
- }
400
- function getClaudeTokenMultiplier() {
401
- return getConfig().claudeTokenMultiplier ?? 1.15;
402
- }
403
-
404
- //#endregion
405
- //#region src/lib/account-affinity.ts
406
- const DEFAULT_MAX_ENTRIES = 1e4;
407
- const DEFAULT_TTL_MS = 3600 * 1e3;
408
- /**
409
- * In-memory LRU cache with TTL for account affinity mappings.
410
- *
411
- * Uses Map insertion order for LRU eviction: accessed/updated entries are
412
- * deleted and re-inserted so they move to the "newest" end.
413
- */
414
- var AccountAffinityCache = class {
415
- cache = /* @__PURE__ */ new Map();
416
- maxEntries;
417
- ttlMs;
418
- constructor(maxEntries = DEFAULT_MAX_ENTRIES, ttlMs = DEFAULT_TTL_MS) {
419
- this.maxEntries = maxEntries;
420
- this.ttlMs = ttlMs;
421
- }
422
- /** Look up the preferred account ID for a cache key. Returns undefined if not found or expired. */
423
- get(key) {
424
- const entry = this.cache.get(key);
425
- if (!entry) return;
426
- if (Date.now() >= entry.expiresAt) {
427
- this.cache.delete(key);
428
- return;
429
- }
430
- return entry.accountId;
431
- }
432
- /** Record a successful account mapping. Refreshes TTL and moves the entry to the newest position. */
433
- set(key, accountId) {
434
- this.cache.delete(key);
435
- while (this.cache.size >= this.maxEntries) {
436
- const oldest = this.cache.keys().next();
437
- if (oldest.done) break;
438
- this.cache.delete(oldest.value);
439
- }
440
- this.cache.set(key, {
441
- accountId,
442
- expiresAt: Date.now() + this.ttlMs
443
- });
444
- }
445
- /** Remove a specific entry. */
446
- delete(key) {
447
- return this.cache.delete(key);
448
- }
449
- /** Remove all entries. */
450
- clear() {
451
- this.cache.clear();
452
- }
453
- /** Current number of entries (including potentially expired ones). */
454
- get size() {
455
- return this.cache.size;
456
- }
457
- };
458
- /**
459
- * Extract the affinity key from the request context.
460
- * Uses the upstream request ID which is deterministic for the same user message.
461
- */
462
- function extractAffinityKey(context) {
463
- return context.requestId?.trim() || void 0;
464
- }
465
- /**
466
- * Build the full cache key by combining the affinity key with the model ID.
467
- * This prevents cross-model pollution (same session requesting different models
468
- * can be routed to different accounts).
469
- */
470
- function buildAffinityCacheKey(affinityKey, modelId) {
471
- return `${affinityKey}:${modelId}`;
472
- }
473
- /**
474
- * Check whether an account is a valid affinity candidate.
475
- * An account is valid if it is not failed and is present in the provided
476
- * runtime list.
477
- */
478
- function isAffinityAccountUsable(accountId, accounts) {
479
- const account = accounts.find((a) => a.id === accountId);
480
- if (!account) return void 0;
481
- if (account.failed) return void 0;
482
- return account;
483
- }
484
-
485
- //#endregion
486
- //#region src/lib/accounts-manager-auth.ts
487
- const takeAuthSnapshot = (account) => ({
488
- githubToken: account.githubToken,
489
- accountType: account.accountType
490
- });
491
- const isAuthSnapshotCurrent = (account, snapshot) => account.githubToken === snapshot.githubToken && account.accountType === snapshot.accountType;
492
- const isSameAuthSnapshot = (a, b) => {
493
- if (!a) return false;
494
- return a.githubToken === b.githubToken && a.accountType === b.accountType;
495
- };
496
- const toAccountContextFromSnapshot = (account, snapshot, copilotToken) => ({
497
- accountLogin: account.accountLogin,
498
- githubToken: snapshot.githubToken,
499
- copilotToken,
500
- ...account.copilotApiUrl !== void 0 ? { copilotApiUrl: account.copilotApiUrl } : {},
501
- accountType: snapshot.accountType,
502
- vsCodeVersion: account.vsCodeVersion,
503
- clientDeviceId: account.clientDeviceId,
504
- clientMachineId: account.clientMachineId,
505
- clientSessionId: account.clientSessionId
506
- });
507
- const applyCopilotTokenIfCurrent = (account, snapshot, copilotToken) => {
508
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
509
- account.copilotToken = copilotToken;
510
- return true;
511
- };
512
- const applyModelsIfCurrent = (account, snapshot, models) => {
513
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
514
- account.models = models;
515
- return true;
516
- };
517
- const applyTokenRefreshSuccessIfCurrent = (account, snapshot, token) => {
518
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
519
- account.copilotToken = token;
520
- account.failed = false;
521
- account.failureReason = void 0;
522
- return true;
523
- };
524
- const applyTokenRefreshFailureIfCurrent = (account, snapshot, error) => {
525
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
526
- account.failed = true;
527
- account.failureReason = String(error);
528
- return true;
529
- };
530
- const applyQuotaRefreshSuccessIfCurrent = (account, snapshot, result) => {
531
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
532
- const { premium, copilotApiUrl } = result;
533
- account.premiumEntitlement = premium.entitlement;
534
- account.premiumRemaining = premium.remaining;
535
- account.unlimited = premium.unlimited;
536
- account.overagePermitted = premium.overage_permitted;
537
- if (copilotApiUrl) account.copilotApiUrl = copilotApiUrl;
538
- account.lastQuotaFetch = Date.now();
539
- account.failed = false;
540
- account.failureReason = void 0;
541
- return true;
542
- };
543
- const setAccountFailedState = (account, reason) => {
544
- account.failed = true;
545
- account.failureReason = reason;
546
- consola.warn(`Account ${account.id} marked as failed: ${reason}`);
547
- };
548
- const applyUnauthorizedIfCurrent = (account, snapshot, reason) => {
549
- if (!isAuthSnapshotCurrent(account, snapshot)) return false;
550
- setAccountFailedState(account, reason);
551
- return true;
552
- };
553
-
554
- //#endregion
555
- //#region src/lib/accounts-manager-quota.ts
556
- const getCostUnits = (model) => {
557
- const billing = model.billing;
558
- if (!billing) return 0;
559
- if (billing.is_premium !== true) return 0;
560
- const multiplier = billing.multiplier;
561
- if (typeof multiplier !== "number" || !Number.isFinite(multiplier) || multiplier <= 0) return 1;
562
- return multiplier;
563
- };
564
- const getEffectivePremiumRemaining = (account) => {
565
- if (account.premiumRemaining === void 0) return;
566
- const reserved = account.premiumReserved ?? 0;
567
- return account.premiumRemaining - reserved;
568
- };
569
- const reservePremiumUnits = (account, units) => {
570
- if (units <= 0) return;
571
- const id = Symbol("quotaReservation");
572
- if (!account.premiumReservations) account.premiumReservations = /* @__PURE__ */ new Map();
573
- account.premiumReservations.set(id, units);
574
- account.premiumReserved = (account.premiumReserved ?? 0) + units;
575
- return { id };
576
- };
577
- const releasePremiumReservation = (account, reservation) => {
578
- if (!reservation) return;
579
- const reservations = account.premiumReservations;
580
- if (!reservations) return;
581
- const reservedUnits = reservations.get(reservation.id);
582
- if (reservedUnits === void 0) return;
583
- reservations.delete(reservation.id);
584
- const nextReserved = (account.premiumReserved ?? 0) - reservedUnits;
585
- account.premiumReserved = Math.max(0, nextReserved);
586
- if (reservations.size === 0) account.premiumReservations = void 0;
587
- };
588
-
589
- //#endregion
590
- //#region src/lib/accounts-manager.ts
591
- /** Quota cache TTL in milliseconds (45 seconds) for pre-request selection. */
592
- const QUOTA_CACHE_TTL = 45 * 1e3;
593
- /** Debounce delay for registry reload in milliseconds */
594
- const RELOAD_DEBOUNCE_MS = 500;
595
- /** Registry watcher restart initial delay in milliseconds */
596
- const WATCHER_RESTART_INITIAL_DELAY_MS = 1e3;
597
- /** Registry watcher restart max delay in milliseconds */
598
- const WATCHER_RESTART_MAX_DELAY_MS = 60 * 1e3;
599
- /** Session refresh base interval in milliseconds. */
600
- const SESSION_REFRESH_BASE_MS = 3600 * 1e3;
601
- /** Session refresh jitter window in milliseconds. */
602
- const SESSION_REFRESH_JITTER_MS = 1200 * 1e3;
603
- /** Minimum delay between account initializations in milliseconds. */
604
- const INIT_STAGGER_MIN_MS = 2e3;
605
- /** Maximum delay between account initializations in milliseconds. */
606
- const INIT_STAGGER_MAX_MS = 5e3;
607
- /** Random jitter window added to token refresh delay in milliseconds. */
608
- const TOKEN_REFRESH_JITTER_MS = 3e4;
609
- /** Manages multiple GitHub Copilot accounts at runtime. */
610
- var AccountsManager = class {
611
- accounts = /* @__PURE__ */ new Map();
612
- accountOrder = [];
613
- temporaryAccount;
614
- vsCodeVersion;
615
- accountAffinityEnabled = true;
616
- affinityCache = new AccountAffinityCache();
617
- loadBalanceCursor = 0;
618
- quotaRefreshSnapshotByAccount = /* @__PURE__ */ new WeakMap();
619
- modelsRefreshSnapshotByAccount = /* @__PURE__ */ new WeakMap();
620
- tokenRefreshEnabledAccounts = /* @__PURE__ */ new WeakSet();
621
- modelsRefreshTimer;
622
- modelsRefreshIntervalMs = 0;
623
- registryWatcher;
624
- reloadDebounceTimer;
625
- registryWatcherRestartTimer;
626
- registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
627
- isReloading = false;
628
- currentReloadPromise;
629
- /** Initialize accounts manager (load registry, migrate legacy token). */
630
- async initialize(vsCodeVersion) {
631
- this.vsCodeVersion = vsCodeVersion;
632
- const hasReg = await hasRegistry();
633
- const hasLegacy = await hasLegacyToken();
634
- if (!hasReg && hasLegacy) await this.migrateLegacyToken();
635
- const accountMetas = await listAccountsFromRegistry();
636
- for (const meta of accountMetas) {
637
- const token = await loadAccountToken(meta.id);
638
- if (!token) {
639
- consola.warn(`No token found for account ${meta.id}, skipping`);
640
- continue;
641
- }
642
- const runtime = {
643
- ...meta,
644
- accountLogin: meta.id,
645
- githubToken: token,
646
- vsCodeVersion: this.vsCodeVersion
647
- };
648
- this.accounts.set(meta.id, runtime);
649
- this.accountOrder.push(meta.id);
650
- }
651
- let isFirstAccount = true;
652
- for (const account of this.accounts.values()) {
653
- if (!isFirstAccount) {
654
- const staggerDelay = INIT_STAGGER_MIN_MS + Math.floor(Math.random() * (INIT_STAGGER_MAX_MS - INIT_STAGGER_MIN_MS));
655
- consola.debug(`Staggering initialization of account ${account.id} by ${staggerDelay}ms`);
656
- await new Promise((resolve) => setTimeout(resolve, staggerDelay));
657
- }
658
- isFirstAccount = false;
659
- try {
660
- await this.initializeAccount(account);
661
- } catch (error) {
662
- consola.error(`Failed to initialize account ${account.id}:`, error);
663
- account.failed = true;
664
- account.failureReason = String(error);
665
- }
666
- }
667
- consola.info(`Loaded ${this.accounts.size} account(s)`);
668
- this.startRegistryWatcher();
669
- }
670
- setAccountAffinityEnabled(enabled) {
671
- this.accountAffinityEnabled = enabled;
672
- if (!enabled) this.affinityCache.clear();
673
- }
674
- setModelsRefreshIntervalMs(intervalMs) {
675
- this.modelsRefreshIntervalMs = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 0;
676
- this.scheduleModelsRefresh();
677
- }
678
- computeTokenRefreshDelayMs(refreshInSeconds) {
679
- const baseDelay = Math.max((refreshInSeconds - 60) * 1e3, 1e3);
680
- const jitter = Math.floor(Math.random() * TOKEN_REFRESH_JITTER_MS);
681
- const maxSafeDelay = Math.max(refreshInSeconds * 1e3 - 1e3, 1e3);
682
- return Math.min(baseDelay + jitter, maxSafeDelay);
683
- }
684
- computeSessionRefreshDelayMs() {
685
- return SESSION_REFRESH_BASE_MS + Math.floor(Math.random() * SESSION_REFRESH_JITTER_MS);
686
- }
687
- resolveAccountLogin(account) {
688
- return account.accountLogin ?? account.id;
689
- }
690
- commitAccountIdentity(account, { identityKey, login, deviceId, machineId }) {
691
- account.accountLogin = login;
692
- account.identityKey = identityKey;
693
- account.clientDeviceId = deviceId;
694
- account.clientMachineId = machineId;
695
- }
696
- async applyAccountIdentity(account) {
697
- const login = this.resolveAccountLogin(account);
698
- const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment();
699
- const identityKey = buildIdentityKey({
700
- login,
701
- oauthApp,
702
- enterpriseDomain
703
- });
704
- const identity = await ensureAccountClientIdentity({
705
- login,
706
- oauthApp,
707
- enterpriseDomain
708
- });
709
- this.commitAccountIdentity(account, {
710
- identityKey,
711
- login,
712
- deviceId: identity.deviceId,
713
- machineId: identity.machineId
714
- });
715
- if (!account.clientSessionId) {
716
- account.clientSessionId = createAccountSessionId();
717
- consola.debug(`Generated VSCode session ID for account ${account.id}: ${account.clientSessionId}`);
718
- }
719
- this.startSessionRefresh(account);
720
- }
721
- shouldContinueTokenRefresh(account, snapshot) {
722
- return this.tokenRefreshEnabledAccounts.has(account) && isAuthSnapshotCurrent(account, snapshot);
723
- }
724
- async runTokenRefreshTick(account, snapshot, refreshInSeconds) {
725
- if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
726
- try {
727
- const { token, refresh_in } = await getCopilotToken(toAccountContextFromSnapshot(account, snapshot));
728
- if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
729
- if (!applyTokenRefreshSuccessIfCurrent(account, snapshot, token)) return;
730
- consola.debug(`Refreshed token for account ${account.id}`);
731
- if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
732
- this.startTokenRefresh(account, refresh_in);
733
- } catch (error) {
734
- consola.error(`Failed to refresh token for ${account.id}:`, error);
735
- if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
736
- applyTokenRefreshFailureIfCurrent(account, snapshot, error);
737
- if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
738
- this.startTokenRefresh(account, refreshInSeconds);
739
- }
740
- }
741
- finalizeQuotaRefreshPromise(account, promise) {
742
- if (account.quotaRefreshPromise !== promise) return;
743
- account.isRefreshingQuota = false;
744
- account.quotaRefreshPromise = void 0;
745
- this.quotaRefreshSnapshotByAccount.delete(account);
746
- }
747
- /** Initialize a single account. */
748
- async initializeAccount(account) {
749
- await this.applyAccountIdentity(account);
750
- const snapshot = takeAuthSnapshot(account);
751
- try {
752
- const { token, refresh_in } = await getCopilotToken(toAccountContextFromSnapshot(account, snapshot));
753
- if (!applyCopilotTokenIfCurrent(account, snapshot, token)) return;
754
- await this.refreshQuota(account);
755
- this.startTokenRefresh(account, refresh_in);
756
- if (!applyModelsIfCurrent(account, snapshot, await getModels(toAccountContextFromSnapshot(account, snapshot, token)))) return;
757
- account.lastModelsFetch = Date.now();
758
- consola.debug(`Account ${account.id} initialized`);
759
- } catch (error) {
760
- if (!isAuthSnapshotCurrent(account, snapshot)) return;
761
- throw error;
762
- }
763
- }
764
- /** Migrate legacy github_token to the new multi-account system. */
765
- async migrateLegacyToken() {
766
- const token = await readLegacyToken();
767
- if (!token) return;
768
- try {
769
- const id = (await getGitHubUser({
770
- githubToken: token,
771
- accountType: "individual"
772
- })).login;
773
- await saveAccountToken(id, token);
774
- await addAccountToRegistry({
775
- id,
776
- accountType: "individual",
777
- addedAt: Date.now()
778
- });
779
- consola.info(`Migrated legacy token to account: ${id}`);
780
- } catch (error) {
781
- consola.error("Failed to migrate legacy token:", error);
782
- }
783
- }
784
- /** Start token refresh timer for an account. */
785
- startTokenRefresh(account, refreshInSeconds) {
786
- this.stopTokenRefresh(account);
787
- this.tokenRefreshEnabledAccounts.add(account);
788
- const snapshot = takeAuthSnapshot(account);
789
- const delayMs = this.computeTokenRefreshDelayMs(refreshInSeconds);
790
- account.refreshTimer = setTimeout(() => {
791
- this.runTokenRefreshTick(account, snapshot, refreshInSeconds);
792
- }, delayMs);
793
- }
794
- /** Stop token refresh timer for an account. */
795
- stopTokenRefresh(account) {
796
- this.tokenRefreshEnabledAccounts.delete(account);
797
- if (account.refreshTimer) {
798
- clearTimeout(account.refreshTimer);
799
- account.refreshTimer = void 0;
800
- }
801
- }
802
- /** Stop all token refresh timers. */
803
- stopAllTokenRefresh() {
804
- for (const account of this.accounts.values()) this.stopTokenRefresh(account);
805
- if (this.temporaryAccount) this.stopTokenRefresh(this.temporaryAccount);
806
- }
807
- startSessionRefresh(account) {
808
- this.stopSessionRefresh(account);
809
- const delayMs = this.computeSessionRefreshDelayMs();
810
- consola.debug(`Scheduling next VSCode session ID refresh for ${account.id} in ${Math.round(delayMs / 1e3)} seconds`);
811
- account.sessionRefreshTimer = setTimeout(() => {
812
- try {
813
- account.clientSessionId = createAccountSessionId();
814
- consola.debug(`Refreshed VSCode session ID for account ${account.id}: ${account.clientSessionId}`);
815
- } catch (error) {
816
- consola.error(`Failed to refresh VSCode session ID for ${account.id}, rescheduling...`, error);
817
- } finally {
818
- this.startSessionRefresh(account);
819
- }
820
- }, delayMs);
821
- }
822
- stopSessionRefresh(account) {
823
- if (account.sessionRefreshTimer) {
824
- clearTimeout(account.sessionRefreshTimer);
825
- account.sessionRefreshTimer = void 0;
826
- }
827
- }
828
- stopAllSessionRefresh() {
829
- for (const account of this.accounts.values()) this.stopSessionRefresh(account);
830
- if (this.temporaryAccount) this.stopSessionRefresh(this.temporaryAccount);
831
- }
832
- scheduleModelsRefresh() {
833
- this.stopModelsRefresh();
834
- if (!this.modelsRefreshIntervalMs || this.modelsRefreshIntervalMs <= 0) return;
835
- this.modelsRefreshTimer = setTimeout(() => {
836
- this.runModelsRefreshTick();
837
- }, this.modelsRefreshIntervalMs);
838
- }
839
- stopModelsRefresh() {
840
- if (this.modelsRefreshTimer) {
841
- clearTimeout(this.modelsRefreshTimer);
842
- this.modelsRefreshTimer = void 0;
843
- }
844
- }
845
- async runModelsRefreshTick() {
846
- try {
847
- await this.refreshAllModels();
848
- } catch (error) {
849
- consola.error("Failed to refresh models:", error);
850
- } finally {
851
- this.scheduleModelsRefresh();
852
- }
853
- }
854
- finalizeModelsRefreshPromise(account, promise) {
855
- if (account.modelsRefreshPromise !== promise) return;
856
- account.isRefreshingModels = false;
857
- account.modelsRefreshPromise = void 0;
858
- this.modelsRefreshSnapshotByAccount.delete(account);
859
- }
860
- async refreshModels(account) {
861
- if (!account.copilotToken) {
862
- consola.debug(`Skip model refresh for ${account.id}: missing Copilot token`);
863
- return;
864
- }
865
- const snapshot = takeAuthSnapshot(account);
866
- if (account.modelsRefreshPromise) {
867
- if (isSameAuthSnapshot(this.modelsRefreshSnapshotByAccount.get(account), snapshot)) {
868
- await account.modelsRefreshPromise;
869
- return;
870
- }
871
- }
872
- account.isRefreshingModels = true;
873
- const ctx = toAccountContextFromSnapshot(account, snapshot, account.copilotToken);
874
- const promise = (async () => {
875
- try {
876
- if (applyModelsIfCurrent(account, snapshot, await getModels(ctx))) account.lastModelsFetch = Date.now();
877
- } catch (error) {
878
- if (error instanceof HTTPError && error.response.status === 401) {
879
- applyUnauthorizedIfCurrent(account, snapshot, "Unauthorized (401)");
880
- return;
881
- }
882
- consola.error(`Failed to refresh models for ${account.id}:`, error);
883
- }
884
- })();
885
- account.modelsRefreshPromise = promise;
886
- this.modelsRefreshSnapshotByAccount.set(account, snapshot);
887
- promise.finally(() => {
888
- this.finalizeModelsRefreshPromise(account, promise);
889
- });
890
- await promise;
891
- }
892
- async refreshAllModels() {
893
- const accounts = [];
894
- if (this.temporaryAccount) accounts.push(this.temporaryAccount);
895
- for (const id of this.accountOrder) {
896
- const account = this.accounts.get(id);
897
- if (account) accounts.push(account);
898
- }
899
- if (accounts.length === 0) return;
900
- await Promise.allSettled(accounts.map((account) => this.refreshModels(account)));
901
- }
902
- /** Refresh quota information for an account. */
903
- async refreshQuota(account) {
904
- const snapshot = takeAuthSnapshot(account);
905
- if (account.quotaRefreshPromise) {
906
- if (isSameAuthSnapshot(this.quotaRefreshSnapshotByAccount.get(account), snapshot)) {
907
- await account.quotaRefreshPromise;
908
- return;
909
- }
910
- }
911
- account.isRefreshingQuota = true;
912
- const ctx = toAccountContextFromSnapshot(account, snapshot);
913
- const promise = (async () => {
914
- try {
915
- const usage = await getCopilotUsage(ctx);
916
- const premium = usage.quota_snapshots.premium_interactions;
917
- applyQuotaRefreshSuccessIfCurrent(account, snapshot, {
918
- premium,
919
- copilotApiUrl: usage.endpoints.api
920
- });
921
- } catch (error) {
922
- if (error instanceof HTTPError && error.response.status === 401) {
923
- applyUnauthorizedIfCurrent(account, snapshot, "Unauthorized (401)");
924
- return;
925
- }
926
- consola.error(`Failed to refresh quota for ${account.id}:`, error);
927
- }
928
- })();
929
- account.quotaRefreshPromise = promise;
930
- this.quotaRefreshSnapshotByAccount.set(account, snapshot);
931
- promise.finally(() => {
932
- this.finalizeQuotaRefreshPromise(account, promise);
933
- });
934
- await promise;
935
- }
936
- /** Check if quota cache is expired. */
937
- isQuotaCacheExpired(account) {
938
- if (!account.lastQuotaFetch) return true;
939
- return Date.now() - account.lastQuotaFetch > QUOTA_CACHE_TTL;
940
- }
941
- isAccountFailed(account) {
942
- return account.failed === true;
943
- }
944
- useOverageFallback(fallback) {
945
- const reservation = reservePremiumUnits(fallback.account, fallback.costUnits);
946
- return {
947
- ok: true,
948
- account: fallback.account,
949
- selectedModel: fallback.model,
950
- endpoint: fallback.endpoint,
951
- costUnits: fallback.costUnits,
952
- reservation
953
- };
954
- }
955
- isModelSupportedForEndpoint(model, endpoint) {
956
- if (endpoint === "/responses") return model.supported_endpoints?.includes(endpoint) ?? false;
957
- const supported = model.supported_endpoints;
958
- if (!supported) return true;
959
- return supported.includes(endpoint);
960
- }
961
- pickSupportedCandidate(account, candidates) {
962
- const models = account.models?.data;
963
- if (!models) return null;
964
- for (const candidate of candidates) {
965
- const model = models.find((m) => m.id === candidate.modelId);
966
- if (!model) continue;
967
- if (!this.isModelSupportedForEndpoint(model, candidate.endpoint)) continue;
968
- return {
969
- candidate,
970
- model
971
- };
972
- }
973
- return null;
974
- }
975
- async selectAccountForCandidates(orderedAccounts, candidates) {
976
- if (orderedAccounts.length === 0) return {
977
- ok: false,
978
- reason: "NO_ACCOUNTS"
979
- };
980
- let supportedCandidateFound = false;
981
- let overageFallback;
982
- for (const account of orderedAccounts) {
983
- if (this.isAccountFailed(account)) continue;
984
- const supported = this.pickSupportedCandidate(account, candidates);
985
- if (!supported) continue;
986
- supportedCandidateFound = true;
987
- const { candidate, model } = supported;
988
- const costUnits = getCostUnits(model);
989
- if (costUnits <= 0) return {
990
- ok: true,
991
- account,
992
- selectedModel: model,
993
- endpoint: candidate.endpoint,
994
- costUnits
995
- };
996
- if (!account.unlimited && this.isQuotaCacheExpired(account)) await this.refreshQuota(account);
997
- if (this.isAccountFailed(account)) continue;
998
- if (account.unlimited) return {
999
- ok: true,
1000
- account,
1001
- selectedModel: model,
1002
- endpoint: candidate.endpoint,
1003
- costUnits
1004
- };
1005
- const effectiveRemaining = getEffectivePremiumRemaining(account);
1006
- if (effectiveRemaining !== void 0 && effectiveRemaining < costUnits) {
1007
- if (account.overagePermitted && !overageFallback) overageFallback = {
1008
- account,
1009
- model,
1010
- endpoint: candidate.endpoint,
1011
- costUnits
1012
- };
1013
- continue;
1014
- }
1015
- const reservation = reservePremiumUnits(account, costUnits);
1016
- return {
1017
- ok: true,
1018
- account,
1019
- selectedModel: model,
1020
- endpoint: candidate.endpoint,
1021
- costUnits,
1022
- reservation
1023
- };
1024
- }
1025
- if (!supportedCandidateFound) return {
1026
- ok: false,
1027
- reason: "MODEL_NOT_SUPPORTED"
1028
- };
1029
- return overageFallback ? this.useOverageFallback(overageFallback) : {
1030
- ok: false,
1031
- reason: "NO_QUOTA"
1032
- };
1033
- }
1034
- /**
1035
- * Try to use a preferred (affinity) account for the request.
1036
- * Returns a successful selection if the account is usable; null otherwise.
1037
- */
1038
- async tryAffinityAccount(preferredAccountId, orderedAccounts, candidates) {
1039
- const account = isAffinityAccountUsable(preferredAccountId, orderedAccounts);
1040
- if (!account) return null;
1041
- const supported = this.pickSupportedCandidate(account, candidates) ?? this.pickAliasFallbackCandidate(account, candidates);
1042
- if (!supported) return null;
1043
- return this.validateAffinityQuota(account, supported);
1044
- }
1045
- /**
1046
- * Resolve model aliases and try to pick a supported candidate.
1047
- * Returns null if no alias differs or the account doesn't support the alias.
1048
- */
1049
- pickAliasFallbackCandidate(account, candidates) {
1050
- const aliasCandidates = candidates.map((candidate) => {
1051
- const modelId = resolveModelAlias(candidate.modelId);
1052
- if (modelId === candidate.modelId) return candidate;
1053
- return {
1054
- ...candidate,
1055
- modelId
1056
- };
1057
- });
1058
- if (!aliasCandidates.some((candidate, index) => candidate.modelId !== candidates[index].modelId)) return null;
1059
- return this.pickSupportedCandidate(account, aliasCandidates);
1060
- }
1061
- /**
1062
- * Validate quota for an affinity candidate. Free models pass immediately;
1063
- * premium models go through quota refresh / reservation.
1064
- */
1065
- async validateAffinityQuota(account, supported) {
1066
- const { candidate, model } = supported;
1067
- const costUnits = getCostUnits(model);
1068
- if (costUnits <= 0) return {
1069
- ok: true,
1070
- account,
1071
- selectedModel: model,
1072
- endpoint: candidate.endpoint,
1073
- costUnits
1074
- };
1075
- if (!account.unlimited && this.isQuotaCacheExpired(account)) await this.refreshQuota(account);
1076
- if (this.isAccountFailed(account)) return null;
1077
- if (account.unlimited) return {
1078
- ok: true,
1079
- account,
1080
- selectedModel: model,
1081
- endpoint: candidate.endpoint,
1082
- costUnits
1083
- };
1084
- const effectiveRemaining = getEffectivePremiumRemaining(account);
1085
- if (effectiveRemaining !== void 0 && effectiveRemaining < costUnits) return null;
1086
- const reservation = reservePremiumUnits(account, costUnits);
1087
- return {
1088
- ok: true,
1089
- account,
1090
- selectedModel: model,
1091
- endpoint: candidate.endpoint,
1092
- costUnits,
1093
- reservation
1094
- };
1095
- }
1096
- /**
1097
- * Select an available account for a specific request (model + endpoint).
1098
- * When account affinity is enabled, routes to the previously successful account
1099
- * for the same affinity key + model combination.
1100
- * Uses reservation to avoid oversubscribing premium quota under concurrency.
1101
- */
1102
- async selectAccountForRequest(candidates, affinityContext) {
1103
- if (candidates.length === 0) throw new Error("selectAccountForRequest requires at least one candidate");
1104
- const orderedAccounts = [...this.temporaryAccount ? [this.temporaryAccount] : [], ...this.accountOrder.map((id) => this.accounts.get(id)).filter((account) => account !== void 0)];
1105
- const affinityKey = this.accountAffinityEnabled && affinityContext ? extractAffinityKey(affinityContext) : void 0;
1106
- const modelKey = candidates[0].modelId;
1107
- const cacheKey = affinityKey ? buildAffinityCacheKey(affinityKey, modelKey) : void 0;
1108
- if (cacheKey) {
1109
- const preferredId = this.affinityCache.get(cacheKey);
1110
- if (preferredId) {
1111
- const affinityResult = await this.tryAffinityAccount(preferredId, orderedAccounts, candidates);
1112
- if (affinityResult) {
1113
- affinityResult.affinityHit = true;
1114
- affinityResult.affinityCacheKey = cacheKey;
1115
- affinityResult.confirmAffinity = () => {
1116
- if (!this.accountAffinityEnabled) return;
1117
- this.affinityCache.set(cacheKey, affinityResult.account.id);
1118
- };
1119
- return affinityResult;
1120
- }
1121
- }
1122
- }
1123
- const accountsForSelection = this.accountAffinityEnabled && orderedAccounts.length > 1 ? this.rotateAccounts(orderedAccounts) : orderedAccounts;
1124
- const result = await this.selectWithAliasFallback(accountsForSelection, candidates);
1125
- if (result.ok) this.loadBalanceCursor++;
1126
- if (result.ok && cacheKey) {
1127
- const successResult = result;
1128
- successResult.confirmAffinity = () => {
1129
- if (!this.accountAffinityEnabled) return;
1130
- this.affinityCache.set(cacheKey, successResult.account.id);
1131
- };
1132
- }
1133
- return result;
1134
- }
1135
- /**
1136
- * Rotate the accounts array by the current load-balance cursor for round-robin distribution.
1137
- * This ensures cache-miss requests are spread across accounts instead of always hitting the first.
1138
- */
1139
- rotateAccounts(accounts) {
1140
- const start = this.loadBalanceCursor % accounts.length;
1141
- if (start === 0) return accounts;
1142
- return [...accounts.slice(start), ...accounts.slice(0, start)];
1143
- }
1144
- /**
1145
- * Normal account selection with alias fallback.
1146
- * Extracted to keep selectAccountForRequest readable after adding affinity logic.
1147
- */
1148
- async selectWithAliasFallback(orderedAccounts, candidates) {
1149
- const primary = await this.selectAccountForCandidates(orderedAccounts, candidates);
1150
- if (primary.ok || primary.reason !== "MODEL_NOT_SUPPORTED") return primary;
1151
- const aliasCandidates = candidates.map((candidate) => {
1152
- const modelId = resolveModelAlias(candidate.modelId);
1153
- if (modelId === candidate.modelId) return candidate;
1154
- return {
1155
- ...candidate,
1156
- modelId
1157
- };
1158
- });
1159
- if (!aliasCandidates.some((candidate, index) => candidate.modelId !== candidates[index].modelId)) return primary;
1160
- return this.selectAccountForCandidates(orderedAccounts, aliasCandidates);
1161
- }
1162
- /**
1163
- * Finalize quota after a request completes.
1164
- * This releases any in-flight reservation and refreshes the actual quota from the API.
1165
- */
1166
- async finalizeQuota(account, reservation) {
1167
- releasePremiumReservation(account, reservation);
1168
- try {
1169
- await this.refreshQuota(account);
1170
- } catch (error) {
1171
- consola.debug(`Failed to finalize quota for ${account.id}:`, error);
1172
- }
1173
- }
1174
- /**
1175
- * Mark an account as failed.
1176
- */
1177
- markAccountFailed(id, reason) {
1178
- const account = this.accounts.get(id);
1179
- if (account) {
1180
- setAccountFailedState(account, reason);
1181
- return;
1182
- }
1183
- if (this.temporaryAccount && this.temporaryAccount.id === id) setAccountFailedState(this.temporaryAccount, reason);
1184
- }
1185
- /**
1186
- * Get status of all accounts.
1187
- */
1188
- getAccountStatus() {
1189
- const statuses = [];
1190
- if (this.temporaryAccount) statuses.push({
1191
- id: "(temporary)",
1192
- entitlement: this.temporaryAccount.premiumEntitlement,
1193
- remaining: this.temporaryAccount.premiumRemaining,
1194
- unlimited: this.temporaryAccount.unlimited,
1195
- overagePermitted: this.temporaryAccount.overagePermitted,
1196
- failed: this.temporaryAccount.failed,
1197
- failureReason: this.temporaryAccount.failureReason
1198
- });
1199
- for (const id of this.accountOrder) {
1200
- const account = this.accounts.get(id);
1201
- if (account) statuses.push({
1202
- id: account.id,
1203
- entitlement: account.premiumEntitlement,
1204
- remaining: account.premiumRemaining,
1205
- unlimited: account.unlimited,
1206
- overagePermitted: account.overagePermitted,
1207
- failed: account.failed,
1208
- failureReason: account.failureReason
1209
- });
1210
- }
1211
- return statuses;
1212
- }
1213
- /**
1214
- * Set a temporary account from a GitHub token (--github-token).
1215
- * This account takes priority over registered accounts.
1216
- */
1217
- async setTemporaryAccount(githubToken, accountType) {
1218
- const user = await getGitHubUser({
1219
- githubToken,
1220
- accountType
1221
- });
1222
- if (this.temporaryAccount) {
1223
- this.stopTokenRefresh(this.temporaryAccount);
1224
- this.stopSessionRefresh(this.temporaryAccount);
1225
- }
1226
- const runtime = {
1227
- id: "(temporary)",
1228
- accountLogin: user.login,
1229
- accountType,
1230
- addedAt: Date.now(),
1231
- githubToken,
1232
- vsCodeVersion: this.vsCodeVersion
1233
- };
1234
- try {
1235
- await this.initializeAccount(runtime);
1236
- this.temporaryAccount = runtime;
1237
- consola.info("Temporary account initialized");
1238
- } catch (error) {
1239
- consola.error("Failed to initialize temporary account:", error);
1240
- throw error;
1241
- }
1242
- }
1243
- /**
1244
- * Check if any accounts are available.
1245
- */
1246
- hasAccounts() {
1247
- return this.accounts.size > 0 || this.temporaryAccount !== void 0;
1248
- }
1249
- /**
1250
- * Get the first available account's models.
1251
- * Used for caching models in legacy compatibility mode.
1252
- */
1253
- getFirstAccountModels() {
1254
- if (this.temporaryAccount?.models) return this.temporaryAccount.models;
1255
- for (const id of this.accountOrder) {
1256
- const account = this.accounts.get(id);
1257
- if (account?.models) return account.models;
1258
- }
1259
- }
1260
- /**
1261
- * Get account context by index.
1262
- * Index 0 is the temporary account (if exists), otherwise the first registered account.
1263
- * Returns null if index is out of bounds.
1264
- */
1265
- getAccountContextByIndex(index) {
1266
- const allAccounts = [];
1267
- if (this.temporaryAccount) allAccounts.push(this.temporaryAccount);
1268
- for (const id of this.accountOrder) {
1269
- const account = this.accounts.get(id);
1270
- if (account) allAccounts.push(account);
1271
- }
1272
- if (index < 0 || index >= allAccounts.length) return null;
1273
- return this.toAccountContext(allAccounts[index]);
1274
- }
1275
- /**
1276
- * Get the total number of accounts (including temporary).
1277
- */
1278
- getAccountCount() {
1279
- return (this.temporaryAccount ? 1 : 0) + this.accountOrder.length;
1280
- }
1281
- /**
1282
- * Convert AccountRuntime to AccountContext for service calls.
1283
- */
1284
- toAccountContext(account) {
1285
- return {
1286
- accountLogin: account.accountLogin,
1287
- githubToken: account.githubToken,
1288
- copilotToken: account.copilotToken,
1289
- ...account.copilotApiUrl !== void 0 ? { copilotApiUrl: account.copilotApiUrl } : {},
1290
- accountType: account.accountType,
1291
- vsCodeVersion: account.vsCodeVersion,
1292
- clientDeviceId: account.clientDeviceId,
1293
- clientMachineId: account.clientMachineId,
1294
- clientSessionId: account.clientSessionId
1295
- };
1296
- }
1297
- /**
1298
- * Start watching the registry file for changes.
1299
- * Enables hot reload of accounts when the file is modified.
1300
- */
1301
- startRegistryWatcher() {
1302
- this.stopRegistryWatcher();
1303
- try {
1304
- this.registryWatcher = fs.watch(PATHS.ACCOUNTS_REGISTRY_PATH, (eventType) => {
1305
- if (eventType === "change") this.scheduleReload();
1306
- });
1307
- this.registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
1308
- if (this.registryWatcherRestartTimer) {
1309
- clearTimeout(this.registryWatcherRestartTimer);
1310
- this.registryWatcherRestartTimer = void 0;
1311
- }
1312
- this.registryWatcher.on("error", (error) => {
1313
- consola.debug("Registry watcher error:", error);
1314
- const delayMs = this.registryWatcherRestartDelayMs;
1315
- this.registryWatcherRestartDelayMs = Math.min(this.registryWatcherRestartDelayMs * 2, WATCHER_RESTART_MAX_DELAY_MS);
1316
- this.stopRegistryWatcher();
1317
- this.registryWatcherRestartTimer = setTimeout(() => {
1318
- this.registryWatcherRestartTimer = void 0;
1319
- this.startRegistryWatcher();
1320
- }, delayMs);
1321
- consola.debug(`Restarting registry watcher in ${delayMs}ms`);
1322
- });
1323
- consola.debug("Started registry file watcher");
1324
- } catch (error) {
1325
- consola.warn("Failed to start registry watcher:", error);
1326
- }
1327
- }
1328
- /**
1329
- * Immediately reload the registry, bypassing the debounce delay.
1330
- * If a reload is already running, waits for it to complete and then
1331
- * runs another reload to ensure the latest changes are captured.
1332
- */
1333
- async reloadRegistryNow() {
1334
- if (this.reloadDebounceTimer) {
1335
- clearTimeout(this.reloadDebounceTimer);
1336
- this.reloadDebounceTimer = void 0;
1337
- }
1338
- if (this.currentReloadPromise) await this.currentReloadPromise;
1339
- await this.reloadRegistry();
1340
- }
1341
- /**
1342
- * Schedule a registry reload with debouncing.
1343
- */
1344
- scheduleReload() {
1345
- if (this.reloadDebounceTimer) clearTimeout(this.reloadDebounceTimer);
1346
- this.reloadDebounceTimer = setTimeout(() => {
1347
- this.reloadRegistry();
1348
- }, RELOAD_DEBOUNCE_MS);
1349
- }
1350
- /**
1351
- * Reload the registry and perform incremental updates.
1352
- * Adds new accounts, removes deleted ones, and reinitializes existing accounts
1353
- * when token/accountType changes.
1354
- */
1355
- async reloadRegistry() {
1356
- if (this.isReloading) return;
1357
- this.isReloading = true;
1358
- this.currentReloadPromise = (async () => {
1359
- try {
1360
- const newMetas = await listAccountsFromRegistry();
1361
- const newIds = new Set(newMetas.map((m) => m.id));
1362
- const currentIds = new Set(this.accountOrder);
1363
- const added = [];
1364
- const removed = [];
1365
- const updated = [];
1366
- this.removeDeletedAccounts(currentIds, newIds, removed);
1367
- for (const meta of newMetas) if (!currentIds.has(meta.id)) await this.addNewAccount(meta, added);
1368
- await this.reinitializeUpdatedAccounts(newMetas, currentIds, updated);
1369
- this.accountOrder = newMetas.map((m) => m.id).filter((id) => this.accounts.has(id));
1370
- this.loadBalanceCursor = 0;
1371
- this.logRegistryReloadChanges(added, removed, updated);
1372
- } catch (error) {
1373
- consola.error("Failed to reload registry:", error);
1374
- this.shutdown();
1375
- process.exit(1);
1376
- } finally {
1377
- this.isReloading = false;
1378
- this.currentReloadPromise = void 0;
1379
- }
1380
- })();
1381
- await this.currentReloadPromise;
1382
- }
1383
- removeDeletedAccounts(currentIds, newIds, removed) {
1384
- for (const id of currentIds) if (!newIds.has(id)) {
1385
- const account = this.accounts.get(id);
1386
- if (!account) continue;
1387
- this.stopTokenRefresh(account);
1388
- this.stopSessionRefresh(account);
1389
- this.accounts.delete(id);
1390
- removed.push(id);
1391
- }
1392
- }
1393
- async reinitializeUpdatedAccounts(newMetas, currentIds, updated) {
1394
- for (const meta of newMetas) {
1395
- if (!currentIds.has(meta.id)) continue;
1396
- const account = this.accounts.get(meta.id);
1397
- if (!account) continue;
1398
- const token = await loadAccountToken(meta.id);
1399
- if (!token) {
1400
- consola.warn(`No token found for account ${meta.id}, skipping update`);
1401
- continue;
1402
- }
1403
- const accountTypeChanged = account.accountType !== meta.accountType;
1404
- const tokenChanged = account.githubToken !== token;
1405
- const addedAtChanged = account.addedAt !== meta.addedAt;
1406
- if (accountTypeChanged) account.accountType = meta.accountType;
1407
- if (addedAtChanged) account.addedAt = meta.addedAt;
1408
- account.accountLogin = meta.id;
1409
- if (tokenChanged) account.githubToken = token;
1410
- if (!accountTypeChanged && !tokenChanged) continue;
1411
- try {
1412
- await this.initializeAccount(account);
1413
- updated.push(meta.id);
1414
- } catch (error) {
1415
- consola.error(`Failed to reinitialize account ${meta.id} after update:`, error);
1416
- account.failed = true;
1417
- account.failureReason = String(error);
1418
- updated.push(`${meta.id} (failed)`);
1419
- }
1420
- }
1421
- }
1422
- logRegistryReloadChanges(added, removed, updated) {
1423
- if (added.length === 0 && removed.length === 0 && updated.length === 0) return;
1424
- const changes = [];
1425
- if (added.length > 0) changes.push(`added: ${added.join(", ")}`);
1426
- if (removed.length > 0) changes.push(`removed: ${removed.join(", ")}`);
1427
- if (updated.length > 0) changes.push(`updated: ${updated.join(", ")}`);
1428
- consola.info(`Registry reloaded (${changes.join("; ")}). Total: ${this.accounts.size} account(s)`);
1429
- }
1430
- /**
1431
- * Helper to add a new account during reload.
1432
- */
1433
- async addNewAccount(meta, added) {
1434
- const token = await loadAccountToken(meta.id);
1435
- if (!token) {
1436
- consola.warn(`No token found for new account ${meta.id}, skipping`);
1437
- return;
1438
- }
1439
- const runtime = {
1440
- ...meta,
1441
- accountLogin: meta.id,
1442
- githubToken: token,
1443
- vsCodeVersion: this.vsCodeVersion
1444
- };
1445
- try {
1446
- await this.initializeAccount(runtime);
1447
- this.accounts.set(meta.id, runtime);
1448
- added.push(meta.id);
1449
- } catch (error) {
1450
- consola.error(`Failed to initialize new account ${meta.id}:`, error);
1451
- runtime.failed = true;
1452
- runtime.failureReason = String(error);
1453
- this.accounts.set(meta.id, runtime);
1454
- added.push(`${meta.id} (failed)`);
1455
- }
1456
- }
1457
- /**
1458
- * Stop the registry file watcher.
1459
- */
1460
- stopRegistryWatcher() {
1461
- if (this.reloadDebounceTimer) {
1462
- clearTimeout(this.reloadDebounceTimer);
1463
- this.reloadDebounceTimer = void 0;
1464
- }
1465
- if (this.registryWatcherRestartTimer) {
1466
- clearTimeout(this.registryWatcherRestartTimer);
1467
- this.registryWatcherRestartTimer = void 0;
1468
- }
1469
- if (this.registryWatcher) {
1470
- this.registryWatcher.close();
1471
- this.registryWatcher = void 0;
1472
- }
1473
- }
1474
- /**
1475
- * Shutdown the manager and clean up resources.
1476
- */
1477
- shutdown() {
1478
- this.stopRegistryWatcher();
1479
- this.stopAllTokenRefresh();
1480
- this.stopAllSessionRefresh();
1481
- this.stopModelsRefresh();
1482
- this.affinityCache.clear();
1483
- this.loadBalanceCursor = 0;
1484
- this.accounts.clear();
1485
- this.accountOrder = [];
1486
- this.temporaryAccount = void 0;
1487
- }
1488
- };
1489
- /** Singleton instance of AccountsManager */
1490
- const accountsManager = new AccountsManager();
1491
-
1492
- //#endregion
1493
- export { isMessagesApiEnabled as _, getClaudeTokenMultiplier as a, mergeConfigWithDefaults as b, getModelAliases as c, getProviderConfig as d, getReasoningEffortForModel as f, isMessageStartInputTokensFallbackEnabled as g, isForceAgentEnabled as h, getAnthropicApiKey as i, getModelAliasesInfo as l, isAccountAffinityEnabled as m, PROVIDER_TYPE_ANTHROPIC as n, getConfig as o, getSmallModel as p, getAliasTargetSet as r, getExtraPromptForModel as s, accountsManager as t, getModelRefreshIntervalMs as u, isResponsesApiContextManagementModel as v, shouldCompactUseSmallModel as x, isResponsesApiWebSearchEnabled as y };
1494
- //# sourceMappingURL=accounts-manager-BE-Dq5Wn.js.map