copilot-api-plus 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-manager-DktL5osZ.js → account-manager-2psqVsSO.js} +150 -3
- package/dist/account-manager-2psqVsSO.js.map +1 -0
- package/dist/error-C7zHD_5f.js +3 -0
- package/dist/get-user-CZ4szDap.js +3 -0
- package/dist/main.js +70 -15
- package/dist/main.js.map +1 -1
- package/dist/token-CRJg2Pnb.js +4 -0
- package/dist/{token-B8FDrdsQ.js → token-DokbvagK.js} +2 -2
- package/dist/{token-B8FDrdsQ.js.map → token-DokbvagK.js.map} +1 -1
- package/package.json +1 -1
- package/dist/account-manager-DktL5osZ.js.map +0 -1
- package/dist/error-BaXXuCDb.js +0 -3
- package/dist/get-user-Ct5NqLcM.js +0 -3
- package/dist/token-DEcUuJp7.js +0 -4
|
@@ -171,6 +171,59 @@ var HTTPError = class extends Error {
|
|
|
171
171
|
this.response = response;
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
|
+
/**
|
|
175
|
+
* Compute the earliest "retry after" timestamp (ms epoch) across all
|
|
176
|
+
* accounts' known rate-limit windows. Used to surface a countdown to
|
|
177
|
+
* downstream clients (Claude Code / Codex) so the user sees when the
|
|
178
|
+
* proxy is expected to recover.
|
|
179
|
+
*
|
|
180
|
+
* Returns:
|
|
181
|
+
* - the earliest resetAt timestamp in ms,
|
|
182
|
+
* - the source label ("copilot-5h", "github-core", "weekly"),
|
|
183
|
+
* - the seconds remaining until that reset.
|
|
184
|
+
*
|
|
185
|
+
* Returns undefined if no account has a known active limit.
|
|
186
|
+
*/
|
|
187
|
+
function earliestRateLimitReset() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
let earliest;
|
|
190
|
+
for (const account of accountManager.getAccounts()) {
|
|
191
|
+
const limits = account.limits;
|
|
192
|
+
if (!limits) continue;
|
|
193
|
+
const session = limits.copilotSession;
|
|
194
|
+
if (session && session.estimatedResetAt > now) {
|
|
195
|
+
const secs = Math.ceil((session.estimatedResetAt - now) / 1e3);
|
|
196
|
+
if (!earliest || session.estimatedResetAt < earliest.resetAt) earliest = {
|
|
197
|
+
resetAt: session.estimatedResetAt,
|
|
198
|
+
source: "copilot-5h",
|
|
199
|
+
secondsRemaining: secs
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const gh = limits.github;
|
|
203
|
+
if (gh && gh.remaining === 0) {
|
|
204
|
+
const resetMs = gh.reset * 1e3;
|
|
205
|
+
if (resetMs > now) {
|
|
206
|
+
const secs = Math.ceil((resetMs - now) / 1e3);
|
|
207
|
+
if (!earliest || resetMs < earliest.resetAt) earliest = {
|
|
208
|
+
resetAt: resetMs,
|
|
209
|
+
source: "github-core",
|
|
210
|
+
secondsRemaining: secs
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return earliest;
|
|
216
|
+
}
|
|
217
|
+
/** Format seconds as a compact human countdown ("≤ 4h 32m" / "12m 03s"). */
|
|
218
|
+
function formatCountdown(seconds) {
|
|
219
|
+
if (seconds <= 0) return "now";
|
|
220
|
+
const h = Math.floor(seconds / 3600);
|
|
221
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
222
|
+
const s = seconds % 60;
|
|
223
|
+
if (h > 0) return `≤ ${h}h ${m}m`;
|
|
224
|
+
if (m > 0) return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
225
|
+
return `${s}s`;
|
|
226
|
+
}
|
|
174
227
|
async function forwardError(c, error) {
|
|
175
228
|
if (error instanceof HTTPError) {
|
|
176
229
|
let errorText;
|
|
@@ -191,9 +244,23 @@ async function forwardError(c, error) {
|
|
|
191
244
|
}
|
|
192
245
|
const isCopilotSessionLimit = error.response.status === 429 && errorText.includes("user_global_rate_limited:pro_plus");
|
|
193
246
|
if (isCopilotSessionLimit) c.header("x-should-retry", "false");
|
|
247
|
+
let augmentedMessage = errorText;
|
|
248
|
+
let retrySeconds;
|
|
249
|
+
if (error.response.status === 429 || isCopilotSessionLimit) {
|
|
250
|
+
const reset = earliestRateLimitReset();
|
|
251
|
+
if (reset) {
|
|
252
|
+
retrySeconds = reset.secondsRemaining;
|
|
253
|
+
c.header("retry-after", String(reset.secondsRemaining));
|
|
254
|
+
c.header("x-ratelimit-reset", String(Math.floor(reset.resetAt / 1e3)));
|
|
255
|
+
c.header("x-ratelimit-source", reset.source);
|
|
256
|
+
const suffix = `\n\nProxy rate-limit (${reset.source}): retry in ${formatCountdown(reset.secondsRemaining)} (resets at ${new Date(reset.resetAt).toISOString()}).`;
|
|
257
|
+
augmentedMessage = `${errorText}${suffix}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
194
260
|
return c.json({ error: {
|
|
195
|
-
message:
|
|
196
|
-
type: "error"
|
|
261
|
+
message: augmentedMessage,
|
|
262
|
+
type: "error",
|
|
263
|
+
...retrySeconds !== void 0 && { retry_after_seconds: retrySeconds }
|
|
197
264
|
} }, isCopilotSessionLimit ? 403 : error.response.status);
|
|
198
265
|
}
|
|
199
266
|
const message = error.message || String(error);
|
|
@@ -592,6 +659,25 @@ const getCopilotUsage = async (githubToken) => {
|
|
|
592
659
|
return await response.json();
|
|
593
660
|
};
|
|
594
661
|
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/services/github/get-rate-limit.ts
|
|
664
|
+
/**
|
|
665
|
+
* GET /rate_limit — returns GitHub REST API rate-limit snapshots for the
|
|
666
|
+
* authenticated user. Calling this endpoint does NOT count against the
|
|
667
|
+
* user's quota (per the GitHub docs).
|
|
668
|
+
*
|
|
669
|
+
* @param githubToken Optional explicit token. Falls back to `state.githubToken`.
|
|
670
|
+
*/
|
|
671
|
+
async function getGitHubRateLimit(githubToken) {
|
|
672
|
+
const token = githubToken ?? state.githubToken;
|
|
673
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/rate_limit`, { headers: {
|
|
674
|
+
authorization: `token ${token}`,
|
|
675
|
+
...standardHeaders()
|
|
676
|
+
} });
|
|
677
|
+
if (!response.ok) throw new HTTPError("Failed to get GitHub rate_limit", response);
|
|
678
|
+
return await response.json();
|
|
679
|
+
}
|
|
680
|
+
|
|
595
681
|
//#endregion
|
|
596
682
|
//#region src/services/github/get-user.ts
|
|
597
683
|
/**
|
|
@@ -838,6 +924,60 @@ var AccountManager = class {
|
|
|
838
924
|
consola.debug(`Account ${account.label}: failed to refresh usage:`, err);
|
|
839
925
|
}
|
|
840
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Refresh GitHub REST API rate-limit snapshot for a single account.
|
|
929
|
+
*
|
|
930
|
+
* Stored on `account.limits.github`. Called on startup, after account
|
|
931
|
+
* creation, and after upstream 429s — the endpoint itself is free.
|
|
932
|
+
*/
|
|
933
|
+
async refreshGithubRateLimit(account) {
|
|
934
|
+
try {
|
|
935
|
+
const core = (await getGitHubRateLimit(account.githubToken)).resources.core;
|
|
936
|
+
account.limits = {
|
|
937
|
+
...account.limits,
|
|
938
|
+
github: {
|
|
939
|
+
limit: core.limit,
|
|
940
|
+
remaining: core.remaining,
|
|
941
|
+
used: core.used,
|
|
942
|
+
reset: core.reset,
|
|
943
|
+
fetchedAt: Date.now()
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
this.debouncedSave();
|
|
947
|
+
} catch (err) {
|
|
948
|
+
consola.debug(`Account ${account.label}: failed to refresh GitHub rate_limit:`, err);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Mark a Copilot ~5h session-level rate limit. Called when upstream returns
|
|
953
|
+
* 429 with `user_global_rate_limited:pro_plus` (the Pro+ 5h cooldown).
|
|
954
|
+
*
|
|
955
|
+
* The reset time is *estimated* (blockedAt + 5h) because GitHub does not
|
|
956
|
+
* surface the actual reset moment. UI/error responses display a "≤ 5h"
|
|
957
|
+
* countdown so users know roughly when the cooldown lifts.
|
|
958
|
+
*/
|
|
959
|
+
markCopilotSessionLimit(id, reason) {
|
|
960
|
+
const account = this.accounts.find((a) => a.id === id);
|
|
961
|
+
if (!account) return;
|
|
962
|
+
const now = Date.now();
|
|
963
|
+
account.limits = {
|
|
964
|
+
...account.limits,
|
|
965
|
+
copilotSession: {
|
|
966
|
+
blockedAt: now,
|
|
967
|
+
estimatedResetAt: now + 300 * 60 * 1e3,
|
|
968
|
+
reason
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
this.debouncedSave();
|
|
972
|
+
}
|
|
973
|
+
/** Clear the Copilot 5h session marker (e.g. after a successful request). */
|
|
974
|
+
clearCopilotSessionLimit(id) {
|
|
975
|
+
const account = this.accounts.find((a) => a.id === id);
|
|
976
|
+
if (!account?.limits?.copilotSession) return;
|
|
977
|
+
const { copilotSession: _drop,...rest } = account.limits;
|
|
978
|
+
account.limits = rest;
|
|
979
|
+
this.debouncedSave();
|
|
980
|
+
}
|
|
841
981
|
/** Refresh Copilot tokens for all non-disabled accounts. */
|
|
842
982
|
async refreshAllTokens() {
|
|
843
983
|
const targets = this.accounts.filter((a) => a.status !== "disabled");
|
|
@@ -848,6 +988,11 @@ var AccountManager = class {
|
|
|
848
988
|
const targets = this.accounts.filter((a) => a.status !== "disabled");
|
|
849
989
|
await Promise.allSettled(targets.map((a) => this.refreshAccountUsage(a)));
|
|
850
990
|
}
|
|
991
|
+
/** Refresh GitHub /rate_limit for all non-disabled accounts. */
|
|
992
|
+
async refreshAllGithubRateLimits() {
|
|
993
|
+
const targets = this.accounts.filter((a) => a.status !== "disabled");
|
|
994
|
+
await Promise.allSettled(targets.map((a) => this.refreshGithubRateLimit(a)));
|
|
995
|
+
}
|
|
851
996
|
/**
|
|
852
997
|
* Start periodic background refresh loops.
|
|
853
998
|
*
|
|
@@ -861,11 +1006,13 @@ var AccountManager = class {
|
|
|
861
1006
|
this.stopBackgroundRefresh();
|
|
862
1007
|
await this.refreshAllTokens();
|
|
863
1008
|
this.refreshAllUsage();
|
|
1009
|
+
this.refreshAllGithubRateLimits();
|
|
864
1010
|
this.refreshInterval = setInterval(() => {
|
|
865
1011
|
this.refreshAllTokens();
|
|
866
1012
|
}, tokenIntervalMs);
|
|
867
1013
|
this.usageInterval = setInterval(() => {
|
|
868
1014
|
this.refreshAllUsage();
|
|
1015
|
+
this.refreshAllGithubRateLimits();
|
|
869
1016
|
}, usageIntervalMs);
|
|
870
1017
|
consola.debug(`Background refresh started (tokens: ${tokenIntervalMs / 6e4}m, usage: ${usageIntervalMs / 6e4}m)`);
|
|
871
1018
|
startConnectionRecycling();
|
|
@@ -931,4 +1078,4 @@ const accountManager = new AccountManager();
|
|
|
931
1078
|
|
|
932
1079
|
//#endregion
|
|
933
1080
|
export { GITHUB_APP_SCOPES, GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountManager, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, findModel, forwardError, getAccountDispatcher, getCopilotToken, getCopilotUsage, getGitHubUser, initProxyFromEnv, isAccountProxied, isNullish, isProxyActive, notifyStreamEnd, notifyStreamStart, resetAccountConnections, resetConnections, rootCause, sleep, standardHeaders, state };
|
|
934
|
-
//# sourceMappingURL=account-manager-
|
|
1081
|
+
//# sourceMappingURL=account-manager-2psqVsSO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-manager-2psqVsSO.js","names":["headers: Record<string, string>","state: State","source: TokenSource","earliest:\n | { resetAt: number; source: string; secondsRemaining: number }\n | undefined","errorText: string","errorJson: unknown","retrySeconds: number | undefined","ACCOUNTS_PATH","direct: Agent | undefined","keepaliveTimer: ReturnType<typeof setInterval> | undefined","proxyUrl: string | undefined","connectionRecycleTimer: ReturnType<typeof setInterval> | undefined","fetchOptions: RequestInit","lastError: unknown","response: Response | undefined","error: unknown","err: unknown","data: Array<PersistedAccount>","account: Account"],"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-rate-limit.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 /**\n * When true, Anthropic /v1/messages requests are NEVER routed through the\n * native Copilot /v1/messages endpoint — the legacy translation layer\n * (Anthropic → OpenAI chat-completions → Anthropic) is used instead.\n * Default: false (native passthrough enabled by capability).\n */\n disableAnthropicPassthrough: boolean\n\n /**\n * When true, requests that don't specify a `thinking` field will get the\n * model's maximum thinking budget injected automatically (adaptive for\n * adaptive-thinking models, otherwise `enabled` with `max_thinking_budget`).\n *\n * Trade-off:\n * - Pre-2026-06-01 Copilot billing is per-request (token count doesn't\n * change cost), so leaving this on is \"free quality\".\n * - Post-2026-06-01 billing switches to per-token, at which point\n * auto-injection burns tokens. Users on token billing should turn this\n * off (or upgrade to a release that flips the default).\n *\n * Default: true (preserve existing v1.3.x quality-first behavior).\n */\n maxThinking: 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 disableAnthropicPassthrough: false,\n maxThinking: true,\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 * Per-model whitelist of `output_config.effort` values that Copilot's\n * `/v1/messages` mirror will accept for adaptive-thinking models.\n * As of 2026-05, Copilot caps Opus 4.7 to `[\"medium\"]` and allows\n * Sonnet 4.6 `[\"low\",\"medium\",\"high\"]`. Sending anything outside\n * this list returns 400 \"not supported by model X; supported values: [...]\".\n */\n reasoning_effort?: Array<string>\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 * Optional list of native upstream endpoints this model supports.\n * GitHub Copilot's `/models` response advertises this for some models\n * (e.g. Claude family typically includes \"anthropic-messages\") so we\n * can route directly to the native API instead of translating.\n */\n supported_endpoints?: Array<string>\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 { accountManager } from \"~/lib/account-manager\"\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\n/**\n * Compute the earliest \"retry after\" timestamp (ms epoch) across all\n * accounts' known rate-limit windows. Used to surface a countdown to\n * downstream clients (Claude Code / Codex) so the user sees when the\n * proxy is expected to recover.\n *\n * Returns:\n * - the earliest resetAt timestamp in ms,\n * - the source label (\"copilot-5h\", \"github-core\", \"weekly\"),\n * - the seconds remaining until that reset.\n *\n * Returns undefined if no account has a known active limit.\n */\nfunction earliestRateLimitReset():\n | {\n resetAt: number\n source: string\n secondsRemaining: number\n }\n | undefined {\n const now = Date.now()\n let earliest:\n | { resetAt: number; source: string; secondsRemaining: number }\n | undefined\n\n for (const account of accountManager.getAccounts()) {\n const limits = account.limits\n if (!limits) continue\n\n // Copilot 5h session limit\n const session = limits.copilotSession\n if (session && session.estimatedResetAt > now) {\n const secs = Math.ceil((session.estimatedResetAt - now) / 1000)\n if (!earliest || session.estimatedResetAt < earliest.resetAt) {\n earliest = {\n resetAt: session.estimatedResetAt,\n source: \"copilot-5h\",\n secondsRemaining: secs,\n }\n }\n }\n\n // GitHub REST API core quota (reset is unix seconds)\n const gh = limits.github\n if (gh && gh.remaining === 0) {\n const resetMs = gh.reset * 1000\n if (resetMs > now) {\n const secs = Math.ceil((resetMs - now) / 1000)\n if (!earliest || resetMs < earliest.resetAt) {\n earliest = {\n resetAt: resetMs,\n source: \"github-core\",\n secondsRemaining: secs,\n }\n }\n }\n }\n }\n\n return earliest\n}\n\n/** Format seconds as a compact human countdown (\"≤ 4h 32m\" / \"12m 03s\"). */\nfunction formatCountdown(seconds: number): string {\n if (seconds <= 0) return \"now\"\n const h = Math.floor(seconds / 3600)\n const m = Math.floor((seconds % 3600) / 60)\n const s = seconds % 60\n if (h > 0) return `≤ ${h}h ${m}m`\n if (m > 0) return `${m}m ${String(s).padStart(2, \"0\")}s`\n return `${s}s`\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 const isCopilotSessionLimit =\n error.response.status === 429\n && errorText.includes(\"user_global_rate_limited:pro_plus\")\n if (isCopilotSessionLimit) {\n c.header(\"x-should-retry\", \"false\")\n }\n\n // For 429 (or any flavour we surface as 403), add a countdown so\n // downstream clients can display \"X minutes until proxy recovers\".\n let augmentedMessage = errorText\n let retrySeconds: number | undefined\n if (error.response.status === 429 || isCopilotSessionLimit) {\n const reset = earliestRateLimitReset()\n if (reset) {\n retrySeconds = reset.secondsRemaining\n c.header(\"retry-after\", String(reset.secondsRemaining))\n c.header(\"x-ratelimit-reset\", String(Math.floor(reset.resetAt / 1000)))\n c.header(\"x-ratelimit-source\", reset.source)\n const suffix = `\\n\\nProxy rate-limit (${reset.source}): retry in ${formatCountdown(reset.secondsRemaining)} (resets at ${new Date(reset.resetAt).toISOString()}).`\n augmentedMessage = `${errorText}${suffix}`\n }\n }\n\n return c.json(\n {\n error: {\n message: augmentedMessage,\n type: \"error\",\n ...(retrySeconds !== undefined && {\n retry_after_seconds: retrySeconds,\n }),\n },\n },\n (isCopilotSessionLimit ? 403 : (\n error.response.status\n )) 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 // Disable body timeout for SSE: undici's default 300s body timeout kills\n // long-running SSE streams during model thinking phases with no data.\n bodyTimeout: 0,\n connect: {\n timeout: 15_000,\n keepAlive: true,\n keepAliveInitialDelay: 10_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/** Whether an environment-level proxy is configured for Copilot URLs. */\nexport function isProxyActive(): boolean {\n return proxyActive\n}\n\n/**\n * Check whether a specific request will be proxied.\n * Account-level proxy takes precedence; falls back to environment proxy.\n */\nexport function isAccountProxied(accountProxy?: string): boolean {\n if (accountProxy) return true\n return proxyActive\n}\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 = 20_000\nconst KEEPALIVE_URL = \"https://api.individual.githubcopilot.com/\"\n\ninterface ActiveStreamInfo {\n count: number\n accountProxy?: string\n apiBaseUrl?: string\n}\n\n/** Account info attached to streaming responses for keepalive targeting. */\nexport interface StreamAccountInfo {\n accountId?: string\n accountProxy?: string\n apiBaseUrl?: 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 // Use the tracked API base URL so the keepalive request goes through\n // the SAME origin (and thus the same HTTP/2 session / CONNECT tunnel)\n // as the active SSE stream.\n const pingUrl = info.apiBaseUrl || KEEPALIVE_URL\n\n if (key === \"__global__\") {\n // Global connection — use standard fetch (goes through global dispatcher)\n fetch(pingUrl, { method: \"HEAD\" }).catch(() => {})\n consola.debug(\n `Proxy keepalive ping sent (global → ${new URL(pingUrl).hostname})`,\n )\n } else {\n // Per-account connection — ping through the account's own dispatcher\n const dispatcher = getAccountDispatcher(key, info.accountProxy)\n fetch(pingUrl, {\n method: \"HEAD\",\n dispatcher: dispatcher as unknown as undefined,\n } as RequestInit).catch(() => {})\n consola.debug(\n `Proxy keepalive ping sent (account ${key.slice(0, 8)} → ${new URL(pingUrl).hostname})`,\n )\n }\n }\n }, KEEPALIVE_INTERVAL_MS)\n // Don't prevent Node from exiting because of this timer.\n keepaliveTimer.unref()\n consola.debug(\n \"Proxy keepalive started (20s interval, targeting active stream origins)\",\n )\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?: StreamAccountInfo): 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 // Update apiBaseUrl if provided (may differ across streams)\n if (accountInfo?.apiBaseUrl) existing.apiBaseUrl = accountInfo.apiBaseUrl\n } else {\n activeStreams.set(key, {\n count: 1,\n accountProxy: accountInfo?.accountProxy,\n apiBaseUrl: accountInfo?.apiBaseUrl,\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?: StreamAccountInfo): 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\") {\n // Bun's native fetch automatically respects HTTP_PROXY/HTTPS_PROXY.\n // We still need to detect proxy presence so that keepalive and heartbeat\n // logic activates correctly.\n const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy\n const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy\n proxyActive = Boolean(httpProxy || httpsProxy)\n if (proxyActive) {\n consola.debug(\"Bun runtime: proxy detected from environment variables\")\n }\n return\n }\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 // Only activate proxy keepalive if a proxy is actually configured\n // for at least one relevant URL.\n const get = getProxyForUrl as unknown as (u: string) => string | undefined\n const testUrls = [\n \"https://api.individual.githubcopilot.com/\",\n \"https://api.business.githubcopilot.com/\",\n \"https://api.github.com/\",\n ]\n proxyActive = testUrls.some((u) => {\n const raw = get(u)\n return raw !== undefined && raw.length > 0\n })\n\n if (proxyActive) {\n consola.debug(\n \"Proxy active: undici dispatcher configured with bodyTimeout=0, allowH2=true\",\n )\n } else {\n consola.debug(\n \"HTTP proxy dispatcher installed but no proxy URLs detected\",\n )\n }\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.debug(\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 * GET /rate_limit — returns GitHub REST API rate-limit snapshots for the\n * authenticated user. Calling this endpoint does NOT count against the\n * user's quota (per the GitHub docs).\n *\n * @param githubToken Optional explicit token. Falls back to `state.githubToken`.\n */\nexport async function getGitHubRateLimit(\n githubToken?: string,\n): Promise<GithubRateLimitResponse> {\n const token = githubToken ?? state.githubToken\n const response = await fetch(`${GITHUB_API_BASE_URL}/rate_limit`, {\n headers: {\n authorization: `token ${token}`,\n ...standardHeaders(),\n },\n })\n\n if (!response.ok) {\n throw new HTTPError(\"Failed to get GitHub rate_limit\", response)\n }\n\n return (await response.json()) as GithubRateLimitResponse\n}\n\n/** Per-resource quota block returned by GitHub. */\nexport interface GithubRateLimitResource {\n limit: number\n remaining: number\n used: number\n /** Unix seconds. */\n reset: number\n}\n\n/** Trimmed shape — only fields we actually use. */\nexport interface GithubRateLimitResponse {\n resources: Record<string, GithubRateLimitResource> & {\n core: GithubRateLimitResource\n }\n rate?: GithubRateLimitResource\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 { getGitHubRateLimit } from \"~/services/github/get-rate-limit\"\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 /** Whether the upstream account is allowed to exceed its monthly quota. */\n premium_overage_permitted?: boolean\n quotaResetDate: string\n lastCheckedAt: number\n }\n\n /**\n * Rate-limit snapshots — surfaced on the web UI as countdowns and injected\n * into 429 error responses so downstream clients (Claude Code / Codex) can\n * display the remaining cooldown to the user.\n */\n limits?: {\n /** GitHub REST API limit (from GET /rate_limit `resources.core`). */\n github?: {\n limit: number\n remaining: number\n used: number\n /** Reset time as unix seconds (matches GitHub API). */\n reset: number\n fetchedAt: number\n }\n /**\n * Copilot ~5h session-level rate limit (Pro+ only). Surfaced when\n * upstream returns 429 `user_global_rate_limited:pro_plus`. We estimate\n * the reset 5h after the first 429 since GitHub does not expose it\n * directly today.\n */\n copilotSession?: {\n blockedAt: number\n /** ms epoch — estimated `blockedAt + 5h`. */\n estimatedResetAt: number\n reason: string\n }\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 /** True if accounts.json existed on disk when loadAccounts() was called. */\n accountsFileExisted = 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 this.accountsFileExisted = true\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.accountsFileExisted = false\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, sessionId: _session, ...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 premium_overage_permitted: snap.premium_interactions.overage_permitted,\n quotaResetDate: data.quota_reset_date,\n lastCheckedAt: Date.now(),\n }\n\n const overagePermitted = snap.premium_interactions.overage_permitted\n\n // Transition between active ↔ exhausted.\n // If the upstream account allows overage (paid extra), do NOT mark it\n // exhausted just because remaining went negative — the user can keep\n // making requests and pay per-use. We only flip back from \"exhausted\"\n // to \"active\" automatically; we never auto-flip into \"exhausted\" when\n // overage is permitted.\n if (\n account.usage.premium_remaining <= 0\n && account.status === \"active\"\n && !overagePermitted\n ) {\n this.markAccountStatus(\n account.id,\n \"exhausted\",\n \"Premium quota exhausted\",\n )\n } else if (\n account.status === \"exhausted\"\n && (account.usage.premium_remaining > 0 || overagePermitted)\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 /**\n * Refresh GitHub REST API rate-limit snapshot for a single account.\n *\n * Stored on `account.limits.github`. Called on startup, after account\n * creation, and after upstream 429s — the endpoint itself is free.\n */\n async refreshGithubRateLimit(account: Account): Promise<void> {\n try {\n const data = await getGitHubRateLimit(account.githubToken)\n const core = data.resources.core\n\n account.limits = {\n ...account.limits,\n github: {\n limit: core.limit,\n remaining: core.remaining,\n used: core.used,\n reset: core.reset,\n fetchedAt: Date.now(),\n },\n }\n this.debouncedSave()\n } catch (err) {\n consola.debug(\n `Account ${account.label}: failed to refresh GitHub rate_limit:`,\n err,\n )\n }\n }\n\n /**\n * Mark a Copilot ~5h session-level rate limit. Called when upstream returns\n * 429 with `user_global_rate_limited:pro_plus` (the Pro+ 5h cooldown).\n *\n * The reset time is *estimated* (blockedAt + 5h) because GitHub does not\n * surface the actual reset moment. UI/error responses display a \"≤ 5h\"\n * countdown so users know roughly when the cooldown lifts.\n */\n markCopilotSessionLimit(id: string, reason: string): void {\n const account = this.accounts.find((a) => a.id === id)\n if (!account) return\n const now = Date.now()\n account.limits = {\n ...account.limits,\n copilotSession: {\n blockedAt: now,\n estimatedResetAt: now + 5 * 60 * 60 * 1000,\n reason,\n },\n }\n this.debouncedSave()\n }\n\n /** Clear the Copilot 5h session marker (e.g. after a successful request). */\n clearCopilotSessionLimit(id: string): void {\n const account = this.accounts.find((a) => a.id === id)\n if (!account?.limits?.copilotSession) return\n const { copilotSession: _drop, ...rest } = account.limits\n account.limits = rest\n this.debouncedSave()\n }\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 /** Refresh GitHub /rate_limit for all non-disabled accounts. */\n async refreshAllGithubRateLimits(): Promise<void> {\n const targets = this.accounts.filter((a) => a.status !== \"disabled\")\n await Promise.allSettled(targets.map((a) => this.refreshGithubRateLimit(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 // Query GitHub /rate_limit for already-authorized accounts at startup.\n // The endpoint is free (does not consume quota), so this is safe.\n void this.refreshAllGithubRateLimits()\n\n this.refreshInterval = setInterval(() => {\n void this.refreshAllTokens()\n }, tokenIntervalMs)\n\n this.usageInterval = setInterval(() => {\n void this.refreshAllUsage()\n void this.refreshAllGithubRateLimits()\n }, usageIntervalMs)\n\n consola.debug(\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.debug(\"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":"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"}
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountManager, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, findModel, forwardError, getAccountDispatcher, getCopilotUsage, initProxyFromEnv, isAccountProxied, isNullish, isProxyActive, notifyStreamEnd, notifyStreamStart, resetAccountConnections, resetConnections, rootCause, sleep, standardHeaders, state } from "./account-manager-
|
|
3
|
-
import { clearGithubToken, getDeviceCode, pollAccessToken, refreshCopilotToken, setupCopilotToken, setupGitHubToken, stopCopilotTokenRefresh } from "./token-
|
|
2
|
+
import { GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountManager, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, findModel, forwardError, getAccountDispatcher, getCopilotUsage, initProxyFromEnv, isAccountProxied, isNullish, isProxyActive, notifyStreamEnd, notifyStreamStart, resetAccountConnections, resetConnections, rootCause, sleep, standardHeaders, state } from "./account-manager-2psqVsSO.js";
|
|
3
|
+
import { clearGithubToken, getDeviceCode, pollAccessToken, refreshCopilotToken, setupCopilotToken, setupGitHubToken, stopCopilotTokenRefresh } from "./token-DokbvagK.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
import consola from "consola";
|
|
@@ -1392,6 +1392,7 @@ accountRoutes.post("/", async (c) => {
|
|
|
1392
1392
|
const body = await c.req.json();
|
|
1393
1393
|
if (!body.githubToken || !body.label) return c.json({ error: "githubToken and label are required" }, 400);
|
|
1394
1394
|
const account = await accountManager.addAccount(body.githubToken, body.label, body.accountType);
|
|
1395
|
+
accountManager.refreshGithubRateLimit(account);
|
|
1395
1396
|
if (body.proxy) {
|
|
1396
1397
|
try {
|
|
1397
1398
|
const proxyUrl = new URL(body.proxy);
|
|
@@ -1479,6 +1480,7 @@ accountRoutes.post("/:id/refresh", async (c) => {
|
|
|
1479
1480
|
if (!account) return c.json({ error: "Account not found" }, 404);
|
|
1480
1481
|
await accountManager.refreshAccountToken(account);
|
|
1481
1482
|
await accountManager.refreshAccountUsage(account);
|
|
1483
|
+
await accountManager.refreshGithubRateLimit(account);
|
|
1482
1484
|
return c.json({ account: sanitiseAccount(account) });
|
|
1483
1485
|
} catch (error) {
|
|
1484
1486
|
consola.warn(`Error refreshing account: ${rootCause(error)}`);
|
|
@@ -1486,6 +1488,32 @@ accountRoutes.post("/:id/refresh", async (c) => {
|
|
|
1486
1488
|
return c.json({ error: "Failed to refresh account" }, 500);
|
|
1487
1489
|
}
|
|
1488
1490
|
});
|
|
1491
|
+
accountRoutes.post("/:id/refresh-limits", async (c) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const id = c.req.param("id");
|
|
1494
|
+
const account = accountManager.getAccountById(id);
|
|
1495
|
+
if (!account) return c.json({ error: "Account not found" }, 404);
|
|
1496
|
+
await accountManager.refreshGithubRateLimit(account);
|
|
1497
|
+
return c.json({ account: sanitiseAccount(account) });
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
consola.warn(`Error refreshing rate limits: ${rootCause(error)}`);
|
|
1500
|
+
consola.debug("Error refreshing rate limits:", error);
|
|
1501
|
+
return c.json({ error: "Failed to refresh rate limits" }, 500);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
accountRoutes.post("/:id/clear-session-limit", (c) => {
|
|
1505
|
+
try {
|
|
1506
|
+
const id = c.req.param("id");
|
|
1507
|
+
const account = accountManager.getAccountById(id);
|
|
1508
|
+
if (!account) return c.json({ error: "Account not found" }, 404);
|
|
1509
|
+
accountManager.clearCopilotSessionLimit(id);
|
|
1510
|
+
return c.json({ account: sanitiseAccount(account) });
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
consola.warn(`Error clearing session limit: ${rootCause(error)}`);
|
|
1513
|
+
consola.debug("Error clearing session limit:", error);
|
|
1514
|
+
return c.json({ error: "Failed to clear session limit" }, 500);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1489
1517
|
accountRoutes.post("/auth/start", async (c) => {
|
|
1490
1518
|
try {
|
|
1491
1519
|
if (cachedDeviceCode && Date.now() < cachedDeviceCodeExpiresAt) {
|
|
@@ -1579,6 +1607,7 @@ accountRoutes.post("/auth/poll", async (c) => {
|
|
|
1579
1607
|
clearAuthFlowState();
|
|
1580
1608
|
const accountLabel = label || `Account ${accountManager.accountCount + 1}`;
|
|
1581
1609
|
const account = await accountManager.addAccount(json.access_token, accountLabel, account_type || "individual");
|
|
1610
|
+
accountManager.refreshGithubRateLimit(account);
|
|
1582
1611
|
return c.json({
|
|
1583
1612
|
status: "complete",
|
|
1584
1613
|
account: sanitiseAccount(account)
|
|
@@ -3196,6 +3225,29 @@ function isNonAccountError(errMsg) {
|
|
|
3196
3225
|
* Returns the successful retry result, or null if the error was handled
|
|
3197
3226
|
* without a successful retry.
|
|
3198
3227
|
*/
|
|
3228
|
+
/**
|
|
3229
|
+
* Handle a 429 from upstream: detect Copilot 5h Pro+ session-limit signature,
|
|
3230
|
+
* snapshot GitHub /rate_limit, and decide whether to mark the account or
|
|
3231
|
+
* propagate the error to the client (single-account guard).
|
|
3232
|
+
*/
|
|
3233
|
+
async function handle429(error, account, hasOtherAccount) {
|
|
3234
|
+
let body;
|
|
3235
|
+
try {
|
|
3236
|
+
body = await error.response.clone().text();
|
|
3237
|
+
} catch {
|
|
3238
|
+
body = error.message || "";
|
|
3239
|
+
}
|
|
3240
|
+
const isCopilotSessionLimit = body.includes("user_global_rate_limited:pro_plus");
|
|
3241
|
+
if (isCopilotSessionLimit) accountManager.markCopilotSessionLimit(account.id, "user_global_rate_limited:pro_plus");
|
|
3242
|
+
accountManager.refreshGithubRateLimit(account);
|
|
3243
|
+
if (!hasOtherAccount) {
|
|
3244
|
+
consola.warn(`Account ${account.label}: 429 — only account, propagating to client without marking`);
|
|
3245
|
+
error.__nonAccountError = true;
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
3248
|
+
accountManager.markAccountStatus(account.id, "rate_limited", isCopilotSessionLimit ? "429 Copilot 5h session limit" : "429 Rate limited");
|
|
3249
|
+
return null;
|
|
3250
|
+
}
|
|
3199
3251
|
async function handleMultiAccountHttpError(error, account, retryContext) {
|
|
3200
3252
|
switch (error.response.status) {
|
|
3201
3253
|
case 401:
|
|
@@ -3209,14 +3261,7 @@ async function handleMultiAccountHttpError(error, account, retryContext) {
|
|
|
3209
3261
|
}
|
|
3210
3262
|
accountManager.markAccountStatus(account.id, "banned", "403 Forbidden");
|
|
3211
3263
|
return null;
|
|
3212
|
-
case 429:
|
|
3213
|
-
if (!retryContext.hasOtherAccount) {
|
|
3214
|
-
consola.warn(`Account ${account.label}: 429 — only account, propagating to client without marking`);
|
|
3215
|
-
error.__nonAccountError = true;
|
|
3216
|
-
return null;
|
|
3217
|
-
}
|
|
3218
|
-
accountManager.markAccountStatus(account.id, "rate_limited", "429 Rate limited");
|
|
3219
|
-
return null;
|
|
3264
|
+
case 429: return handle429(error, account, retryContext.hasOtherAccount);
|
|
3220
3265
|
case 408:
|
|
3221
3266
|
consola.warn(`Account ${account.label}: 408 request timeout (network issue, not rotating)`);
|
|
3222
3267
|
error.__nonAccountError = true;
|
|
@@ -4229,7 +4274,7 @@ async function createWithMultiAccount(payload, options$1) {
|
|
|
4229
4274
|
} catch (error) {
|
|
4230
4275
|
lastError = error;
|
|
4231
4276
|
if (error instanceof HTTPError) {
|
|
4232
|
-
const action = handleAnthropicHttpError(error, account, triedAccountIds);
|
|
4277
|
+
const action = await handleAnthropicHttpError(error, account, triedAccountIds);
|
|
4233
4278
|
if (action === "refresh401") return handleMultiAccount401(ctx, account);
|
|
4234
4279
|
if (action === "throw") throw error;
|
|
4235
4280
|
continue;
|
|
@@ -4261,11 +4306,21 @@ async function createWithMultiAccount(payload, options$1) {
|
|
|
4261
4306
|
* would disable the proxy entirely, so 429 / 403 are propagated unchanged
|
|
4262
4307
|
* to the client when no other account is available.
|
|
4263
4308
|
*/
|
|
4264
|
-
function handleAnthropicHttpError(error, account, triedAccountIds) {
|
|
4309
|
+
async function handleAnthropicHttpError(error, account, triedAccountIds) {
|
|
4265
4310
|
const status = error.response.status;
|
|
4266
4311
|
if (status === 401) return "refresh401";
|
|
4267
4312
|
if (status === 429 || status === 403) {
|
|
4268
4313
|
const isRateLimit = status === 429;
|
|
4314
|
+
if (isRateLimit) {
|
|
4315
|
+
let body;
|
|
4316
|
+
try {
|
|
4317
|
+
body = await error.response.clone().text();
|
|
4318
|
+
} catch {
|
|
4319
|
+
body = error.message || "";
|
|
4320
|
+
}
|
|
4321
|
+
if (body.includes("user_global_rate_limited:pro_plus")) accountManager.markCopilotSessionLimit(account.id, "user_global_rate_limited:pro_plus");
|
|
4322
|
+
accountManager.refreshGithubRateLimit(account);
|
|
4323
|
+
}
|
|
4269
4324
|
if (hasAnotherAnthropicAccountToTry(triedAccountIds)) {
|
|
4270
4325
|
accountManager.markAccountStatus(account.id, isRateLimit ? "rate_limited" : "banned", isRateLimit ? "429 Rate limited" : "403 Forbidden");
|
|
4271
4326
|
consola.warn(`Account ${account.label}: ${status} on /v1/messages, trying next account`);
|
|
@@ -4995,7 +5050,7 @@ async function validateGitHubToken(token) {
|
|
|
4995
5050
|
state.githubToken = token;
|
|
4996
5051
|
consola.info("Using provided GitHub token");
|
|
4997
5052
|
try {
|
|
4998
|
-
const { getGitHubUser } = await import("./get-user-
|
|
5053
|
+
const { getGitHubUser } = await import("./get-user-CZ4szDap.js");
|
|
4999
5054
|
const user = await getGitHubUser();
|
|
5000
5055
|
consola.info(`Logged in as ${user.login}`);
|
|
5001
5056
|
} catch (error) {
|
|
@@ -5057,10 +5112,10 @@ async function runServer(options$1) {
|
|
|
5057
5112
|
try {
|
|
5058
5113
|
await setupCopilotToken();
|
|
5059
5114
|
} catch (error) {
|
|
5060
|
-
const { HTTPError: HTTPError$1 } = await import("./error-
|
|
5115
|
+
const { HTTPError: HTTPError$1 } = await import("./error-C7zHD_5f.js");
|
|
5061
5116
|
if (error instanceof HTTPError$1 && error.response.status === 401) {
|
|
5062
5117
|
consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
|
|
5063
|
-
const { clearGithubToken: clearGithubToken$1 } = await import("./token-
|
|
5118
|
+
const { clearGithubToken: clearGithubToken$1 } = await import("./token-CRJg2Pnb.js");
|
|
5064
5119
|
await clearGithubToken$1();
|
|
5065
5120
|
consola.info("Please restart to re-authenticate");
|
|
5066
5121
|
}
|