copilot-api-plus 1.2.19 → 1.2.21

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 { 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";
2
+ import { C as state, D as copilotBaseUrl, E as GITHUB_CLIENT_ID, O as copilotHeaders, S as sleep, T as GITHUB_BASE_URL, _ as cacheModels, a as getAccountDispatcher, b as isNullish, c as isProxyActive, d as resetAccountConnections, f as resetConnections, g as forwardError, h as HTTPError, k as standardHeaders, l as notifyStreamEnd, m as ensurePaths, o as initProxyFromEnv, p as PATHS, r as getCopilotUsage, s as isAccountProxied, t as accountManager, u as notifyStreamStart, v as cacheVSCodeVersion, x as rootCause, y as findModel } from "./account-manager-hTatSbhl.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-DoqfK3CD.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  import consola from "consola";
@@ -1351,6 +1351,16 @@ accountRoutes.post("/", async (c) => {
1351
1351
  if (!body.githubToken || !body.label) return c.json({ error: "githubToken and label are required" }, 400);
1352
1352
  const account = await accountManager.addAccount(body.githubToken, body.label, body.accountType);
1353
1353
  if (body.proxy) {
1354
+ try {
1355
+ const proxyUrl = new URL(body.proxy);
1356
+ if (![
1357
+ "http:",
1358
+ "https:",
1359
+ "socks5:"
1360
+ ].includes(proxyUrl.protocol)) return c.json({ error: "proxy must use http://, https://, or socks5:// protocol" }, 400);
1361
+ } catch {
1362
+ return c.json({ error: "proxy must be a valid URL" }, 400);
1363
+ }
1354
1364
  account.proxy = body.proxy;
1355
1365
  await accountManager.saveAccounts();
1356
1366
  }
@@ -1393,6 +1403,33 @@ accountRoutes.put("/:id/status", async (c) => {
1393
1403
  return c.json({ error: "Failed to update account status" }, 500);
1394
1404
  }
1395
1405
  });
1406
+ accountRoutes.put("/:id/proxy", async (c) => {
1407
+ try {
1408
+ const id = c.req.param("id");
1409
+ const body = await c.req.json();
1410
+ const account = accountManager.getAccountById(id);
1411
+ if (!account) return c.json({ error: "Account not found" }, 404);
1412
+ if (body.proxy) {
1413
+ try {
1414
+ const proxyUrl = new URL(body.proxy);
1415
+ if (![
1416
+ "http:",
1417
+ "https:",
1418
+ "socks5:"
1419
+ ].includes(proxyUrl.protocol)) return c.json({ error: "proxy must use http://, https://, or socks5:// protocol" }, 400);
1420
+ } catch {
1421
+ return c.json({ error: "proxy must be a valid URL" }, 400);
1422
+ }
1423
+ account.proxy = body.proxy;
1424
+ } else account.proxy = void 0;
1425
+ await accountManager.saveAccounts();
1426
+ return c.json({ account: sanitiseAccount(account) });
1427
+ } catch (error) {
1428
+ consola.warn(`Error updating account proxy: ${rootCause(error)}`);
1429
+ consola.debug("Error updating account proxy:", error);
1430
+ return c.json({ error: "Failed to update account proxy" }, 500);
1431
+ }
1432
+ });
1396
1433
  accountRoutes.post("/:id/refresh", async (c) => {
1397
1434
  try {
1398
1435
  const id = c.req.param("id");
@@ -1630,15 +1667,6 @@ async function checkRateLimit(state) {
1630
1667
  * ~120s to start streaming, so we give a generous timeout for headers.
1631
1668
  */
1632
1669
  const FETCH_TIMEOUT_MS = 12e4;
1633
- /**
1634
- * Retry delays in ms. Empty = no retries.
1635
- *
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).
1640
- */
1641
- const RETRY_DELAYS = [];
1642
1670
  /** Minimum interval (ms) between requests on the same account. */
1643
1671
  const MIN_SAME_ACCOUNT_INTERVAL_MS = 1e3;
1644
1672
  /** Random jitter range (ms) added when switching between accounts. */
@@ -1670,37 +1698,28 @@ async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accou
1670
1698
  }
1671
1699
  }
1672
1700
  /**
1673
- * Retry loop for fetch: retries on network errors with exponential back-off.
1701
+ * Single-attempt fetch with connection pool reset on network errors.
1674
1702
  *
1675
- * Returns `{ response }` on success.
1676
- * Throws the last network error if all retries are exhausted.
1703
+ * Retries are intentionally disabled each Copilot request consumes a
1704
+ * credit, and the caller (e.g. Claude Code) already retries at the
1705
+ * application level. Our retry + caller retry created a request cascade
1706
+ * that caused account bans (367 requests in 52 minutes).
1707
+ *
1708
+ * On network failure (NOT timeout), the pooled connections are destroyed
1709
+ * so that the caller's next attempt gets a fresh socket instantly.
1677
1710
  */
1678
1711
  async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {}) {
1679
- let lastError;
1680
- const maxAttempts = RETRY_DELAYS.length + 1;
1681
- for (let attempt = 0; attempt < maxAttempts; attempt++) try {
1682
- const timeout = FETCH_TIMEOUT_MS;
1712
+ try {
1683
1713
  return await fetchWithTimeout(url, buildInit(), {
1684
- timeoutMs: timeout,
1714
+ timeoutMs: FETCH_TIMEOUT_MS,
1685
1715
  accountId,
1686
1716
  accountProxy
1687
1717
  });
1688
1718
  } catch (error) {
1689
- lastError = error;
1690
- const msg = error instanceof Error ? error.message : String(error);
1691
- if (msg.includes("timed out")) {
1692
- consola.warn(`Request timed out on attempt ${attempt + 1}/${maxAttempts} — not retrying (credit likely consumed):`, msg);
1693
- break;
1694
- }
1695
- if (attempt === 0) if (accountId) resetAccountConnections(accountId);
1719
+ if (!(error instanceof Error ? error.message : String(error)).includes("timed out")) if (accountId) resetAccountConnections(accountId);
1696
1720
  else resetConnections();
1697
- if (attempt < maxAttempts - 1) {
1698
- const delay = RETRY_DELAYS[attempt];
1699
- consola.warn(`Network error on attempt ${attempt + 1}/${maxAttempts}, retrying in ${delay}ms:`, error instanceof Error ? error.message : error);
1700
- await new Promise((r) => setTimeout(r, delay));
1701
- }
1721
+ throw error;
1702
1722
  }
1703
- throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Network request failed");
1704
1723
  }
1705
1724
  /**
1706
1725
  * Wraps an AsyncGenerator so that `releaseSlot` is called when the generator
@@ -1809,7 +1828,9 @@ const createChatCompletions = async (payload) => {
1809
1828
  const result = await dispatchRequest(thinkingPayload);
1810
1829
  if (Symbol.asyncIterator in result) {
1811
1830
  const accountInfo = result.__accountInfo;
1812
- return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1831
+ const wrapped = wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1832
+ wrapped.__accountInfo = accountInfo;
1833
+ return wrapped;
1813
1834
  }
1814
1835
  releaseSlot();
1815
1836
  return result;
@@ -1819,14 +1840,14 @@ const createChatCompletions = async (payload) => {
1819
1840
  if (wasInjected && errMsg.includes("Unrecognized request argument")) {
1820
1841
  reasoningUnsupportedModels.add(resolvedModel);
1821
1842
  consola.info(`Model "${resolvedModel}" does not support reasoning_effort — disabled for future requests`);
1822
- return retryWithoutReasoning(routedPayload, releaseSlot);
1843
+ return retryWithModifiedPayload(routedPayload, releaseSlot);
1823
1844
  }
1824
1845
  if (errMsg.includes("is not supported by model")) {
1825
1846
  const currentEffort = thinkingPayload.reasoning_effort;
1826
1847
  if (currentEffort && currentEffort !== "medium" && currentEffort !== "low") {
1827
1848
  reasoningEffortCap.set(resolvedModel, "medium");
1828
1849
  consola.info(`Model "${resolvedModel}" rejected reasoning_effort="${currentEffort}" — downgrading to "medium" for future requests`);
1829
- return retryWithDowngradedReasoning({
1850
+ return retryWithModifiedPayload({
1830
1851
  ...routedPayload,
1831
1852
  reasoning_effort: "medium"
1832
1853
  }, releaseSlot);
@@ -1838,33 +1859,18 @@ const createChatCompletions = async (payload) => {
1838
1859
  }
1839
1860
  };
1840
1861
  /**
1841
- * Retry a request without reasoning_effort after the model rejected it.
1862
+ * Retry a request after modifying the payload (e.g. stripping or
1863
+ * downgrading reasoning_effort).
1842
1864
  * Handles slot release for both streaming and non-streaming responses.
1843
1865
  */
1844
- async function retryWithoutReasoning(payload, releaseSlot) {
1845
- try {
1846
- const result = await dispatchRequest(payload);
1847
- if (Symbol.asyncIterator in result) {
1848
- const accountInfo = result.__accountInfo;
1849
- return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1850
- }
1851
- releaseSlot();
1852
- return result;
1853
- } catch (retryError) {
1854
- releaseSlot();
1855
- throw retryError;
1856
- }
1857
- }
1858
- /**
1859
- * Retry a request with a downgraded reasoning_effort after the model
1860
- * rejected the higher value (e.g. "high" → "medium").
1861
- */
1862
- async function retryWithDowngradedReasoning(payload, releaseSlot) {
1866
+ async function retryWithModifiedPayload(payload, releaseSlot) {
1863
1867
  try {
1864
1868
  const result = await dispatchRequest(payload);
1865
1869
  if (Symbol.asyncIterator in result) {
1866
1870
  const accountInfo = result.__accountInfo;
1867
- return wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1871
+ const wrapped = wrapGeneratorWithRelease(result, releaseSlot, accountInfo);
1872
+ wrapped.__accountInfo = accountInfo;
1873
+ return wrapped;
1868
1874
  }
1869
1875
  releaseSlot();
1870
1876
  return result;
@@ -1881,7 +1887,7 @@ function dispatchRequest(payload) {
1881
1887
  }
1882
1888
  async function createWithSingleAccount(payload) {
1883
1889
  if (!state.copilotToken) throw new Error("Copilot token not found");
1884
- const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
1890
+ const enableVision = payload.messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
1885
1891
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
1886
1892
  const buildHeaders = () => ({
1887
1893
  ...copilotHeaders(state, enableVision),
@@ -1937,7 +1943,7 @@ async function tryRefreshAndRetry(account, payload, tokenSource) {
1937
1943
  try {
1938
1944
  await accountManager.refreshAccountToken(account);
1939
1945
  tokenSource.copilotToken = account.copilotToken;
1940
- const result = await doFetch(payload, tokenSource);
1946
+ const result = await doFetch(payload, tokenSource, account.id);
1941
1947
  accountManager.markAccountSuccess(account.id);
1942
1948
  return result;
1943
1949
  } catch {
@@ -2040,7 +2046,7 @@ async function createWithMultiAccount(payload) {
2040
2046
  * construction / retry / error‐surfacing logic in one place.
2041
2047
  */
2042
2048
  async function doFetch(payload, source, accountId) {
2043
- const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
2049
+ const enableVision = payload.messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
2044
2050
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
2045
2051
  const buildHeaders = () => ({
2046
2052
  ...copilotHeaders(source, enableVision),
@@ -2835,6 +2841,86 @@ function translateErrorToAnthropicErrorEvent() {
2835
2841
  }
2836
2842
  //#endregion
2837
2843
  //#region src/routes/messages/handler.ts
2844
+ /** Heartbeat interval — keeps the downstream connection alive. */
2845
+ const HEARTBEAT_PROXIED_MS = 15e3;
2846
+ const HEARTBEAT_DIRECT_MS = 3e4;
2847
+ /**
2848
+ * Upstream silence timeout — if no SSE data arrives for this long,
2849
+ * treat the upstream as dead and close the stream with an error.
2850
+ */
2851
+ const UPSTREAM_TIMEOUT_PROXIED_MS = 9e4;
2852
+ const UPSTREAM_TIMEOUT_DIRECT_MS = 3e5;
2853
+ /** Sentinel value returned by the sleep branch of Promise.race. */
2854
+ const HEARTBEAT = Symbol("heartbeat");
2855
+ /** Simple non-cancellable sleep that resolves to a sentinel. */
2856
+ function heartbeatDelay(ms) {
2857
+ return new Promise((resolve) => setTimeout(() => resolve(HEARTBEAT), ms));
2858
+ }
2859
+ /** Send an error event to the downstream client, ignoring write failures. */
2860
+ async function sendErrorEvent(stream) {
2861
+ try {
2862
+ const errorEvent = translateErrorToAnthropicErrorEvent();
2863
+ await stream.writeSSE({
2864
+ event: errorEvent.type,
2865
+ data: JSON.stringify(errorEvent)
2866
+ });
2867
+ } catch {}
2868
+ }
2869
+ /**
2870
+ * Consume the upstream SSE async iterator with heartbeat injection.
2871
+ *
2872
+ * Uses `Promise.race` between the next upstream event and a heartbeat
2873
+ * timer. The same `iter.next()` promise is reused across heartbeat
2874
+ * cycles to prevent data loss.
2875
+ *
2876
+ * No external requests are made — heartbeat pings are written to the
2877
+ * downstream HTTP response only.
2878
+ */
2879
+ async function consumeStreamWithHeartbeat(response, stream, opts) {
2880
+ const { streamState, heartbeatMs, upstreamTimeoutMs } = opts;
2881
+ const iter = response[Symbol.asyncIterator]();
2882
+ let pendingNext = iter.next();
2883
+ let lastDataAt = Date.now();
2884
+ while (true) {
2885
+ const raceResult = await Promise.race([pendingNext.then((r) => ({
2886
+ kind: "data",
2887
+ result: r
2888
+ })), heartbeatDelay(heartbeatMs)]);
2889
+ if (raceResult === HEARTBEAT) {
2890
+ const silenceMs = Date.now() - lastDataAt;
2891
+ if (silenceMs >= upstreamTimeoutMs) {
2892
+ consola.warn(`Upstream silent for ${Math.round(silenceMs / 1e3)}s (limit ${upstreamTimeoutMs / 1e3}s), closing stream`);
2893
+ resetConnections();
2894
+ await sendErrorEvent(stream);
2895
+ break;
2896
+ }
2897
+ await stream.writeSSE({
2898
+ event: "ping",
2899
+ data: "{\"type\":\"ping\"}"
2900
+ });
2901
+ consola.debug(`SSE heartbeat ping sent (silent ${Math.round(silenceMs / 1e3)}s)`);
2902
+ continue;
2903
+ }
2904
+ const { result: iterResult } = raceResult;
2905
+ if (iterResult.done) break;
2906
+ lastDataAt = Date.now();
2907
+ pendingNext = iter.next();
2908
+ const rawEvent = iterResult.value;
2909
+ if (rawEvent.data === "[DONE]") break;
2910
+ if (!rawEvent.data) continue;
2911
+ let chunk;
2912
+ try {
2913
+ chunk = JSON.parse(rawEvent.data);
2914
+ } catch {
2915
+ consola.debug("Skipping malformed SSE chunk");
2916
+ continue;
2917
+ }
2918
+ for (const event of translateChunkToAnthropicEvents(chunk, streamState)) await stream.writeSSE({
2919
+ event: event.type,
2920
+ data: JSON.stringify(event)
2921
+ });
2922
+ }
2923
+ }
2838
2924
  async function handleCompletion(c) {
2839
2925
  await checkRateLimit(state);
2840
2926
  const anthropicPayload = await c.req.json();
@@ -2850,10 +2936,12 @@ async function handleCompletion(c) {
2850
2936
  const openAIPayload = translateToOpenAI(anthropicPayload);
2851
2937
  if (state.manualApprove) await awaitApproval();
2852
2938
  const response = await createChatCompletions(openAIPayload);
2853
- if (isNonStreaming(response)) {
2854
- const anthropicResponse = translateToAnthropic(response);
2855
- return c.json(anthropicResponse);
2856
- }
2939
+ if (isNonStreaming(response)) return c.json(translateToAnthropic(response));
2940
+ const accountInfo = response.__accountInfo;
2941
+ const proxied = accountInfo ? isAccountProxied(accountInfo.accountProxy) : isProxyActive();
2942
+ const heartbeatMs = proxied ? HEARTBEAT_PROXIED_MS : HEARTBEAT_DIRECT_MS;
2943
+ const upstreamTimeoutMs = proxied ? UPSTREAM_TIMEOUT_PROXIED_MS : UPSTREAM_TIMEOUT_DIRECT_MS;
2944
+ consola.debug(`SSE stream config: proxied=${proxied}, heartbeat=${heartbeatMs / 1e3}s, timeout=${upstreamTimeoutMs / 1e3}s`);
2857
2945
  return streamSSE(c, async (stream) => {
2858
2946
  const streamState = {
2859
2947
  messageStartSent: false,
@@ -2864,34 +2952,16 @@ async function handleCompletion(c) {
2864
2952
  thinkingRequested: Boolean(anthropicPayload.thinking)
2865
2953
  };
2866
2954
  try {
2867
- for await (const rawEvent of response) {
2868
- const event = rawEvent;
2869
- if (event.data === "[DONE]") break;
2870
- if (!event.data) continue;
2871
- let chunk;
2872
- try {
2873
- chunk = JSON.parse(event.data);
2874
- } catch {
2875
- consola.debug("Skipping malformed SSE chunk");
2876
- continue;
2877
- }
2878
- const events = translateChunkToAnthropicEvents(chunk, streamState);
2879
- for (const event of events) await stream.writeSSE({
2880
- event: event.type,
2881
- data: JSON.stringify(event)
2882
- });
2883
- }
2955
+ await consumeStreamWithHeartbeat(response, stream, {
2956
+ streamState,
2957
+ heartbeatMs,
2958
+ upstreamTimeoutMs
2959
+ });
2884
2960
  } catch (error) {
2885
2961
  const message = error.message || String(error);
2886
2962
  consola.warn(`SSE stream interrupted: ${message}`);
2887
2963
  resetConnections();
2888
- try {
2889
- const errorEvent = translateErrorToAnthropicErrorEvent();
2890
- await stream.writeSSE({
2891
- event: errorEvent.type,
2892
- data: JSON.stringify(errorEvent)
2893
- });
2894
- } catch {}
2964
+ await sendErrorEvent(stream);
2895
2965
  }
2896
2966
  });
2897
2967
  }
@@ -3085,7 +3155,7 @@ async function validateGitHubToken(token) {
3085
3155
  state.githubToken = token;
3086
3156
  consola.info("Using provided GitHub token");
3087
3157
  try {
3088
- const { getGitHubUser } = await import("./get-user-BSYESgez.js");
3158
+ const { getGitHubUser } = await import("./get-user-BuSGshPt.js");
3089
3159
  const user = await getGitHubUser();
3090
3160
  consola.info(`Logged in as ${user.login}`);
3091
3161
  } catch (error) {
@@ -3136,10 +3206,10 @@ async function runServer(options) {
3136
3206
  try {
3137
3207
  await setupCopilotToken();
3138
3208
  } catch (error) {
3139
- const { HTTPError } = await import("./error-BDOdv5Up.js");
3209
+ const { HTTPError } = await import("./error-BZbc7idf.js");
3140
3210
  if (error instanceof HTTPError && error.response.status === 401) {
3141
3211
  consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
3142
- const { clearGithubToken } = await import("./token-C9gxxgzi.js");
3212
+ const { clearGithubToken } = await import("./token-DKdhI9cl.js");
3143
3213
  await clearGithubToken();
3144
3214
  consola.info("Please restart to re-authenticate");
3145
3215
  }