@nick3/copilot-api 1.5.6 → 1.5.9

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 (31) hide show
  1. package/README.md +8 -1
  2. package/dist/{account-AacnHem5.js → account-CbYMFuS4.js} +12 -4
  3. package/dist/account-CbYMFuS4.js.map +1 -0
  4. package/dist/accounts-manager-BKG9aZEL.js +2899 -0
  5. package/dist/accounts-manager-BKG9aZEL.js.map +1 -0
  6. package/dist/admin/assets/index-BFN8rXmt.css +1 -0
  7. package/dist/admin/assets/index-HnEqzcKv.js +101 -0
  8. package/dist/admin/index.html +2 -2
  9. package/dist/{auth-B7x3wjry.js → auth-Ckj1wD43.js} +3 -3
  10. package/dist/{auth-B7x3wjry.js.map → auth-Ckj1wD43.js.map} +1 -1
  11. package/dist/{check-usage-B1cbDEOI.js → check-usage-bIbj_1Q_.js} +3 -3
  12. package/dist/check-usage-bIbj_1Q_.js.map +1 -0
  13. package/dist/{get-copilot-token-cha9rQwA.js → get-copilot-token-MAZsr5Vu.js} +2 -2
  14. package/dist/{get-copilot-token-cha9rQwA.js.map → get-copilot-token-MAZsr5Vu.js.map} +1 -1
  15. package/dist/main.js +3 -3
  16. package/dist/{poll-access-token-DFooFWhY.js → poll-access-token-DiwBJNtK.js} +58 -33
  17. package/dist/poll-access-token-DiwBJNtK.js.map +1 -0
  18. package/dist/{server-DVpkQrk2.js → server-DAxpfPde.js} +866 -894
  19. package/dist/server-DAxpfPde.js.map +1 -0
  20. package/dist/{start-fPbCDj4c.js → start-8dkfsQqd.js} +7 -6
  21. package/dist/start-8dkfsQqd.js.map +1 -0
  22. package/package.json +1 -1
  23. package/dist/account-AacnHem5.js.map +0 -1
  24. package/dist/accounts-manager-BE-Dq5Wn.js +0 -1494
  25. package/dist/accounts-manager-BE-Dq5Wn.js.map +0 -1
  26. package/dist/admin/assets/index-CdoHTemy.css +0 -1
  27. package/dist/admin/assets/index-wcoGQpIM.js +0 -66
  28. package/dist/check-usage-B1cbDEOI.js.map +0 -1
  29. package/dist/poll-access-token-DFooFWhY.js.map +0 -1
  30. package/dist/server-DVpkQrk2.js.map +0 -1
  31. package/dist/start-fPbCDj4c.js.map +0 -1
@@ -0,0 +1,2899 @@
1
+ import { O as accountFromState, _ as HTTPError, g as getCopilotUsage, m as getGitHubUser, x as copilotModelsHeaders, y as copilotBaseUrl } from "./poll-access-token-DiwBJNtK.js";
2
+ import { b as getCurrentIdentityEnvironment, c as isAccountEnabled, f as readLegacyToken, h as saveAccountToken, i as ensureAccountClientIdentity, l as listAccountsFromRegistry, o as hasLegacyToken, r as addAccountToRegistry, s as hasRegistry, u as loadAccountToken, v as buildIdentityKey, y as createAccountSessionId } from "./account-CbYMFuS4.js";
3
+ import { t as PATHS } from "./paths-DGlr310R.js";
4
+ import { t as getCopilotToken } from "./get-copilot-token-MAZsr5Vu.js";
5
+ import consola, { consola as consola$1 } from "consola";
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import fs$1 from "node:fs";
9
+ import { Database } from "bun:sqlite";
10
+
11
+ //#region src/lib/config.ts
12
+ const PROVIDER_TYPE_ANTHROPIC = "anthropic";
13
+ const gpt5ExplorationPrompt = `## Exploration and reading files
14
+ - **Think first.** Before any tool call, decide ALL files/resources you will need.
15
+ - **Batch everything.** If you need multiple files (even from different places), read them together.
16
+ - **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.
17
+ - **Only make sequential calls if you truly cannot know the next file without seeing a result first.**
18
+ - **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`;
19
+ const gpt5CommentaryPrompt = `# Working with the user
20
+
21
+ You interact with the user through a terminal. You have 2 ways of communicating with the users:
22
+ - Share intermediary updates in \`commentary\` channel.
23
+ - After you have completed all your work, send a message to the \`final\` channel.
24
+
25
+ ## Intermediary updates
26
+
27
+ - Intermediary updates go to the \`commentary\` channel.
28
+ - User updates are short updates while you are working, they are NOT final answers.
29
+ - You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.
30
+ - Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.
31
+ - You provide user updates frequently, every 20s.
32
+ - 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.
33
+ - 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.
34
+ - 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).
35
+ - Before performing file edits of any kind, you provide updates explaining what edits you are making.
36
+ - 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.
37
+ - Tone of your updates MUST match your personality.`;
38
+ const defaultConfig = {
39
+ auth: { apiKeys: [] },
40
+ providers: {},
41
+ extraPrompts: {
42
+ "gpt-5-mini": gpt5ExplorationPrompt,
43
+ "gpt-5.3-codex": gpt5CommentaryPrompt,
44
+ "gpt-5.4-mini": gpt5CommentaryPrompt,
45
+ "gpt-5.4": gpt5CommentaryPrompt
46
+ },
47
+ smallModel: "gpt-5-mini",
48
+ accountAffinity: true,
49
+ responsesApiContextManagementModels: [],
50
+ modelReasoningEfforts: {
51
+ "gpt-5-mini": "low",
52
+ "gpt-5.3-codex": "xhigh",
53
+ "gpt-5.4-mini": "xhigh",
54
+ "gpt-5.4": "xhigh"
55
+ },
56
+ allowOriginalModelNamesForAliases: false,
57
+ useFunctionApplyPatch: true,
58
+ forceAgent: false,
59
+ compactUseSmallModel: true,
60
+ messageStartInputTokensFallback: false,
61
+ modelRefreshIntervalHours: 24,
62
+ sessionAffinityRetentionDays: 7,
63
+ useMessagesApi: true,
64
+ useResponsesApiWebSearch: true,
65
+ logLevel: "info"
66
+ };
67
+ let cachedConfig = null;
68
+ function isPlainObject(value) {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
71
+ function normalizeAuthApiKeys(value) {
72
+ if (!Array.isArray(value)) return [];
73
+ return [...new Set(value.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0))];
74
+ }
75
+ function normalizeNonNegativeNumber(value) {
76
+ if (typeof value !== "number") return void 0;
77
+ if (!Number.isFinite(value)) return void 0;
78
+ if (value < 0) return void 0;
79
+ return value;
80
+ }
81
+ const LOG_LEVELS = new Set([
82
+ "error",
83
+ "warn",
84
+ "info",
85
+ "debug"
86
+ ]);
87
+ function normalizeLogLevel(value) {
88
+ if (typeof value !== "string") return void 0;
89
+ return LOG_LEVELS.has(value) ? value : void 0;
90
+ }
91
+ function ensureConfigFile() {
92
+ try {
93
+ fs$1.accessSync(PATHS.CONFIG_PATH, fs$1.constants.R_OK);
94
+ return;
95
+ } catch {}
96
+ try {
97
+ fs$1.mkdirSync(PATHS.APP_DIR, { recursive: true });
98
+ fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
99
+ try {
100
+ fs$1.chmodSync(PATHS.CONFIG_PATH, 384);
101
+ } catch {}
102
+ } catch {}
103
+ }
104
+ function readConfigFromDisk() {
105
+ ensureConfigFile();
106
+ try {
107
+ const raw = fs$1.readFileSync(PATHS.CONFIG_PATH, "utf8");
108
+ if (!raw.trim()) {
109
+ fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
110
+ return defaultConfig;
111
+ }
112
+ return JSON.parse(raw);
113
+ } catch (error) {
114
+ consola.error("Failed to read config file, using default config", error);
115
+ return defaultConfig;
116
+ }
117
+ }
118
+ function mergeDefaultConfig(config) {
119
+ const extraPrompts = config.extraPrompts ?? {};
120
+ const defaultExtraPrompts = defaultConfig.extraPrompts ?? {};
121
+ const modelReasoningEfforts = config.modelReasoningEfforts ?? {};
122
+ const defaultModelReasoningEfforts = defaultConfig.modelReasoningEfforts ?? {};
123
+ const hasForceAgent = typeof config.forceAgent === "boolean";
124
+ const defaultForceAgent = defaultConfig.forceAgent ?? false;
125
+ const missingExtraPromptModels = Object.keys(defaultExtraPrompts).filter((model) => !Object.hasOwn(extraPrompts, model));
126
+ const missingReasoningEffortModels = Object.keys(defaultModelReasoningEfforts).filter((model) => !Object.hasOwn(modelReasoningEfforts, model));
127
+ const hasExtraPromptChanges = missingExtraPromptModels.length > 0;
128
+ const hasReasoningEffortChanges = missingReasoningEffortModels.length > 0;
129
+ if (!hasExtraPromptChanges && !hasReasoningEffortChanges && !!hasForceAgent) return {
130
+ mergedConfig: config,
131
+ changed: false
132
+ };
133
+ return {
134
+ mergedConfig: {
135
+ ...config,
136
+ extraPrompts: {
137
+ ...defaultExtraPrompts,
138
+ ...extraPrompts
139
+ },
140
+ modelReasoningEfforts: {
141
+ ...defaultModelReasoningEfforts,
142
+ ...modelReasoningEfforts
143
+ },
144
+ forceAgent: hasForceAgent ? config.forceAgent : defaultForceAgent
145
+ },
146
+ changed: true
147
+ };
148
+ }
149
+ function mergeDefaultAuth(config) {
150
+ const authConfig = isPlainObject(config.auth) ? config.auth : void 0;
151
+ const nextAuth = { apiKeys: normalizeAuthApiKeys(Array.isArray(authConfig?.apiKeys) ? authConfig.apiKeys : void 0) };
152
+ if (authConfig && JSON.stringify(authConfig) === JSON.stringify(nextAuth)) return {
153
+ mergedConfig: config,
154
+ changed: false
155
+ };
156
+ return {
157
+ mergedConfig: {
158
+ ...config,
159
+ auth: nextAuth
160
+ },
161
+ changed: true
162
+ };
163
+ }
164
+ function mergeDefaultAccountAffinity(config) {
165
+ const raw = config;
166
+ const hasOld = typeof raw.freeModelLoadBalancing === "boolean";
167
+ const hasNew = typeof config.accountAffinity === "boolean";
168
+ if (hasOld) {
169
+ const next = { ...config };
170
+ if (!hasNew) next.accountAffinity = raw.freeModelLoadBalancing;
171
+ delete next.freeModelLoadBalancing;
172
+ return {
173
+ mergedConfig: next,
174
+ changed: true
175
+ };
176
+ }
177
+ if (hasNew) return {
178
+ mergedConfig: config,
179
+ changed: false
180
+ };
181
+ return {
182
+ mergedConfig: {
183
+ ...config,
184
+ accountAffinity: defaultConfig.accountAffinity ?? true
185
+ },
186
+ changed: true
187
+ };
188
+ }
189
+ function mergeDefaultModelRefreshInterval(config) {
190
+ if (normalizeNonNegativeNumber(config.modelRefreshIntervalHours) !== void 0) return {
191
+ mergedConfig: config,
192
+ changed: false
193
+ };
194
+ return {
195
+ mergedConfig: {
196
+ ...config,
197
+ modelRefreshIntervalHours: defaultConfig.modelRefreshIntervalHours ?? 24
198
+ },
199
+ changed: true
200
+ };
201
+ }
202
+ function mergeDefaultSessionAffinityRetention(config) {
203
+ if (normalizeNonNegativeNumber(config.sessionAffinityRetentionDays) !== void 0) return {
204
+ mergedConfig: config,
205
+ changed: false
206
+ };
207
+ return {
208
+ mergedConfig: {
209
+ ...config,
210
+ sessionAffinityRetentionDays: defaultConfig.sessionAffinityRetentionDays ?? 7
211
+ },
212
+ changed: true
213
+ };
214
+ }
215
+ function mergeDefaultLogLevel(config) {
216
+ if (normalizeLogLevel(config.logLevel) !== void 0) return {
217
+ mergedConfig: config,
218
+ changed: false
219
+ };
220
+ return {
221
+ mergedConfig: {
222
+ ...config,
223
+ logLevel: defaultConfig.logLevel ?? "info"
224
+ },
225
+ changed: true
226
+ };
227
+ }
228
+ function applyConfigMerges(config, mergeFns) {
229
+ return mergeFns.reduce((acc, mergeFn) => {
230
+ const result = mergeFn(acc.mergedConfig);
231
+ return {
232
+ mergedConfig: result.mergedConfig,
233
+ changed: acc.changed || result.changed
234
+ };
235
+ }, {
236
+ mergedConfig: config,
237
+ changed: false
238
+ });
239
+ }
240
+ function mergeConfigWithDefaults() {
241
+ const { mergedConfig, changed } = applyConfigMerges(readConfigFromDisk(), [
242
+ mergeDefaultAuth,
243
+ mergeDefaultConfig,
244
+ mergeDefaultAccountAffinity,
245
+ mergeDefaultModelRefreshInterval,
246
+ mergeDefaultSessionAffinityRetention,
247
+ mergeDefaultLogLevel
248
+ ]);
249
+ if (changed) try {
250
+ fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
251
+ } catch (writeError) {
252
+ consola.warn("Failed to write merged config defaults", writeError);
253
+ }
254
+ cachedConfig = mergedConfig;
255
+ return mergedConfig;
256
+ }
257
+ function getConfig() {
258
+ cachedConfig ??= readConfigFromDisk();
259
+ return cachedConfig;
260
+ }
261
+ function normalizeAliasKey(value) {
262
+ const trimmed = value.trim().toLowerCase();
263
+ return trimmed.length > 0 ? trimmed : null;
264
+ }
265
+ function normalizeAliasTarget(value) {
266
+ const trimmed = value.trim();
267
+ return trimmed.length > 0 ? trimmed : null;
268
+ }
269
+ function normalizeAliasSpec(value) {
270
+ if (typeof value === "string") {
271
+ const normalizedTarget$1 = normalizeAliasTarget(value);
272
+ return normalizedTarget$1 ? { target: normalizedTarget$1 } : null;
273
+ }
274
+ if (!value || typeof value !== "object") return null;
275
+ const targetValue = value.target;
276
+ if (typeof targetValue !== "string") return null;
277
+ const normalizedTarget = normalizeAliasTarget(targetValue);
278
+ if (!normalizedTarget) return null;
279
+ const allowOriginalValue = value.allowOriginal;
280
+ return {
281
+ target: normalizedTarget,
282
+ allowOriginal: typeof allowOriginalValue === "boolean" ? allowOriginalValue : void 0
283
+ };
284
+ }
285
+ function getModelAliasesInfo() {
286
+ const raw = getConfig().modelAliases ?? {};
287
+ const normalized = {};
288
+ for (const [alias, rawSpec] of Object.entries(raw)) {
289
+ const normalizedAlias = normalizeAliasKey(alias);
290
+ const normalizedSpec = normalizeAliasSpec(rawSpec);
291
+ if (!normalizedAlias || !normalizedSpec) continue;
292
+ if (!Object.hasOwn(normalized, normalizedAlias)) normalized[normalizedAlias] = normalizedSpec;
293
+ }
294
+ return normalized;
295
+ }
296
+ function getModelAliases() {
297
+ const info = getModelAliasesInfo();
298
+ const normalized = {};
299
+ for (const [alias, spec] of Object.entries(info)) normalized[alias] = spec.target;
300
+ return normalized;
301
+ }
302
+ function resolveModelAlias(modelId) {
303
+ const normalized = normalizeAliasKey(modelId);
304
+ if (!normalized) return modelId;
305
+ return getModelAliases()[normalized] ?? modelId;
306
+ }
307
+ function isOriginalModelNameAllowedForAliases() {
308
+ return getConfig().allowOriginalModelNamesForAliases ?? false;
309
+ }
310
+ function getAliasTargetSet() {
311
+ const aliases = getModelAliasesInfo();
312
+ const allowOriginalDefault = isOriginalModelNameAllowedForAliases();
313
+ const targetAllowMap = /* @__PURE__ */ new Map();
314
+ for (const { target, allowOriginal } of Object.values(aliases)) {
315
+ const normalizedTarget = target.toLowerCase();
316
+ const effectiveAllow = allowOriginal ?? allowOriginalDefault;
317
+ const currentAllow = targetAllowMap.get(normalizedTarget);
318
+ if (currentAllow === true) continue;
319
+ if (effectiveAllow) targetAllowMap.set(normalizedTarget, true);
320
+ else if (currentAllow === void 0) targetAllowMap.set(normalizedTarget, false);
321
+ }
322
+ const blockedTargets = /* @__PURE__ */ new Set();
323
+ for (const [target, allowed] of targetAllowMap.entries()) if (!allowed) blockedTargets.add(target);
324
+ return blockedTargets;
325
+ }
326
+ function isOriginalModelNameAllowedForTarget(modelId) {
327
+ const normalized = normalizeAliasKey(modelId);
328
+ if (!normalized) return true;
329
+ return !getAliasTargetSet().has(normalized);
330
+ }
331
+ function getPreferredAliasForTarget(modelId) {
332
+ return getAliasKeysForTarget(modelId, getModelAliases())[0] ?? null;
333
+ }
334
+ function getAliasKeysForTarget(target, aliases) {
335
+ const normalizedTarget = target.toLowerCase();
336
+ return Object.entries(aliases).filter(([, model]) => model.toLowerCase() === normalizedTarget).map(([alias]) => alias).sort();
337
+ }
338
+ function getAliasFallbackValue(record, modelId, aliases) {
339
+ if (!record) return void 0;
340
+ const aliasKeys = getAliasKeysForTarget(modelId, aliases);
341
+ if (aliasKeys.length === 0) return void 0;
342
+ const recordByAlias = /* @__PURE__ */ new Map();
343
+ for (const [key, value] of Object.entries(record)) {
344
+ const normalized = normalizeAliasKey(key);
345
+ if (normalized) recordByAlias.set(normalized, value);
346
+ }
347
+ for (const alias of aliasKeys) {
348
+ const value = recordByAlias.get(alias);
349
+ if (value !== void 0) return value;
350
+ }
351
+ }
352
+ function getExtraPromptForModel(model) {
353
+ const config = getConfig();
354
+ const direct = config.extraPrompts?.[model];
355
+ if (direct !== void 0) return direct;
356
+ const aliases = getModelAliases();
357
+ return getAliasFallbackValue(config.extraPrompts, model, aliases) ?? "";
358
+ }
359
+ function getSmallModel() {
360
+ const model = getConfig().smallModel ?? "gpt-5-mini";
361
+ if (isOriginalModelNameAllowedForTarget(model)) return model;
362
+ return getPreferredAliasForTarget(model) ?? model;
363
+ }
364
+ function getLogLevel() {
365
+ return normalizeLogLevel(getConfig().logLevel) ?? defaultConfig.logLevel ?? "info";
366
+ }
367
+ function isAccountAffinityEnabled() {
368
+ return getConfig().accountAffinity ?? true;
369
+ }
370
+ function getModelRefreshIntervalHours() {
371
+ return normalizeNonNegativeNumber(getConfig().modelRefreshIntervalHours) ?? defaultConfig.modelRefreshIntervalHours ?? 24;
372
+ }
373
+ function getModelRefreshIntervalMs() {
374
+ const hours = getModelRefreshIntervalHours();
375
+ if (!Number.isFinite(hours) || hours <= 0) return 0;
376
+ return hours * 60 * 60 * 1e3;
377
+ }
378
+ function getSessionAffinityRetentionDays() {
379
+ return normalizeNonNegativeNumber(getConfig().sessionAffinityRetentionDays) ?? defaultConfig.sessionAffinityRetentionDays ?? 7;
380
+ }
381
+ function getSessionAffinityRetentionMs() {
382
+ const days = getSessionAffinityRetentionDays();
383
+ if (!Number.isFinite(days) || days <= 0) return 0;
384
+ return days * 24 * 60 * 60 * 1e3;
385
+ }
386
+ function isMessageStartInputTokensFallbackEnabled() {
387
+ return getConfig().messageStartInputTokensFallback ?? false;
388
+ }
389
+ function shouldCompactUseSmallModel() {
390
+ return getConfig().compactUseSmallModel ?? true;
391
+ }
392
+ function getResponsesApiContextManagementModels() {
393
+ return getConfig().responsesApiContextManagementModels ?? defaultConfig.responsesApiContextManagementModels ?? [];
394
+ }
395
+ function isResponsesApiContextManagementModel(model) {
396
+ return getResponsesApiContextManagementModels().includes(model);
397
+ }
398
+ function getReasoningEffortForModel(model) {
399
+ const config = getConfig();
400
+ const direct = config.modelReasoningEfforts?.[model];
401
+ if (direct !== void 0) return direct;
402
+ const aliases = getModelAliases();
403
+ return getAliasFallbackValue(config.modelReasoningEfforts, model, aliases) ?? "high";
404
+ }
405
+ function isForceAgentEnabled() {
406
+ return getConfig().forceAgent ?? false;
407
+ }
408
+ function normalizeProviderBaseUrl(url) {
409
+ return url.trim().replace(/\/+$/u, "");
410
+ }
411
+ function resolveProviderAuthType(providerName, authType) {
412
+ if (authType === void 0 || authType === "x-api-key") return "x-api-key";
413
+ if (authType === "authorization") return authType;
414
+ consola.warn(`Provider ${providerName} has invalid authType '${authType}', ignoring provider`);
415
+ return null;
416
+ }
417
+ function getProviderConfig(name) {
418
+ const providerName = name.trim();
419
+ if (!providerName) return null;
420
+ const provider = getConfig().providers?.[providerName];
421
+ if (!provider) return null;
422
+ if (provider.enabled === false) return null;
423
+ if ((provider.type ?? PROVIDER_TYPE_ANTHROPIC) !== PROVIDER_TYPE_ANTHROPIC) {
424
+ consola.warn(`Provider ${providerName} is ignored because only anthropic type is supported`);
425
+ return null;
426
+ }
427
+ const baseUrl = normalizeProviderBaseUrl(provider.baseUrl ?? "");
428
+ const apiKey = (provider.apiKey ?? "").trim();
429
+ const authType = resolveProviderAuthType(providerName, provider.authType);
430
+ if (!authType) return null;
431
+ if (!baseUrl || !apiKey) {
432
+ consola.warn(`Provider ${providerName} is enabled but missing baseUrl or apiKey`);
433
+ return null;
434
+ }
435
+ return {
436
+ name: providerName,
437
+ type: PROVIDER_TYPE_ANTHROPIC,
438
+ baseUrl,
439
+ apiKey,
440
+ authType,
441
+ models: provider.models,
442
+ adjustInputTokens: provider.adjustInputTokens
443
+ };
444
+ }
445
+ function isMessagesApiEnabled() {
446
+ return getConfig().useMessagesApi ?? true;
447
+ }
448
+ function getAnthropicApiKey() {
449
+ return getConfig().anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? void 0;
450
+ }
451
+ function isResponsesApiWebSearchEnabled() {
452
+ return getConfig().useResponsesApiWebSearch ?? true;
453
+ }
454
+ function getClaudeTokenMultiplier() {
455
+ return getConfig().claudeTokenMultiplier ?? 1.15;
456
+ }
457
+
458
+ //#endregion
459
+ //#region src/lib/account-affinity.ts
460
+ const DEFAULT_MAX_ENTRIES$1 = 1e4;
461
+ const DEFAULT_TTL_MS$1 = 3600 * 1e3;
462
+ /**
463
+ * In-memory LRU cache with TTL for account affinity mappings.
464
+ *
465
+ * Uses Map insertion order for LRU eviction: updated or rehydrated entries are
466
+ * deleted and re-inserted so they move to the "newest" end.
467
+ */
468
+ var AccountAffinityCache = class {
469
+ cache = /* @__PURE__ */ new Map();
470
+ maxEntries;
471
+ ttlMs;
472
+ persistentStore;
473
+ persistentStoreProvider;
474
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES$1, ttlMs = DEFAULT_TTL_MS$1, persistentStore) {
475
+ this.maxEntries = maxEntries;
476
+ this.ttlMs = ttlMs;
477
+ if (typeof persistentStore === "function") this.persistentStoreProvider = persistentStore;
478
+ else this.persistentStore = persistentStore;
479
+ }
480
+ /** Look up the preferred account ID for a cache key. Returns undefined if not found or expired. */
481
+ get(key) {
482
+ const entry = this.cache.get(key);
483
+ if (entry) if (Date.now() >= entry.expiresAt) this.cache.delete(key);
484
+ else return entry.accountId;
485
+ const accountId = this.readPersistentEntry(key);
486
+ if (!accountId) return;
487
+ this.setMemory(key, accountId);
488
+ return accountId;
489
+ }
490
+ /** Record a successful account mapping. Refreshes TTL and moves the entry to the newest position. */
491
+ set(key, accountId) {
492
+ this.setMemory(key, accountId);
493
+ this.writePersistentEntry(key, accountId);
494
+ }
495
+ /** Remove a specific entry. */
496
+ delete(key) {
497
+ const deleted = this.cache.delete(key);
498
+ this.deletePersistentEntry(key);
499
+ return deleted;
500
+ }
501
+ /** Remove all in-memory entries. */
502
+ clearMemory() {
503
+ this.cache.clear();
504
+ }
505
+ /** Remove all entries. */
506
+ clear() {
507
+ this.clearMemory();
508
+ this.clearPersistentEntries();
509
+ }
510
+ /** Current number of entries (including potentially expired ones). */
511
+ get size() {
512
+ return this.cache.size;
513
+ }
514
+ getPersistentStore() {
515
+ if (this.persistentStore) return this.persistentStore;
516
+ if (!this.persistentStoreProvider) return;
517
+ try {
518
+ const store = this.persistentStoreProvider();
519
+ if (store) this.persistentStore = store;
520
+ return store;
521
+ } catch (error) {
522
+ this.persistentStoreProvider = void 0;
523
+ consola.warn("Failed to resolve affinity persistence store:", error);
524
+ return;
525
+ }
526
+ }
527
+ readPersistentEntry(key) {
528
+ const store = this.getPersistentStore();
529
+ if (!store) return;
530
+ try {
531
+ return store.get(key);
532
+ } catch (error) {
533
+ consola.warn("Failed to read affinity mapping from persistent store:", error);
534
+ return;
535
+ }
536
+ }
537
+ writePersistentEntry(key, accountId) {
538
+ const store = this.getPersistentStore();
539
+ if (!store) return;
540
+ try {
541
+ store.set(key, accountId);
542
+ } catch (error) {
543
+ consola.warn("Failed to persist affinity mapping:", error);
544
+ }
545
+ }
546
+ deletePersistentEntry(key) {
547
+ const store = this.getPersistentStore();
548
+ if (!store) return;
549
+ try {
550
+ store.delete(key);
551
+ } catch (error) {
552
+ consola.warn("Failed to delete affinity mapping:", error);
553
+ }
554
+ }
555
+ clearPersistentEntries() {
556
+ const store = this.getPersistentStore();
557
+ if (!store) return;
558
+ try {
559
+ store.clear();
560
+ } catch (error) {
561
+ consola.warn("Failed to clear persistent affinity mappings:", error);
562
+ }
563
+ }
564
+ setMemory(key, accountId) {
565
+ this.cache.delete(key);
566
+ while (this.cache.size >= this.maxEntries) {
567
+ const oldest = this.cache.keys().next();
568
+ if (oldest.done) break;
569
+ this.cache.delete(oldest.value);
570
+ }
571
+ this.cache.set(key, {
572
+ accountId,
573
+ expiresAt: Date.now() + this.ttlMs
574
+ });
575
+ }
576
+ };
577
+ /**
578
+ * Extract the affinity key from the request context.
579
+ * Uses the upstream request ID which is deterministic for the same user message.
580
+ */
581
+ function extractAffinityKey(context) {
582
+ return context.requestId?.trim() || void 0;
583
+ }
584
+ /**
585
+ * Build the full cache key by combining the affinity key with the model ID.
586
+ * This prevents cross-model pollution (same session requesting different models
587
+ * can be routed to different accounts).
588
+ */
589
+ function buildAffinityCacheKey(affinityKey, modelId) {
590
+ return `${affinityKey}:${modelId}`;
591
+ }
592
+ /**
593
+ * Check whether an account is a valid affinity candidate.
594
+ * An account is valid if it is not failed and is present in the provided
595
+ * runtime list.
596
+ */
597
+ function isAffinityAccountUsable(accountId, accounts) {
598
+ const account = accounts.find((a) => a.id === accountId);
599
+ if (!account) return void 0;
600
+ if (account.failed) return void 0;
601
+ return account;
602
+ }
603
+
604
+ //#endregion
605
+ //#region src/services/copilot/get-models.ts
606
+ const getModels = async (account) => {
607
+ const ctx = account ?? accountFromState();
608
+ const response = await fetch(`${copilotBaseUrl(ctx)}/models`, { headers: copilotModelsHeaders(ctx) });
609
+ if (!response.ok) {
610
+ const errorText = await response.clone().text();
611
+ consola.error("Failed to get models response body", errorText);
612
+ throw new HTTPError("Failed to get models", response);
613
+ }
614
+ const models = await response.json();
615
+ try {
616
+ await fs.mkdir(PATHS.APP_DIR, { recursive: true });
617
+ await fs.writeFile(PATHS.MODELS_PATH, `${JSON.stringify(models, null, 2)}\n`, {
618
+ encoding: "utf8",
619
+ mode: 384
620
+ });
621
+ } catch {}
622
+ return models;
623
+ };
624
+
625
+ //#endregion
626
+ //#region src/lib/accounts-manager-auth.ts
627
+ const takeAuthSnapshot = (account) => ({
628
+ githubToken: account.githubToken,
629
+ accountType: account.accountType
630
+ });
631
+ const isAuthSnapshotCurrent = (account, snapshot) => account.githubToken === snapshot.githubToken && account.accountType === snapshot.accountType;
632
+ const isSameAuthSnapshot = (a, b) => {
633
+ if (!a) return false;
634
+ return a.githubToken === b.githubToken && a.accountType === b.accountType;
635
+ };
636
+ const toAccountContextFromSnapshot = (account, snapshot, copilotToken) => ({
637
+ accountLogin: account.accountLogin,
638
+ githubToken: snapshot.githubToken,
639
+ copilotToken,
640
+ ...account.copilotApiUrl !== void 0 ? { copilotApiUrl: account.copilotApiUrl } : {},
641
+ accountType: snapshot.accountType,
642
+ vsCodeVersion: account.vsCodeVersion,
643
+ clientDeviceId: account.clientDeviceId,
644
+ clientMachineId: account.clientMachineId,
645
+ clientSessionId: account.clientSessionId
646
+ });
647
+ const applyCopilotTokenIfCurrent = (account, snapshot, copilotToken) => {
648
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
649
+ account.copilotToken = copilotToken;
650
+ return true;
651
+ };
652
+ const applyModelsIfCurrent = (account, snapshot, models) => {
653
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
654
+ account.models = models;
655
+ return true;
656
+ };
657
+ const applyTokenRefreshSuccessIfCurrent = (account, snapshot, token) => {
658
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
659
+ account.copilotToken = token;
660
+ account.failed = false;
661
+ account.failureReason = void 0;
662
+ return true;
663
+ };
664
+ const applyTokenRefreshFailureIfCurrent = (account, snapshot, error) => {
665
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
666
+ account.failed = true;
667
+ account.failureReason = String(error);
668
+ return true;
669
+ };
670
+ const applyQuotaRefreshSuccessIfCurrent = (account, snapshot, result) => {
671
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
672
+ const { premium, copilotApiUrl } = result;
673
+ account.premiumEntitlement = premium.entitlement;
674
+ account.premiumRemaining = premium.remaining;
675
+ account.unlimited = premium.unlimited;
676
+ account.overagePermitted = premium.overage_permitted;
677
+ if (copilotApiUrl) account.copilotApiUrl = copilotApiUrl;
678
+ account.lastQuotaFetch = Date.now();
679
+ account.failed = false;
680
+ account.failureReason = void 0;
681
+ return true;
682
+ };
683
+ const setAccountFailedState = (account, reason) => {
684
+ account.failed = true;
685
+ account.failureReason = reason;
686
+ consola.warn(`Account ${account.id} marked as failed: ${reason}`);
687
+ };
688
+ const applyUnauthorizedIfCurrent = (account, snapshot, reason) => {
689
+ if (!isAuthSnapshotCurrent(account, snapshot)) return false;
690
+ setAccountFailedState(account, reason);
691
+ return true;
692
+ };
693
+
694
+ //#endregion
695
+ //#region src/lib/accounts-manager-quota.ts
696
+ const getCostUnits = (model) => {
697
+ const billing = model.billing;
698
+ if (!billing) return 0;
699
+ if (billing.is_premium !== true) return 0;
700
+ const multiplier = billing.multiplier;
701
+ if (typeof multiplier !== "number" || !Number.isFinite(multiplier) || multiplier <= 0) return 1;
702
+ return multiplier;
703
+ };
704
+ const getEffectivePremiumRemaining = (account) => {
705
+ if (account.premiumRemaining === void 0) return;
706
+ const reserved = account.premiumReserved ?? 0;
707
+ return account.premiumRemaining - reserved;
708
+ };
709
+ const reservePremiumUnits = (account, units) => {
710
+ if (units <= 0) return;
711
+ const id = Symbol("quotaReservation");
712
+ if (!account.premiumReservations) account.premiumReservations = /* @__PURE__ */ new Map();
713
+ account.premiumReservations.set(id, units);
714
+ account.premiumReserved = (account.premiumReserved ?? 0) + units;
715
+ return { id };
716
+ };
717
+ const releasePremiumReservation = (account, reservation) => {
718
+ if (!reservation) return;
719
+ const reservations = account.premiumReservations;
720
+ if (!reservations) return;
721
+ const reservedUnits = reservations.get(reservation.id);
722
+ if (reservedUnits === void 0) return;
723
+ reservations.delete(reservation.id);
724
+ const nextReserved = (account.premiumReserved ?? 0) - reservedUnits;
725
+ account.premiumReserved = Math.max(0, nextReserved);
726
+ if (reservations.size === 0) account.premiumReservations = void 0;
727
+ };
728
+
729
+ //#endregion
730
+ //#region src/lib/admin-db.ts
731
+ const DEFAULT_DB_PATH = path.join(PATHS.APP_DIR, "admin.sqlite");
732
+ let sharedDb = null;
733
+ let initialized = false;
734
+ const INIT_WARN_THROTTLE_MS = 3e4;
735
+ let lastInitWarnAtMs = 0;
736
+ let suppressedInitWarnCount = 0;
737
+ function warnAdminDbInitFailure(error) {
738
+ const now = Date.now();
739
+ if (now - lastInitWarnAtMs < INIT_WARN_THROTTLE_MS) {
740
+ suppressedInitWarnCount++;
741
+ return;
742
+ }
743
+ const suppressed = suppressedInitWarnCount;
744
+ suppressedInitWarnCount = 0;
745
+ lastInitWarnAtMs = now;
746
+ const suffix = suppressed > 0 ? ` (suppressed ${suppressed} similar errors)` : "";
747
+ consola.warn(`Failed to initialize admin DB; admin features disabled${suffix}`, error);
748
+ }
749
+ function getAdminDbPath() {
750
+ return DEFAULT_DB_PATH;
751
+ }
752
+ function openAdminDb(filePath = DEFAULT_DB_PATH) {
753
+ return new Database(filePath);
754
+ }
755
+ function initAdminDb(db) {
756
+ db.run("PRAGMA journal_mode = WAL;");
757
+ db.run("PRAGMA synchronous = NORMAL;");
758
+ db.run("PRAGMA busy_timeout = 3000;");
759
+ db.run("PRAGMA foreign_keys = ON;");
760
+ migrateAdminDb(db);
761
+ }
762
+ function getAdminDb() {
763
+ if (!sharedDb) sharedDb = openAdminDb();
764
+ if (!initialized) try {
765
+ initAdminDb(sharedDb);
766
+ initialized = true;
767
+ } catch (error) {
768
+ warnAdminDbInitFailure(error);
769
+ }
770
+ return sharedDb;
771
+ }
772
+ function getAdminDbUserVersion(db = getAdminDb()) {
773
+ try {
774
+ return db.query("PRAGMA user_version;").get()?.user_version ?? 0;
775
+ } catch {
776
+ return 0;
777
+ }
778
+ }
779
+ function hasRequestLogColumn(db, columnName) {
780
+ return db.query("PRAGMA table_info(request_log);").all().some((row) => row.name === columnName);
781
+ }
782
+ function migrateV1(db) {
783
+ db.run(`
784
+ CREATE TABLE IF NOT EXISTS request_log (
785
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
786
+ request_id TEXT NOT NULL UNIQUE,
787
+
788
+ started_at_ms INTEGER NOT NULL,
789
+ finished_at_ms INTEGER,
790
+ duration_ms INTEGER,
791
+ ttfb_ms INTEGER,
792
+
793
+ method TEXT NOT NULL,
794
+ path TEXT NOT NULL,
795
+ upstream_endpoint TEXT,
796
+ stream INTEGER NOT NULL DEFAULT 0,
797
+
798
+ account_id TEXT,
799
+ account_type TEXT,
800
+ cost_units REAL,
801
+ client_model TEXT,
802
+ upstream_model TEXT,
803
+
804
+ client_ip TEXT,
805
+ client_ip_source TEXT,
806
+ user_agent TEXT,
807
+
808
+ tokens_input INTEGER,
809
+ tokens_output INTEGER,
810
+ tokens_total INTEGER,
811
+ tokens_cached_input INTEGER,
812
+ usage_json TEXT,
813
+
814
+ premium_remaining_before REAL,
815
+ premium_remaining_after REAL,
816
+ premium_remaining_diff REAL,
817
+ premium_unlimited_before INTEGER,
818
+ premium_unlimited_after INTEGER,
819
+
820
+ http_status INTEGER,
821
+ error_name TEXT,
822
+ error_status INTEGER,
823
+ error_message TEXT,
824
+ selection_failure_reason TEXT
825
+ );
826
+
827
+ CREATE INDEX IF NOT EXISTS idx_request_log_started_at
828
+ ON request_log(started_at_ms DESC);
829
+ CREATE INDEX IF NOT EXISTS idx_request_log_account_started_at
830
+ ON request_log(account_id, started_at_ms DESC);
831
+ CREATE INDEX IF NOT EXISTS idx_request_log_model_started_at
832
+ ON request_log(upstream_model, started_at_ms DESC);
833
+ CREATE INDEX IF NOT EXISTS idx_request_log_endpoint_started_at
834
+ ON request_log(upstream_endpoint, started_at_ms DESC);
835
+ CREATE INDEX IF NOT EXISTS idx_request_log_status_started_at
836
+ ON request_log(http_status, started_at_ms DESC);
837
+
838
+ PRAGMA user_version = 1;
839
+ `);
840
+ }
841
+ function migrateV9(db) {
842
+ db.run(`
843
+ CREATE TABLE IF NOT EXISTS daily_premium_stats (
844
+ date TEXT NOT NULL,
845
+ account_id TEXT NOT NULL,
846
+ request_count INTEGER NOT NULL DEFAULT 0,
847
+ cost_units_sum REAL NOT NULL DEFAULT 0,
848
+ tokens_total INTEGER NOT NULL DEFAULT 0,
849
+ error_count INTEGER NOT NULL DEFAULT 0,
850
+ updated_at_ms INTEGER NOT NULL,
851
+ PRIMARY KEY (date, account_id)
852
+ );
853
+
854
+ CREATE INDEX IF NOT EXISTS idx_daily_premium_stats_date
855
+ ON daily_premium_stats(date);
856
+ `);
857
+ if (db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'request_log' LIMIT 1;").get()) {
858
+ const nowMs = Date.now();
859
+ db.run(`INSERT INTO daily_premium_stats
860
+ (date, account_id, request_count, cost_units_sum, tokens_total, error_count, updated_at_ms)
861
+ SELECT
862
+ date(started_at_ms / 1000, 'unixepoch', 'localtime') AS date,
863
+ account_id,
864
+ COUNT(*) AS request_count,
865
+ SUM(cost_units) AS cost_units_sum,
866
+ COALESCE(SUM(tokens_total), 0) AS tokens_total,
867
+ SUM(CASE WHEN error_name IS NOT NULL THEN 1 ELSE 0 END) AS error_count,
868
+ ? AS updated_at_ms
869
+ FROM request_log
870
+ WHERE cost_units > 0
871
+ AND account_id IS NOT NULL
872
+ GROUP BY 1, 2
873
+ ON CONFLICT(date, account_id) DO UPDATE SET
874
+ request_count = excluded.request_count,
875
+ cost_units_sum = excluded.cost_units_sum,
876
+ tokens_total = excluded.tokens_total,
877
+ error_count = excluded.error_count,
878
+ updated_at_ms = excluded.updated_at_ms;`, [nowMs]);
879
+ }
880
+ db.run("PRAGMA user_version = 9;");
881
+ }
882
+ function migrateV8(db) {
883
+ db.run(`
884
+ CREATE TABLE IF NOT EXISTS session_affinity (
885
+ cache_key TEXT PRIMARY KEY,
886
+ account_id TEXT NOT NULL,
887
+ created_at_ms INTEGER NOT NULL,
888
+ last_confirmed_at_ms INTEGER NOT NULL,
889
+ last_used_at_ms INTEGER NOT NULL
890
+ );
891
+
892
+ CREATE INDEX IF NOT EXISTS idx_session_affinity_last_used
893
+ ON session_affinity(last_used_at_ms);
894
+
895
+ CREATE INDEX IF NOT EXISTS idx_session_affinity_account
896
+ ON session_affinity(account_id);
897
+
898
+ PRAGMA user_version = 8;
899
+ `);
900
+ }
901
+ function migrateV10(db) {
902
+ db.run(`
903
+ CREATE TABLE IF NOT EXISTS quota_snapshots (
904
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
905
+ account_id TEXT NOT NULL,
906
+ snapshot_at_ms INTEGER NOT NULL,
907
+ remaining INTEGER NOT NULL,
908
+ entitlement INTEGER NOT NULL,
909
+ unlimited INTEGER NOT NULL DEFAULT 0,
910
+ source TEXT NOT NULL DEFAULT 'refresh'
911
+ );
912
+
913
+ CREATE INDEX IF NOT EXISTS idx_quota_snapshots_account_time
914
+ ON quota_snapshots(account_id, snapshot_at_ms);
915
+
916
+ CREATE INDEX IF NOT EXISTS idx_quota_snapshots_time
917
+ ON quota_snapshots(snapshot_at_ms);
918
+ `);
919
+ if (db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'request_log' LIMIT 1;").get()) db.run(`
920
+ INSERT INTO quota_snapshots
921
+ (account_id, snapshot_at_ms, remaining, entitlement, unlimited, source)
922
+ SELECT
923
+ account_id,
924
+ finished_at_ms,
925
+ CAST(premium_remaining_after AS INTEGER),
926
+ 0,
927
+ COALESCE(premium_unlimited_after, 0),
928
+ 'backfill'
929
+ FROM request_log
930
+ WHERE premium_remaining_after IS NOT NULL
931
+ AND account_id IS NOT NULL
932
+ AND finished_at_ms IS NOT NULL
933
+ ORDER BY finished_at_ms;
934
+ `);
935
+ db.run("PRAGMA user_version = 10;");
936
+ }
937
+ function migrateAdminDb(db) {
938
+ const current = db.query("PRAGMA user_version;").get()?.user_version ?? 0;
939
+ if (current >= 10) return;
940
+ if (current < 1) migrateV1(db);
941
+ if (current < 2) db.run(`
942
+ ALTER TABLE request_log ADD COLUMN user_id TEXT;
943
+ ALTER TABLE request_log ADD COLUMN safety_identifier TEXT;
944
+ ALTER TABLE request_log ADD COLUMN prompt_cache_key TEXT;
945
+ ALTER TABLE request_log ADD COLUMN initiator TEXT;
946
+ ALTER TABLE request_log ADD COLUMN upstream_request_id TEXT;
947
+
948
+ PRAGMA user_version = 2;
949
+ `);
950
+ if (current < 3) db.run(`
951
+ CREATE INDEX IF NOT EXISTS idx_request_log_session_finished
952
+ ON request_log(
953
+ prompt_cache_key,
954
+ safety_identifier,
955
+ finished_at_ms DESC
956
+ )
957
+ WHERE finished_at_ms IS NOT NULL
958
+ AND tokens_input IS NOT NULL;
959
+
960
+ PRAGMA user_version = 3;
961
+ `);
962
+ if (current < 4) db.run(`
963
+ CREATE INDEX IF NOT EXISTS idx_request_log_session_finished_by_client_model
964
+ ON request_log(
965
+ prompt_cache_key,
966
+ safety_identifier,
967
+ client_model,
968
+ finished_at_ms DESC
969
+ )
970
+ WHERE finished_at_ms IS NOT NULL
971
+ AND tokens_input IS NOT NULL;
972
+
973
+ PRAGMA user_version = 4;
974
+ `);
975
+ if (current < 5) db.run(`
976
+ ALTER TABLE request_log ADD COLUMN affinity_hit INTEGER;
977
+ ALTER TABLE request_log ADD COLUMN affinity_cache_key TEXT;
978
+
979
+ PRAGMA user_version = 5;
980
+ `);
981
+ if (current < 6) {
982
+ if (!hasRequestLogColumn(db, "is_subagent")) db.run("ALTER TABLE request_log ADD COLUMN is_subagent INTEGER;");
983
+ db.run("PRAGMA user_version = 6;");
984
+ }
985
+ if (current < 7) {
986
+ if (!hasRequestLogColumn(db, "affinity_key_used")) db.run("ALTER TABLE request_log ADD COLUMN affinity_key_used TEXT;");
987
+ if (!hasRequestLogColumn(db, "affinity_key_source")) db.run("ALTER TABLE request_log ADD COLUMN affinity_key_source TEXT;");
988
+ if (!hasRequestLogColumn(db, "selection_reason")) db.run("ALTER TABLE request_log ADD COLUMN selection_reason TEXT;");
989
+ if (!hasRequestLogColumn(db, "upstream_error_message_raw")) db.run("ALTER TABLE request_log ADD COLUMN upstream_error_message_raw TEXT;");
990
+ db.run("PRAGMA user_version = 7;");
991
+ }
992
+ migrateV8(db);
993
+ migrateV9(db);
994
+ migrateV10(db);
995
+ }
996
+
997
+ //#endregion
998
+ //#region src/lib/stats-store.ts
999
+ const DEFAULT_STATS_RETENTION_DAYS = 180;
1000
+ function toLocalDateString(ms) {
1001
+ const d = new Date(ms);
1002
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1003
+ }
1004
+ var StatsStore = class {
1005
+ db;
1006
+ upsertStmt;
1007
+ insertSnapshotStmt;
1008
+ constructor(db) {
1009
+ this.db = db;
1010
+ this.upsertStmt = db.query(`
1011
+ INSERT INTO daily_premium_stats
1012
+ (date, account_id, request_count, cost_units_sum, tokens_total, error_count, updated_at_ms)
1013
+ VALUES (?, ?, 1, ?, ?, ?, ?)
1014
+ ON CONFLICT(date, account_id) DO UPDATE SET
1015
+ request_count = request_count + 1,
1016
+ cost_units_sum = cost_units_sum + excluded.cost_units_sum,
1017
+ tokens_total = tokens_total + excluded.tokens_total,
1018
+ error_count = error_count + excluded.error_count,
1019
+ updated_at_ms = excluded.updated_at_ms
1020
+ `);
1021
+ this.insertSnapshotStmt = db.query(`
1022
+ INSERT INTO quota_snapshots
1023
+ (account_id, snapshot_at_ms, remaining, entitlement, unlimited, source)
1024
+ VALUES (?, ?, ?, ?, ?, ?)
1025
+ `);
1026
+ }
1027
+ upsertDailyStats(record) {
1028
+ const date = toLocalDateString(record.startedAtMs);
1029
+ this.upsertStmt.run(date, record.accountId, record.costUnits, record.tokensTotal, record.hasError ? 1 : 0, Date.now());
1030
+ }
1031
+ insertQuotaSnapshot(record) {
1032
+ this.insertSnapshotStmt.run(record.accountId, record.snapshotAtMs, record.remaining, record.entitlement, record.unlimited ? 1 : 0, record.source);
1033
+ }
1034
+ getConsumptionFromSnapshots(params) {
1035
+ const dateExpr = params.granularity === "hour" ? "strftime('%Y-%m-%d %H:00', snapshot_at_ms / 1000, 'unixepoch', 'localtime')" : "date(snapshot_at_ms / 1000, 'unixepoch', 'localtime')";
1036
+ const accountFilter = params.accountId ? " AND account_id = ?" : "";
1037
+ const args = [];
1038
+ args.push(params.fromMs);
1039
+ if (params.accountId) args.push(params.accountId);
1040
+ args.push(params.fromMs, params.toMs);
1041
+ if (params.accountId) args.push(params.accountId);
1042
+ args.push(params.fromMs);
1043
+ return this.db.query(`WITH baseline AS (
1044
+ SELECT qs.account_id, qs.snapshot_at_ms, qs.remaining, qs.id
1045
+ FROM quota_snapshots qs
1046
+ INNER JOIN (
1047
+ SELECT account_id, MAX(snapshot_at_ms) AS max_ts
1048
+ FROM quota_snapshots
1049
+ WHERE snapshot_at_ms < ?
1050
+ AND unlimited = 0${accountFilter}
1051
+ GROUP BY account_id
1052
+ ) latest ON qs.account_id = latest.account_id
1053
+ AND qs.snapshot_at_ms = latest.max_ts
1054
+ ),
1055
+ all_snaps AS (
1056
+ SELECT account_id, snapshot_at_ms, remaining, id
1057
+ FROM baseline
1058
+ UNION ALL
1059
+ SELECT account_id, snapshot_at_ms, remaining, id
1060
+ FROM quota_snapshots
1061
+ WHERE snapshot_at_ms >= ? AND snapshot_at_ms <= ?
1062
+ AND unlimited = 0${accountFilter}
1063
+ ),
1064
+ with_prev AS (
1065
+ SELECT
1066
+ account_id,
1067
+ snapshot_at_ms,
1068
+ remaining,
1069
+ LAG(remaining) OVER (
1070
+ PARTITION BY account_id ORDER BY snapshot_at_ms, id
1071
+ ) AS prev_remaining
1072
+ FROM all_snaps
1073
+ )
1074
+ SELECT
1075
+ ${dateExpr} AS date,
1076
+ account_id,
1077
+ SUM(
1078
+ CASE WHEN prev_remaining IS NOT NULL AND prev_remaining > remaining
1079
+ THEN prev_remaining - remaining
1080
+ ELSE 0
1081
+ END
1082
+ ) AS premium_consumed
1083
+ FROM with_prev
1084
+ WHERE snapshot_at_ms >= ?
1085
+ GROUP BY 1, 2
1086
+ ORDER BY 1, 2`).all(...args);
1087
+ }
1088
+ getDailyPremiumStats(params) {
1089
+ const accountFilter = params.accountId ? " AND account_id = ?" : "";
1090
+ const args = [params.from, params.to];
1091
+ if (params.accountId) args.push(params.accountId);
1092
+ const dailyMetrics = this.db.query(`SELECT date,
1093
+ SUM(request_count) AS request_count,
1094
+ SUM(tokens_total) AS tokens_total,
1095
+ SUM(error_count) AS error_count
1096
+ FROM daily_premium_stats
1097
+ WHERE date >= ? AND date <= ?${accountFilter}
1098
+ GROUP BY date
1099
+ ORDER BY date`).all(...args);
1100
+ const byAccountMetrics = this.db.query(`SELECT date, account_id, request_count, tokens_total, error_count
1101
+ FROM daily_premium_stats
1102
+ WHERE date >= ? AND date <= ?${accountFilter}
1103
+ ORDER BY date, account_id`).all(...args);
1104
+ const fromMs = this.localDateToMs(params.from);
1105
+ const toMs = this.localDateEndMs(params.to);
1106
+ const consumption = this.getConsumptionFromSnapshots({
1107
+ fromMs,
1108
+ toMs,
1109
+ granularity: "day",
1110
+ accountId: params.accountId
1111
+ });
1112
+ return this.mergeMetricsAndConsumption(dailyMetrics, byAccountMetrics, consumption);
1113
+ }
1114
+ getHourlyPremiumStats(params) {
1115
+ const accountFilter = params.accountId ? " AND account_id = ?" : "";
1116
+ const dailyArgs = [params.fromMs, params.toMs];
1117
+ if (params.accountId) dailyArgs.push(params.accountId);
1118
+ const dailyMetrics = this.db.query(`SELECT strftime('%Y-%m-%d %H:00', started_at_ms / 1000, 'unixepoch', 'localtime') AS date,
1119
+ COUNT(*) AS request_count,
1120
+ COALESCE(SUM(tokens_total), 0) AS tokens_total,
1121
+ SUM(CASE WHEN error_name IS NOT NULL THEN 1 ELSE 0 END) AS error_count
1122
+ FROM request_log
1123
+ WHERE cost_units > 0
1124
+ AND started_at_ms >= ? AND started_at_ms <= ?${accountFilter}
1125
+ GROUP BY 1
1126
+ ORDER BY 1`).all(...dailyArgs);
1127
+ const byAccountFilter = params.accountId ? " AND account_id = ?" : " AND account_id IS NOT NULL";
1128
+ const byAccountArgs = [params.fromMs, params.toMs];
1129
+ if (params.accountId) byAccountArgs.push(params.accountId);
1130
+ const byAccountMetrics = this.db.query(`SELECT strftime('%Y-%m-%d %H:00', started_at_ms / 1000, 'unixepoch', 'localtime') AS date,
1131
+ account_id,
1132
+ COUNT(*) AS request_count,
1133
+ COALESCE(SUM(tokens_total), 0) AS tokens_total,
1134
+ SUM(CASE WHEN error_name IS NOT NULL THEN 1 ELSE 0 END) AS error_count
1135
+ FROM request_log
1136
+ WHERE cost_units > 0
1137
+ AND started_at_ms >= ? AND started_at_ms <= ?${byAccountFilter}
1138
+ GROUP BY 1, 2
1139
+ ORDER BY 1, 2`).all(...byAccountArgs);
1140
+ const consumption = this.getConsumptionFromSnapshots({
1141
+ fromMs: params.fromMs,
1142
+ toMs: params.toMs,
1143
+ granularity: "hour",
1144
+ accountId: params.accountId
1145
+ });
1146
+ return this.mergeMetricsAndConsumption(dailyMetrics, byAccountMetrics, consumption);
1147
+ }
1148
+ cleanupStatsRetention(retentionDays = DEFAULT_STATS_RETENTION_DAYS) {
1149
+ try {
1150
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
1151
+ const cutoffDate = toLocalDateString(cutoffMs);
1152
+ this.db.query("DELETE FROM daily_premium_stats WHERE date < ?;").run(cutoffDate);
1153
+ this.db.query(`DELETE FROM quota_snapshots
1154
+ WHERE snapshot_at_ms < ?
1155
+ AND id NOT IN (
1156
+ SELECT qs.id
1157
+ FROM quota_snapshots qs
1158
+ INNER JOIN (
1159
+ SELECT account_id, MAX(snapshot_at_ms) AS max_ts
1160
+ FROM quota_snapshots
1161
+ WHERE snapshot_at_ms < ?
1162
+ GROUP BY account_id
1163
+ ) latest ON qs.account_id = latest.account_id
1164
+ AND qs.snapshot_at_ms = latest.max_ts
1165
+ );`).run(cutoffMs, cutoffMs);
1166
+ } catch (error) {
1167
+ consola$1.debug("Failed to cleanup stats retention", error);
1168
+ }
1169
+ }
1170
+ localDateToMs(dateStr) {
1171
+ const [y, m, d] = dateStr.split("-").map(Number);
1172
+ return new Date(y, m - 1, d).getTime();
1173
+ }
1174
+ /** End-of-day ms (next local midnight - 1ms), DST-safe. */
1175
+ localDateEndMs(dateStr) {
1176
+ const [y, m, d] = dateStr.split("-").map(Number);
1177
+ return new Date(y, m - 1, d + 1).getTime() - 1;
1178
+ }
1179
+ mergeMetricsAndConsumption(dailyMetrics, byAccountMetrics, consumption) {
1180
+ const consumptionMap = /* @__PURE__ */ new Map();
1181
+ const dateConsumptionMap = /* @__PURE__ */ new Map();
1182
+ for (const c of consumption) {
1183
+ consumptionMap.set(`${c.date}|${c.account_id}`, c.premium_consumed);
1184
+ dateConsumptionMap.set(c.date, (dateConsumptionMap.get(c.date) ?? 0) + c.premium_consumed);
1185
+ }
1186
+ const allDates = new Set([...dailyMetrics.map((m) => m.date), ...consumption.map((c) => c.date)]);
1187
+ const metricsMap = new Map(dailyMetrics.map((m) => [m.date, m]));
1188
+ const daily = [...allDates].sort().map((date) => {
1189
+ const m = metricsMap.get(date);
1190
+ return {
1191
+ date,
1192
+ request_count: m?.request_count ?? 0,
1193
+ premium_consumed: dateConsumptionMap.get(date) ?? 0,
1194
+ tokens_total: m?.tokens_total ?? 0,
1195
+ error_count: m?.error_count ?? 0
1196
+ };
1197
+ });
1198
+ const allAccountDateKeys = new Set([...byAccountMetrics.map((m) => `${m.date}|${m.account_id}`), ...consumption.map((c) => `${c.date}|${c.account_id}`)]);
1199
+ const byAccountMetricsMap = new Map(byAccountMetrics.map((m) => [`${m.date}|${m.account_id}`, m]));
1200
+ return {
1201
+ daily,
1202
+ byAccount: [...allAccountDateKeys].sort().map((key) => {
1203
+ const [date, account_id] = key.split("|");
1204
+ const m = byAccountMetricsMap.get(key);
1205
+ return {
1206
+ date,
1207
+ account_id,
1208
+ request_count: m?.request_count ?? 0,
1209
+ premium_consumed: consumptionMap.get(key) ?? 0,
1210
+ tokens_total: m?.tokens_total ?? 0,
1211
+ error_count: m?.error_count ?? 0
1212
+ };
1213
+ })
1214
+ };
1215
+ }
1216
+ };
1217
+
1218
+ //#endregion
1219
+ //#region src/lib/request-history.ts
1220
+ const DEFAULT_RETENTION_DAYS = 14;
1221
+ const DEFAULT_MAX_ROWS = 2e5;
1222
+ const INSERT_WARN_THROTTLE_MS = 3e4;
1223
+ let lastInsertWarnAtMs = 0;
1224
+ let suppressedInsertWarnCount = 0;
1225
+ function warnInsertFailure(error) {
1226
+ const now = Date.now();
1227
+ if (now - lastInsertWarnAtMs < INSERT_WARN_THROTTLE_MS) {
1228
+ suppressedInsertWarnCount++;
1229
+ return;
1230
+ }
1231
+ const suppressed = suppressedInsertWarnCount;
1232
+ suppressedInsertWarnCount = 0;
1233
+ lastInsertWarnAtMs = now;
1234
+ const suffix = suppressed > 0 ? ` (suppressed ${suppressed} similar errors)` : "";
1235
+ consola.warn(`Failed to insert request log${suffix}`, error);
1236
+ }
1237
+ function toDbNull(value) {
1238
+ return value === void 0 ? null : value;
1239
+ }
1240
+ function toDbBool(value) {
1241
+ if (value === true) return 1;
1242
+ if (value === false) return 0;
1243
+ return null;
1244
+ }
1245
+ function getClientIpInfo(c) {
1246
+ const cf = c.req.header("cf-connecting-ip");
1247
+ if (cf) return {
1248
+ ip: cf.trim(),
1249
+ source: "cf-connecting-ip"
1250
+ };
1251
+ const xff = c.req.header("x-forwarded-for");
1252
+ if (xff) {
1253
+ const first = xff.split(",")[0]?.trim();
1254
+ if (first) return {
1255
+ ip: first,
1256
+ source: "x-forwarded-for"
1257
+ };
1258
+ }
1259
+ const xri = c.req.header("x-real-ip");
1260
+ if (xri) return {
1261
+ ip: xri.trim(),
1262
+ source: "x-real-ip"
1263
+ };
1264
+ return {};
1265
+ }
1266
+ function normalizeChatCompletionsUsage(usage) {
1267
+ if (!usage) return {};
1268
+ const cached = usage.prompt_tokens_details?.cached_tokens ?? 0;
1269
+ const prompt = usage.prompt_tokens;
1270
+ const completion = usage.completion_tokens;
1271
+ const total = usage.total_tokens;
1272
+ return {
1273
+ tokensCachedInput: cached,
1274
+ tokensInput: Math.max(0, prompt - cached),
1275
+ tokensOutput: completion,
1276
+ tokensTotal: total,
1277
+ usageJson: JSON.stringify(usage)
1278
+ };
1279
+ }
1280
+ function normalizeResponsesUsage(usage) {
1281
+ if (!usage) return {};
1282
+ const cached = usage.input_tokens_details?.cached_tokens ?? 0;
1283
+ const input = usage.input_tokens;
1284
+ const output = usage.output_tokens ?? 0;
1285
+ const total = usage.total_tokens;
1286
+ return {
1287
+ tokensCachedInput: cached,
1288
+ tokensInput: Math.max(0, input - cached),
1289
+ tokensOutput: output,
1290
+ tokensTotal: total,
1291
+ usageJson: JSON.stringify(usage)
1292
+ };
1293
+ }
1294
+ function normalizeMessagesUsage(usage) {
1295
+ if (!usage) return {};
1296
+ const cached = usage.cache_read_input_tokens ?? 0;
1297
+ const input = usage.input_tokens;
1298
+ const output = usage.output_tokens;
1299
+ const hasInput = typeof input === "number";
1300
+ const hasOutput = typeof output === "number";
1301
+ return {
1302
+ tokensCachedInput: cached,
1303
+ tokensInput: hasInput ? Math.max(0, input - cached) : void 0,
1304
+ tokensOutput: hasOutput ? output : void 0,
1305
+ tokensTotal: hasInput || hasOutput ? (input ?? 0) + (output ?? 0) : void 0,
1306
+ usageJson: JSON.stringify(usage)
1307
+ };
1308
+ }
1309
+ function normalizeEmbeddingsUsage(usage) {
1310
+ if (!usage) return {};
1311
+ return {
1312
+ tokensCachedInput: 0,
1313
+ tokensInput: usage.prompt_tokens,
1314
+ tokensOutput: 0,
1315
+ tokensTotal: usage.total_tokens,
1316
+ usageJson: JSON.stringify(usage)
1317
+ };
1318
+ }
1319
+ var RequestHistoryStore = class {
1320
+ db;
1321
+ insertStmt;
1322
+ getByRequestIdStmt;
1323
+ getLastCompletedUsageBySessionStmt;
1324
+ constructor(db) {
1325
+ this.db = db;
1326
+ this.insertStmt = db.query(`
1327
+ INSERT INTO request_log (
1328
+ request_id,
1329
+ started_at_ms,
1330
+ finished_at_ms,
1331
+ duration_ms,
1332
+ ttfb_ms,
1333
+ method,
1334
+ path,
1335
+ upstream_endpoint,
1336
+ stream,
1337
+ account_id,
1338
+ account_type,
1339
+ cost_units,
1340
+ client_model,
1341
+ upstream_model,
1342
+ client_ip,
1343
+ client_ip_source,
1344
+ user_agent,
1345
+ user_id,
1346
+ safety_identifier,
1347
+ prompt_cache_key,
1348
+ initiator,
1349
+ is_subagent,
1350
+ upstream_request_id,
1351
+ affinity_key_used,
1352
+ affinity_key_source,
1353
+ selection_reason,
1354
+ tokens_input,
1355
+ tokens_output,
1356
+ tokens_total,
1357
+ tokens_cached_input,
1358
+ usage_json,
1359
+ premium_remaining_before,
1360
+ premium_remaining_after,
1361
+ premium_remaining_diff,
1362
+ premium_unlimited_before,
1363
+ premium_unlimited_after,
1364
+ http_status,
1365
+ error_name,
1366
+ error_status,
1367
+ error_message,
1368
+ upstream_error_message_raw,
1369
+ selection_failure_reason,
1370
+ affinity_hit,
1371
+ affinity_cache_key
1372
+ ) VALUES (
1373
+ ?,?,?,?,?,?,?,?,
1374
+ ?,?,?,?,?,?,?,?,
1375
+ ?,?,?,?,?,?,?,?,
1376
+ ?,?,?,?,?,?,?,?,
1377
+ ?,?,?,?,?,?,?,?,
1378
+ ?,?,?,?
1379
+ );
1380
+ `);
1381
+ this.getByRequestIdStmt = db.query("SELECT * FROM request_log WHERE request_id = ? LIMIT 1;");
1382
+ this.getLastCompletedUsageBySessionStmt = db.query(`
1383
+ SELECT
1384
+ tokens_input,
1385
+ tokens_output,
1386
+ tokens_total,
1387
+ tokens_cached_input
1388
+ FROM request_log
1389
+ WHERE prompt_cache_key = ?
1390
+ AND safety_identifier = ?
1391
+ AND client_model = ?
1392
+ AND finished_at_ms IS NOT NULL
1393
+ AND tokens_input IS NOT NULL
1394
+ ORDER BY finished_at_ms DESC
1395
+ LIMIT 1;
1396
+ `);
1397
+ }
1398
+ insert(record) {
1399
+ try {
1400
+ const args = [
1401
+ record.requestId,
1402
+ record.startedAtMs,
1403
+ toDbNull(record.finishedAtMs),
1404
+ toDbNull(record.durationMs),
1405
+ toDbNull(record.ttfbMs),
1406
+ record.method,
1407
+ record.path,
1408
+ toDbNull(record.upstreamEndpoint),
1409
+ record.stream ? 1 : 0,
1410
+ toDbNull(record.accountId),
1411
+ toDbNull(record.accountType),
1412
+ toDbNull(record.costUnits),
1413
+ toDbNull(record.clientModel),
1414
+ toDbNull(record.upstreamModel),
1415
+ toDbNull(record.clientIp),
1416
+ toDbNull(record.clientIpSource),
1417
+ toDbNull(record.userAgent),
1418
+ toDbNull(record.userId),
1419
+ toDbNull(record.safetyIdentifier),
1420
+ toDbNull(record.promptCacheKey),
1421
+ toDbNull(record.initiator),
1422
+ toDbBool(record.isSubagent),
1423
+ toDbNull(record.upstreamRequestId),
1424
+ toDbNull(record.affinityKeyUsed),
1425
+ toDbNull(record.affinityKeySource),
1426
+ toDbNull(record.selectionReason),
1427
+ toDbNull(record.tokensInput),
1428
+ toDbNull(record.tokensOutput),
1429
+ toDbNull(record.tokensTotal),
1430
+ toDbNull(record.tokensCachedInput),
1431
+ toDbNull(record.usageJson),
1432
+ toDbNull(record.premiumRemainingBefore),
1433
+ toDbNull(record.premiumRemainingAfter),
1434
+ toDbNull(record.premiumRemainingDiff),
1435
+ toDbBool(record.premiumUnlimitedBefore),
1436
+ toDbBool(record.premiumUnlimitedAfter),
1437
+ toDbNull(record.httpStatus),
1438
+ toDbNull(record.errorName),
1439
+ toDbNull(record.errorStatus),
1440
+ toDbNull(record.errorMessage),
1441
+ toDbNull(record.upstreamErrorMessageRaw),
1442
+ toDbNull(record.selectionFailureReason),
1443
+ toDbBool(record.affinityHit),
1444
+ toDbNull(record.affinityCacheKey)
1445
+ ];
1446
+ this.insertStmt.run(...args);
1447
+ if (record.costUnits !== void 0 && record.costUnits > 0 && record.accountId) try {
1448
+ getStatsStoreInstance()?.upsertDailyStats({
1449
+ startedAtMs: record.startedAtMs,
1450
+ accountId: record.accountId,
1451
+ costUnits: record.costUnits,
1452
+ tokensTotal: record.tokensTotal ?? 0,
1453
+ hasError: record.errorName !== void 0
1454
+ });
1455
+ } catch {}
1456
+ } catch (error) {
1457
+ warnInsertFailure(error);
1458
+ }
1459
+ }
1460
+ getByRequestId(requestId) {
1461
+ try {
1462
+ return this.getByRequestIdStmt.get(requestId) ?? null;
1463
+ } catch (error) {
1464
+ consola.debug("Failed to fetch request log by request_id", error);
1465
+ return null;
1466
+ }
1467
+ }
1468
+ getLastCompletedUsageBySession(session) {
1469
+ if (!session.promptCacheKey || !session.safetyIdentifier || !session.clientModel) return null;
1470
+ try {
1471
+ const row = this.getLastCompletedUsageBySessionStmt.get(session.promptCacheKey, session.safetyIdentifier, session.clientModel);
1472
+ if (!row || row.tokens_input === null) return null;
1473
+ const tokensOutput = row.tokens_output === null ? void 0 : row.tokens_output;
1474
+ const tokensTotal = row.tokens_total === null ? void 0 : row.tokens_total;
1475
+ const tokensCachedInput = row.tokens_cached_input === null ? void 0 : row.tokens_cached_input;
1476
+ return {
1477
+ tokensInput: row.tokens_input,
1478
+ tokensOutput,
1479
+ tokensTotal,
1480
+ tokensCachedInput
1481
+ };
1482
+ } catch (error) {
1483
+ consola.debug("Failed to fetch last completed usage by session", error);
1484
+ return null;
1485
+ }
1486
+ }
1487
+ query(params) {
1488
+ const limit = Math.max(1, Math.min(params.limit, 200));
1489
+ const where = [];
1490
+ const values = [];
1491
+ if (params.cursorId !== void 0) {
1492
+ where.push("id < ?");
1493
+ values.push(params.cursorId);
1494
+ }
1495
+ if (params.accountId) {
1496
+ where.push("account_id = ?");
1497
+ values.push(params.accountId);
1498
+ }
1499
+ if (params.upstreamModel) {
1500
+ where.push("upstream_model = ?");
1501
+ values.push(params.upstreamModel);
1502
+ }
1503
+ if (params.clientModel) {
1504
+ where.push("client_model = ?");
1505
+ values.push(params.clientModel);
1506
+ }
1507
+ if (params.upstreamEndpoint) {
1508
+ where.push("upstream_endpoint = ?");
1509
+ values.push(params.upstreamEndpoint);
1510
+ }
1511
+ if (params.path) {
1512
+ where.push("path = ?");
1513
+ values.push(params.path);
1514
+ }
1515
+ if (params.status !== void 0) {
1516
+ where.push("http_status = ?");
1517
+ values.push(params.status);
1518
+ }
1519
+ if (params.hasError === true) where.push("http_status >= 400");
1520
+ if (params.hasError === false) where.push("http_status < 400");
1521
+ if (params.fromMs !== void 0) {
1522
+ where.push("started_at_ms >= ?");
1523
+ values.push(params.fromMs);
1524
+ }
1525
+ if (params.toMs !== void 0) {
1526
+ where.push("started_at_ms <= ?");
1527
+ values.push(params.toMs);
1528
+ }
1529
+ const sql = `
1530
+ SELECT *
1531
+ FROM request_log
1532
+ ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
1533
+ ORDER BY id DESC
1534
+ LIMIT ?;
1535
+ `;
1536
+ const rows = this.db.query(sql).all(...values, limit + 1);
1537
+ const items = rows.slice(0, limit);
1538
+ const hasMore = rows.length > limit;
1539
+ return {
1540
+ items,
1541
+ nextCursorId: hasMore ? items.at(-1)?.id : void 0,
1542
+ hasMore
1543
+ };
1544
+ }
1545
+ getAccountStatsSince(sinceMs) {
1546
+ try {
1547
+ const rows = this.db.query(`
1548
+ SELECT
1549
+ account_id,
1550
+ COUNT(*) AS request_count,
1551
+ SUM(CASE WHEN http_status >= 400 THEN 1 ELSE 0 END) AS error_count,
1552
+ COALESCE(SUM(tokens_total), 0) AS tokens_total,
1553
+ COALESCE(AVG(duration_ms), 0) AS avg_duration_ms,
1554
+ COALESCE(MAX(started_at_ms), 0) AS last_request_at_ms
1555
+ FROM request_log
1556
+ WHERE started_at_ms >= ? AND account_id IS NOT NULL
1557
+ GROUP BY account_id;
1558
+ `).all(sinceMs);
1559
+ const map = {};
1560
+ for (const row of rows) map[row.account_id] = row;
1561
+ return map;
1562
+ } catch (error) {
1563
+ consola.debug("Failed to fetch account stats", error);
1564
+ return {};
1565
+ }
1566
+ }
1567
+ cleanupRetention(retentionDays = DEFAULT_RETENTION_DAYS, maxRows = DEFAULT_MAX_ROWS) {
1568
+ try {
1569
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
1570
+ this.db.query("DELETE FROM request_log WHERE started_at_ms < ?;").run(cutoffMs);
1571
+ if (this.db.query("SELECT COUNT(*) AS c FROM request_log;").get().c <= maxRows) return;
1572
+ const offset = maxRows - 1;
1573
+ const thresholdId = this.db.query("SELECT id FROM request_log ORDER BY id DESC LIMIT 1 OFFSET ?;").get(offset)?.id;
1574
+ if (!thresholdId) return;
1575
+ this.db.query("DELETE FROM request_log WHERE id < ?;").run(thresholdId);
1576
+ } catch (error) {
1577
+ consola.debug("Failed to cleanup request_log retention", error);
1578
+ }
1579
+ }
1580
+ meta() {
1581
+ return {
1582
+ dbPath: getAdminDbPath(),
1583
+ userVersion: getAdminDbUserVersion(this.db),
1584
+ retentionDays: DEFAULT_RETENTION_DAYS,
1585
+ maxRows: DEFAULT_MAX_ROWS
1586
+ };
1587
+ }
1588
+ };
1589
+ const STORE_INIT_WARN_THROTTLE_MS = 3e4;
1590
+ const STORE_INIT_RETRY_DELAY_MS = 3e4;
1591
+ let lastStoreInitWarnAtMs = 0;
1592
+ let suppressedStoreInitWarnCount = 0;
1593
+ let nextStoreRetryAtMs = 0;
1594
+ function warnStoreInitFailure(error) {
1595
+ const now = Date.now();
1596
+ if (now - lastStoreInitWarnAtMs < STORE_INIT_WARN_THROTTLE_MS) {
1597
+ suppressedStoreInitWarnCount++;
1598
+ return;
1599
+ }
1600
+ const suppressed = suppressedStoreInitWarnCount;
1601
+ suppressedStoreInitWarnCount = 0;
1602
+ lastStoreInitWarnAtMs = now;
1603
+ const suffix = suppressed > 0 ? ` (suppressed ${suppressed} similar errors)` : "";
1604
+ consola.warn(`Request history store is disabled${suffix}`, error);
1605
+ }
1606
+ const disabledStore = {
1607
+ insert: () => {},
1608
+ getByRequestId: () => null,
1609
+ getLastCompletedUsageBySession: () => null,
1610
+ query: () => ({
1611
+ items: [],
1612
+ hasMore: false
1613
+ }),
1614
+ getAccountStatsSince: () => ({}),
1615
+ cleanupRetention: () => {},
1616
+ meta: () => ({
1617
+ dbPath: getAdminDbPath(),
1618
+ userVersion: 0,
1619
+ retentionDays: DEFAULT_RETENTION_DAYS,
1620
+ maxRows: DEFAULT_MAX_ROWS
1621
+ })
1622
+ };
1623
+ let sharedStore = null;
1624
+ let maintenanceStarted = false;
1625
+ let sharedStatsStore = null;
1626
+ function getStatsStoreInstance() {
1627
+ if (sharedStatsStore) return sharedStatsStore;
1628
+ try {
1629
+ sharedStatsStore = new StatsStore(getAdminDb());
1630
+ return sharedStatsStore;
1631
+ } catch {
1632
+ return null;
1633
+ }
1634
+ }
1635
+ function getRequestHistoryStore() {
1636
+ if (sharedStore) return sharedStore;
1637
+ const now = Date.now();
1638
+ if (now < nextStoreRetryAtMs) return disabledStore;
1639
+ try {
1640
+ sharedStore = new RequestHistoryStore(getAdminDb());
1641
+ if (!maintenanceStarted) {
1642
+ maintenanceStarted = true;
1643
+ sharedStore.cleanupRetention();
1644
+ getStatsStoreInstance()?.cleanupStatsRetention();
1645
+ setInterval(() => {
1646
+ sharedStore?.cleanupRetention();
1647
+ try {
1648
+ getStatsStoreInstance()?.cleanupStatsRetention();
1649
+ } catch {}
1650
+ }, 1440 * 60 * 1e3);
1651
+ }
1652
+ return sharedStore;
1653
+ } catch (error) {
1654
+ nextStoreRetryAtMs = now + STORE_INIT_RETRY_DELAY_MS;
1655
+ warnStoreInitFailure(error);
1656
+ return disabledStore;
1657
+ }
1658
+ }
1659
+ function extractResponsesUsageFromStreamEvent(event) {
1660
+ if (event.type === "response.completed" || event.type === "response.incomplete") return normalizeResponsesUsage(event.response.usage);
1661
+ if (event.type === "response.failed") return normalizeResponsesUsage(event.response.usage);
1662
+ return {};
1663
+ }
1664
+ function extractResponsesUsageFromResult(result) {
1665
+ return normalizeResponsesUsage(result.usage);
1666
+ }
1667
+ function getStatsStore() {
1668
+ return getStatsStoreInstance();
1669
+ }
1670
+
1671
+ //#endregion
1672
+ //#region src/lib/session-affinity-store.ts
1673
+ const DAY_MS = 1440 * 60 * 1e3;
1674
+ const DEFAULT_MAX_AGE_MS = 7 * DAY_MS;
1675
+ const CLEANUP_INTERVAL_MS = DAY_MS;
1676
+ const maybeUnref = (timer) => {
1677
+ timer.unref();
1678
+ };
1679
+ var SessionAffinityStore = class {
1680
+ db;
1681
+ constructor(db) {
1682
+ this.db = db;
1683
+ }
1684
+ get(cacheKey) {
1685
+ let row;
1686
+ try {
1687
+ row = this.db.query("SELECT account_id FROM session_affinity WHERE cache_key = ? LIMIT 1;").get(cacheKey);
1688
+ } catch (error) {
1689
+ consola.warn("Failed to read session affinity mapping:", error);
1690
+ return;
1691
+ }
1692
+ if (!row?.account_id) return;
1693
+ try {
1694
+ this.db.query("UPDATE session_affinity SET last_used_at_ms = ? WHERE cache_key = ?;").run(Date.now(), cacheKey);
1695
+ } catch (error) {
1696
+ consola.warn("Failed to update session affinity last_used_at_ms:", error);
1697
+ }
1698
+ return row.account_id;
1699
+ }
1700
+ set(cacheKey, accountId) {
1701
+ const now = Date.now();
1702
+ try {
1703
+ this.db.query(`
1704
+ INSERT INTO session_affinity (
1705
+ cache_key,
1706
+ account_id,
1707
+ created_at_ms,
1708
+ last_confirmed_at_ms,
1709
+ last_used_at_ms
1710
+ ) VALUES (?, ?, ?, ?, ?)
1711
+ ON CONFLICT(cache_key) DO UPDATE SET
1712
+ account_id = excluded.account_id,
1713
+ last_confirmed_at_ms = excluded.last_confirmed_at_ms,
1714
+ last_used_at_ms = excluded.last_used_at_ms;
1715
+ `).run(cacheKey, accountId, now, now, now);
1716
+ } catch (error) {
1717
+ consola.warn("Failed to persist session affinity mapping:", error);
1718
+ }
1719
+ }
1720
+ delete(cacheKey) {
1721
+ try {
1722
+ this.db.query("DELETE FROM session_affinity WHERE cache_key = ?;").run(cacheKey);
1723
+ } catch (error) {
1724
+ consola.warn("Failed to delete session affinity mapping:", error);
1725
+ }
1726
+ }
1727
+ clear() {
1728
+ try {
1729
+ this.db.query("DELETE FROM session_affinity;").run();
1730
+ } catch (error) {
1731
+ consola.warn("Failed to clear session affinity mappings:", error);
1732
+ }
1733
+ }
1734
+ cleanup(maxAgeMs = DEFAULT_MAX_AGE_MS) {
1735
+ try {
1736
+ this.db.query("DELETE FROM session_affinity WHERE last_used_at_ms < ?;").run(Date.now() - maxAgeMs);
1737
+ } catch (error) {
1738
+ consola.warn("Failed to cleanup session affinity mappings:", error);
1739
+ }
1740
+ }
1741
+ };
1742
+ let sharedSessionAffinityStore = null;
1743
+ let sharedCleanupInterval;
1744
+ function clearSharedSessionAffinityCleanup() {
1745
+ if (!sharedCleanupInterval) return;
1746
+ clearInterval(sharedCleanupInterval);
1747
+ sharedCleanupInterval = void 0;
1748
+ }
1749
+ function getSharedSessionAffinityStore() {
1750
+ if (!sharedSessionAffinityStore) sharedSessionAffinityStore = new SessionAffinityStore(getAdminDb());
1751
+ return sharedSessionAffinityStore;
1752
+ }
1753
+ function applySharedSessionAffinityRetention(retentionMs = getSessionAffinityRetentionMs()) {
1754
+ clearSharedSessionAffinityCleanup();
1755
+ if (!Number.isFinite(retentionMs) || retentionMs <= 0) return;
1756
+ try {
1757
+ const store = getSharedSessionAffinityStore();
1758
+ store.cleanup(retentionMs);
1759
+ sharedCleanupInterval = setInterval(() => {
1760
+ store.cleanup(retentionMs);
1761
+ }, CLEANUP_INTERVAL_MS);
1762
+ maybeUnref(sharedCleanupInterval);
1763
+ } catch (error) {
1764
+ consola.warn("Failed to apply session affinity retention:", error);
1765
+ }
1766
+ }
1767
+
1768
+ //#endregion
1769
+ //#region src/lib/session-ownership.ts
1770
+ const DEFAULT_MAX_ENTRIES = 1e4;
1771
+ const DEFAULT_TTL_MS = 3600 * 1e3;
1772
+ /**
1773
+ * In-memory TTL/LRU cache for root-session ownership.
1774
+ *
1775
+ * Uses Map insertion order for eviction: read/write hits move an entry to the
1776
+ * newest position, and writes refresh TTL.
1777
+ */
1778
+ var SessionOwnershipCache = class {
1779
+ cache = /* @__PURE__ */ new Map();
1780
+ maxEntries;
1781
+ ttlMs;
1782
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES, ttlMs = DEFAULT_TTL_MS) {
1783
+ this.maxEntries = maxEntries;
1784
+ this.ttlMs = ttlMs;
1785
+ }
1786
+ get(rootSessionId) {
1787
+ const entry = this.cache.get(rootSessionId);
1788
+ if (!entry) return;
1789
+ if (Date.now() >= entry.expiresAt) {
1790
+ this.cache.delete(rootSessionId);
1791
+ return;
1792
+ }
1793
+ this.cache.delete(rootSessionId);
1794
+ this.cache.set(rootSessionId, entry);
1795
+ return entry.accountId;
1796
+ }
1797
+ set(rootSessionId, accountId) {
1798
+ this.cache.delete(rootSessionId);
1799
+ while (this.cache.size >= this.maxEntries) {
1800
+ const oldest = this.cache.keys().next();
1801
+ if (oldest.done) break;
1802
+ this.cache.delete(oldest.value);
1803
+ }
1804
+ this.cache.set(rootSessionId, {
1805
+ accountId,
1806
+ expiresAt: Date.now() + this.ttlMs
1807
+ });
1808
+ }
1809
+ clear() {
1810
+ this.cache.clear();
1811
+ }
1812
+ };
1813
+
1814
+ //#endregion
1815
+ //#region src/lib/accounts-manager.ts
1816
+ /** Quota cache TTL in milliseconds (45 seconds) for pre-request selection. */
1817
+ const QUOTA_CACHE_TTL = 45 * 1e3;
1818
+ /** Debounce delay for registry reload in milliseconds */
1819
+ const RELOAD_DEBOUNCE_MS = 500;
1820
+ /** Registry watcher restart initial delay in milliseconds */
1821
+ const WATCHER_RESTART_INITIAL_DELAY_MS = 1e3;
1822
+ /** Registry watcher restart max delay in milliseconds */
1823
+ const WATCHER_RESTART_MAX_DELAY_MS = 60 * 1e3;
1824
+ /** Session refresh base interval in milliseconds. */
1825
+ const SESSION_REFRESH_BASE_MS = 3600 * 1e3;
1826
+ /** Session refresh jitter window in milliseconds. */
1827
+ const SESSION_REFRESH_JITTER_MS = 1200 * 1e3;
1828
+ /** Minimum delay between account initializations in milliseconds. */
1829
+ const INIT_STAGGER_MIN_MS = 2e3;
1830
+ /** Maximum delay between account initializations in milliseconds. */
1831
+ const INIT_STAGGER_MAX_MS = 5e3;
1832
+ /** Random jitter window added to token refresh delay in milliseconds. */
1833
+ const TOKEN_REFRESH_JITTER_MS = 3e4;
1834
+ function getInitialSelectionReason(cacheKey, rotationStart) {
1835
+ if (!cacheKey) return "no_session_key";
1836
+ return rotationStart > 0 ? "rotated_after_miss" : "affinity_miss";
1837
+ }
1838
+ function preserveSubagentSelectionReason(initialSelectionReason, nextSelectionReason) {
1839
+ return initialSelectionReason.startsWith("subagent_") ? initialSelectionReason : nextSelectionReason;
1840
+ }
1841
+ /** Manages multiple GitHub Copilot accounts at runtime. */
1842
+ var AccountsManager = class {
1843
+ accounts = /* @__PURE__ */ new Map();
1844
+ accountOrder = [];
1845
+ temporaryAccount;
1846
+ vsCodeVersion;
1847
+ accountAffinityEnabled = true;
1848
+ affinityCache;
1849
+ sessionOwnership = new SessionOwnershipCache();
1850
+ sessionOwnershipGeneration = 0;
1851
+ loadBalanceCursor = 0;
1852
+ constructor(options = {}) {
1853
+ const { persistentAffinityStore } = options;
1854
+ this.affinityCache = new AccountAffinityCache(void 0, void 0, persistentAffinityStore);
1855
+ }
1856
+ quotaRefreshSnapshotByAccount = /* @__PURE__ */ new WeakMap();
1857
+ modelsRefreshSnapshotByAccount = /* @__PURE__ */ new WeakMap();
1858
+ tokenRefreshEnabledAccounts = /* @__PURE__ */ new WeakSet();
1859
+ modelsRefreshTimer;
1860
+ modelsRefreshIntervalMs = 0;
1861
+ registryWatcher;
1862
+ reloadDebounceTimer;
1863
+ registryWatcherRestartTimer;
1864
+ registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
1865
+ isReloading = false;
1866
+ currentReloadPromise;
1867
+ /** Initialize accounts manager (load registry, migrate legacy token). */
1868
+ async initialize(vsCodeVersion) {
1869
+ this.vsCodeVersion = vsCodeVersion;
1870
+ const hasReg = await hasRegistry();
1871
+ const hasLegacy = await hasLegacyToken();
1872
+ if (!hasReg && hasLegacy) await this.migrateLegacyToken();
1873
+ const accountMetas = await listAccountsFromRegistry();
1874
+ for (const meta of accountMetas) {
1875
+ const token = await loadAccountToken(meta.id);
1876
+ if (!token) {
1877
+ consola.warn(`No token found for account ${meta.id}, skipping`);
1878
+ continue;
1879
+ }
1880
+ const runtime = {
1881
+ ...meta,
1882
+ accountLogin: meta.id,
1883
+ githubToken: token,
1884
+ vsCodeVersion: this.vsCodeVersion
1885
+ };
1886
+ this.accounts.set(meta.id, runtime);
1887
+ this.accountOrder.push(meta.id);
1888
+ }
1889
+ let isFirstAccount = true;
1890
+ for (const account of this.accounts.values()) {
1891
+ if (!isFirstAccount) {
1892
+ const staggerDelay = INIT_STAGGER_MIN_MS + Math.floor(Math.random() * (INIT_STAGGER_MAX_MS - INIT_STAGGER_MIN_MS));
1893
+ consola.debug(`Staggering initialization of account ${account.id} by ${staggerDelay}ms`);
1894
+ await new Promise((resolve) => setTimeout(resolve, staggerDelay));
1895
+ }
1896
+ isFirstAccount = false;
1897
+ try {
1898
+ await this.initializeAccount(account);
1899
+ } catch (error) {
1900
+ consola.error(`Failed to initialize account ${account.id}:`, error);
1901
+ account.failed = true;
1902
+ account.failureReason = String(error);
1903
+ }
1904
+ }
1905
+ consola.info(`Loaded ${this.accounts.size} account(s)`);
1906
+ this.startRegistryWatcher();
1907
+ }
1908
+ setAccountAffinityEnabled(enabled) {
1909
+ this.accountAffinityEnabled = enabled;
1910
+ if (!enabled) this.affinityCache.clearMemory();
1911
+ }
1912
+ setModelsRefreshIntervalMs(intervalMs) {
1913
+ this.modelsRefreshIntervalMs = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 0;
1914
+ this.scheduleModelsRefresh();
1915
+ }
1916
+ computeTokenRefreshDelayMs(refreshInSeconds) {
1917
+ const baseDelay = Math.max((refreshInSeconds - 60) * 1e3, 1e3);
1918
+ const jitter = Math.floor(Math.random() * TOKEN_REFRESH_JITTER_MS);
1919
+ const maxSafeDelay = Math.max(refreshInSeconds * 1e3 - 1e3, 1e3);
1920
+ return Math.min(baseDelay + jitter, maxSafeDelay);
1921
+ }
1922
+ computeSessionRefreshDelayMs() {
1923
+ return SESSION_REFRESH_BASE_MS + Math.floor(Math.random() * SESSION_REFRESH_JITTER_MS);
1924
+ }
1925
+ resolveAccountLogin(account) {
1926
+ return account.accountLogin ?? account.id;
1927
+ }
1928
+ commitAccountIdentity(account, { identityKey, login, deviceId, machineId }) {
1929
+ account.accountLogin = login;
1930
+ account.identityKey = identityKey;
1931
+ account.clientDeviceId = deviceId;
1932
+ account.clientMachineId = machineId;
1933
+ }
1934
+ async applyAccountIdentity(account) {
1935
+ const login = this.resolveAccountLogin(account);
1936
+ const { oauthApp, enterpriseDomain } = getCurrentIdentityEnvironment();
1937
+ const identityKey = buildIdentityKey({
1938
+ login,
1939
+ oauthApp,
1940
+ enterpriseDomain
1941
+ });
1942
+ const identity = await ensureAccountClientIdentity({
1943
+ login,
1944
+ oauthApp,
1945
+ enterpriseDomain
1946
+ });
1947
+ this.commitAccountIdentity(account, {
1948
+ identityKey,
1949
+ login,
1950
+ deviceId: identity.deviceId,
1951
+ machineId: identity.machineId
1952
+ });
1953
+ if (!account.clientSessionId) {
1954
+ account.clientSessionId = createAccountSessionId();
1955
+ consola.debug(`Generated VSCode session ID for account ${account.id}: ${account.clientSessionId}`);
1956
+ }
1957
+ this.startSessionRefresh(account);
1958
+ }
1959
+ shouldContinueTokenRefresh(account, snapshot) {
1960
+ return this.tokenRefreshEnabledAccounts.has(account) && isAuthSnapshotCurrent(account, snapshot);
1961
+ }
1962
+ async runTokenRefreshTick(account, snapshot, refreshInSeconds) {
1963
+ if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
1964
+ try {
1965
+ const { token, refresh_in } = await getCopilotToken(toAccountContextFromSnapshot(account, snapshot));
1966
+ if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
1967
+ if (!applyTokenRefreshSuccessIfCurrent(account, snapshot, token)) return;
1968
+ consola.debug(`Refreshed token for account ${account.id}`);
1969
+ if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
1970
+ this.startTokenRefresh(account, refresh_in);
1971
+ } catch (error) {
1972
+ consola.error(`Failed to refresh token for ${account.id}:`, error);
1973
+ if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
1974
+ applyTokenRefreshFailureIfCurrent(account, snapshot, error);
1975
+ if (!this.shouldContinueTokenRefresh(account, snapshot)) return;
1976
+ this.startTokenRefresh(account, refreshInSeconds);
1977
+ }
1978
+ }
1979
+ finalizeQuotaRefreshPromise(account, promise) {
1980
+ if (account.quotaRefreshPromise !== promise) return;
1981
+ account.isRefreshingQuota = false;
1982
+ account.quotaRefreshPromise = void 0;
1983
+ this.quotaRefreshSnapshotByAccount.delete(account);
1984
+ }
1985
+ /** Initialize a single account. */
1986
+ async initializeAccount(account) {
1987
+ await this.applyAccountIdentity(account);
1988
+ const snapshot = takeAuthSnapshot(account);
1989
+ try {
1990
+ const { token, refresh_in } = await getCopilotToken(toAccountContextFromSnapshot(account, snapshot));
1991
+ if (!applyCopilotTokenIfCurrent(account, snapshot, token)) return;
1992
+ await this.refreshQuota(account);
1993
+ this.startTokenRefresh(account, refresh_in);
1994
+ if (!applyModelsIfCurrent(account, snapshot, await getModels(toAccountContextFromSnapshot(account, snapshot, token)))) return;
1995
+ account.lastModelsFetch = Date.now();
1996
+ consola.debug(`Account ${account.id} initialized`);
1997
+ } catch (error) {
1998
+ if (!isAuthSnapshotCurrent(account, snapshot)) return;
1999
+ throw error;
2000
+ }
2001
+ }
2002
+ /** Migrate legacy github_token to the new multi-account system. */
2003
+ async migrateLegacyToken() {
2004
+ const token = await readLegacyToken();
2005
+ if (!token) return;
2006
+ try {
2007
+ const id = (await getGitHubUser({
2008
+ githubToken: token,
2009
+ accountType: "individual"
2010
+ })).login;
2011
+ await saveAccountToken(id, token);
2012
+ await addAccountToRegistry({
2013
+ id,
2014
+ accountType: "individual",
2015
+ addedAt: Date.now()
2016
+ });
2017
+ consola.info(`Migrated legacy token to account: ${id}`);
2018
+ } catch (error) {
2019
+ consola.error("Failed to migrate legacy token:", error);
2020
+ }
2021
+ }
2022
+ /** Start token refresh timer for an account. */
2023
+ startTokenRefresh(account, refreshInSeconds) {
2024
+ this.stopTokenRefresh(account);
2025
+ this.tokenRefreshEnabledAccounts.add(account);
2026
+ const snapshot = takeAuthSnapshot(account);
2027
+ const delayMs = this.computeTokenRefreshDelayMs(refreshInSeconds);
2028
+ account.refreshTimer = setTimeout(() => {
2029
+ this.runTokenRefreshTick(account, snapshot, refreshInSeconds);
2030
+ }, delayMs);
2031
+ }
2032
+ /** Stop token refresh timer for an account. */
2033
+ stopTokenRefresh(account) {
2034
+ this.tokenRefreshEnabledAccounts.delete(account);
2035
+ if (account.refreshTimer) {
2036
+ clearTimeout(account.refreshTimer);
2037
+ account.refreshTimer = void 0;
2038
+ }
2039
+ }
2040
+ /** Stop all token refresh timers. */
2041
+ stopAllTokenRefresh() {
2042
+ for (const account of this.accounts.values()) this.stopTokenRefresh(account);
2043
+ if (this.temporaryAccount) this.stopTokenRefresh(this.temporaryAccount);
2044
+ }
2045
+ startSessionRefresh(account) {
2046
+ this.stopSessionRefresh(account);
2047
+ const delayMs = this.computeSessionRefreshDelayMs();
2048
+ consola.debug(`Scheduling next VSCode session ID refresh for ${account.id} in ${Math.round(delayMs / 1e3)} seconds`);
2049
+ account.sessionRefreshTimer = setTimeout(() => {
2050
+ try {
2051
+ account.clientSessionId = createAccountSessionId();
2052
+ consola.debug(`Refreshed VSCode session ID for account ${account.id}: ${account.clientSessionId}`);
2053
+ } catch (error) {
2054
+ consola.error(`Failed to refresh VSCode session ID for ${account.id}, rescheduling...`, error);
2055
+ } finally {
2056
+ this.startSessionRefresh(account);
2057
+ }
2058
+ }, delayMs);
2059
+ }
2060
+ stopSessionRefresh(account) {
2061
+ if (account.sessionRefreshTimer) {
2062
+ clearTimeout(account.sessionRefreshTimer);
2063
+ account.sessionRefreshTimer = void 0;
2064
+ }
2065
+ }
2066
+ stopAllSessionRefresh() {
2067
+ for (const account of this.accounts.values()) this.stopSessionRefresh(account);
2068
+ if (this.temporaryAccount) this.stopSessionRefresh(this.temporaryAccount);
2069
+ }
2070
+ scheduleModelsRefresh() {
2071
+ this.stopModelsRefresh();
2072
+ if (!this.modelsRefreshIntervalMs || this.modelsRefreshIntervalMs <= 0) return;
2073
+ this.modelsRefreshTimer = setTimeout(() => {
2074
+ this.runModelsRefreshTick();
2075
+ }, this.modelsRefreshIntervalMs);
2076
+ }
2077
+ stopModelsRefresh() {
2078
+ if (this.modelsRefreshTimer) {
2079
+ clearTimeout(this.modelsRefreshTimer);
2080
+ this.modelsRefreshTimer = void 0;
2081
+ }
2082
+ }
2083
+ async runModelsRefreshTick() {
2084
+ try {
2085
+ await this.refreshAllModels();
2086
+ } catch (error) {
2087
+ consola.error("Failed to refresh models:", error);
2088
+ } finally {
2089
+ this.scheduleModelsRefresh();
2090
+ }
2091
+ }
2092
+ finalizeModelsRefreshPromise(account, promise) {
2093
+ if (account.modelsRefreshPromise !== promise) return;
2094
+ account.isRefreshingModels = false;
2095
+ account.modelsRefreshPromise = void 0;
2096
+ this.modelsRefreshSnapshotByAccount.delete(account);
2097
+ }
2098
+ async refreshModels(account) {
2099
+ if (!account.copilotToken) {
2100
+ consola.debug(`Skip model refresh for ${account.id}: missing Copilot token`);
2101
+ return;
2102
+ }
2103
+ const snapshot = takeAuthSnapshot(account);
2104
+ if (account.modelsRefreshPromise) {
2105
+ if (isSameAuthSnapshot(this.modelsRefreshSnapshotByAccount.get(account), snapshot)) {
2106
+ await account.modelsRefreshPromise;
2107
+ return;
2108
+ }
2109
+ }
2110
+ account.isRefreshingModels = true;
2111
+ const ctx = toAccountContextFromSnapshot(account, snapshot, account.copilotToken);
2112
+ const promise = (async () => {
2113
+ try {
2114
+ if (applyModelsIfCurrent(account, snapshot, await getModels(ctx))) account.lastModelsFetch = Date.now();
2115
+ } catch (error) {
2116
+ if (error instanceof HTTPError && error.response.status === 401) {
2117
+ applyUnauthorizedIfCurrent(account, snapshot, "Unauthorized (401)");
2118
+ return;
2119
+ }
2120
+ consola.error(`Failed to refresh models for ${account.id}:`, error);
2121
+ }
2122
+ })();
2123
+ account.modelsRefreshPromise = promise;
2124
+ this.modelsRefreshSnapshotByAccount.set(account, snapshot);
2125
+ promise.finally(() => {
2126
+ this.finalizeModelsRefreshPromise(account, promise);
2127
+ });
2128
+ await promise;
2129
+ }
2130
+ async refreshAllModels() {
2131
+ const accounts = [];
2132
+ if (this.temporaryAccount) accounts.push(this.temporaryAccount);
2133
+ for (const id of this.accountOrder) {
2134
+ const account = this.accounts.get(id);
2135
+ if (account) accounts.push(account);
2136
+ }
2137
+ if (accounts.length === 0) return;
2138
+ await Promise.allSettled(accounts.map((account) => this.refreshModels(account)));
2139
+ }
2140
+ /** Refresh quota information for an account. */
2141
+ async refreshQuota(account) {
2142
+ const snapshot = takeAuthSnapshot(account);
2143
+ if (account.quotaRefreshPromise) {
2144
+ if (isSameAuthSnapshot(this.quotaRefreshSnapshotByAccount.get(account), snapshot)) {
2145
+ await account.quotaRefreshPromise;
2146
+ return;
2147
+ }
2148
+ }
2149
+ account.isRefreshingQuota = true;
2150
+ const ctx = toAccountContextFromSnapshot(account, snapshot);
2151
+ const promise = (async () => {
2152
+ try {
2153
+ const usage = await getCopilotUsage(ctx);
2154
+ const premium = usage.quota_snapshots.premium_interactions;
2155
+ if (applyQuotaRefreshSuccessIfCurrent(account, snapshot, {
2156
+ premium,
2157
+ copilotApiUrl: usage.endpoints.api
2158
+ })) try {
2159
+ getStatsStore()?.insertQuotaSnapshot({
2160
+ accountId: account.id,
2161
+ snapshotAtMs: Date.now(),
2162
+ remaining: premium.remaining,
2163
+ entitlement: premium.entitlement,
2164
+ unlimited: premium.unlimited,
2165
+ source: "refresh"
2166
+ });
2167
+ } catch {}
2168
+ } catch (error) {
2169
+ if (error instanceof HTTPError && error.response.status === 401) {
2170
+ applyUnauthorizedIfCurrent(account, snapshot, "Unauthorized (401)");
2171
+ return;
2172
+ }
2173
+ consola.error(`Failed to refresh quota for ${account.id}:`, error);
2174
+ }
2175
+ })();
2176
+ account.quotaRefreshPromise = promise;
2177
+ this.quotaRefreshSnapshotByAccount.set(account, snapshot);
2178
+ promise.finally(() => {
2179
+ this.finalizeQuotaRefreshPromise(account, promise);
2180
+ });
2181
+ await promise;
2182
+ }
2183
+ /** Check if quota cache is expired. */
2184
+ isQuotaCacheExpired(account) {
2185
+ if (!account.lastQuotaFetch) return true;
2186
+ return Date.now() - account.lastQuotaFetch > QUOTA_CACHE_TTL;
2187
+ }
2188
+ isAccountFailed(account) {
2189
+ return account.failed === true;
2190
+ }
2191
+ useOverageFallback(fallback) {
2192
+ const reservation = reservePremiumUnits(fallback.account, fallback.costUnits);
2193
+ return {
2194
+ ok: true,
2195
+ account: fallback.account,
2196
+ selectedModel: fallback.model,
2197
+ endpoint: fallback.endpoint,
2198
+ costUnits: fallback.costUnits,
2199
+ reservation
2200
+ };
2201
+ }
2202
+ isModelSupportedForEndpoint(model, endpoint) {
2203
+ if (endpoint === "/responses") return model.supported_endpoints?.includes(endpoint) ?? false;
2204
+ const supported = model.supported_endpoints;
2205
+ if (!supported) return true;
2206
+ return supported.includes(endpoint);
2207
+ }
2208
+ pickSupportedCandidate(account, candidates) {
2209
+ const models = account.models?.data;
2210
+ if (!models) return null;
2211
+ for (const candidate of candidates) {
2212
+ const model = models.find((m) => m.id === candidate.modelId);
2213
+ if (!model) continue;
2214
+ if (!this.isModelSupportedForEndpoint(model, candidate.endpoint)) continue;
2215
+ return {
2216
+ candidate,
2217
+ model
2218
+ };
2219
+ }
2220
+ return null;
2221
+ }
2222
+ async selectAccountForCandidates(orderedAccounts, candidates) {
2223
+ if (orderedAccounts.length === 0) return {
2224
+ ok: false,
2225
+ reason: "NO_ACCOUNTS"
2226
+ };
2227
+ let supportedCandidateFound = false;
2228
+ let overageFallback;
2229
+ for (const account of orderedAccounts) {
2230
+ if (this.isAccountFailed(account)) continue;
2231
+ const supported = this.pickSupportedCandidate(account, candidates);
2232
+ if (!supported) continue;
2233
+ supportedCandidateFound = true;
2234
+ const { candidate, model } = supported;
2235
+ const costUnits = getCostUnits(model);
2236
+ if (costUnits <= 0) return {
2237
+ ok: true,
2238
+ account,
2239
+ selectedModel: model,
2240
+ endpoint: candidate.endpoint,
2241
+ costUnits
2242
+ };
2243
+ if (!account.unlimited && this.isQuotaCacheExpired(account)) await this.refreshQuota(account);
2244
+ if (this.isAccountFailed(account)) continue;
2245
+ if (account.unlimited) return {
2246
+ ok: true,
2247
+ account,
2248
+ selectedModel: model,
2249
+ endpoint: candidate.endpoint,
2250
+ costUnits
2251
+ };
2252
+ const effectiveRemaining = getEffectivePremiumRemaining(account);
2253
+ if (effectiveRemaining !== void 0 && effectiveRemaining < costUnits) {
2254
+ if (account.overagePermitted && !overageFallback) overageFallback = {
2255
+ account,
2256
+ model,
2257
+ endpoint: candidate.endpoint,
2258
+ costUnits
2259
+ };
2260
+ continue;
2261
+ }
2262
+ const reservation = reservePremiumUnits(account, costUnits);
2263
+ return {
2264
+ ok: true,
2265
+ account,
2266
+ selectedModel: model,
2267
+ endpoint: candidate.endpoint,
2268
+ costUnits,
2269
+ reservation
2270
+ };
2271
+ }
2272
+ if (!supportedCandidateFound) return {
2273
+ ok: false,
2274
+ reason: "MODEL_NOT_SUPPORTED"
2275
+ };
2276
+ return overageFallback ? this.useOverageFallback(overageFallback) : {
2277
+ ok: false,
2278
+ reason: "NO_QUOTA"
2279
+ };
2280
+ }
2281
+ /**
2282
+ * Try to use a preferred (affinity) account for the request.
2283
+ * Returns a successful selection if the account is usable; null otherwise.
2284
+ */
2285
+ async tryAffinityAccount(preferredAccountId, orderedAccounts, candidates) {
2286
+ const account = isAffinityAccountUsable(preferredAccountId, orderedAccounts);
2287
+ if (!account) return null;
2288
+ const supported = this.pickSupportedCandidate(account, candidates) ?? this.pickAliasFallbackCandidate(account, candidates);
2289
+ if (!supported) return null;
2290
+ return this.validateAffinityQuota(account, supported);
2291
+ }
2292
+ /**
2293
+ * Resolve model aliases and try to pick a supported candidate.
2294
+ * Returns null if no alias differs or the account doesn't support the alias.
2295
+ */
2296
+ pickAliasFallbackCandidate(account, candidates) {
2297
+ const aliasCandidates = candidates.map((candidate) => {
2298
+ const modelId = resolveModelAlias(candidate.modelId);
2299
+ if (modelId === candidate.modelId) return candidate;
2300
+ return {
2301
+ ...candidate,
2302
+ modelId
2303
+ };
2304
+ });
2305
+ if (!aliasCandidates.some((candidate, index) => candidate.modelId !== candidates[index].modelId)) return null;
2306
+ return this.pickSupportedCandidate(account, aliasCandidates);
2307
+ }
2308
+ /**
2309
+ * Validate quota for an affinity candidate. Free models pass immediately;
2310
+ * premium models go through quota refresh / reservation.
2311
+ */
2312
+ async validateAffinityQuota(account, supported) {
2313
+ const { candidate, model } = supported;
2314
+ const costUnits = getCostUnits(model);
2315
+ if (costUnits <= 0) return {
2316
+ ok: true,
2317
+ account,
2318
+ selectedModel: model,
2319
+ endpoint: candidate.endpoint,
2320
+ costUnits
2321
+ };
2322
+ if (!account.unlimited && this.isQuotaCacheExpired(account)) await this.refreshQuota(account);
2323
+ if (this.isAccountFailed(account)) return null;
2324
+ if (account.unlimited) return {
2325
+ ok: true,
2326
+ account,
2327
+ selectedModel: model,
2328
+ endpoint: candidate.endpoint,
2329
+ costUnits
2330
+ };
2331
+ const effectiveRemaining = getEffectivePremiumRemaining(account);
2332
+ if (effectiveRemaining !== void 0 && effectiveRemaining < costUnits) return null;
2333
+ const reservation = reservePremiumUnits(account, costUnits);
2334
+ return {
2335
+ ok: true,
2336
+ account,
2337
+ selectedModel: model,
2338
+ endpoint: candidate.endpoint,
2339
+ costUnits,
2340
+ reservation
2341
+ };
2342
+ }
2343
+ /**
2344
+ * Select an available account for a specific request (model + endpoint).
2345
+ * When account affinity is enabled, routes to the previously successful account
2346
+ * for the same affinity key + model combination.
2347
+ * Uses reservation to avoid oversubscribing premium quota under concurrency.
2348
+ */
2349
+ async selectAccountForRequest(candidates, context) {
2350
+ if (candidates.length === 0) throw new Error("selectAccountForRequest requires at least one candidate");
2351
+ const orderedAccounts = this.getOrderedEnabledAccounts();
2352
+ const ownerSelection = await this.selectPreferredSessionOwner({
2353
+ lookupSessionId: context?.ownershipLookupSessionId,
2354
+ orderedAccounts,
2355
+ candidates
2356
+ });
2357
+ if (ownerSelection.result) {
2358
+ this.attachConfirmOwnership(ownerSelection.result, context?.ownershipWriteSessionId);
2359
+ return ownerSelection.result;
2360
+ }
2361
+ const affinityPlan = this.buildAffinitySelectionPlan({
2362
+ orderedAccounts,
2363
+ candidates,
2364
+ context,
2365
+ ownerSelectionReason: ownerSelection.selectionReason
2366
+ });
2367
+ const preferredSelection = await this.selectPreferredAffinityAccount({
2368
+ cacheKey: affinityPlan.cacheKey,
2369
+ orderedAccounts,
2370
+ candidates,
2371
+ initialSelectionReason: affinityPlan.initialSelectionReason
2372
+ });
2373
+ if (preferredSelection.result) {
2374
+ this.attachConfirmOwnership(preferredSelection.result, context?.ownershipWriteSessionId);
2375
+ return preferredSelection.result;
2376
+ }
2377
+ let accountsForSelection = affinityPlan.defaultAccountsForSelection;
2378
+ let premiumRemainingOrderedAccountIds = /* @__PURE__ */ new Set();
2379
+ let selectionReason = preferredSelection.selectionReason;
2380
+ if (affinityPlan.canReorderOnAffinityCacheMiss && preferredSelection.affinityCacheMiss) {
2381
+ const affinityMissOrder = this.orderAccountsForAffinityCacheMiss(orderedAccounts);
2382
+ accountsForSelection = affinityMissOrder.accounts;
2383
+ premiumRemainingOrderedAccountIds = affinityMissOrder.premiumRemainingOrderedAccountIds;
2384
+ }
2385
+ const result = await this.selectWithAliasFallback(accountsForSelection, candidates);
2386
+ if (result.ok && premiumRemainingOrderedAccountIds.has(result.account.id)) selectionReason = "affinity_miss";
2387
+ return this.finalizeSelectedAccount({
2388
+ result,
2389
+ cacheKey: affinityPlan.cacheKey,
2390
+ selectionReason,
2391
+ ownershipWriteSessionId: context?.ownershipWriteSessionId
2392
+ });
2393
+ }
2394
+ getOrderedEnabledAccounts() {
2395
+ return [...this.temporaryAccount ? [this.temporaryAccount] : [], ...this.accountOrder.map((id) => this.accounts.get(id)).filter((account) => account !== void 0)].filter((account) => isAccountEnabled(account));
2396
+ }
2397
+ buildAffinitySelectionPlan(params) {
2398
+ const { orderedAccounts, candidates, context, ownerSelectionReason } = params;
2399
+ const affinityKey = this.accountAffinityEnabled && context ? extractAffinityKey(context) : void 0;
2400
+ const modelKey = context?.affinityModelId ?? candidates[0].modelId;
2401
+ const cacheKey = affinityKey ? buildAffinityCacheKey(affinityKey, modelKey) : void 0;
2402
+ const shouldRotate = this.accountAffinityEnabled && orderedAccounts.length > 1;
2403
+ const rotationStart = shouldRotate ? this.loadBalanceCursor % orderedAccounts.length : 0;
2404
+ return {
2405
+ cacheKey,
2406
+ defaultAccountsForSelection: shouldRotate ? this.rotateAccounts(orderedAccounts) : orderedAccounts,
2407
+ initialSelectionReason: ownerSelectionReason ?? getInitialSelectionReason(cacheKey, rotationStart),
2408
+ canReorderOnAffinityCacheMiss: ownerSelectionReason === void 0 && cacheKey !== void 0
2409
+ };
2410
+ }
2411
+ finalizeSelectedAccount(params) {
2412
+ const { result, cacheKey, selectionReason, ownershipWriteSessionId } = params;
2413
+ if (!result.ok) return {
2414
+ ...result,
2415
+ selectionReason
2416
+ };
2417
+ this.loadBalanceCursor++;
2418
+ result.selectionReason = selectionReason;
2419
+ result.affinityCacheKey = cacheKey;
2420
+ if (cacheKey) result.confirmAffinity = () => {
2421
+ if (!this.accountAffinityEnabled) return;
2422
+ this.affinityCache.set(cacheKey, result.account.id);
2423
+ };
2424
+ this.attachConfirmOwnership(result, ownershipWriteSessionId);
2425
+ return result;
2426
+ }
2427
+ attachConfirmOwnership(result, ownershipWriteSessionId) {
2428
+ const rootSessionId = ownershipWriteSessionId?.trim();
2429
+ if (!rootSessionId) return;
2430
+ const generation = this.sessionOwnershipGeneration;
2431
+ result.confirmOwnership = () => {
2432
+ if (generation !== this.sessionOwnershipGeneration) return;
2433
+ this.sessionOwnership.set(rootSessionId, result.account.id);
2434
+ };
2435
+ }
2436
+ async selectPreferredSessionOwner(params) {
2437
+ const { lookupSessionId, orderedAccounts, candidates } = params;
2438
+ if (lookupSessionId === void 0) return {};
2439
+ const rootSessionId = lookupSessionId.trim();
2440
+ if (!rootSessionId) return { selectionReason: "subagent_marker_invalid_fallback" };
2441
+ const preferredAccountId = this.sessionOwnership.get(rootSessionId);
2442
+ if (!preferredAccountId) return { selectionReason: "subagent_owner_miss" };
2443
+ const ownerResult = await this.tryAffinityAccount(preferredAccountId, orderedAccounts, candidates);
2444
+ if (!ownerResult) return { selectionReason: "subagent_owner_unusable_fallback" };
2445
+ ownerResult.selectionReason = "subagent_owner_hit";
2446
+ return { result: ownerResult };
2447
+ }
2448
+ /**
2449
+ * Try the preferred account from the affinity cache before normal selection.
2450
+ */
2451
+ async selectPreferredAffinityAccount(params) {
2452
+ const { cacheKey, orderedAccounts, candidates, initialSelectionReason } = params;
2453
+ if (!cacheKey) return {
2454
+ selectionReason: initialSelectionReason,
2455
+ affinityCacheMiss: false
2456
+ };
2457
+ const preferredId = this.affinityCache.get(cacheKey);
2458
+ if (!preferredId) return {
2459
+ selectionReason: initialSelectionReason,
2460
+ affinityCacheMiss: true
2461
+ };
2462
+ const affinityResult = await this.tryAffinityAccount(preferredId, orderedAccounts, candidates);
2463
+ if (!affinityResult) {
2464
+ this.affinityCache.delete(cacheKey);
2465
+ return {
2466
+ selectionReason: preserveSubagentSelectionReason(initialSelectionReason, "preferred_account_unavailable"),
2467
+ affinityCacheMiss: false
2468
+ };
2469
+ }
2470
+ const selectionReason = preserveSubagentSelectionReason(initialSelectionReason, "affinity_hit");
2471
+ affinityResult.affinityHit = true;
2472
+ affinityResult.affinityCacheKey = cacheKey;
2473
+ affinityResult.selectionReason = selectionReason;
2474
+ affinityResult.confirmAffinity = () => {
2475
+ if (!this.accountAffinityEnabled) return;
2476
+ this.affinityCache.set(cacheKey, affinityResult.account.id);
2477
+ };
2478
+ return {
2479
+ result: affinityResult,
2480
+ selectionReason,
2481
+ affinityCacheMiss: false
2482
+ };
2483
+ }
2484
+ /**
2485
+ * Rotate the accounts array by the current load-balance cursor for round-robin distribution.
2486
+ * This ensures cache-miss requests are spread across accounts instead of always hitting the first.
2487
+ */
2488
+ rotateAccounts(accounts) {
2489
+ const start = this.loadBalanceCursor % accounts.length;
2490
+ if (start === 0) return accounts;
2491
+ return [...accounts.slice(start), ...accounts.slice(0, start)];
2492
+ }
2493
+ orderAccountsForAffinityCacheMiss(orderedAccounts) {
2494
+ const scoredAccounts = orderedAccounts.map((account) => ({
2495
+ account,
2496
+ effectiveRemaining: getEffectivePremiumRemaining(account)
2497
+ })).filter((entry) => entry.effectiveRemaining !== void 0).sort((left, right) => right.effectiveRemaining - left.effectiveRemaining);
2498
+ if (scoredAccounts.length > 0) {
2499
+ const scoredAccountsInSelectionOrder = this.shuffleWithinEqualRemainingBuckets(scoredAccounts).map(({ account }) => account);
2500
+ const scoredAccountSet = new Set(scoredAccountsInSelectionOrder);
2501
+ const unknownQuotaAccounts$1 = orderedAccounts.filter((account) => !scoredAccountSet.has(account) && !account.unlimited);
2502
+ const unlimitedAccounts$1 = orderedAccounts.filter((account) => !scoredAccountSet.has(account) && account.unlimited);
2503
+ return {
2504
+ accounts: [
2505
+ ...scoredAccountsInSelectionOrder,
2506
+ ...unknownQuotaAccounts$1,
2507
+ ...unlimitedAccounts$1
2508
+ ],
2509
+ premiumRemainingOrderedAccountIds: new Set(scoredAccountsInSelectionOrder.map((account) => account.id))
2510
+ };
2511
+ }
2512
+ const premiumRemainingOrderedAccountIds = /* @__PURE__ */ new Set();
2513
+ const unlimitedAccounts = orderedAccounts.filter((account) => account.unlimited);
2514
+ if (unlimitedAccounts.length === 0) return {
2515
+ accounts: orderedAccounts,
2516
+ premiumRemainingOrderedAccountIds
2517
+ };
2518
+ const unknownQuotaAccounts = orderedAccounts.filter((account) => !account.unlimited);
2519
+ return {
2520
+ accounts: [...this.shuffleArray(unlimitedAccounts), ...unknownQuotaAccounts],
2521
+ premiumRemainingOrderedAccountIds
2522
+ };
2523
+ }
2524
+ shuffleWithinEqualRemainingBuckets(scoredAccounts) {
2525
+ const shuffled = [];
2526
+ let index = 0;
2527
+ while (index < scoredAccounts.length) {
2528
+ const currentRemaining = scoredAccounts[index].effectiveRemaining;
2529
+ let bucketEnd = index + 1;
2530
+ while (bucketEnd < scoredAccounts.length && scoredAccounts[bucketEnd].effectiveRemaining === currentRemaining) bucketEnd++;
2531
+ shuffled.push(...this.shuffleArray(scoredAccounts.slice(index, bucketEnd)));
2532
+ index = bucketEnd;
2533
+ }
2534
+ return shuffled;
2535
+ }
2536
+ shuffleArray(items) {
2537
+ if (items.length <= 1) return items;
2538
+ const shuffled = [...items];
2539
+ for (let index = shuffled.length - 1; index > 0; index--) {
2540
+ const randomIndex = Math.floor(Math.random() * (index + 1));
2541
+ [shuffled[index], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[index]];
2542
+ }
2543
+ return shuffled;
2544
+ }
2545
+ /**
2546
+ * Normal account selection with alias fallback.
2547
+ * Extracted to keep selectAccountForRequest readable after adding affinity logic.
2548
+ */
2549
+ async selectWithAliasFallback(orderedAccounts, candidates) {
2550
+ const primary = await this.selectAccountForCandidates(orderedAccounts, candidates);
2551
+ if (primary.ok || primary.reason !== "MODEL_NOT_SUPPORTED") return primary;
2552
+ const aliasCandidates = candidates.map((candidate) => {
2553
+ const modelId = resolveModelAlias(candidate.modelId);
2554
+ if (modelId === candidate.modelId) return candidate;
2555
+ return {
2556
+ ...candidate,
2557
+ modelId
2558
+ };
2559
+ });
2560
+ if (!aliasCandidates.some((candidate, index) => candidate.modelId !== candidates[index].modelId)) return primary;
2561
+ return this.selectAccountForCandidates(orderedAccounts, aliasCandidates);
2562
+ }
2563
+ /**
2564
+ * Finalize quota after a request completes.
2565
+ * This releases any in-flight reservation and refreshes the actual quota from the API.
2566
+ */
2567
+ async finalizeQuota(account, reservation) {
2568
+ releasePremiumReservation(account, reservation);
2569
+ try {
2570
+ await this.refreshQuota(account);
2571
+ } catch (error) {
2572
+ consola.debug(`Failed to finalize quota for ${account.id}:`, error);
2573
+ }
2574
+ }
2575
+ /**
2576
+ * Mark an account as failed.
2577
+ */
2578
+ markAccountFailed(id, reason) {
2579
+ const account = this.accounts.get(id);
2580
+ if (account) {
2581
+ setAccountFailedState(account, reason);
2582
+ return;
2583
+ }
2584
+ if (this.temporaryAccount && this.temporaryAccount.id === id) setAccountFailedState(this.temporaryAccount, reason);
2585
+ }
2586
+ /**
2587
+ * Get status of all accounts.
2588
+ */
2589
+ getAccountStatus() {
2590
+ const statuses = [];
2591
+ if (this.temporaryAccount) statuses.push({
2592
+ id: "(temporary)",
2593
+ entitlement: this.temporaryAccount.premiumEntitlement,
2594
+ remaining: this.temporaryAccount.premiumRemaining,
2595
+ unlimited: this.temporaryAccount.unlimited,
2596
+ overagePermitted: this.temporaryAccount.overagePermitted,
2597
+ failed: this.temporaryAccount.failed,
2598
+ failureReason: this.temporaryAccount.failureReason
2599
+ });
2600
+ for (const id of this.accountOrder) {
2601
+ const account = this.accounts.get(id);
2602
+ if (account) statuses.push({
2603
+ id: account.id,
2604
+ entitlement: account.premiumEntitlement,
2605
+ remaining: account.premiumRemaining,
2606
+ unlimited: account.unlimited,
2607
+ overagePermitted: account.overagePermitted,
2608
+ failed: account.failed,
2609
+ failureReason: account.failureReason,
2610
+ enabled: account.enabled
2611
+ });
2612
+ }
2613
+ return statuses;
2614
+ }
2615
+ /**
2616
+ * Set a temporary account from a GitHub token (--github-token).
2617
+ * This account takes priority over registered accounts.
2618
+ */
2619
+ async setTemporaryAccount(githubToken, accountType) {
2620
+ const user = await getGitHubUser({
2621
+ githubToken,
2622
+ accountType
2623
+ });
2624
+ if (this.temporaryAccount) {
2625
+ this.stopTokenRefresh(this.temporaryAccount);
2626
+ this.stopSessionRefresh(this.temporaryAccount);
2627
+ }
2628
+ const runtime = {
2629
+ id: "(temporary)",
2630
+ accountLogin: user.login,
2631
+ accountType,
2632
+ addedAt: Date.now(),
2633
+ githubToken,
2634
+ vsCodeVersion: this.vsCodeVersion
2635
+ };
2636
+ try {
2637
+ await this.initializeAccount(runtime);
2638
+ this.temporaryAccount = runtime;
2639
+ consola.info("Temporary account initialized");
2640
+ } catch (error) {
2641
+ consola.error("Failed to initialize temporary account:", error);
2642
+ throw error;
2643
+ }
2644
+ }
2645
+ /**
2646
+ * Check if any accounts are available.
2647
+ */
2648
+ hasAccounts() {
2649
+ return this.accounts.size > 0 || this.temporaryAccount !== void 0;
2650
+ }
2651
+ /**
2652
+ * Get the first available account's models.
2653
+ * Used for caching models in legacy compatibility mode.
2654
+ */
2655
+ getFirstAccountModels() {
2656
+ if (this.temporaryAccount?.models) return this.temporaryAccount.models;
2657
+ for (const id of this.accountOrder) {
2658
+ const account = this.accounts.get(id);
2659
+ if (account?.models) return account.models;
2660
+ }
2661
+ }
2662
+ /**
2663
+ * Get account context by index.
2664
+ * Index 0 is the temporary account (if exists), otherwise the first registered account.
2665
+ * Returns null if index is out of bounds.
2666
+ */
2667
+ getAccountContextByIndex(index) {
2668
+ const allAccounts = [];
2669
+ if (this.temporaryAccount) allAccounts.push(this.temporaryAccount);
2670
+ for (const id of this.accountOrder) {
2671
+ const account = this.accounts.get(id);
2672
+ if (account) allAccounts.push(account);
2673
+ }
2674
+ if (index < 0 || index >= allAccounts.length) return null;
2675
+ return this.toAccountContext(allAccounts[index]);
2676
+ }
2677
+ /**
2678
+ * Get the total number of accounts (including temporary).
2679
+ */
2680
+ getAccountCount() {
2681
+ return (this.temporaryAccount ? 1 : 0) + this.accountOrder.length;
2682
+ }
2683
+ /**
2684
+ * Convert AccountRuntime to AccountContext for service calls.
2685
+ */
2686
+ toAccountContext(account) {
2687
+ return {
2688
+ accountLogin: account.accountLogin,
2689
+ githubToken: account.githubToken,
2690
+ copilotToken: account.copilotToken,
2691
+ ...account.copilotApiUrl !== void 0 ? { copilotApiUrl: account.copilotApiUrl } : {},
2692
+ accountType: account.accountType,
2693
+ vsCodeVersion: account.vsCodeVersion,
2694
+ clientDeviceId: account.clientDeviceId,
2695
+ clientMachineId: account.clientMachineId,
2696
+ clientSessionId: account.clientSessionId
2697
+ };
2698
+ }
2699
+ /**
2700
+ * Start watching the registry file for changes.
2701
+ * Enables hot reload of accounts when the file is modified.
2702
+ */
2703
+ startRegistryWatcher() {
2704
+ this.stopRegistryWatcher();
2705
+ try {
2706
+ this.registryWatcher = fs$1.watch(PATHS.ACCOUNTS_REGISTRY_PATH, (eventType) => {
2707
+ if (eventType === "change") this.scheduleReload();
2708
+ });
2709
+ this.registryWatcherRestartDelayMs = WATCHER_RESTART_INITIAL_DELAY_MS;
2710
+ if (this.registryWatcherRestartTimer) {
2711
+ clearTimeout(this.registryWatcherRestartTimer);
2712
+ this.registryWatcherRestartTimer = void 0;
2713
+ }
2714
+ this.registryWatcher.on("error", (error) => {
2715
+ consola.debug("Registry watcher error:", error);
2716
+ const delayMs = this.registryWatcherRestartDelayMs;
2717
+ this.registryWatcherRestartDelayMs = Math.min(this.registryWatcherRestartDelayMs * 2, WATCHER_RESTART_MAX_DELAY_MS);
2718
+ this.stopRegistryWatcher();
2719
+ this.registryWatcherRestartTimer = setTimeout(() => {
2720
+ this.registryWatcherRestartTimer = void 0;
2721
+ this.startRegistryWatcher();
2722
+ }, delayMs);
2723
+ consola.debug(`Restarting registry watcher in ${delayMs}ms`);
2724
+ });
2725
+ consola.debug("Started registry file watcher");
2726
+ } catch (error) {
2727
+ consola.warn("Failed to start registry watcher:", error);
2728
+ }
2729
+ }
2730
+ /**
2731
+ * Immediately reload the registry, bypassing the debounce delay.
2732
+ * If a reload is already running, waits for it to complete and then
2733
+ * runs another reload to ensure the latest changes are captured.
2734
+ */
2735
+ async reloadRegistryNow() {
2736
+ if (this.reloadDebounceTimer) {
2737
+ clearTimeout(this.reloadDebounceTimer);
2738
+ this.reloadDebounceTimer = void 0;
2739
+ }
2740
+ if (this.currentReloadPromise) await this.currentReloadPromise;
2741
+ await this.reloadRegistry();
2742
+ }
2743
+ /**
2744
+ * Schedule a registry reload with debouncing.
2745
+ */
2746
+ scheduleReload() {
2747
+ if (this.reloadDebounceTimer) clearTimeout(this.reloadDebounceTimer);
2748
+ this.reloadDebounceTimer = setTimeout(() => {
2749
+ this.reloadRegistry();
2750
+ }, RELOAD_DEBOUNCE_MS);
2751
+ }
2752
+ /**
2753
+ * Reload the registry and perform incremental updates.
2754
+ * Adds new accounts, removes deleted ones, and reinitializes existing accounts
2755
+ * when token/accountType changes.
2756
+ */
2757
+ async reloadRegistry() {
2758
+ if (this.isReloading) return;
2759
+ this.isReloading = true;
2760
+ this.currentReloadPromise = (async () => {
2761
+ try {
2762
+ const newMetas = await listAccountsFromRegistry();
2763
+ const newIds = new Set(newMetas.map((m) => m.id));
2764
+ const currentIds = new Set(this.accountOrder);
2765
+ const added = [];
2766
+ const removed = [];
2767
+ const updated = [];
2768
+ this.removeDeletedAccounts(currentIds, newIds, removed);
2769
+ for (const meta of newMetas) if (!currentIds.has(meta.id)) await this.addNewAccount(meta, added);
2770
+ await this.reinitializeUpdatedAccounts(newMetas, currentIds, updated);
2771
+ this.accountOrder = newMetas.map((m) => m.id).filter((id) => this.accounts.has(id));
2772
+ this.loadBalanceCursor = 0;
2773
+ this.logRegistryReloadChanges(added, removed, updated);
2774
+ } catch (error) {
2775
+ consola.error("Failed to reload registry:", error);
2776
+ this.shutdown();
2777
+ process.exit(1);
2778
+ } finally {
2779
+ this.isReloading = false;
2780
+ this.currentReloadPromise = void 0;
2781
+ }
2782
+ })();
2783
+ await this.currentReloadPromise;
2784
+ }
2785
+ removeDeletedAccounts(currentIds, newIds, removed) {
2786
+ for (const id of currentIds) if (!newIds.has(id)) {
2787
+ const account = this.accounts.get(id);
2788
+ if (!account) continue;
2789
+ this.stopTokenRefresh(account);
2790
+ this.stopSessionRefresh(account);
2791
+ this.accounts.delete(id);
2792
+ removed.push(id);
2793
+ }
2794
+ }
2795
+ async reinitializeUpdatedAccounts(newMetas, currentIds, updated) {
2796
+ for (const meta of newMetas) {
2797
+ if (!currentIds.has(meta.id)) continue;
2798
+ const account = this.accounts.get(meta.id);
2799
+ if (!account) continue;
2800
+ const token = await loadAccountToken(meta.id);
2801
+ if (!token) {
2802
+ consola.warn(`No token found for account ${meta.id}, skipping update`);
2803
+ continue;
2804
+ }
2805
+ const accountTypeChanged = account.accountType !== meta.accountType;
2806
+ const tokenChanged = account.githubToken !== token;
2807
+ const addedAtChanged = account.addedAt !== meta.addedAt;
2808
+ if (accountTypeChanged) account.accountType = meta.accountType;
2809
+ if (addedAtChanged) account.addedAt = meta.addedAt;
2810
+ account.enabled = meta.enabled;
2811
+ account.accountLogin = meta.id;
2812
+ if (tokenChanged) account.githubToken = token;
2813
+ if (!accountTypeChanged && !tokenChanged) continue;
2814
+ try {
2815
+ await this.initializeAccount(account);
2816
+ updated.push(meta.id);
2817
+ } catch (error) {
2818
+ consola.error(`Failed to reinitialize account ${meta.id} after update:`, error);
2819
+ account.failed = true;
2820
+ account.failureReason = String(error);
2821
+ updated.push(`${meta.id} (failed)`);
2822
+ }
2823
+ }
2824
+ }
2825
+ logRegistryReloadChanges(added, removed, updated) {
2826
+ if (added.length === 0 && removed.length === 0 && updated.length === 0) return;
2827
+ const changes = [];
2828
+ if (added.length > 0) changes.push(`added: ${added.join(", ")}`);
2829
+ if (removed.length > 0) changes.push(`removed: ${removed.join(", ")}`);
2830
+ if (updated.length > 0) changes.push(`updated: ${updated.join(", ")}`);
2831
+ consola.info(`Registry reloaded (${changes.join("; ")}). Total: ${this.accounts.size} account(s)`);
2832
+ }
2833
+ /**
2834
+ * Helper to add a new account during reload.
2835
+ */
2836
+ async addNewAccount(meta, added) {
2837
+ const token = await loadAccountToken(meta.id);
2838
+ if (!token) {
2839
+ consola.warn(`No token found for new account ${meta.id}, skipping`);
2840
+ return;
2841
+ }
2842
+ const runtime = {
2843
+ ...meta,
2844
+ accountLogin: meta.id,
2845
+ githubToken: token,
2846
+ vsCodeVersion: this.vsCodeVersion
2847
+ };
2848
+ try {
2849
+ await this.initializeAccount(runtime);
2850
+ this.accounts.set(meta.id, runtime);
2851
+ added.push(meta.id);
2852
+ } catch (error) {
2853
+ consola.error(`Failed to initialize new account ${meta.id}:`, error);
2854
+ runtime.failed = true;
2855
+ runtime.failureReason = String(error);
2856
+ this.accounts.set(meta.id, runtime);
2857
+ added.push(`${meta.id} (failed)`);
2858
+ }
2859
+ }
2860
+ /**
2861
+ * Stop the registry file watcher.
2862
+ */
2863
+ stopRegistryWatcher() {
2864
+ if (this.reloadDebounceTimer) {
2865
+ clearTimeout(this.reloadDebounceTimer);
2866
+ this.reloadDebounceTimer = void 0;
2867
+ }
2868
+ if (this.registryWatcherRestartTimer) {
2869
+ clearTimeout(this.registryWatcherRestartTimer);
2870
+ this.registryWatcherRestartTimer = void 0;
2871
+ }
2872
+ if (this.registryWatcher) {
2873
+ this.registryWatcher.close();
2874
+ this.registryWatcher = void 0;
2875
+ }
2876
+ }
2877
+ /**
2878
+ * Shutdown the manager and clean up resources.
2879
+ */
2880
+ shutdown() {
2881
+ this.sessionOwnershipGeneration++;
2882
+ this.stopRegistryWatcher();
2883
+ this.stopAllTokenRefresh();
2884
+ this.stopAllSessionRefresh();
2885
+ this.stopModelsRefresh();
2886
+ this.affinityCache.clearMemory();
2887
+ this.sessionOwnership.clear();
2888
+ this.loadBalanceCursor = 0;
2889
+ this.accounts.clear();
2890
+ this.accountOrder = [];
2891
+ this.temporaryAccount = void 0;
2892
+ }
2893
+ };
2894
+ /** Singleton instance of AccountsManager */
2895
+ const accountsManager = new AccountsManager({ persistentAffinityStore: getSharedSessionAffinityStore });
2896
+
2897
+ //#endregion
2898
+ export { isResponsesApiWebSearchEnabled as A, getReasoningEffortForModel as C, isMessageStartInputTokensFallbackEnabled as D, isForceAgentEnabled as E, resolveModelAlias as M, shouldCompactUseSmallModel as N, isMessagesApiEnabled as O, getProviderConfig as S, isAccountAffinityEnabled as T, getExtraPromptForModel as _, getClientIpInfo as a, getModelAliasesInfo as b, normalizeChatCompletionsUsage as c, toLocalDateString as d, PROVIDER_TYPE_ANTHROPIC as f, getConfig as g, getClaudeTokenMultiplier as h, extractResponsesUsageFromStreamEvent as i, mergeConfigWithDefaults as j, isResponsesApiContextManagementModel as k, normalizeEmbeddingsUsage as l, getAnthropicApiKey as m, applySharedSessionAffinityRetention as n, getRequestHistoryStore as o, getAliasTargetSet as p, extractResponsesUsageFromResult as r, getStatsStore as s, accountsManager as t, normalizeMessagesUsage as u, getLogLevel as v, getSmallModel as w, getModelRefreshIntervalMs as x, getModelAliases as y };
2899
+ //# sourceMappingURL=accounts-manager-BKG9aZEL.js.map