copilot-api-plus 1.2.17 → 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/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { _ as GITHUB_BASE_URL, a as PATHS, b as copilotHeaders, c as forwardError, d as findModel, f as isNullish, h as state, l as cacheModels, m as sleep, o as ensurePaths, p as rootCause, r as getCopilotUsage, s as HTTPError, t as accountManager, u as cacheVSCodeVersion, v as GITHUB_CLIENT_ID, x as standardHeaders, y as copilotBaseUrl } from "./account-manager-DmXXcFBW.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-BRQK8jBj.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";
@@ -8,8 +8,6 @@ import { timingSafeEqual } from "node:crypto";
8
8
  import fs from "node:fs/promises";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
- import { getProxyForUrl } from "proxy-from-env";
12
- import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
13
11
  import * as p from "@clack/prompts";
14
12
  import clipboard from "clipboardy";
15
13
  import { serve } from "srvx";
@@ -119,144 +117,6 @@ async function applyProxyConfig() {
119
117
  return true;
120
118
  }
121
119
  //#endregion
122
- //#region src/lib/proxy.ts
123
- const agentOptions = {
124
- keepAliveTimeout: 3e5,
125
- keepAliveMaxTimeout: 6e5,
126
- allowH2: true,
127
- connect: {
128
- timeout: 15e3,
129
- keepAlive: true,
130
- keepAliveInitialDelay: 15e3
131
- }
132
- };
133
- let direct;
134
- let proxies = /* @__PURE__ */ new Map();
135
- /** Whether a proxy is actually configured and in use. */
136
- let proxyActive = false;
137
- /**
138
- * Many proxy nodes (especially third-party VPN/airport services) kill
139
- * CONNECT tunnels that are idle for ~60 s. During long model thinking
140
- * phases the SSE stream carries no data, which looks "idle" to the proxy.
141
- *
142
- * This keepalive sends a tiny HEAD request to the Copilot API every 45 s
143
- * through the same proxy. The encrypted packets flowing through the
144
- * CONNECT tunnel reset the proxy's idle timer, keeping the tunnel alive.
145
- *
146
- * The keepalive is active ONLY while there are SSE streams in flight
147
- * (tracked via `streamCount`). When no streams are active it stops to
148
- * avoid unnecessary traffic.
149
- */
150
- let keepaliveTimer;
151
- let streamCount = 0;
152
- const KEEPALIVE_INTERVAL_MS = 45e3;
153
- const KEEPALIVE_URL = "https://api.individual.githubcopilot.com/";
154
- function startKeepalive() {
155
- if (keepaliveTimer) return;
156
- keepaliveTimer = setInterval(() => {
157
- fetch(KEEPALIVE_URL, { method: "HEAD" }).catch(() => {});
158
- consola.debug("Proxy keepalive ping sent");
159
- }, KEEPALIVE_INTERVAL_MS);
160
- keepaliveTimer.unref();
161
- consola.debug("Proxy keepalive started (45 s interval)");
162
- }
163
- function stopKeepalive() {
164
- if (keepaliveTimer) {
165
- clearInterval(keepaliveTimer);
166
- keepaliveTimer = void 0;
167
- consola.debug("Proxy keepalive stopped (no active streams)");
168
- }
169
- }
170
- /**
171
- * Call when an SSE stream starts. Activates the proxy-tunnel keepalive
172
- * if this is the first active stream and a proxy is configured.
173
- */
174
- function notifyStreamStart() {
175
- if (!proxyActive) return;
176
- streamCount++;
177
- if (streamCount === 1) startKeepalive();
178
- }
179
- /**
180
- * Call when an SSE stream ends (success or error). Stops the keepalive
181
- * once no streams are active.
182
- */
183
- function notifyStreamEnd() {
184
- if (!proxyActive) return;
185
- streamCount = Math.max(0, streamCount - 1);
186
- if (streamCount === 0) stopKeepalive();
187
- }
188
- function initProxyFromEnv() {
189
- if (typeof Bun !== "undefined") return;
190
- try {
191
- direct = new Agent(agentOptions);
192
- proxies = /* @__PURE__ */ new Map();
193
- setGlobalDispatcher({
194
- dispatch(options, handler) {
195
- try {
196
- const origin = typeof options.origin === "string" ? new URL(options.origin) : options.origin;
197
- const raw = getProxyForUrl(origin.toString());
198
- const proxyUrl = raw && raw.length > 0 ? raw : void 0;
199
- if (!proxyUrl) {
200
- consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
201
- return direct.dispatch(options, handler);
202
- }
203
- let agent = proxies.get(proxyUrl);
204
- if (!agent) {
205
- agent = new ProxyAgent({
206
- uri: proxyUrl,
207
- ...agentOptions
208
- });
209
- proxies.set(proxyUrl, agent);
210
- }
211
- let label = proxyUrl;
212
- try {
213
- const u = new URL(proxyUrl);
214
- label = `${u.protocol}//${u.host}`;
215
- } catch {}
216
- consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`);
217
- return agent.dispatch(options, handler);
218
- } catch {
219
- return direct.dispatch(options, handler);
220
- }
221
- },
222
- close() {
223
- for (const agent of proxies.values()) agent.close();
224
- return direct.close();
225
- },
226
- destroy() {
227
- for (const agent of proxies.values()) agent.destroy();
228
- return direct.destroy();
229
- }
230
- });
231
- proxyActive = true;
232
- consola.debug("HTTP proxy configured from environment (per-URL)");
233
- } catch (err) {
234
- consola.debug("Proxy setup skipped:", err);
235
- }
236
- }
237
- /**
238
- * Destroy all pooled connections (direct + proxy agents) and replace them
239
- * with fresh instances. The global dispatcher's `dispatch` method captures
240
- * `direct` and `proxies` by reference, so subsequent requests automatically
241
- * use the new agents — no need to call `setGlobalDispatcher` again.
242
- *
243
- * Call this after a network error to discard stale/half-closed sockets that
244
- * would otherwise cause every retry to wait ~60 s before timing out.
245
- *
246
- * Under the Bun runtime (which doesn't use undici) this is a no-op.
247
- */
248
- function resetConnections() {
249
- if (typeof Bun !== "undefined") return;
250
- if (!direct) return;
251
- const oldDirect = direct;
252
- const oldProxies = proxies;
253
- direct = new Agent(agentOptions);
254
- proxies = /* @__PURE__ */ new Map();
255
- oldDirect.close().catch(() => {});
256
- for (const agent of oldProxies.values()) agent.close().catch(() => {});
257
- consola.debug("Connection pool reset — stale sockets cleared");
258
- }
259
- //#endregion
260
120
  //#region src/account.ts
261
121
  const addAccount = defineCommand({
262
122
  meta: {
@@ -1490,6 +1350,10 @@ accountRoutes.post("/", async (c) => {
1490
1350
  const body = await c.req.json();
1491
1351
  if (!body.githubToken || !body.label) return c.json({ error: "githubToken and label are required" }, 400);
1492
1352
  const account = await accountManager.addAccount(body.githubToken, body.label, body.accountType);
1353
+ if (body.proxy) {
1354
+ account.proxy = body.proxy;
1355
+ await accountManager.saveAccounts();
1356
+ }
1493
1357
  return c.json({ account: sanitiseAccount(account) }, 201);
1494
1358
  } catch (error) {
1495
1359
  consola.warn(`Error adding account: ${rootCause(error)}`);
@@ -1767,40 +1631,37 @@ async function checkRateLimit(state) {
1767
1631
  */
1768
1632
  const FETCH_TIMEOUT_MS = 12e4;
1769
1633
  /**
1770
- * Retry delays in ms. After the first failure the connection pool is reset
1771
- * (see `resetConnections`), so retries use fresh sockets.
1772
- *
1773
- * We only allow 1 retry (2 total attempts) to minimize credit waste.
1774
- * Timeout errors are NOT retried at all — they indicate the request likely
1775
- * reached Copilot (consuming a credit) and the upstream is slow or the
1776
- * proxy killed the connection mid-flight.
1777
- */
1778
- const RETRY_DELAYS = [2e3];
1779
- /**
1780
- * Timeout for retry attempts (waiting for response headers only).
1781
- * Response headers typically arrive within 3–5 s, even on slow models.
1782
- * 30 s is generous enough for a fresh socket to connect and receive
1783
- * headers, while still failing fast when the upstream is truly down.
1634
+ * Retry delays in ms. Empty = no retries.
1784
1635
  *
1785
- * NOTE: This does NOT affect the SSE streaming phase — once headers
1786
- * arrive, the timeout is cleared and the stream runs until completion
1787
- * or interruption.
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).
1788
1640
  */
1789
- const RETRY_TIMEOUT_MS = 3e4;
1641
+ const RETRY_DELAYS = [];
1642
+ /** Minimum interval (ms) between requests on the same account. */
1643
+ const MIN_SAME_ACCOUNT_INTERVAL_MS = 1e3;
1644
+ /** Random jitter range (ms) added when switching between accounts. */
1645
+ const ACCOUNT_SWITCH_JITTER_MIN_MS = 1e3;
1646
+ const ACCOUNT_SWITCH_JITTER_MAX_MS = 5e3;
1647
+ /** Track the last-used account ID to detect account switches. */
1648
+ let lastUsedAccountId;
1790
1649
  /**
1791
1650
  * Wrapper around `fetch()` that aborts if the server doesn't respond within
1792
1651
  * `timeoutMs`. The timeout only covers the period until the response headers
1793
1652
  * arrive – once the body starts streaming, the timeout is cleared so that
1794
1653
  * long SSE responses are not interrupted.
1795
1654
  */
1796
- async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
1655
+ async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accountId, accountProxy } = {}) {
1797
1656
  const controller = new AbortController();
1798
1657
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1799
1658
  try {
1800
- return await fetch(url, {
1659
+ const fetchOptions = {
1801
1660
  ...init,
1802
1661
  signal: controller.signal
1803
- });
1662
+ };
1663
+ if (accountId) fetchOptions.dispatcher = getAccountDispatcher(accountId, accountProxy);
1664
+ return await fetch(url, fetchOptions);
1804
1665
  } catch (error) {
1805
1666
  if (error instanceof DOMException && error.name === "AbortError") throw new Error(`Request timed out after ${timeoutMs}ms`);
1806
1667
  throw error;
@@ -1814,12 +1675,16 @@ async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
1814
1675
  * Returns `{ response }` on success.
1815
1676
  * Throws the last network error if all retries are exhausted.
1816
1677
  */
1817
- async function fetchWithRetry(url, buildInit) {
1678
+ async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {}) {
1818
1679
  let lastError;
1819
1680
  const maxAttempts = RETRY_DELAYS.length + 1;
1820
1681
  for (let attempt = 0; attempt < maxAttempts; attempt++) try {
1821
- const timeout = attempt === 0 ? FETCH_TIMEOUT_MS : RETRY_TIMEOUT_MS;
1822
- return await fetchWithTimeout(url, buildInit(), timeout);
1682
+ const timeout = FETCH_TIMEOUT_MS;
1683
+ return await fetchWithTimeout(url, buildInit(), {
1684
+ timeoutMs: timeout,
1685
+ accountId,
1686
+ accountProxy
1687
+ });
1823
1688
  } catch (error) {
1824
1689
  lastError = error;
1825
1690
  const msg = error instanceof Error ? error.message : String(error);
@@ -1827,7 +1692,8 @@ async function fetchWithRetry(url, buildInit) {
1827
1692
  consola.warn(`Request timed out on attempt ${attempt + 1}/${maxAttempts} — not retrying (credit likely consumed):`, msg);
1828
1693
  break;
1829
1694
  }
1830
- if (attempt === 0) resetConnections();
1695
+ if (attempt === 0) if (accountId) resetAccountConnections(accountId);
1696
+ else resetConnections();
1831
1697
  if (attempt < maxAttempts - 1) {
1832
1698
  const delay = RETRY_DELAYS[attempt];
1833
1699
  consola.warn(`Network error on attempt ${attempt + 1}/${maxAttempts}, retrying in ${delay}ms:`, error instanceof Error ? error.message : error);
@@ -1841,8 +1707,8 @@ async function fetchWithRetry(url, buildInit) {
1841
1707
  * finishes (return or throw), not when the outer function returns.
1842
1708
  * Also tracks active streams for the proxy-tunnel keepalive mechanism.
1843
1709
  */
1844
- async function* wrapGeneratorWithRelease(gen, releaseSlot) {
1845
- notifyStreamStart();
1710
+ async function* wrapGeneratorWithRelease(gen, releaseSlot, accountInfo) {
1711
+ notifyStreamStart(accountInfo);
1846
1712
  let streamError = false;
1847
1713
  try {
1848
1714
  yield* gen;
@@ -1850,9 +1716,10 @@ async function* wrapGeneratorWithRelease(gen, releaseSlot) {
1850
1716
  streamError = true;
1851
1717
  throw error;
1852
1718
  } finally {
1853
- notifyStreamEnd();
1719
+ notifyStreamEnd(accountInfo);
1854
1720
  releaseSlot();
1855
- if (streamError) resetConnections();
1721
+ if (streamError) if (accountInfo?.accountId) resetAccountConnections(accountInfo.accountId);
1722
+ else resetConnections();
1856
1723
  }
1857
1724
  }
1858
1725
  /**
@@ -1940,7 +1807,10 @@ const createChatCompletions = async (payload) => {
1940
1807
  const releaseSlot = await modelRouter.acquireSlot(resolvedModel);
1941
1808
  try {
1942
1809
  const result = await dispatchRequest(thinkingPayload);
1943
- 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
+ }
1944
1814
  releaseSlot();
1945
1815
  return result;
1946
1816
  } catch (error) {
@@ -1974,7 +1844,10 @@ const createChatCompletions = async (payload) => {
1974
1844
  async function retryWithoutReasoning(payload, releaseSlot) {
1975
1845
  try {
1976
1846
  const result = await dispatchRequest(payload);
1977
- 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
+ }
1978
1851
  releaseSlot();
1979
1852
  return result;
1980
1853
  } catch (retryError) {
@@ -1989,7 +1862,10 @@ async function retryWithoutReasoning(payload, releaseSlot) {
1989
1862
  async function retryWithDowngradedReasoning(payload, releaseSlot) {
1990
1863
  try {
1991
1864
  const result = await dispatchRequest(payload);
1992
- 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
+ }
1993
1869
  releaseSlot();
1994
1870
  return result;
1995
1871
  } catch (retryError) {
@@ -2117,11 +1993,29 @@ async function createWithMultiAccount(payload) {
2117
1993
  copilotApiEndpoint: account.copilotApiEndpoint,
2118
1994
  accountType: account.accountType,
2119
1995
  githubToken: account.githubToken,
2120
- vsCodeVersion: state.vsCodeVersion
1996
+ vsCodeVersion: state.vsCodeVersion,
1997
+ machineId: account.machineId,
1998
+ sessionId: account.sessionId,
1999
+ proxy: account.proxy
2121
2000
  };
2122
2001
  try {
2123
- const result = await doFetch(payload, tokenSource);
2002
+ if (account.lastRequestAt) {
2003
+ const elapsed = Date.now() - account.lastRequestAt;
2004
+ if (elapsed < MIN_SAME_ACCOUNT_INTERVAL_MS) await new Promise((r) => setTimeout(r, MIN_SAME_ACCOUNT_INTERVAL_MS - elapsed));
2005
+ }
2006
+ if (lastUsedAccountId && lastUsedAccountId !== account.id) {
2007
+ const jitter = ACCOUNT_SWITCH_JITTER_MIN_MS + Math.random() * (ACCOUNT_SWITCH_JITTER_MAX_MS - ACCOUNT_SWITCH_JITTER_MIN_MS);
2008
+ consola.debug(`Account switch jitter: ${Math.round(jitter)}ms (${lastUsedAccountId.slice(0, 8)} → ${account.id.slice(0, 8)})`);
2009
+ await new Promise((r) => setTimeout(r, jitter));
2010
+ }
2011
+ lastUsedAccountId = account.id;
2012
+ const result = await doFetch(payload, tokenSource, account.id);
2013
+ account.lastRequestAt = Date.now();
2124
2014
  accountManager.markAccountSuccess(account.id);
2015
+ if (Symbol.asyncIterator in result) result.__accountInfo = {
2016
+ accountId: account.id,
2017
+ accountProxy: account.proxy
2018
+ };
2125
2019
  return result;
2126
2020
  } catch (error) {
2127
2021
  lastError = error;
@@ -2145,7 +2039,7 @@ async function createWithMultiAccount(payload) {
2145
2039
  * call it with different `TokenSource` objects while keeping all the header
2146
2040
  * construction / retry / error‐surfacing logic in one place.
2147
2041
  */
2148
- async function doFetch(payload, source) {
2042
+ async function doFetch(payload, source, accountId) {
2149
2043
  const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
2150
2044
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
2151
2045
  const buildHeaders = () => ({
@@ -2166,7 +2060,10 @@ async function doFetch(payload, source) {
2166
2060
  method: "POST",
2167
2061
  headers: buildHeaders(),
2168
2062
  body: bodyString
2169
- }));
2063
+ }), {
2064
+ accountId,
2065
+ accountProxy: source.proxy
2066
+ });
2170
2067
  if (!response.ok) {
2171
2068
  const errorBody = await response.text();
2172
2069
  if (response.status === 400) consola.warn(`400: ${errorBody}`);
@@ -3188,7 +3085,7 @@ async function validateGitHubToken(token) {
3188
3085
  state.githubToken = token;
3189
3086
  consola.info("Using provided GitHub token");
3190
3087
  try {
3191
- const { getGitHubUser } = await import("./get-user-DHr540ak.js");
3088
+ const { getGitHubUser } = await import("./get-user-BSYESgez.js");
3192
3089
  const user = await getGitHubUser();
3193
3090
  consola.info(`Logged in as ${user.login}`);
3194
3091
  } catch (error) {
@@ -3239,10 +3136,10 @@ async function runServer(options) {
3239
3136
  try {
3240
3137
  await setupCopilotToken();
3241
3138
  } catch (error) {
3242
- const { HTTPError } = await import("./error-Cc8bY0ph.js");
3139
+ const { HTTPError } = await import("./error-BDOdv5Up.js");
3243
3140
  if (error instanceof HTTPError && error.response.status === 401) {
3244
3141
  consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
3245
- const { clearGithubToken } = await import("./token-M99mSdhH.js");
3142
+ const { clearGithubToken } = await import("./token-C9gxxgzi.js");
3246
3143
  await clearGithubToken();
3247
3144
  consola.info("Please restart to re-authenticate");
3248
3145
  }