copilot-api-plus 1.2.18 → 1.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -16,6 +16,7 @@ English | [简体中文](README.md)
16
16
  - [Usage Guide](#-usage-guide)
17
17
  - [GitHub Copilot Mode](#1-github-copilot-mode-default)
18
18
  - [Multi-Account Management](#-multi-account-management)
19
+ - [Multi-Account Anti-Correlation](#️-multi-account-anti-correlation)
19
20
  - [Model Routing](#-model-routing)
20
21
  - [Proxy Configuration](#-proxy-configuration)
21
22
  - [Claude Code Integration](#-claude-code-integration)
@@ -218,6 +219,66 @@ If you were using single-account mode before, your existing account is automatic
218
219
 
219
220
  ---
220
221
 
222
+ ## 🛡️ Multi-Account Anti-Correlation
223
+
224
+ New in v1.2.18. Anti-correlation measures are automatically enabled in multi-account mode to prevent GitHub from linking multiple accounts to the same user.
225
+
226
+ ### Automatic (No Configuration Needed)
227
+
228
+ The following measures run **fully automatically** in multi-account mode — no user action required:
229
+
230
+ | Measure | Description |
231
+ |---------|-------------|
232
+ | **Device Fingerprint Isolation** | Each account gets a unique `vscode-machineid` (persisted to accounts.json) and `vscode-sessionid` (regenerated on every startup) |
233
+ | **Connection Pool Isolation** | Each account uses its own TCP/TLS connection pool — no shared connections |
234
+ | **Request Timing Isolation** | 1-5s random delay when switching accounts; minimum 1s interval between same-account requests |
235
+ | **Connection Pool Recycling** | Connection pools are automatically recreated every ~4 hours (±25% jitter), simulating client restarts |
236
+
237
+ ### Advanced: Per-Account Proxy (Optional)
238
+
239
+ If you have multiple proxy endpoints, you can assign each account its own proxy for **exit IP isolation** — the strongest anti-correlation measure.
240
+
241
+ > Most users only have one proxy and don't need this. The automatic measures above are sufficient.
242
+
243
+ #### Configuration
244
+
245
+ Edit `~/.local/share/copilot-api-plus/accounts.json` and add a `proxy` field to each account:
246
+
247
+ ```jsonc
248
+ [
249
+ {
250
+ "id": "...",
251
+ "label": "Account 1",
252
+ "githubToken": "ghu_xxx",
253
+ "proxy": "http://127.0.0.1:7891" // Exit via proxy A
254
+ },
255
+ {
256
+ "id": "...",
257
+ "label": "Account 2",
258
+ "githubToken": "ghu_yyy",
259
+ "proxy": "socks5://127.0.0.1:7892" // Exit via proxy B
260
+ },
261
+ {
262
+ "id": "...",
263
+ "label": "Account 3",
264
+ "githubToken": "ghu_zzz"
265
+ // No proxy = use global proxy or direct connection
266
+ }
267
+ ]
268
+ ```
269
+
270
+ You can also specify the proxy when adding an account via API:
271
+
272
+ ```bash
273
+ curl -X POST http://localhost:4141/admin/accounts \
274
+ -H "Content-Type: application/json" \
275
+ -d '{"githubToken": "ghu_xxx", "label": "Account 1", "proxy": "http://127.0.0.1:7891"}'
276
+ ```
277
+
278
+ Supports `http://`, `https://`, and `socks5://` protocols. Restart the server after editing for changes to take effect.
279
+
280
+ ---
281
+
221
282
  ## 🔀 Model Routing
222
283
 
223
284
  New in v1.1.0. Flexible model name mapping and per-model concurrency control.
package/README.md CHANGED
@@ -18,6 +18,7 @@
18
18
  - [详细使用指南](#-详细使用指南)
19
19
  - [GitHub Copilot 模式](#1-github-copilot-模式默认)
20
20
  - [多账号管理](#-多账号管理)
21
+ - [多账号反关联](#️-多账号反关联)
21
22
  - [模型路由](#-模型路由)
22
23
  - [代理配置](#-代理配置)
23
24
  - [Claude Code 集成](#-claude-code-集成)
@@ -219,6 +220,66 @@ npx copilot-api-plus@latest remove-account 1
219
220
 
220
221
  ---
221
222
 
223
+ ## 🛡️ 多账号反关联
224
+
225
+ v1.2.18 新增。多账号模式下自动启用反关联措施,防止 GitHub 将多个账号关联到同一用户。
226
+
227
+ ### 自动生效(无需配置)
228
+
229
+ 以下措施在多账号模式下**全自动运行**,用户无需任何操作:
230
+
231
+ | 措施 | 说明 |
232
+ |------|------|
233
+ | **设备指纹隔离** | 每个账号自动生成独立的 `vscode-machineid`(持久化到 accounts.json)和 `vscode-sessionid`(每次启动重新生成) |
234
+ | **连接池隔离** | 每个账号使用独立的 TCP/TLS 连接池,不共享连接 |
235
+ | **请求时间隔离** | 切换账号时自动添加 1-5 秒随机延迟,同一账号最小请求间隔 1 秒 |
236
+ | **连接池定期重建** | 约 4 小时自动重建连接池(±25% 随机抖动),模拟客户端重启 |
237
+
238
+ ### 高级:Per-account 代理(可选)
239
+
240
+ 如果你有多个代理出口,可以为每个账号配置独立的代理地址,实现**出口 IP 隔离**——这是最强的反关联手段。
241
+
242
+ > 大部分用户只有一个代理,无需配置此项。上面的自动措施已经足够。
243
+
244
+ #### 配置方法
245
+
246
+ 直接编辑 `~/.local/share/copilot-api-plus/accounts.json`,为账号添加 `proxy` 字段:
247
+
248
+ ```jsonc
249
+ [
250
+ {
251
+ "id": "...",
252
+ "label": "账号1",
253
+ "githubToken": "ghu_xxx",
254
+ "proxy": "http://127.0.0.1:7891" // 走代理出口 A
255
+ },
256
+ {
257
+ "id": "...",
258
+ "label": "账号2",
259
+ "githubToken": "ghu_yyy",
260
+ "proxy": "socks5://127.0.0.1:7892" // 走代理出口 B
261
+ },
262
+ {
263
+ "id": "...",
264
+ "label": "账号3",
265
+ "githubToken": "ghu_zzz"
266
+ // 不配置 = 使用全局代理或直连
267
+ }
268
+ ]
269
+ ```
270
+
271
+ 也可以在添加账号时通过 API 指定:
272
+
273
+ ```bash
274
+ curl -X POST http://localhost:4141/admin/accounts \
275
+ -H "Content-Type: application/json" \
276
+ -d '{"githubToken": "ghu_xxx", "label": "账号1", "proxy": "http://127.0.0.1:7891"}'
277
+ ```
278
+
279
+ 支持 `http://`、`https://`、`socks5://` 协议。修改后重启服务生效。
280
+
281
+ ---
282
+
222
283
  ## 🔀 模型路由
223
284
 
224
285
  v1.1.0 新增模型名映射和并发控制功能。
@@ -237,26 +237,40 @@ let proxyActive = false;
237
237
  * CONNECT tunnels that are idle for ~60 s. During long model thinking
238
238
  * phases the SSE stream carries no data, which looks "idle" to the proxy.
239
239
  *
240
- * This keepalive sends a tiny HEAD request to the Copilot API every 45 s
241
- * through the same proxy. The encrypted packets flowing through the
242
- * CONNECT tunnel reset the proxy's idle timer, keeping the tunnel alive.
240
+ * This keepalive sends a tiny HEAD request to the Copilot API every 30 s
241
+ * through the SAME connection pool that the SSE stream uses. For
242
+ * per-account connections, this means pinging through each account's own
243
+ * dispatcher, not the global one — so the correct proxy tunnel is kept
244
+ * alive.
243
245
  *
244
246
  * The keepalive is active ONLY while there are SSE streams in flight
245
- * (tracked via `streamCount`). When no streams are active it stops to
246
- * avoid unnecessary traffic.
247
+ * (tracked per-account via `activeStreams`).
247
248
  */
248
249
  let keepaliveTimer;
249
- let streamCount = 0;
250
- const KEEPALIVE_INTERVAL_MS = 45e3;
250
+ const KEEPALIVE_INTERVAL_MS = 3e4;
251
251
  const KEEPALIVE_URL = "https://api.individual.githubcopilot.com/";
252
+ /** Track active streams: global (key = "__global__") and per-account. */
253
+ const activeStreams = /* @__PURE__ */ new Map();
252
254
  function startKeepalive() {
253
255
  if (keepaliveTimer) return;
254
256
  keepaliveTimer = setInterval(() => {
255
- fetch(KEEPALIVE_URL, { method: "HEAD" }).catch(() => {});
256
- consola.debug("Proxy keepalive ping sent");
257
+ for (const [key, info] of activeStreams) {
258
+ if (info.count <= 0) continue;
259
+ if (key === "__global__") {
260
+ fetch(KEEPALIVE_URL, { method: "HEAD" }).catch(() => {});
261
+ consola.debug("Proxy keepalive ping sent (global)");
262
+ } else {
263
+ const dispatcher = getAccountDispatcher(key, info.accountProxy);
264
+ fetch(KEEPALIVE_URL, {
265
+ method: "HEAD",
266
+ dispatcher
267
+ }).catch(() => {});
268
+ consola.debug(`Proxy keepalive ping sent (account ${key.slice(0, 8)})`);
269
+ }
270
+ }
257
271
  }, KEEPALIVE_INTERVAL_MS);
258
272
  keepaliveTimer.unref();
259
- consola.debug("Proxy keepalive started (45 s interval)");
273
+ consola.debug("Proxy keepalive started (30 s interval)");
260
274
  }
261
275
  function stopKeepalive() {
262
276
  if (keepaliveTimer) {
@@ -265,23 +279,42 @@ function stopKeepalive() {
265
279
  consola.debug("Proxy keepalive stopped (no active streams)");
266
280
  }
267
281
  }
282
+ function getTotalStreamCount() {
283
+ let total = 0;
284
+ for (const info of activeStreams.values()) total += info.count;
285
+ return total;
286
+ }
268
287
  /**
269
288
  * Call when an SSE stream starts. Activates the proxy-tunnel keepalive
270
289
  * if this is the first active stream and a proxy is configured.
290
+ *
291
+ * @param accountInfo If provided, keepalive pings go through this
292
+ * account's own connection pool (not the global one).
271
293
  */
272
- function notifyStreamStart() {
294
+ function notifyStreamStart(accountInfo) {
273
295
  if (!proxyActive) return;
274
- streamCount++;
275
- if (streamCount === 1) startKeepalive();
296
+ const key = accountInfo?.accountId ?? "__global__";
297
+ const existing = activeStreams.get(key);
298
+ if (existing) existing.count++;
299
+ else activeStreams.set(key, {
300
+ count: 1,
301
+ accountProxy: accountInfo?.accountProxy
302
+ });
303
+ if (getTotalStreamCount() === 1) startKeepalive();
276
304
  }
277
305
  /**
278
306
  * Call when an SSE stream ends (success or error). Stops the keepalive
279
307
  * once no streams are active.
280
308
  */
281
- function notifyStreamEnd() {
309
+ function notifyStreamEnd(accountInfo) {
282
310
  if (!proxyActive) return;
283
- streamCount = Math.max(0, streamCount - 1);
284
- if (streamCount === 0) stopKeepalive();
311
+ const key = accountInfo?.accountId ?? "__global__";
312
+ const existing = activeStreams.get(key);
313
+ if (existing) {
314
+ existing.count = Math.max(0, existing.count - 1);
315
+ if (existing.count === 0) activeStreams.delete(key);
316
+ }
317
+ if (getTotalStreamCount() === 0) stopKeepalive();
285
318
  }
286
319
  function initProxyFromEnv() {
287
320
  if (typeof Bun !== "undefined") return;
@@ -840,4 +873,4 @@ const accountManager = new AccountManager();
840
873
  //#endregion
841
874
  export { GITHUB_BASE_URL as C, standardHeaders as D, copilotHeaders as E, GITHUB_APP_SCOPES as S, copilotBaseUrl as T, findModel as _, getAccountDispatcher as a, sleep as b, notifyStreamStart as c, PATHS as d, ensurePaths as f, cacheVSCodeVersion as g, cacheModels as h, getCopilotToken as i, resetAccountConnections as l, forwardError as m, getGitHubUser as n, initProxyFromEnv as o, HTTPError as p, getCopilotUsage as r, notifyStreamEnd as s, accountManager as t, resetConnections as u, isNullish as v, GITHUB_CLIENT_ID as w, state as x, rootCause as y };
842
875
 
843
- //# sourceMappingURL=account-manager-COJx3i2R.js.map
876
+ //# sourceMappingURL=account-manager-BLD3jHgL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-manager-BLD3jHgL.js","names":["ACCOUNTS_PATH"],"sources":["../src/lib/api-config.ts","../src/lib/state.ts","../src/services/copilot/get-models.ts","../src/services/get-vscode-version.ts","../src/lib/utils.ts","../src/lib/error.ts","../src/lib/paths.ts","../src/lib/proxy.ts","../src/services/github/get-copilot-token.ts","../src/services/github/get-copilot-usage.ts","../src/services/github/get-user.ts","../src/lib/account-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\"\n\nexport const standardHeaders = () => ({\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n})\n\nconst COPILOT_VERSION = \"0.38.2\"\nconst EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`\nconst USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`\n\n// Updated to match latest Zed implementation - 2025-10-01 returns Claude models\nconst API_VERSION = \"2025-10-01\"\n\n/**\n * Common interface for anything that can supply Copilot/GitHub credentials.\n *\n * Both `State` and `Account` satisfy this interface, so all header/URL\n * helpers can accept either without an explicit overload.\n */\nexport interface TokenSource {\n copilotToken?: string\n copilotApiEndpoint?: string\n accountType: string\n githubToken?: string\n vsCodeVersion?: string\n machineId?: string\n sessionId?: string\n proxy?: string\n}\n\n// Re-export constants used by other modules for building headers manually\nexport { API_VERSION, EDITOR_PLUGIN_VERSION, USER_AGENT }\n\n// Use the API endpoint from token response if available, otherwise fall back to default\nexport const copilotBaseUrl = (source: TokenSource) => {\n if (source.copilotApiEndpoint) {\n return source.copilotApiEndpoint\n }\n return source.accountType === \"individual\" ?\n \"https://api.githubcopilot.com\"\n : `https://api.${source.accountType}.githubcopilot.com`\n}\nexport const copilotHeaders = (\n source: TokenSource,\n vision: boolean = false,\n) => {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${source.copilotToken}`,\n \"content-type\": standardHeaders()[\"content-type\"],\n \"copilot-integration-id\": \"vscode-chat\",\n \"editor-version\": `vscode/${source.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"openai-intent\": \"conversation-agent\",\n \"x-interaction-type\": \"conversation-agent\",\n \"x-agent-task-id\": randomUUID(),\n \"x-github-api-version\": API_VERSION,\n \"x-request-id\": randomUUID(),\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n // Anti-correlation: per-account device identifiers\n ...(source.machineId && { \"vscode-machineid\": source.machineId }),\n ...(source.sessionId && { \"vscode-sessionid\": source.sessionId }),\n }\n\n if (vision) headers[\"copilot-vision-request\"] = \"true\"\n\n return headers\n}\n\nexport const GITHUB_API_BASE_URL = \"https://api.github.com\"\nexport const githubHeaders = (source: TokenSource) => ({\n ...standardHeaders(),\n authorization: `token ${source.githubToken}`,\n \"editor-version\": `vscode/${source.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"x-github-api-version\": API_VERSION,\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n})\n\nexport const GITHUB_BASE_URL = \"https://github.com\"\nexport const GITHUB_CLIENT_ID = \"Iv1.b507a08c87ecfe98\"\nexport const GITHUB_APP_SCOPES = [\"read:user\"].join(\" \")\n","import type { ModelsResponse } from \"~/services/copilot/get-models\"\n\nexport interface State {\n githubToken?: string\n copilotToken?: string\n copilotApiEndpoint?: string // API endpoint returned by token response\n\n accountType: string\n models?: ModelsResponse\n vsCodeVersion?: string\n\n manualApprove: boolean\n rateLimitWait: boolean\n showToken: boolean\n\n // Rate limiting configuration\n rateLimitSeconds?: number\n lastRequestTimestamp?: number\n\n // API key authentication\n apiKeys?: Array<string>\n\n // Multi-account mode\n multiAccountEnabled: boolean\n\n // Selected models (from --claude-code setup)\n selectedModel?: string\n selectedSmallModel?: string\n}\n\nexport const state: State = {\n accountType: \"individual\",\n manualApprove: false,\n rateLimitWait: false,\n showToken: false,\n multiAccountEnabled: false,\n}\n","import { accountManager } from \"~/lib/account-manager\"\nimport {\n copilotBaseUrl,\n copilotHeaders,\n type TokenSource,\n} from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\nexport const getModels = async () => {\n // In multi-account mode, use the active account's token\n let source: TokenSource = state\n if (state.multiAccountEnabled && accountManager.hasAccounts()) {\n const account = accountManager.getActiveAccount()\n if (account?.copilotToken) {\n source = {\n copilotToken: account.copilotToken,\n copilotApiEndpoint: account.copilotApiEndpoint,\n accountType: account.accountType,\n githubToken: account.githubToken,\n vsCodeVersion: state.vsCodeVersion,\n }\n }\n }\n\n const url = `${copilotBaseUrl(source)}/models`\n\n const response = await fetch(url, {\n headers: copilotHeaders(source),\n })\n\n if (!response.ok) throw new HTTPError(\"Failed to get models\", response)\n\n const data = (await response.json()) as ModelsResponse\n\n return data\n}\n\nexport interface ModelsResponse {\n data: Array<Model>\n object: string\n}\n\ninterface ModelLimits {\n max_context_window_tokens?: number\n max_output_tokens?: number\n max_prompt_tokens?: number\n max_inputs?: number\n}\n\ninterface ModelSupports {\n tool_calls?: boolean\n parallel_tool_calls?: boolean\n dimensions?: boolean\n // Thinking/reasoning capabilities\n max_thinking_budget?: number\n min_thinking_budget?: number\n adaptive_thinking?: boolean\n}\n\ninterface ModelCapabilities {\n family: string\n limits: ModelLimits\n object: string\n supports: ModelSupports\n tokenizer: string\n type: string\n}\n\nexport interface Model {\n capabilities: ModelCapabilities\n id: string\n model_picker_enabled: boolean\n name: string\n object: string\n preview: boolean\n vendor: string\n version: string\n policy?: {\n state: string\n terms: string\n }\n billing?: {\n is_premium: boolean\n multiplier: number\n restricted_to?: Array<string>\n }\n}\n","const FALLBACK = \"1.104.3\"\n\nexport async function getVSCodeVersion() {\n const controller = new AbortController()\n const timeout = setTimeout(() => {\n controller.abort()\n }, 5000)\n\n try {\n const response = await fetch(\n \"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin\",\n {\n signal: controller.signal,\n },\n )\n\n const pkgbuild = await response.text()\n const pkgverRegex = /pkgver=([0-9.]+)/\n const match = pkgbuild.match(pkgverRegex)\n\n if (match) {\n return match[1]\n }\n\n return FALLBACK\n } catch {\n return FALLBACK\n } finally {\n clearTimeout(timeout)\n }\n}\n","import consola from \"consola\"\n\nimport type { Model } from \"~/services/copilot/get-models\"\n\nimport { getModels } from \"~/services/copilot/get-models\"\nimport { getVSCodeVersion } from \"~/services/get-vscode-version\"\n\nimport { state } from \"./state\"\n\nexport const sleep = (ms: number) =>\n new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n\n/**\n * Extract the root-cause message from an unknown thrown value.\n *\n * If the error wraps another error via the standard `cause` property, the\n * inner message is returned instead — giving a one-line summary of *why*\n * the operation failed without the noise of the full stack trace.\n */\nexport function rootCause(err: unknown): string {\n if (err instanceof Error) {\n return err.cause instanceof Error ? err.cause.message : err.message\n }\n return String(err)\n}\n\nexport const isNullish = (value: unknown): value is null | undefined =>\n value === null || value === undefined\n\nexport async function cacheModels(): Promise<void> {\n const models = await getModels()\n state.models = models\n}\n\nexport const cacheVSCodeVersion = async () => {\n const response = await getVSCodeVersion()\n state.vsCodeVersion = response\n\n consola.info(`Using VSCode version: ${response}`)\n}\n\n/**\n * Find a model in state.models using multi-strategy exact matching.\n *\n * Strategies (in order):\n * 1. Exact match on model ID\n * 2. Strip date suffix (claude-opus-4-6-20251101 → claude-opus-4-6)\n * 3. Dash to dot version (claude-opus-4-5 → claude-opus-4.5)\n * 4. Dot to dash version (claude-opus-4.5 → claude-opus-4-5)\n *\n * No fuzzy/family matching — all strategies produce deterministic exact IDs.\n */\nexport function findModel(modelName: string): Model | undefined {\n const models = state.models?.data\n if (!models || models.length === 0) {\n return undefined\n }\n\n // 1. Exact match\n const exact = models.find((m) => m.id === modelName)\n if (exact) return exact\n\n // 2. Strip date suffix\n const base = modelName.replace(/-\\d{8}$/, \"\")\n if (base !== modelName) {\n const baseMatch = models.find((m) => m.id === base)\n if (baseMatch) return baseMatch\n }\n\n // 3. Dash to dot version (4-5 → 4.5)\n const withDot = base.replace(/-(\\d+)-(\\d+)$/, \"-$1.$2\")\n if (withDot !== base) {\n const dotMatch = models.find((m) => m.id === withDot)\n if (dotMatch) return dotMatch\n }\n\n // 4. Dot to dash version (4.5 → 4-5)\n const withDash = modelName.replace(/(\\d+)\\.(\\d+)/, \"$1-$2\")\n if (withDash !== modelName) {\n const dashMatch = models.find((m) => m.id === withDash)\n if (dashMatch) return dashMatch\n }\n\n return undefined\n}\n","import type { Context } from \"hono\"\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\"\n\nimport consola from \"consola\"\n\nimport { rootCause } from \"~/lib/utils\"\n\nexport class HTTPError extends Error {\n response: Response\n\n constructor(message: string, response: Response) {\n super(message)\n this.response = response\n }\n}\n\nexport async function forwardError(c: Context, error: unknown) {\n if (error instanceof HTTPError) {\n // Try to read error body, but it may already be consumed by the caller\n let errorText: string\n try {\n errorText = await error.response.text()\n } catch {\n // Body already read — fall back to the error message\n errorText = error.message\n }\n\n // 400 errors: concise log, already detailed upstream\n if (error.response.status === 400) {\n // no extra logging, upstream already printed details\n } else {\n let errorJson: unknown\n try {\n errorJson = JSON.parse(errorText)\n } catch {\n errorJson = errorText\n }\n consola.warn(`Error occurred: ${rootCause(error)}`)\n consola.debug(\"HTTP error:\", errorJson)\n }\n\n return c.json(\n {\n error: {\n message: errorText,\n type: \"error\",\n },\n },\n error.response.status as ContentfulStatusCode,\n )\n }\n\n // Network errors (fetch failed, TLS disconnect, etc.) — concise log\n const message = (error as Error).message || String(error)\n const cause = (error as { cause?: Error }).cause\n if (cause) {\n consola.error(`${message}: ${cause.message}`)\n } else {\n consola.error(message)\n }\n return c.json(\n {\n error: {\n message: (error as Error).message,\n type: \"error\",\n },\n },\n 500,\n )\n}\n","import fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nconst APP_DIR = path.join(os.homedir(), \".local\", \"share\", \"copilot-api-plus\")\n\nconst GITHUB_TOKEN_PATH = path.join(APP_DIR, \"github_token\")\n\nconst ACCOUNTS_PATH = path.join(APP_DIR, \"accounts.json\")\n\nexport const PATHS = {\n APP_DIR,\n DATA_DIR: APP_DIR,\n GITHUB_TOKEN_PATH,\n ACCOUNTS_PATH,\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n","import consola from \"consola\"\nimport { getProxyForUrl } from \"proxy-from-env\"\nimport { Agent, ProxyAgent, setGlobalDispatcher, type Dispatcher } from \"undici\"\n\n// Module-level references so that `resetConnections` can swap them out.\n// Initialised by `initProxyFromEnv`; the dispatcher closure captures the\n// *variables* (not their values), so replacing them is enough.\nconst agentOptions = {\n keepAliveTimeout: 300_000,\n keepAliveMaxTimeout: 600_000,\n // Allow HTTP/2 when the target supports it. Inside a CONNECT tunnel\n // the ALPN negotiation and any h2 frames are encrypted traffic — proxy\n // nodes see it as \"data flowing\" and won't kill the connection for\n // being idle.\n allowH2: true,\n connect: {\n timeout: 15_000,\n keepAlive: true,\n keepAliveInitialDelay: 15_000,\n },\n}\nlet direct: Agent | undefined\nlet proxies = new Map<string, ProxyAgent>()\n/** Whether a proxy is actually configured and in use. */\nlet proxyActive = false\n\n// ---------------------------------------------------------------------------\n// Proxy-tunnel keepalive: periodic lightweight requests through the proxy\n// ---------------------------------------------------------------------------\n\n/**\n * Many proxy nodes (especially third-party VPN/airport services) kill\n * CONNECT tunnels that are idle for ~60 s. During long model thinking\n * phases the SSE stream carries no data, which looks \"idle\" to the proxy.\n *\n * This keepalive sends a tiny HEAD request to the Copilot API every 30 s\n * through the SAME connection pool that the SSE stream uses. For\n * per-account connections, this means pinging through each account's own\n * dispatcher, not the global one — so the correct proxy tunnel is kept\n * alive.\n *\n * The keepalive is active ONLY while there are SSE streams in flight\n * (tracked per-account via `activeStreams`).\n */\nlet keepaliveTimer: ReturnType<typeof setInterval> | undefined\nconst KEEPALIVE_INTERVAL_MS = 30_000\nconst KEEPALIVE_URL = \"https://api.individual.githubcopilot.com/\"\n\ninterface ActiveStreamInfo {\n count: number\n accountProxy?: string\n}\n\n/** Track active streams: global (key = \"__global__\") and per-account. */\nconst activeStreams = new Map<string, ActiveStreamInfo>()\n\nfunction startKeepalive(): void {\n if (keepaliveTimer) return\n keepaliveTimer = setInterval(() => {\n // Ping each connection pool that has active streams\n for (const [key, info] of activeStreams) {\n if (info.count <= 0) continue\n\n if (key === \"__global__\") {\n // Global connection — use standard fetch (goes through global dispatcher)\n fetch(KEEPALIVE_URL, { method: \"HEAD\" }).catch(() => {})\n consola.debug(\"Proxy keepalive ping sent (global)\")\n } else {\n // Per-account connection — ping through the account's own dispatcher\n const dispatcher = getAccountDispatcher(key, info.accountProxy)\n fetch(KEEPALIVE_URL, {\n method: \"HEAD\",\n dispatcher: dispatcher as unknown as undefined,\n } as RequestInit).catch(() => {})\n consola.debug(`Proxy keepalive ping sent (account ${key.slice(0, 8)})`)\n }\n }\n }, KEEPALIVE_INTERVAL_MS)\n // Don't prevent Node from exiting because of this timer.\n keepaliveTimer.unref()\n consola.debug(\"Proxy keepalive started (30 s interval)\")\n}\n\nfunction stopKeepalive(): void {\n if (keepaliveTimer) {\n clearInterval(keepaliveTimer)\n keepaliveTimer = undefined\n consola.debug(\"Proxy keepalive stopped (no active streams)\")\n }\n}\n\nfunction getTotalStreamCount(): number {\n let total = 0\n for (const info of activeStreams.values()) {\n total += info.count\n }\n return total\n}\n\n/**\n * Call when an SSE stream starts. Activates the proxy-tunnel keepalive\n * if this is the first active stream and a proxy is configured.\n *\n * @param accountInfo If provided, keepalive pings go through this\n * account's own connection pool (not the global one).\n */\nexport function notifyStreamStart(accountInfo?: {\n accountId: string\n accountProxy?: string\n}): void {\n if (!proxyActive) return\n\n const key = accountInfo?.accountId ?? \"__global__\"\n const existing = activeStreams.get(key)\n if (existing) {\n existing.count++\n } else {\n activeStreams.set(key, {\n count: 1,\n accountProxy: accountInfo?.accountProxy,\n })\n }\n\n if (getTotalStreamCount() === 1) startKeepalive()\n}\n\n/**\n * Call when an SSE stream ends (success or error). Stops the keepalive\n * once no streams are active.\n */\nexport function notifyStreamEnd(accountInfo?: {\n accountId: string\n accountProxy?: string\n}): void {\n if (!proxyActive) return\n\n const key = accountInfo?.accountId ?? \"__global__\"\n const existing = activeStreams.get(key)\n if (existing) {\n existing.count = Math.max(0, existing.count - 1)\n if (existing.count === 0) activeStreams.delete(key)\n }\n\n if (getTotalStreamCount() === 0) stopKeepalive()\n}\n\nexport function initProxyFromEnv(): void {\n if (typeof Bun !== \"undefined\") return\n\n try {\n direct = new Agent(agentOptions)\n proxies = new Map<string, ProxyAgent>()\n\n // We only need a minimal dispatcher that implements `dispatch` at runtime.\n // Typing the object as `Dispatcher` forces TypeScript to require many\n // additional methods. Instead, keep a plain object and cast when passing\n // to `setGlobalDispatcher`.\n const dispatcher = {\n dispatch(\n options: Dispatcher.DispatchOptions,\n handler: Dispatcher.DispatchHandler,\n ) {\n try {\n const origin =\n typeof options.origin === \"string\" ?\n new URL(options.origin)\n : (options.origin as URL)\n const get = getProxyForUrl as unknown as (\n u: string,\n ) => string | undefined\n const raw = get(origin.toString())\n const proxyUrl = raw && raw.length > 0 ? raw : undefined\n if (!proxyUrl) {\n consola.debug(`HTTP proxy bypass: ${origin.hostname}`)\n return (direct as unknown as Dispatcher).dispatch(options, handler)\n }\n let agent = proxies.get(proxyUrl)\n if (!agent) {\n agent = new ProxyAgent({ uri: proxyUrl, ...agentOptions })\n proxies.set(proxyUrl, agent)\n }\n let label = proxyUrl\n try {\n const u = new URL(proxyUrl)\n label = `${u.protocol}//${u.host}`\n } catch {\n /* noop */\n }\n consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`)\n return (agent as unknown as Dispatcher).dispatch(options, handler)\n } catch {\n return (direct as unknown as Dispatcher).dispatch(options, handler)\n }\n },\n close() {\n for (const agent of proxies.values()) {\n void (agent as unknown as Dispatcher).close()\n }\n // `direct` is always set before the dispatcher is installed.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return direct!.close()\n },\n destroy() {\n for (const agent of proxies.values()) {\n void (agent as unknown as Dispatcher).destroy()\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return direct!.destroy()\n },\n }\n\n setGlobalDispatcher(dispatcher as unknown as Dispatcher)\n proxyActive = true\n consola.debug(\"HTTP proxy configured from environment (per-URL)\")\n } catch (err) {\n consola.debug(\"Proxy setup skipped:\", err)\n }\n}\n\n/**\n * Destroy all pooled connections (direct + proxy agents) and replace them\n * with fresh instances. The global dispatcher's `dispatch` method captures\n * `direct` and `proxies` by reference, so subsequent requests automatically\n * use the new agents — no need to call `setGlobalDispatcher` again.\n *\n * Call this after a network error to discard stale/half-closed sockets that\n * would otherwise cause every retry to wait ~60 s before timing out.\n *\n * Under the Bun runtime (which doesn't use undici) this is a no-op.\n */\nexport function resetConnections(): void {\n if (typeof Bun !== \"undefined\") return\n if (!direct) return\n\n const oldDirect = direct\n const oldProxies = proxies\n\n direct = new Agent(agentOptions)\n proxies = new Map<string, ProxyAgent>()\n\n // Tear down old agents in the background — errors are non-fatal.\n void (oldDirect as unknown as Dispatcher).close().catch(() => {})\n for (const agent of oldProxies.values()) {\n void (agent as unknown as Dispatcher).close().catch(() => {})\n }\n\n consola.debug(\"Connection pool reset — stale sockets cleared\")\n}\n\n// ---------------------------------------------------------------------------\n// Per-account connection isolation\n// ---------------------------------------------------------------------------\n\n/** Separate connection pools per account to prevent cross-account correlation. */\nconst accountAgents = new Map<string, Agent>()\nconst accountProxyAgents = new Map<string, Map<string, ProxyAgent>>()\n\n/**\n * Get or create an isolated undici Agent for a specific account.\n * Each account gets its own connection pool so that GitHub cannot correlate\n * accounts by shared TCP connections or TLS sessions.\n */\nexport function getAccountDispatcher(\n accountId: string,\n accountProxy?: string,\n): {\n dispatch: (\n options: Dispatcher.DispatchOptions,\n handler: Dispatcher.DispatchHandler,\n ) => boolean\n} {\n // Return a dispatcher that routes through per-account agents\n return {\n dispatch(\n options: Dispatcher.DispatchOptions,\n handler: Dispatcher.DispatchHandler,\n ) {\n try {\n const origin =\n typeof options.origin === \"string\" ?\n new URL(options.origin)\n : (options.origin as URL)\n // Account-level proxy takes precedence over environment proxy\n let proxyUrl: string | undefined\n if (accountProxy) {\n proxyUrl = accountProxy\n } else {\n const get = getProxyForUrl as unknown as (\n u: string,\n ) => string | undefined\n const raw = get(origin.toString())\n proxyUrl = raw && raw.length > 0 ? raw : undefined\n }\n\n if (!proxyUrl) {\n // Direct connection — use per-account agent\n let agent = accountAgents.get(accountId)\n if (!agent) {\n agent = new Agent(agentOptions)\n accountAgents.set(accountId, agent)\n }\n return (agent as unknown as Dispatcher).dispatch(options, handler)\n }\n\n // Proxy connection — use per-account proxy agent\n let proxyMap = accountProxyAgents.get(accountId)\n if (!proxyMap) {\n proxyMap = new Map<string, ProxyAgent>()\n accountProxyAgents.set(accountId, proxyMap)\n }\n let proxyAgent = proxyMap.get(proxyUrl)\n if (!proxyAgent) {\n proxyAgent = new ProxyAgent({ uri: proxyUrl, ...agentOptions })\n proxyMap.set(proxyUrl, proxyAgent)\n }\n return (proxyAgent as unknown as Dispatcher).dispatch(options, handler)\n } catch {\n // Fallback to per-account direct agent\n let agent = accountAgents.get(accountId)\n if (!agent) {\n agent = new Agent(agentOptions)\n accountAgents.set(accountId, agent)\n }\n return (agent as unknown as Dispatcher).dispatch(options, handler)\n }\n },\n }\n}\n\n/**\n * Reset connection pools for a specific account.\n */\nexport function resetAccountConnections(accountId: string): void {\n const oldAgent = accountAgents.get(accountId)\n if (oldAgent) {\n void (oldAgent as unknown as Dispatcher).close().catch(() => {})\n accountAgents.delete(accountId)\n }\n const oldProxies = accountProxyAgents.get(accountId)\n if (oldProxies) {\n for (const agent of oldProxies.values()) {\n void (agent as unknown as Dispatcher).close().catch(() => {})\n }\n accountProxyAgents.delete(accountId)\n }\n}\n\n/**\n * Reset all per-account connection pools.\n */\nexport function resetAllAccountConnections(): void {\n for (const [id] of accountAgents) {\n resetAccountConnections(id)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Periodic connection pool recreation\n// ---------------------------------------------------------------------------\n\nlet connectionRecycleTimer: ReturnType<typeof setInterval> | undefined\n\n/**\n * Start periodic connection pool recreation for all per-account pools.\n * Simulates \"VS Code restart\" behavior — stale connections are replaced\n * with fresh ones at randomized intervals to avoid timing correlation.\n *\n * @param baseIntervalMs Base interval (default 4 hours).\n */\nexport function startConnectionRecycling(\n baseIntervalMs: number = 4 * 60 * 60 * 1000,\n): void {\n stopConnectionRecycling()\n\n connectionRecycleTimer = setInterval(() => {\n // Add ±25% jitter to avoid all accounts recycling at the same time\n const jitter = baseIntervalMs * 0.25 * (Math.random() * 2 - 1)\n setTimeout(\n () => {\n resetAllAccountConnections()\n consola.debug(\"Per-account connection pools recycled\")\n },\n Math.max(0, jitter),\n )\n }, baseIntervalMs)\n\n // Don't prevent Node from exiting\n connectionRecycleTimer.unref()\n consola.info(\n `Connection pool recycling started (interval: ~${Math.round(baseIntervalMs / 3_600_000)}h)`,\n )\n}\n\n/**\n * Stop periodic connection pool recreation.\n */\nexport function stopConnectionRecycling(): void {\n if (connectionRecycleTimer) {\n clearInterval(connectionRecycleTimer)\n connectionRecycleTimer = undefined\n }\n}\n","import consola from \"consola\"\n\nimport { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\n/**\n * Fetch a short-lived Copilot JWT from the GitHub API.\n *\n * @param githubToken Optional explicit GitHub PAT. When provided the request\n * uses this token instead of `state.githubToken` and does\n * **not** mutate global state (so multi-account callers\n * stay side-effect-free).\n */\nexport const getCopilotToken = async (githubToken?: string) => {\n const tokenToUse = githubToken ?? state.githubToken\n const isExplicitToken = githubToken !== undefined\n\n const url = `${GITHUB_API_BASE_URL}/copilot_internal/v2/token`\n const fetchOptions: RequestInit = {\n headers: githubHeaders({\n ...state,\n githubToken: tokenToUse,\n }),\n }\n\n // Retry on transient network errors (TLS disconnect, connection timeout, etc.)\n const maxRetries = 2\n let lastError: unknown\n let response: Response | undefined\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n response = await fetch(url, fetchOptions)\n break\n } catch (error: unknown) {\n lastError = error\n if (attempt < maxRetries) {\n const delay = 1000 * (attempt + 1)\n consola.warn(\n `Token fetch error on attempt ${attempt + 1}/${maxRetries + 1}, retrying in ${delay}ms:`,\n error instanceof Error ? error.message : error,\n )\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n }\n\n if (!response) {\n throw lastError\n }\n\n if (!response.ok) throw new HTTPError(\"Failed to get Copilot token\", response)\n\n const data = (await response.json()) as GetCopilotTokenResponse\n\n // Only write to global state when using the default token (single-account mode).\n // When an explicit githubToken is provided (multi-account), the caller is\n // responsible for storing the endpoint on its own Account object.\n if (!isExplicitToken && data.endpoints?.api) {\n // eslint-disable-next-line require-atomic-updates\n state.copilotApiEndpoint = data.endpoints.api\n }\n\n return data\n}\n\n// Full interface matching Zed's implementation\ninterface GetCopilotTokenResponse {\n expires_at: number\n refresh_in: number\n token: string\n endpoints?: {\n api: string\n \"origin-tracker\"?: string\n proxy?: string\n telemetry?: string\n }\n annotations_enabled?: boolean\n chat_enabled?: boolean\n chat_jetbrains_enabled?: boolean\n code_quote_enabled?: boolean\n codesearch?: boolean\n copilot_ide_agent_chat_gpt4_small_prompt?: boolean\n copilotignore_enabled?: boolean\n individual?: boolean\n sku?: string\n tracking_id?: string\n limited_user_quotas?: unknown // Premium request quotas\n}\n","import { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\n/**\n * Fetch the authenticated user's Copilot usage / quota snapshot.\n *\n * @param githubToken Optional explicit GitHub PAT. When provided the request\n * uses this token instead of `state.githubToken`, allowing\n * multi-account callers to query any account without\n * touching global state.\n */\nexport const getCopilotUsage = async (\n githubToken?: string,\n): Promise<CopilotUsageResponse> => {\n const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, {\n headers: githubHeaders({\n ...state,\n githubToken: githubToken ?? state.githubToken,\n }),\n })\n\n if (!response.ok) {\n throw new HTTPError(\"Failed to get Copilot usage\", response)\n }\n\n return (await response.json()) as CopilotUsageResponse\n}\n\nexport interface QuotaDetail {\n entitlement: number\n overage_count: number\n overage_permitted: boolean\n percent_remaining: number\n quota_id: string\n quota_remaining: number\n remaining: number\n unlimited: boolean\n}\n\ninterface QuotaSnapshots {\n chat: QuotaDetail\n completions: QuotaDetail\n premium_interactions: QuotaDetail\n}\n\ninterface CopilotUsageResponse {\n access_type_sku: string\n analytics_tracking_id: string\n assigned_date: string\n can_signup_for_limited: boolean\n chat_enabled: boolean\n copilot_plan: string\n organization_login_list: Array<unknown>\n organization_list: Array<unknown>\n quota_reset_date: string\n quota_snapshots: QuotaSnapshots\n}\n","import { GITHUB_API_BASE_URL, standardHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\n/**\n * Fetch the GitHub user profile.\n *\n * @param githubToken Optional explicit token. When omitted, falls back to\n * the global `state.githubToken`. Prefer passing a token\n * explicitly to avoid race conditions in multi-account mode.\n */\nexport async function getGitHubUser(githubToken?: string) {\n const token = githubToken ?? state.githubToken\n const response = await fetch(`${GITHUB_API_BASE_URL}/user`, {\n headers: {\n authorization: `token ${token}`,\n ...standardHeaders(),\n },\n })\n\n if (!response.ok) throw new HTTPError(\"Failed to get GitHub user\", response)\n\n return (await response.json()) as GithubUserResponse\n}\n\n// Trimmed for the sake of simplicity\ninterface GithubUserResponse {\n login: string\n}\n","import consola from \"consola\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\n\nimport { HTTPError } from \"~/lib/error\"\nimport { PATHS } from \"~/lib/paths\"\nimport { startConnectionRecycling, stopConnectionRecycling } from \"~/lib/proxy\"\nimport { rootCause } from \"~/lib/utils\"\nimport { getCopilotToken } from \"~/services/github/get-copilot-token\"\nimport { getCopilotUsage } from \"~/services/github/get-copilot-usage\"\nimport { getGitHubUser } from \"~/services/github/get-user\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AccountStatus =\n | \"active\"\n | \"exhausted\"\n | \"rate_limited\"\n | \"banned\"\n | \"error\"\n | \"disabled\"\n\nexport interface Account {\n id: string\n label: string\n githubToken: string\n /** Short-lived Copilot JWT – kept in memory only, never persisted. */\n copilotToken?: string\n /** API endpoint extracted from the Copilot token response. */\n copilotApiEndpoint?: string\n accountType: string // \"individual\" | \"business\" | \"enterprise\"\n\n // Status\n status: AccountStatus\n statusMessage?: string\n lastUsedAt?: number\n consecutiveFailures: number\n cooldownUntil?: number\n\n // Quota snapshot (refreshed periodically)\n usage?: {\n premium_remaining: number\n premium_total: number\n chat_remaining: number\n chat_total: number\n quotaResetDate: string\n lastCheckedAt: number\n }\n\n // Anti-correlation\n /** Stable per-account machine identifier – persisted to disk. */\n machineId?: string\n /** Runtime-only session identifier – regenerated on every startup. */\n sessionId?: string\n /** Timestamp of the last request sent using this account. */\n lastRequestAt?: number\n /** Optional per-account proxy URL (e.g. \"http://proxy:8080\" or \"socks5://proxy:1080\"). */\n proxy?: string\n\n // Metadata\n githubLogin?: string\n addedAt: number\n}\n\n// ---------------------------------------------------------------------------\n// Persistence helpers\n// ---------------------------------------------------------------------------\n\n/** Fields excluded from the JSON file (short-lived / runtime-only). */\ntype PersistedAccount = Omit<Account, \"copilotToken\" | \"sessionId\">\n\nconst ACCOUNTS_PATH = PATHS.ACCOUNTS_PATH\n\n// ---------------------------------------------------------------------------\n// AccountManager\n// ---------------------------------------------------------------------------\n\nconst COOLDOWN_MS = 60 * 1000 // 60 seconds\n\nexport class AccountManager {\n private accounts: Array<Account> = []\n private refreshInterval?: ReturnType<typeof setInterval>\n private usageInterval?: ReturnType<typeof setInterval>\n\n // Debounced save\n private saveTimer?: ReturnType<typeof setTimeout>\n private savePending = false\n\n // ---------- Persistence ------------------------------------------------\n\n /**\n * Load accounts from the JSON file on disk.\n * Missing file is treated as empty list – not an error.\n */\n async loadAccounts(): Promise<void> {\n try {\n // eslint-disable-next-line unicorn/prefer-json-parse-buffer\n const raw = await fs.readFile(ACCOUNTS_PATH, \"utf8\")\n const parsed = JSON.parse(raw) as Array<PersistedAccount>\n this.accounts = parsed.map((a) => ({\n ...a,\n copilotToken: undefined,\n sessionId: randomUUID(),\n machineId: a.machineId || randomBytes(32).toString(\"hex\"),\n }))\n consola.info(`Loaded ${this.accounts.length} account(s) from disk`)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n this.accounts = []\n return\n }\n consola.warn(`Failed to load accounts: ${rootCause(err)}`)\n consola.debug(\"Failed to load accounts:\", err)\n this.accounts = []\n }\n }\n\n /**\n * Persist accounts to disk. `copilotToken` is excluded because it is\n * short-lived and will be refreshed on every startup.\n */\n async saveAccounts(): Promise<void> {\n const data: Array<PersistedAccount> = this.accounts.map(\n ({ copilotToken: _dropped, ...rest }) => rest,\n )\n try {\n await fs.writeFile(ACCOUNTS_PATH, JSON.stringify(data, null, 2), {\n encoding: \"utf8\",\n mode: 0o600,\n })\n } catch (err) {\n consola.warn(`Failed to save accounts: ${rootCause(err)}`)\n consola.debug(\"Failed to save accounts:\", err)\n }\n }\n\n /** Schedule a debounced save (coalesces rapid status updates). */\n private debouncedSave(): void {\n if (this.saveTimer) return // already scheduled\n this.savePending = true\n this.saveTimer = setTimeout(async () => {\n this.saveTimer = undefined\n if (this.savePending) {\n this.savePending = false\n await this.saveAccounts()\n }\n }, 1_000)\n }\n\n // ---------- CRUD -------------------------------------------------------\n\n /**\n * Add a new account.\n *\n * 1. Validates the GitHub token by fetching user info.\n * 2. Obtains an initial Copilot token.\n * 3. Persists the account to disk.\n */\n async addAccount(\n githubToken: string,\n label: string,\n accountType: string = \"individual\",\n ): Promise<Account> {\n // 1. Validate token – get GitHub login (pass token explicitly, no state mutation)\n const user = await getGitHubUser(githubToken)\n\n // 2. Obtain Copilot token – pass token directly (no state mutation)\n const tokenData = await getCopilotToken(githubToken)\n\n const account: Account = {\n id: randomUUID(),\n label,\n githubToken,\n copilotToken: tokenData.token,\n copilotApiEndpoint: tokenData.endpoints?.api,\n accountType,\n status: \"active\",\n consecutiveFailures: 0,\n githubLogin: user.login,\n machineId: randomBytes(32).toString(\"hex\"),\n sessionId: randomUUID(),\n addedAt: Date.now(),\n }\n\n this.accounts.push(account)\n await this.saveAccounts()\n\n consola.success(`Account added: ${label} (${user.login})`)\n return account\n }\n\n async removeAccount(id: string): Promise<boolean> {\n const idx = this.accounts.findIndex((a) => a.id === id)\n if (idx === -1) return false\n const [removed] = this.accounts.splice(idx, 1)\n await this.saveAccounts()\n consola.info(`Account removed: ${removed.label}`)\n return true\n }\n\n getAccounts(): Array<Account> {\n return this.accounts\n }\n\n getAccountById(id: string): Account | undefined {\n return this.accounts.find((a) => a.id === id)\n }\n\n // ---------- Smart account selection ------------------------------------\n\n /**\n * Pick the best available account.\n *\n * 1. Filter out disabled, banned, exhausted, and accounts still in cooldown.\n * 2. Prefer accounts with more remaining premium quota.\n * 3. Fall back to round-robin (least-recently-used) when quotas are equal\n * or unknown.\n */\n getActiveAccount(): Account | undefined {\n const now = Date.now()\n\n const eligible = this.accounts.filter((a) => {\n if (\n a.status === \"disabled\"\n || a.status === \"banned\"\n || a.status === \"exhausted\"\n ) {\n return false\n }\n if (a.cooldownUntil && a.cooldownUntil > now) return false\n return true\n })\n\n if (eligible.length === 0) {\n // Fallback: if there is exactly one account and it's only cooling down\n // (not banned/disabled/exhausted), return it anyway. With a single\n // account there is nothing to \"switch to\", so blocking all requests\n // for the cooldown period would be a self-inflicted outage.\n if (this.accounts.length === 1) {\n const solo = this.accounts[0]\n if (\n solo.status !== \"disabled\"\n && solo.status !== \"banned\"\n && solo.status !== \"exhausted\"\n ) {\n return solo\n }\n }\n return undefined\n }\n\n eligible.sort((a, b) => {\n const aRemaining = a.usage?.premium_remaining ?? -1\n const bRemaining = b.usage?.premium_remaining ?? -1\n\n // Higher remaining quota first\n if (aRemaining !== bRemaining) return bRemaining - aRemaining\n\n // Equal / unknown → least recently used first (round-robin)\n const aUsed = a.lastUsedAt ?? 0\n const bUsed = b.lastUsedAt ?? 0\n return aUsed - bUsed\n })\n\n return eligible[0]\n }\n\n // ---------- Status management ------------------------------------------\n\n markAccountStatus(id: string, status: AccountStatus, message?: string): void {\n const account = this.getAccountById(id)\n if (!account) return\n\n account.status = status\n account.statusMessage = message\n\n switch (status) {\n case \"rate_limited\":\n case \"error\": {\n // Only apply cooldown when there are other accounts to switch to.\n // With a single account, cooldown would block ALL requests with\n // \"No available accounts\" — effectively a self-inflicted outage.\n if (this.accounts.length > 1) {\n account.cooldownUntil = Date.now() + COOLDOWN_MS\n }\n account.consecutiveFailures += 1\n\n break\n }\n case \"banned\": {\n account.consecutiveFailures += 1\n\n break\n }\n case \"active\": {\n // Recovering from a failure state — reset\n account.consecutiveFailures = 0\n account.cooldownUntil = undefined\n\n break\n }\n // No default\n }\n // \"exhausted\" and \"disabled\" don't touch consecutiveFailures\n\n this.debouncedSave()\n }\n\n markAccountSuccess(id: string): void {\n const account = this.getAccountById(id)\n if (!account) return\n\n account.consecutiveFailures = 0\n account.lastUsedAt = Date.now()\n\n if (account.status === \"error\" || account.status === \"rate_limited\") {\n account.status = \"active\"\n account.statusMessage = undefined\n account.cooldownUntil = undefined\n }\n\n this.debouncedSave()\n }\n\n // ---------- Token & usage refresh (per account) ------------------------\n\n /**\n * Refresh the short-lived Copilot JWT for a single account.\n *\n * Passes the account's GitHub token directly to `getCopilotToken()` so that\n * global `state.githubToken` is never mutated — safe for concurrent use.\n */\n async refreshAccountToken(account: Account): Promise<void> {\n try {\n const data = await getCopilotToken(account.githubToken)\n // eslint-disable-next-line require-atomic-updates\n account.copilotToken = data.token\n if (data.endpoints?.api) {\n // eslint-disable-next-line require-atomic-updates\n account.copilotApiEndpoint = data.endpoints.api\n }\n } catch (err: unknown) {\n if (err instanceof HTTPError && err.response.status === 401) {\n this.markAccountStatus(account.id, \"banned\", \"GitHub token invalid\")\n consola.warn(\n `Account ${account.label}: token invalid, marked as banned`,\n )\n } else {\n consola.warn(\n `Account ${account.label}: failed to refresh Copilot token: ${rootCause(err)}`,\n )\n consola.debug(\n `Account ${account.label}: failed to refresh Copilot token:`,\n err,\n )\n }\n }\n }\n\n /**\n * Refresh the usage / quota snapshot for a single account.\n *\n * Passes the account's GitHub token directly to `getCopilotUsage()` so that\n * global `state.githubToken` is never mutated — safe for concurrent use.\n */\n async refreshAccountUsage(account: Account): Promise<void> {\n try {\n const data = await getCopilotUsage(account.githubToken)\n const snap = data.quota_snapshots\n\n // eslint-disable-next-line require-atomic-updates\n account.usage = {\n premium_remaining: snap.premium_interactions.remaining,\n premium_total: snap.premium_interactions.entitlement,\n chat_remaining: snap.chat.remaining,\n chat_total: snap.chat.entitlement,\n quotaResetDate: data.quota_reset_date,\n lastCheckedAt: Date.now(),\n }\n\n // Transition between active ↔ exhausted\n if (account.usage.premium_remaining <= 0 && account.status === \"active\") {\n this.markAccountStatus(\n account.id,\n \"exhausted\",\n \"Premium quota exhausted\",\n )\n } else if (\n account.usage.premium_remaining > 0\n && account.status === \"exhausted\"\n ) {\n account.status = \"active\"\n account.statusMessage = undefined\n account.consecutiveFailures = 0\n this.debouncedSave()\n }\n } catch (err) {\n consola.warn(\n `Account ${account.label}: failed to refresh usage: ${rootCause(err)}`,\n )\n consola.debug(`Account ${account.label}: failed to refresh usage:`, err)\n }\n }\n\n // ---------- Background refresh -----------------------------------------\n\n /** Refresh Copilot tokens for all non-disabled accounts. */\n async refreshAllTokens(): Promise<void> {\n const targets = this.accounts.filter((a) => a.status !== \"disabled\")\n await Promise.allSettled(targets.map((a) => this.refreshAccountToken(a)))\n }\n\n /** Refresh usage snapshots for all non-disabled accounts. */\n async refreshAllUsage(): Promise<void> {\n const targets = this.accounts.filter((a) => a.status !== \"disabled\")\n await Promise.allSettled(targets.map((a) => this.refreshAccountUsage(a)))\n }\n\n /**\n * Start periodic background refresh loops.\n *\n * The initial token refresh is awaited to ensure accounts are ready before\n * the first request arrives. Usage refresh runs in the background.\n *\n * @param tokenIntervalMs Token refresh interval (default 25 min).\n * @param usageIntervalMs Usage refresh interval (default 5 min).\n */\n async startBackgroundRefresh(\n tokenIntervalMs: number = 25 * 60 * 1000,\n usageIntervalMs: number = 5 * 60 * 1000,\n ): Promise<void> {\n this.stopBackgroundRefresh()\n\n // Initial refresh — await token refresh so accounts are ready for requests\n await this.refreshAllTokens()\n void this.refreshAllUsage()\n\n this.refreshInterval = setInterval(() => {\n void this.refreshAllTokens()\n }, tokenIntervalMs)\n\n this.usageInterval = setInterval(() => {\n void this.refreshAllUsage()\n }, usageIntervalMs)\n\n consola.info(\n `Background refresh started (tokens: ${tokenIntervalMs / 60_000}m, usage: ${usageIntervalMs / 60_000}m)`,\n )\n\n // Start periodic connection pool recycling (~4h with jitter)\n startConnectionRecycling()\n }\n\n stopBackgroundRefresh(): void {\n stopConnectionRecycling()\n if (this.refreshInterval) {\n clearInterval(this.refreshInterval)\n this.refreshInterval = undefined\n }\n if (this.usageInterval) {\n clearInterval(this.usageInterval)\n this.usageInterval = undefined\n }\n }\n\n // ---------- Helpers ----------------------------------------------------\n\n get accountCount(): number {\n return this.accounts.length\n }\n\n get activeAccountCount(): number {\n return this.accounts.filter((a) => a.status === \"active\").length\n }\n\n hasAccounts(): boolean {\n return this.accounts.length > 0\n }\n\n // ---------- Legacy migration -------------------------------------------\n\n /**\n * Create an Account entry from the legacy single-account global state.\n * Useful for seamless upgrade from single-account to multi-account mode.\n *\n * Falls back to creating a minimal entry if API validation fails — the\n * background refresh will fill in the missing data later.\n */\n async migrateFromLegacy(\n githubToken: string,\n accountType: string,\n ): Promise<Account> {\n // Check if this token is already registered\n const existing = this.accounts.find((a) => a.githubToken === githubToken)\n if (existing) {\n consola.info(\"Legacy account already migrated, skipping\")\n return existing\n }\n\n try {\n const account = await this.addAccount(\n githubToken,\n \"Primary (migrated)\",\n accountType,\n )\n consola.success(\"Legacy single-account migrated to multi-account manager\")\n return account\n } catch (error) {\n // API validation failed — create a minimal entry anyway so the account\n // is visible in the management UI. Background token/usage refresh will\n // fill in the missing data.\n consola.warn(\n \"Could not fully validate legacy account, adding with limited info:\",\n error,\n )\n\n const account: Account = {\n id: randomUUID(),\n label: \"Primary (migrated)\",\n githubToken,\n copilotToken: undefined,\n accountType,\n status: \"active\",\n consecutiveFailures: 0,\n machineId: randomBytes(32).toString(\"hex\"),\n sessionId: randomUUID(),\n addedAt: Date.now(),\n }\n\n this.accounts.push(account)\n await this.saveAccounts()\n return account\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Singleton\n// ---------------------------------------------------------------------------\n\nexport const accountManager = new AccountManager()\n"],"mappings":";;;;;;;;AAEA,MAAa,yBAAyB;CACpC,gBAAgB;CAChB,QAAQ;CACT;AAED,MAAM,kBAAkB;AACxB,MAAM,wBAAwB,gBAAgB;AAC9C,MAAM,aAAa,qBAAqB;AAGxC,MAAM,cAAc;AAuBpB,MAAa,kBAAkB,WAAwB;AACrD,KAAI,OAAO,mBACT,QAAO,OAAO;AAEhB,QAAO,OAAO,gBAAgB,eAC1B,kCACA,eAAe,OAAO,YAAY;;AAExC,MAAa,kBACX,QACA,SAAkB,UACf;CACH,MAAM,UAAkC;EACtC,eAAe,UAAU,OAAO;EAChC,gBAAgB,iBAAiB,CAAC;EAClC,0BAA0B;EAC1B,kBAAkB,UAAU,OAAO;EACnC,yBAAyB;EACzB,cAAc;EACd,iBAAiB;EACjB,sBAAsB;EACtB,mBAAmB,YAAY;EAC/B,wBAAwB;EACxB,gBAAgB,YAAY;EAC5B,uCAAuC;EAEvC,GAAI,OAAO,aAAa,EAAE,oBAAoB,OAAO,WAAW;EAChE,GAAI,OAAO,aAAa,EAAE,oBAAoB,OAAO,WAAW;EACjE;AAED,KAAI,OAAQ,SAAQ,4BAA4B;AAEhD,QAAO;;AAGT,MAAa,sBAAsB;AACnC,MAAa,iBAAiB,YAAyB;CACrD,GAAG,iBAAiB;CACpB,eAAe,SAAS,OAAO;CAC/B,kBAAkB,UAAU,OAAO;CACnC,yBAAyB;CACzB,cAAc;CACd,wBAAwB;CACxB,uCAAuC;CACxC;AAED,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAChC,MAAa,oBAAoB,CAAC,YAAY,CAAC,KAAK,IAAI;;;ACrDxD,MAAa,QAAe;CAC1B,aAAa;CACb,eAAe;CACf,eAAe;CACf,WAAW;CACX,qBAAqB;CACtB;;;AC3BD,MAAa,YAAY,YAAY;CAEnC,IAAI,SAAsB;AAC1B,KAAI,MAAM,uBAAuB,eAAe,aAAa,EAAE;EAC7D,MAAM,UAAU,eAAe,kBAAkB;AACjD,MAAI,SAAS,aACX,UAAS;GACP,cAAc,QAAQ;GACtB,oBAAoB,QAAQ;GAC5B,aAAa,QAAQ;GACrB,aAAa,QAAQ;GACrB,eAAe,MAAM;GACtB;;CAIL,MAAM,MAAM,GAAG,eAAe,OAAO,CAAC;CAEtC,MAAM,WAAW,MAAM,MAAM,KAAK,EAChC,SAAS,eAAe,OAAO,EAChC,CAAC;AAEF,KAAI,CAAC,SAAS,GAAI,OAAM,IAAI,UAAU,wBAAwB,SAAS;AAIvE,QAFc,MAAM,SAAS,MAAM;;;;ACjCrC,MAAM,WAAW;AAEjB,eAAsB,mBAAmB;CACvC,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,UAAU,iBAAiB;AAC/B,aAAW,OAAO;IACjB,IAAK;AAER,KAAI;EAUF,MAAM,SAFW,OAPA,MAAM,MACrB,kFACA,EACE,QAAQ,WAAW,QACpB,CACF,EAE+B,MAAM,EAEf,MADH,mBACqB;AAEzC,MAAI,MACF,QAAO,MAAM;AAGf,SAAO;SACD;AACN,SAAO;WACC;AACR,eAAa,QAAQ;;;;;ACnBzB,MAAa,SAAS,OACpB,IAAI,SAAS,YAAY;AACvB,YAAW,SAAS,GAAG;EACvB;;;;;;;;AASJ,SAAgB,UAAU,KAAsB;AAC9C,KAAI,eAAe,MACjB,QAAO,IAAI,iBAAiB,QAAQ,IAAI,MAAM,UAAU,IAAI;AAE9D,QAAO,OAAO,IAAI;;AAGpB,MAAa,aAAa,UACxB,UAAU,QAAQ,UAAU,KAAA;AAE9B,eAAsB,cAA6B;AAEjD,OAAM,SADS,MAAM,WAAW;;AAIlC,MAAa,qBAAqB,YAAY;CAC5C,MAAM,WAAW,MAAM,kBAAkB;AACzC,OAAM,gBAAgB;AAEtB,SAAQ,KAAK,yBAAyB,WAAW;;;;;;;;;;;;;AAcnD,SAAgB,UAAU,WAAsC;CAC9D,MAAM,SAAS,MAAM,QAAQ;AAC7B,KAAI,CAAC,UAAU,OAAO,WAAW,EAC/B;CAIF,MAAM,QAAQ,OAAO,MAAM,MAAM,EAAE,OAAO,UAAU;AACpD,KAAI,MAAO,QAAO;CAGlB,MAAM,OAAO,UAAU,QAAQ,WAAW,GAAG;AAC7C,KAAI,SAAS,WAAW;EACtB,MAAM,YAAY,OAAO,MAAM,MAAM,EAAE,OAAO,KAAK;AACnD,MAAI,UAAW,QAAO;;CAIxB,MAAM,UAAU,KAAK,QAAQ,iBAAiB,SAAS;AACvD,KAAI,YAAY,MAAM;EACpB,MAAM,WAAW,OAAO,MAAM,MAAM,EAAE,OAAO,QAAQ;AACrD,MAAI,SAAU,QAAO;;CAIvB,MAAM,WAAW,UAAU,QAAQ,gBAAgB,QAAQ;AAC3D,KAAI,aAAa,WAAW;EAC1B,MAAM,YAAY,OAAO,MAAM,MAAM,EAAE,OAAO,SAAS;AACvD,MAAI,UAAW,QAAO;;;;;AC3E1B,IAAa,YAAb,cAA+B,MAAM;CACnC;CAEA,YAAY,SAAiB,UAAoB;AAC/C,QAAM,QAAQ;AACd,OAAK,WAAW;;;AAIpB,eAAsB,aAAa,GAAY,OAAgB;AAC7D,KAAI,iBAAiB,WAAW;EAE9B,IAAI;AACJ,MAAI;AACF,eAAY,MAAM,MAAM,SAAS,MAAM;UACjC;AAEN,eAAY,MAAM;;AAIpB,MAAI,MAAM,SAAS,WAAW,KAAK,QAE5B;GACL,IAAI;AACJ,OAAI;AACF,gBAAY,KAAK,MAAM,UAAU;WAC3B;AACN,gBAAY;;AAEd,WAAQ,KAAK,mBAAmB,UAAU,MAAM,GAAG;AACnD,WAAQ,MAAM,eAAe,UAAU;;AAGzC,SAAO,EAAE,KACP,EACE,OAAO;GACL,SAAS;GACT,MAAM;GACP,EACF,EACD,MAAM,SAAS,OAChB;;CAIH,MAAM,UAAW,MAAgB,WAAW,OAAO,MAAM;CACzD,MAAM,QAAS,MAA4B;AAC3C,KAAI,MACF,SAAQ,MAAM,GAAG,QAAQ,IAAI,MAAM,UAAU;KAE7C,SAAQ,MAAM,QAAQ;AAExB,QAAO,EAAE,KACP,EACE,OAAO;EACL,SAAU,MAAgB;EAC1B,MAAM;EACP,EACF,EACD,IACD;;;;AChEH,MAAM,UAAU,KAAK,KAAK,GAAG,SAAS,EAAE,UAAU,SAAS,mBAAmB;AAM9E,MAAa,QAAQ;CACnB;CACA,UAAU;CACV,mBAPwB,KAAK,KAAK,SAAS,eAAe;CAQ1D,eANoB,KAAK,KAAK,SAAS,gBAAgB;CAOxD;AAED,eAAsB,cAA6B;AACjD,OAAM,GAAG,MAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;AAClD,OAAM,WAAW,MAAM,kBAAkB;;AAG3C,eAAe,WAAW,UAAiC;AACzD,KAAI;AACF,QAAM,GAAG,OAAO,UAAU,GAAG,UAAU,KAAK;SACtC;AACN,QAAM,GAAG,UAAU,UAAU,GAAG;AAChC,QAAM,GAAG,MAAM,UAAU,IAAM;;;;;ACpBnC,MAAM,eAAe;CACnB,kBAAkB;CAClB,qBAAqB;CAKrB,SAAS;CACT,SAAS;EACP,SAAS;EACT,WAAW;EACX,uBAAuB;EACxB;CACF;AACD,IAAI;AACJ,IAAI,0BAAU,IAAI,KAAyB;;AAE3C,IAAI,cAAc;;;;;;;;;;;;;;;AAoBlB,IAAI;AACJ,MAAM,wBAAwB;AAC9B,MAAM,gBAAgB;;AAQtB,MAAM,gCAAgB,IAAI,KAA+B;AAEzD,SAAS,iBAAuB;AAC9B,KAAI,eAAgB;AACpB,kBAAiB,kBAAkB;AAEjC,OAAK,MAAM,CAAC,KAAK,SAAS,eAAe;AACvC,OAAI,KAAK,SAAS,EAAG;AAErB,OAAI,QAAQ,cAAc;AAExB,UAAM,eAAe,EAAE,QAAQ,QAAQ,CAAC,CAAC,YAAY,GAAG;AACxD,YAAQ,MAAM,qCAAqC;UAC9C;IAEL,MAAM,aAAa,qBAAqB,KAAK,KAAK,aAAa;AAC/D,UAAM,eAAe;KACnB,QAAQ;KACI;KACb,CAAgB,CAAC,YAAY,GAAG;AACjC,YAAQ,MAAM,sCAAsC,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG;;;IAG1E,sBAAsB;AAEzB,gBAAe,OAAO;AACtB,SAAQ,MAAM,0CAA0C;;AAG1D,SAAS,gBAAsB;AAC7B,KAAI,gBAAgB;AAClB,gBAAc,eAAe;AAC7B,mBAAiB,KAAA;AACjB,UAAQ,MAAM,8CAA8C;;;AAIhE,SAAS,sBAA8B;CACrC,IAAI,QAAQ;AACZ,MAAK,MAAM,QAAQ,cAAc,QAAQ,CACvC,UAAS,KAAK;AAEhB,QAAO;;;;;;;;;AAUT,SAAgB,kBAAkB,aAGzB;AACP,KAAI,CAAC,YAAa;CAElB,MAAM,MAAM,aAAa,aAAa;CACtC,MAAM,WAAW,cAAc,IAAI,IAAI;AACvC,KAAI,SACF,UAAS;KAET,eAAc,IAAI,KAAK;EACrB,OAAO;EACP,cAAc,aAAa;EAC5B,CAAC;AAGJ,KAAI,qBAAqB,KAAK,EAAG,iBAAgB;;;;;;AAOnD,SAAgB,gBAAgB,aAGvB;AACP,KAAI,CAAC,YAAa;CAElB,MAAM,MAAM,aAAa,aAAa;CACtC,MAAM,WAAW,cAAc,IAAI,IAAI;AACvC,KAAI,UAAU;AACZ,WAAS,QAAQ,KAAK,IAAI,GAAG,SAAS,QAAQ,EAAE;AAChD,MAAI,SAAS,UAAU,EAAG,eAAc,OAAO,IAAI;;AAGrD,KAAI,qBAAqB,KAAK,EAAG,gBAAe;;AAGlD,SAAgB,mBAAyB;AACvC,KAAI,OAAO,QAAQ,YAAa;AAEhC,KAAI;AACF,WAAS,IAAI,MAAM,aAAa;AAChC,4BAAU,IAAI,KAAyB;AA4DvC,sBAtDmB;GACjB,SACE,SACA,SACA;AACA,QAAI;KACF,MAAM,SACJ,OAAO,QAAQ,WAAW,WACxB,IAAI,IAAI,QAAQ,OAAO,GACtB,QAAQ;KAIb,MAAM,MAHM,eAGI,OAAO,UAAU,CAAC;KAClC,MAAM,WAAW,OAAO,IAAI,SAAS,IAAI,MAAM,KAAA;AAC/C,SAAI,CAAC,UAAU;AACb,cAAQ,MAAM,sBAAsB,OAAO,WAAW;AACtD,aAAQ,OAAiC,SAAS,SAAS,QAAQ;;KAErE,IAAI,QAAQ,QAAQ,IAAI,SAAS;AACjC,SAAI,CAAC,OAAO;AACV,cAAQ,IAAI,WAAW;OAAE,KAAK;OAAU,GAAG;OAAc,CAAC;AAC1D,cAAQ,IAAI,UAAU,MAAM;;KAE9B,IAAI,QAAQ;AACZ,SAAI;MACF,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,cAAQ,GAAG,EAAE,SAAS,IAAI,EAAE;aACtB;AAGR,aAAQ,MAAM,qBAAqB,OAAO,SAAS,OAAO,QAAQ;AAClE,YAAQ,MAAgC,SAAS,SAAS,QAAQ;YAC5D;AACN,YAAQ,OAAiC,SAAS,SAAS,QAAQ;;;GAGvE,QAAQ;AACN,SAAK,MAAM,SAAS,QAAQ,QAAQ,CAC5B,OAAgC,OAAO;AAI/C,WAAO,OAAQ,OAAO;;GAExB,UAAU;AACR,SAAK,MAAM,SAAS,QAAQ,QAAQ,CAC5B,OAAgC,SAAS;AAGjD,WAAO,OAAQ,SAAS;;GAE3B,CAEuD;AACxD,gBAAc;AACd,UAAQ,MAAM,mDAAmD;UAC1D,KAAK;AACZ,UAAQ,MAAM,wBAAwB,IAAI;;;;;;;;;;;;;;AAe9C,SAAgB,mBAAyB;AACvC,KAAI,OAAO,QAAQ,YAAa;AAChC,KAAI,CAAC,OAAQ;CAEb,MAAM,YAAY;CAClB,MAAM,aAAa;AAEnB,UAAS,IAAI,MAAM,aAAa;AAChC,2BAAU,IAAI,KAAyB;AAGjC,WAAoC,OAAO,CAAC,YAAY,GAAG;AACjE,MAAK,MAAM,SAAS,WAAW,QAAQ,CAC/B,OAAgC,OAAO,CAAC,YAAY,GAAG;AAG/D,SAAQ,MAAM,gDAAgD;;;AAQhE,MAAM,gCAAgB,IAAI,KAAoB;AAC9C,MAAM,qCAAqB,IAAI,KAAsC;;;;;;AAOrE,SAAgB,qBACd,WACA,cAMA;AAEA,QAAO,EACL,SACE,SACA,SACA;AACA,MAAI;GACF,MAAM,SACJ,OAAO,QAAQ,WAAW,WACxB,IAAI,IAAI,QAAQ,OAAO,GACtB,QAAQ;GAEb,IAAI;AACJ,OAAI,aACF,YAAW;QACN;IAIL,MAAM,MAHM,eAGI,OAAO,UAAU,CAAC;AAClC,eAAW,OAAO,IAAI,SAAS,IAAI,MAAM,KAAA;;AAG3C,OAAI,CAAC,UAAU;IAEb,IAAI,QAAQ,cAAc,IAAI,UAAU;AACxC,QAAI,CAAC,OAAO;AACV,aAAQ,IAAI,MAAM,aAAa;AAC/B,mBAAc,IAAI,WAAW,MAAM;;AAErC,WAAQ,MAAgC,SAAS,SAAS,QAAQ;;GAIpE,IAAI,WAAW,mBAAmB,IAAI,UAAU;AAChD,OAAI,CAAC,UAAU;AACb,+BAAW,IAAI,KAAyB;AACxC,uBAAmB,IAAI,WAAW,SAAS;;GAE7C,IAAI,aAAa,SAAS,IAAI,SAAS;AACvC,OAAI,CAAC,YAAY;AACf,iBAAa,IAAI,WAAW;KAAE,KAAK;KAAU,GAAG;KAAc,CAAC;AAC/D,aAAS,IAAI,UAAU,WAAW;;AAEpC,UAAQ,WAAqC,SAAS,SAAS,QAAQ;UACjE;GAEN,IAAI,QAAQ,cAAc,IAAI,UAAU;AACxC,OAAI,CAAC,OAAO;AACV,YAAQ,IAAI,MAAM,aAAa;AAC/B,kBAAc,IAAI,WAAW,MAAM;;AAErC,UAAQ,MAAgC,SAAS,SAAS,QAAQ;;IAGvE;;;;;AAMH,SAAgB,wBAAwB,WAAyB;CAC/D,MAAM,WAAW,cAAc,IAAI,UAAU;AAC7C,KAAI,UAAU;AACN,WAAmC,OAAO,CAAC,YAAY,GAAG;AAChE,gBAAc,OAAO,UAAU;;CAEjC,MAAM,aAAa,mBAAmB,IAAI,UAAU;AACpD,KAAI,YAAY;AACd,OAAK,MAAM,SAAS,WAAW,QAAQ,CAC/B,OAAgC,OAAO,CAAC,YAAY,GAAG;AAE/D,qBAAmB,OAAO,UAAU;;;;;;AAOxC,SAAgB,6BAAmC;AACjD,MAAK,MAAM,CAAC,OAAO,cACjB,yBAAwB,GAAG;;AAQ/B,IAAI;;;;;;;;AASJ,SAAgB,yBACd,iBAAyB,QAAc,KACjC;AACN,0BAAyB;AAEzB,0BAAyB,kBAAkB;EAEzC,MAAM,SAAS,iBAAiB,OAAQ,KAAK,QAAQ,GAAG,IAAI;AAC5D,mBACQ;AACJ,+BAA4B;AAC5B,WAAQ,MAAM,wCAAwC;KAExD,KAAK,IAAI,GAAG,OAAO,CACpB;IACA,eAAe;AAGlB,wBAAuB,OAAO;AAC9B,SAAQ,KACN,iDAAiD,KAAK,MAAM,iBAAiB,KAAU,CAAC,IACzF;;;;;AAMH,SAAgB,0BAAgC;AAC9C,KAAI,wBAAwB;AAC1B,gBAAc,uBAAuB;AACrC,2BAAyB,KAAA;;;;;;;;;;;;;ACjY7B,MAAa,kBAAkB,OAAO,gBAAyB;CAC7D,MAAM,aAAa,eAAe,MAAM;CACxC,MAAM,kBAAkB,gBAAgB,KAAA;CAExC,MAAM,MAAM,GAAG,oBAAoB;CACnC,MAAM,eAA4B,EAChC,SAAS,cAAc;EACrB,GAAG;EACH,aAAa;EACd,CAAC,EACH;CAGD,MAAM,aAAa;CACnB,IAAI;CACJ,IAAI;AAEJ,MAAK,IAAI,UAAU,GAAG,WAAW,YAAY,UAC3C,KAAI;AACF,aAAW,MAAM,MAAM,KAAK,aAAa;AACzC;UACO,OAAgB;AACvB,cAAY;AACZ,MAAI,UAAU,YAAY;GACxB,MAAM,QAAQ,OAAQ,UAAU;AAChC,WAAQ,KACN,gCAAgC,UAAU,EAAE,GAAG,aAAa,EAAE,gBAAgB,MAAM,MACpF,iBAAiB,QAAQ,MAAM,UAAU,MAC1C;AACD,SAAM,IAAI,SAAS,MAAM,WAAW,GAAG,MAAM,CAAC;;;AAKpD,KAAI,CAAC,SACH,OAAM;AAGR,KAAI,CAAC,SAAS,GAAI,OAAM,IAAI,UAAU,+BAA+B,SAAS;CAE9E,MAAM,OAAQ,MAAM,SAAS,MAAM;AAKnC,KAAI,CAAC,mBAAmB,KAAK,WAAW,IAEtC,OAAM,qBAAqB,KAAK,UAAU;AAG5C,QAAO;;;;;;;;;;;;ACpDT,MAAa,kBAAkB,OAC7B,gBACkC;CAClC,MAAM,WAAW,MAAM,MAAM,GAAG,oBAAoB,yBAAyB,EAC3E,SAAS,cAAc;EACrB,GAAG;EACH,aAAa,eAAe,MAAM;EACnC,CAAC,EACH,CAAC;AAEF,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,UAAU,+BAA+B,SAAS;AAG9D,QAAQ,MAAM,SAAS,MAAM;;;;;;;;;;;ACf/B,eAAsB,cAAc,aAAsB;CACxD,MAAM,QAAQ,eAAe,MAAM;CACnC,MAAM,WAAW,MAAM,MAAM,GAAG,oBAAoB,QAAQ,EAC1D,SAAS;EACP,eAAe,SAAS;EACxB,GAAG,iBAAiB;EACrB,EACF,CAAC;AAEF,KAAI,CAAC,SAAS,GAAI,OAAM,IAAI,UAAU,6BAA6B,SAAS;AAE5E,QAAQ,MAAM,SAAS,MAAM;;;;ACmD/B,MAAM,gBAAgB,MAAM;AAM5B,MAAM,cAAc,KAAK;AAEzB,IAAa,iBAAb,MAA4B;CAC1B,WAAmC,EAAE;CACrC;CACA;CAGA;CACA,cAAsB;;;;;CAQtB,MAAM,eAA8B;AAClC,MAAI;GAEF,MAAM,MAAM,MAAM,GAAG,SAAS,eAAe,OAAO;AAEpD,QAAK,WADU,KAAK,MAAM,IAAI,CACP,KAAK,OAAO;IACjC,GAAG;IACH,cAAc,KAAA;IACd,WAAW,YAAY;IACvB,WAAW,EAAE,aAAa,YAAY,GAAG,CAAC,SAAS,MAAM;IAC1D,EAAE;AACH,WAAQ,KAAK,UAAU,KAAK,SAAS,OAAO,uBAAuB;WAC5D,KAAc;AACrB,OAAK,IAA8B,SAAS,UAAU;AACpD,SAAK,WAAW,EAAE;AAClB;;AAEF,WAAQ,KAAK,4BAA4B,UAAU,IAAI,GAAG;AAC1D,WAAQ,MAAM,4BAA4B,IAAI;AAC9C,QAAK,WAAW,EAAE;;;;;;;CAQtB,MAAM,eAA8B;EAClC,MAAM,OAAgC,KAAK,SAAS,KACjD,EAAE,cAAc,UAAU,GAAG,WAAW,KAC1C;AACD,MAAI;AACF,SAAM,GAAG,UAAU,eAAe,KAAK,UAAU,MAAM,MAAM,EAAE,EAAE;IAC/D,UAAU;IACV,MAAM;IACP,CAAC;WACK,KAAK;AACZ,WAAQ,KAAK,4BAA4B,UAAU,IAAI,GAAG;AAC1D,WAAQ,MAAM,4BAA4B,IAAI;;;;CAKlD,gBAA8B;AAC5B,MAAI,KAAK,UAAW;AACpB,OAAK,cAAc;AACnB,OAAK,YAAY,WAAW,YAAY;AACtC,QAAK,YAAY,KAAA;AACjB,OAAI,KAAK,aAAa;AACpB,SAAK,cAAc;AACnB,UAAM,KAAK,cAAc;;KAE1B,IAAM;;;;;;;;;CAYX,MAAM,WACJ,aACA,OACA,cAAsB,cACJ;EAElB,MAAM,OAAO,MAAM,cAAc,YAAY;EAG7C,MAAM,YAAY,MAAM,gBAAgB,YAAY;EAEpD,MAAM,UAAmB;GACvB,IAAI,YAAY;GAChB;GACA;GACA,cAAc,UAAU;GACxB,oBAAoB,UAAU,WAAW;GACzC;GACA,QAAQ;GACR,qBAAqB;GACrB,aAAa,KAAK;GAClB,WAAW,YAAY,GAAG,CAAC,SAAS,MAAM;GAC1C,WAAW,YAAY;GACvB,SAAS,KAAK,KAAK;GACpB;AAED,OAAK,SAAS,KAAK,QAAQ;AAC3B,QAAM,KAAK,cAAc;AAEzB,UAAQ,QAAQ,kBAAkB,MAAM,IAAI,KAAK,MAAM,GAAG;AAC1D,SAAO;;CAGT,MAAM,cAAc,IAA8B;EAChD,MAAM,MAAM,KAAK,SAAS,WAAW,MAAM,EAAE,OAAO,GAAG;AACvD,MAAI,QAAQ,GAAI,QAAO;EACvB,MAAM,CAAC,WAAW,KAAK,SAAS,OAAO,KAAK,EAAE;AAC9C,QAAM,KAAK,cAAc;AACzB,UAAQ,KAAK,oBAAoB,QAAQ,QAAQ;AACjD,SAAO;;CAGT,cAA8B;AAC5B,SAAO,KAAK;;CAGd,eAAe,IAAiC;AAC9C,SAAO,KAAK,SAAS,MAAM,MAAM,EAAE,OAAO,GAAG;;;;;;;;;;CAa/C,mBAAwC;EACtC,MAAM,MAAM,KAAK,KAAK;EAEtB,MAAM,WAAW,KAAK,SAAS,QAAQ,MAAM;AAC3C,OACE,EAAE,WAAW,cACV,EAAE,WAAW,YACb,EAAE,WAAW,YAEhB,QAAO;AAET,OAAI,EAAE,iBAAiB,EAAE,gBAAgB,IAAK,QAAO;AACrD,UAAO;IACP;AAEF,MAAI,SAAS,WAAW,GAAG;AAKzB,OAAI,KAAK,SAAS,WAAW,GAAG;IAC9B,MAAM,OAAO,KAAK,SAAS;AAC3B,QACE,KAAK,WAAW,cACb,KAAK,WAAW,YAChB,KAAK,WAAW,YAEnB,QAAO;;AAGX;;AAGF,WAAS,MAAM,GAAG,MAAM;GACtB,MAAM,aAAa,EAAE,OAAO,qBAAqB;GACjD,MAAM,aAAa,EAAE,OAAO,qBAAqB;AAGjD,OAAI,eAAe,WAAY,QAAO,aAAa;AAKnD,WAFc,EAAE,cAAc,MAChB,EAAE,cAAc;IAE9B;AAEF,SAAO,SAAS;;CAKlB,kBAAkB,IAAY,QAAuB,SAAwB;EAC3E,MAAM,UAAU,KAAK,eAAe,GAAG;AACvC,MAAI,CAAC,QAAS;AAEd,UAAQ,SAAS;AACjB,UAAQ,gBAAgB;AAExB,UAAQ,QAAR;GACE,KAAK;GACL,KAAK;AAIH,QAAI,KAAK,SAAS,SAAS,EACzB,SAAQ,gBAAgB,KAAK,KAAK,GAAG;AAEvC,YAAQ,uBAAuB;AAE/B;GAEF,KAAK;AACH,YAAQ,uBAAuB;AAE/B;GAEF,KAAK;AAEH,YAAQ,sBAAsB;AAC9B,YAAQ,gBAAgB,KAAA;AAExB;;AAMJ,OAAK,eAAe;;CAGtB,mBAAmB,IAAkB;EACnC,MAAM,UAAU,KAAK,eAAe,GAAG;AACvC,MAAI,CAAC,QAAS;AAEd,UAAQ,sBAAsB;AAC9B,UAAQ,aAAa,KAAK,KAAK;AAE/B,MAAI,QAAQ,WAAW,WAAW,QAAQ,WAAW,gBAAgB;AACnE,WAAQ,SAAS;AACjB,WAAQ,gBAAgB,KAAA;AACxB,WAAQ,gBAAgB,KAAA;;AAG1B,OAAK,eAAe;;;;;;;;CAWtB,MAAM,oBAAoB,SAAiC;AACzD,MAAI;GACF,MAAM,OAAO,MAAM,gBAAgB,QAAQ,YAAY;AAEvD,WAAQ,eAAe,KAAK;AAC5B,OAAI,KAAK,WAAW,IAElB,SAAQ,qBAAqB,KAAK,UAAU;WAEvC,KAAc;AACrB,OAAI,eAAe,aAAa,IAAI,SAAS,WAAW,KAAK;AAC3D,SAAK,kBAAkB,QAAQ,IAAI,UAAU,uBAAuB;AACpE,YAAQ,KACN,WAAW,QAAQ,MAAM,mCAC1B;UACI;AACL,YAAQ,KACN,WAAW,QAAQ,MAAM,qCAAqC,UAAU,IAAI,GAC7E;AACD,YAAQ,MACN,WAAW,QAAQ,MAAM,qCACzB,IACD;;;;;;;;;;CAWP,MAAM,oBAAoB,SAAiC;AACzD,MAAI;GACF,MAAM,OAAO,MAAM,gBAAgB,QAAQ,YAAY;GACvD,MAAM,OAAO,KAAK;AAGlB,WAAQ,QAAQ;IACd,mBAAmB,KAAK,qBAAqB;IAC7C,eAAe,KAAK,qBAAqB;IACzC,gBAAgB,KAAK,KAAK;IAC1B,YAAY,KAAK,KAAK;IACtB,gBAAgB,KAAK;IACrB,eAAe,KAAK,KAAK;IAC1B;AAGD,OAAI,QAAQ,MAAM,qBAAqB,KAAK,QAAQ,WAAW,SAC7D,MAAK,kBACH,QAAQ,IACR,aACA,0BACD;YAED,QAAQ,MAAM,oBAAoB,KAC/B,QAAQ,WAAW,aACtB;AACA,YAAQ,SAAS;AACjB,YAAQ,gBAAgB,KAAA;AACxB,YAAQ,sBAAsB;AAC9B,SAAK,eAAe;;WAEf,KAAK;AACZ,WAAQ,KACN,WAAW,QAAQ,MAAM,6BAA6B,UAAU,IAAI,GACrE;AACD,WAAQ,MAAM,WAAW,QAAQ,MAAM,6BAA6B,IAAI;;;;CAO5E,MAAM,mBAAkC;EACtC,MAAM,UAAU,KAAK,SAAS,QAAQ,MAAM,EAAE,WAAW,WAAW;AACpE,QAAM,QAAQ,WAAW,QAAQ,KAAK,MAAM,KAAK,oBAAoB,EAAE,CAAC,CAAC;;;CAI3E,MAAM,kBAAiC;EACrC,MAAM,UAAU,KAAK,SAAS,QAAQ,MAAM,EAAE,WAAW,WAAW;AACpE,QAAM,QAAQ,WAAW,QAAQ,KAAK,MAAM,KAAK,oBAAoB,EAAE,CAAC,CAAC;;;;;;;;;;;CAY3E,MAAM,uBACJ,kBAA0B,OAAU,KACpC,kBAA0B,MAAS,KACpB;AACf,OAAK,uBAAuB;AAG5B,QAAM,KAAK,kBAAkB;AACxB,OAAK,iBAAiB;AAE3B,OAAK,kBAAkB,kBAAkB;AAClC,QAAK,kBAAkB;KAC3B,gBAAgB;AAEnB,OAAK,gBAAgB,kBAAkB;AAChC,QAAK,iBAAiB;KAC1B,gBAAgB;AAEnB,UAAQ,KACN,uCAAuC,kBAAkB,IAAO,YAAY,kBAAkB,IAAO,IACtG;AAGD,4BAA0B;;CAG5B,wBAA8B;AAC5B,2BAAyB;AACzB,MAAI,KAAK,iBAAiB;AACxB,iBAAc,KAAK,gBAAgB;AACnC,QAAK,kBAAkB,KAAA;;AAEzB,MAAI,KAAK,eAAe;AACtB,iBAAc,KAAK,cAAc;AACjC,QAAK,gBAAgB,KAAA;;;CAMzB,IAAI,eAAuB;AACzB,SAAO,KAAK,SAAS;;CAGvB,IAAI,qBAA6B;AAC/B,SAAO,KAAK,SAAS,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC;;CAG5D,cAAuB;AACrB,SAAO,KAAK,SAAS,SAAS;;;;;;;;;CAYhC,MAAM,kBACJ,aACA,aACkB;EAElB,MAAM,WAAW,KAAK,SAAS,MAAM,MAAM,EAAE,gBAAgB,YAAY;AACzE,MAAI,UAAU;AACZ,WAAQ,KAAK,4CAA4C;AACzD,UAAO;;AAGT,MAAI;GACF,MAAM,UAAU,MAAM,KAAK,WACzB,aACA,sBACA,YACD;AACD,WAAQ,QAAQ,0DAA0D;AAC1E,UAAO;WACA,OAAO;AAId,WAAQ,KACN,sEACA,MACD;GAED,MAAM,UAAmB;IACvB,IAAI,YAAY;IAChB,OAAO;IACP;IACA,cAAc,KAAA;IACd;IACA,QAAQ;IACR,qBAAqB;IACrB,WAAW,YAAY,GAAG,CAAC,SAAS,MAAM;IAC1C,WAAW,YAAY;IACvB,SAAS,KAAK,KAAK;IACpB;AAED,QAAK,SAAS,KAAK,QAAQ;AAC3B,SAAM,KAAK,cAAc;AACzB,UAAO;;;;AASb,MAAa,iBAAiB,IAAI,gBAAgB"}
@@ -0,0 +1,2 @@
1
+ import { p as HTTPError } from "./account-manager-BLD3jHgL.js";
2
+ export { HTTPError };
@@ -0,0 +1,2 @@
1
+ import { n as getGitHubUser } from "./account-manager-BLD3jHgL.js";
2
+ export { getGitHubUser };
package/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { C as GITHUB_BASE_URL, D as standardHeaders, E as copilotHeaders, T as copilotBaseUrl, _ as findModel, a as getAccountDispatcher, b as sleep, c as notifyStreamStart, d as PATHS, f as ensurePaths, g as cacheVSCodeVersion, h as cacheModels, l as resetAccountConnections, m as forwardError, o as initProxyFromEnv, p as HTTPError, r as getCopilotUsage, s as notifyStreamEnd, t as accountManager, u as resetConnections, v as isNullish, w as GITHUB_CLIENT_ID, x as state, y as rootCause } from "./account-manager-COJx3i2R.js";
3
- import { a as stopCopilotTokenRefresh, i as setupGitHubToken, n as refreshCopilotToken, o as pollAccessToken, r as setupCopilotToken, s as getDeviceCode, t as clearGithubToken } from "./token-CswtzkcP.js";
2
+ import { C as GITHUB_BASE_URL, D as standardHeaders, E as copilotHeaders, T as copilotBaseUrl, _ as findModel, a as getAccountDispatcher, b as sleep, c as notifyStreamStart, d as PATHS, f as ensurePaths, g as cacheVSCodeVersion, h as cacheModels, l as resetAccountConnections, m as forwardError, o as initProxyFromEnv, p as HTTPError, r as getCopilotUsage, s as notifyStreamEnd, t as accountManager, u as resetConnections, v as isNullish, w as GITHUB_CLIENT_ID, x as state, y as rootCause } from "./account-manager-BLD3jHgL.js";
3
+ import { a as stopCopilotTokenRefresh, i as setupGitHubToken, n as refreshCopilotToken, o as pollAccessToken, r as setupCopilotToken, s as getDeviceCode, t as clearGithubToken } from "./token-V-OSvQfl.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  import consola from "consola";
@@ -1631,26 +1631,14 @@ async function checkRateLimit(state) {
1631
1631
  */
1632
1632
  const FETCH_TIMEOUT_MS = 12e4;
1633
1633
  /**
1634
- * Retry delays in ms. After the first failure the connection pool is reset
1635
- * (see `resetConnections`), so retries use fresh sockets.
1634
+ * Retry delays in ms. Empty = no retries.
1636
1635
  *
1637
- * We only allow 1 retry (2 total attempts) to minimize credit waste.
1638
- * Timeout errors are NOT retried at all they indicate the request likely
1639
- * reached Copilot (consuming a credit) and the upstream is slow or the
1640
- * proxy killed the connection mid-flight.
1636
+ * IMPORTANT: Retries are DISABLED because each attempt to Copilot consumes
1637
+ * a credit, and the caller (e.g. Claude Code) already retries at the
1638
+ * application level. Our retry + Claude Code's retry created a request
1639
+ * cascade that caused account bans (367 requests in 52 minutes).
1641
1640
  */
1642
- const RETRY_DELAYS = [2e3];
1643
- /**
1644
- * Timeout for retry attempts (waiting for response headers only).
1645
- * Response headers typically arrive within 3–5 s, even on slow models.
1646
- * 30 s is generous enough for a fresh socket to connect and receive
1647
- * headers, while still failing fast when the upstream is truly down.
1648
- *
1649
- * NOTE: This does NOT affect the SSE streaming phase — once headers
1650
- * arrive, the timeout is cleared and the stream runs until completion
1651
- * or interruption.
1652
- */
1653
- const RETRY_TIMEOUT_MS = 3e4;
1641
+ const RETRY_DELAYS = [];
1654
1642
  /** Minimum interval (ms) between requests on the same account. */
1655
1643
  const MIN_SAME_ACCOUNT_INTERVAL_MS = 1e3;
1656
1644
  /** Random jitter range (ms) added when switching between accounts. */
@@ -1691,7 +1679,7 @@ async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {})
1691
1679
  let lastError;
1692
1680
  const maxAttempts = RETRY_DELAYS.length + 1;
1693
1681
  for (let attempt = 0; attempt < maxAttempts; attempt++) try {
1694
- const timeout = attempt === 0 ? FETCH_TIMEOUT_MS : RETRY_TIMEOUT_MS;
1682
+ const timeout = FETCH_TIMEOUT_MS;
1695
1683
  return await fetchWithTimeout(url, buildInit(), {
1696
1684
  timeoutMs: timeout,
1697
1685
  accountId,
@@ -1719,8 +1707,8 @@ async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {})
1719
1707
  * finishes (return or throw), not when the outer function returns.
1720
1708
  * Also tracks active streams for the proxy-tunnel keepalive mechanism.
1721
1709
  */
1722
- async function* wrapGeneratorWithRelease(gen, releaseSlot) {
1723
- notifyStreamStart();
1710
+ async function* wrapGeneratorWithRelease(gen, releaseSlot, accountInfo) {
1711
+ notifyStreamStart(accountInfo);
1724
1712
  let streamError = false;
1725
1713
  try {
1726
1714
  yield* gen;
@@ -1728,9 +1716,10 @@ async function* wrapGeneratorWithRelease(gen, releaseSlot) {
1728
1716
  streamError = true;
1729
1717
  throw error;
1730
1718
  } finally {
1731
- notifyStreamEnd();
1719
+ notifyStreamEnd(accountInfo);
1732
1720
  releaseSlot();
1733
- if (streamError) resetConnections();
1721
+ if (streamError) if (accountInfo?.accountId) resetAccountConnections(accountInfo.accountId);
1722
+ else resetConnections();
1734
1723
  }
1735
1724
  }
1736
1725
  /**
@@ -1818,7 +1807,10 @@ const createChatCompletions = async (payload) => {
1818
1807
  const releaseSlot = await modelRouter.acquireSlot(resolvedModel);
1819
1808
  try {
1820
1809
  const result = await dispatchRequest(thinkingPayload);
1821
- if (Symbol.asyncIterator in result) return wrapGeneratorWithRelease(result, releaseSlot);
1810
+ if (Symbol.asyncIterator in result) {
1811
+ const accountInfo = result.__accountInfo;
1812
+ return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1813
+ }
1822
1814
  releaseSlot();
1823
1815
  return result;
1824
1816
  } catch (error) {
@@ -1852,7 +1844,10 @@ const createChatCompletions = async (payload) => {
1852
1844
  async function retryWithoutReasoning(payload, releaseSlot) {
1853
1845
  try {
1854
1846
  const result = await dispatchRequest(payload);
1855
- if (Symbol.asyncIterator in result) return wrapGeneratorWithRelease(result, releaseSlot);
1847
+ if (Symbol.asyncIterator in result) {
1848
+ const accountInfo = result.__accountInfo;
1849
+ return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1850
+ }
1856
1851
  releaseSlot();
1857
1852
  return result;
1858
1853
  } catch (retryError) {
@@ -1867,7 +1862,10 @@ async function retryWithoutReasoning(payload, releaseSlot) {
1867
1862
  async function retryWithDowngradedReasoning(payload, releaseSlot) {
1868
1863
  try {
1869
1864
  const result = await dispatchRequest(payload);
1870
- if (Symbol.asyncIterator in result) return wrapGeneratorWithRelease(result, releaseSlot);
1865
+ if (Symbol.asyncIterator in result) {
1866
+ const accountInfo = result.__accountInfo;
1867
+ return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1868
+ }
1871
1869
  releaseSlot();
1872
1870
  return result;
1873
1871
  } catch (retryError) {
@@ -2014,6 +2012,10 @@ async function createWithMultiAccount(payload) {
2014
2012
  const result = await doFetch(payload, tokenSource, account.id);
2015
2013
  account.lastRequestAt = Date.now();
2016
2014
  accountManager.markAccountSuccess(account.id);
2015
+ if (Symbol.asyncIterator in result) result.__accountInfo = {
2016
+ accountId: account.id,
2017
+ accountProxy: account.proxy
2018
+ };
2017
2019
  return result;
2018
2020
  } catch (error) {
2019
2021
  lastError = error;
@@ -3083,7 +3085,7 @@ async function validateGitHubToken(token) {
3083
3085
  state.githubToken = token;
3084
3086
  consola.info("Using provided GitHub token");
3085
3087
  try {
3086
- const { getGitHubUser } = await import("./get-user-HaNtbtZZ.js");
3088
+ const { getGitHubUser } = await import("./get-user-BSYESgez.js");
3087
3089
  const user = await getGitHubUser();
3088
3090
  consola.info(`Logged in as ${user.login}`);
3089
3091
  } catch (error) {
@@ -3134,10 +3136,10 @@ async function runServer(options) {
3134
3136
  try {
3135
3137
  await setupCopilotToken();
3136
3138
  } catch (error) {
3137
- const { HTTPError } = await import("./error-Ci5sfamJ.js");
3139
+ const { HTTPError } = await import("./error-BDOdv5Up.js");
3138
3140
  if (error instanceof HTTPError && error.response.status === 401) {
3139
3141
  consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
3140
- const { clearGithubToken } = await import("./token-BlHBeJfE.js");
3142
+ const { clearGithubToken } = await import("./token-C9gxxgzi.js");
3141
3143
  await clearGithubToken();
3142
3144
  consola.info("Please restart to re-authenticate");
3143
3145
  }