copilot-api-plus 1.2.16 → 1.2.18

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-COJx3i2R.js";
3
+ import { a as stopCopilotTokenRefresh, i as setupGitHubToken, n as refreshCopilotToken, o as pollAccessToken, r as setupCopilotToken, s as getDeviceCode, t as clearGithubToken } from "./token-CswtzkcP.js";
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)}`);
@@ -1787,20 +1651,29 @@ const RETRY_DELAYS = [2e3];
1787
1651
  * or interruption.
1788
1652
  */
1789
1653
  const RETRY_TIMEOUT_MS = 3e4;
1654
+ /** Minimum interval (ms) between requests on the same account. */
1655
+ const MIN_SAME_ACCOUNT_INTERVAL_MS = 1e3;
1656
+ /** Random jitter range (ms) added when switching between accounts. */
1657
+ const ACCOUNT_SWITCH_JITTER_MIN_MS = 1e3;
1658
+ const ACCOUNT_SWITCH_JITTER_MAX_MS = 5e3;
1659
+ /** Track the last-used account ID to detect account switches. */
1660
+ let lastUsedAccountId;
1790
1661
  /**
1791
1662
  * Wrapper around `fetch()` that aborts if the server doesn't respond within
1792
1663
  * `timeoutMs`. The timeout only covers the period until the response headers
1793
1664
  * arrive – once the body starts streaming, the timeout is cleared so that
1794
1665
  * long SSE responses are not interrupted.
1795
1666
  */
1796
- async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
1667
+ async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accountId, accountProxy } = {}) {
1797
1668
  const controller = new AbortController();
1798
1669
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1799
1670
  try {
1800
- return await fetch(url, {
1671
+ const fetchOptions = {
1801
1672
  ...init,
1802
1673
  signal: controller.signal
1803
- });
1674
+ };
1675
+ if (accountId) fetchOptions.dispatcher = getAccountDispatcher(accountId, accountProxy);
1676
+ return await fetch(url, fetchOptions);
1804
1677
  } catch (error) {
1805
1678
  if (error instanceof DOMException && error.name === "AbortError") throw new Error(`Request timed out after ${timeoutMs}ms`);
1806
1679
  throw error;
@@ -1814,12 +1687,16 @@ async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
1814
1687
  * Returns `{ response }` on success.
1815
1688
  * Throws the last network error if all retries are exhausted.
1816
1689
  */
1817
- async function fetchWithRetry(url, buildInit) {
1690
+ async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {}) {
1818
1691
  let lastError;
1819
1692
  const maxAttempts = RETRY_DELAYS.length + 1;
1820
1693
  for (let attempt = 0; attempt < maxAttempts; attempt++) try {
1821
1694
  const timeout = attempt === 0 ? FETCH_TIMEOUT_MS : RETRY_TIMEOUT_MS;
1822
- return await fetchWithTimeout(url, buildInit(), timeout);
1695
+ return await fetchWithTimeout(url, buildInit(), {
1696
+ timeoutMs: timeout,
1697
+ accountId,
1698
+ accountProxy
1699
+ });
1823
1700
  } catch (error) {
1824
1701
  lastError = error;
1825
1702
  const msg = error instanceof Error ? error.message : String(error);
@@ -1827,7 +1704,8 @@ async function fetchWithRetry(url, buildInit) {
1827
1704
  consola.warn(`Request timed out on attempt ${attempt + 1}/${maxAttempts} — not retrying (credit likely consumed):`, msg);
1828
1705
  break;
1829
1706
  }
1830
- if (attempt === 0) resetConnections();
1707
+ if (attempt === 0) if (accountId) resetAccountConnections(accountId);
1708
+ else resetConnections();
1831
1709
  if (attempt < maxAttempts - 1) {
1832
1710
  const delay = RETRY_DELAYS[attempt];
1833
1711
  consola.warn(`Network error on attempt ${attempt + 1}/${maxAttempts}, retrying in ${delay}ms:`, error instanceof Error ? error.message : error);
@@ -1863,6 +1741,13 @@ async function* wrapGeneratorWithRelease(gen, releaseSlot) {
1863
1741
  */
1864
1742
  const reasoningUnsupportedModels = /* @__PURE__ */ new Set();
1865
1743
  /**
1744
+ * Models whose reasoning_effort must be capped at a lower level.
1745
+ * e.g. claude-opus-4.7 rejects "high" but accepts "medium".
1746
+ * When a model returns 400 with "is not supported by model", it is added
1747
+ * here with its maximum supported effort level.
1748
+ */
1749
+ const reasoningEffortCap = /* @__PURE__ */ new Map();
1750
+ /**
1866
1751
  * Compute an appropriate thinking_budget from model capabilities.
1867
1752
  * Returns undefined if the model does not support thinking.
1868
1753
  */
@@ -1892,7 +1777,9 @@ function isToolChoiceForced(toolChoice) {
1892
1777
  * 1. If the client already set reasoning_effort or thinking_budget → keep as-is
1893
1778
  * 2. If tool_choice forces tool use → skip (API rejects the combination)
1894
1779
  * 3. If model capabilities declare max_thinking_budget → inject thinking_budget
1895
- * 4. Otherwise → inject reasoning_effort="high" (works on claude-*-4.6)
1780
+ * 4. Otherwise → inject reasoning_effort at the highest level the model supports:
1781
+ * - "high" by default (maximum thinking for most models)
1782
+ * - Capped to "medium"/"low" if the model previously rejected "high"
1896
1783
  *
1897
1784
  * The fallback to reasoning_effort ensures thinking works even when the
1898
1785
  * /models endpoint doesn't expose thinking budget fields.
@@ -1905,16 +1792,17 @@ function injectThinking(payload, resolvedModel) {
1905
1792
  ...payload,
1906
1793
  thinking_budget: budget
1907
1794
  };
1908
- if (!reasoningUnsupportedModels.has(resolvedModel)) return {
1795
+ if (reasoningUnsupportedModels.has(resolvedModel)) return payload;
1796
+ const effort = reasoningEffortCap.get(resolvedModel) ?? "high";
1797
+ return {
1909
1798
  ...payload,
1910
- reasoning_effort: "high"
1799
+ reasoning_effort: effort
1911
1800
  };
1912
- return payload;
1913
1801
  }
1914
1802
  function logThinkingInjection(original, injected, resolvedModel) {
1915
1803
  if (original.reasoning_effort || original.thinking_budget) consola.debug(`Thinking: translated (reasoning_effort=${original.reasoning_effort ?? "none"} / thinking_budget=${original.thinking_budget ?? "none"})`);
1916
1804
  else if (injected.thinking_budget && injected.thinking_budget !== original.thinking_budget) consola.debug(`Thinking: injected thinking_budget=${injected.thinking_budget} for "${resolvedModel}"`);
1917
- else if (injected.reasoning_effort === "high") consola.debug(`Thinking: injected reasoning_effort=high for "${resolvedModel}"`);
1805
+ else if (injected.reasoning_effort && injected.reasoning_effort !== original.reasoning_effort) consola.debug(`Thinking: injected reasoning_effort=${injected.reasoning_effort} for "${resolvedModel}"`);
1918
1806
  else if (reasoningUnsupportedModels.has(resolvedModel)) consola.debug(`Thinking: skipped — "${resolvedModel}" does not support reasoning`);
1919
1807
  }
1920
1808
  const createChatCompletions = async (payload) => {
@@ -1934,10 +1822,24 @@ const createChatCompletions = async (payload) => {
1934
1822
  releaseSlot();
1935
1823
  return result;
1936
1824
  } catch (error) {
1937
- if (wasInjected && error instanceof HTTPError && error.response.status === 400 && error.message.includes("Unrecognized request argument")) {
1938
- reasoningUnsupportedModels.add(resolvedModel);
1939
- consola.info(`Model "${resolvedModel}" does not support reasoning_effort — disabled for future requests`);
1940
- return retryWithoutReasoning(routedPayload, releaseSlot);
1825
+ if (error instanceof HTTPError && error.response.status === 400) {
1826
+ const errMsg = error.message;
1827
+ if (wasInjected && errMsg.includes("Unrecognized request argument")) {
1828
+ reasoningUnsupportedModels.add(resolvedModel);
1829
+ consola.info(`Model "${resolvedModel}" does not support reasoning_effort — disabled for future requests`);
1830
+ return retryWithoutReasoning(routedPayload, releaseSlot);
1831
+ }
1832
+ if (errMsg.includes("is not supported by model")) {
1833
+ const currentEffort = thinkingPayload.reasoning_effort;
1834
+ if (currentEffort && currentEffort !== "medium" && currentEffort !== "low") {
1835
+ reasoningEffortCap.set(resolvedModel, "medium");
1836
+ consola.info(`Model "${resolvedModel}" rejected reasoning_effort="${currentEffort}" — downgrading to "medium" for future requests`);
1837
+ return retryWithDowngradedReasoning({
1838
+ ...routedPayload,
1839
+ reasoning_effort: "medium"
1840
+ }, releaseSlot);
1841
+ }
1842
+ }
1941
1843
  }
1942
1844
  releaseSlot();
1943
1845
  throw error;
@@ -1959,6 +1861,21 @@ async function retryWithoutReasoning(payload, releaseSlot) {
1959
1861
  }
1960
1862
  }
1961
1863
  /**
1864
+ * Retry a request with a downgraded reasoning_effort after the model
1865
+ * rejected the higher value (e.g. "high" → "medium").
1866
+ */
1867
+ async function retryWithDowngradedReasoning(payload, releaseSlot) {
1868
+ try {
1869
+ const result = await dispatchRequest(payload);
1870
+ if (Symbol.asyncIterator in result) return wrapGeneratorWithRelease(result, releaseSlot);
1871
+ releaseSlot();
1872
+ return result;
1873
+ } catch (retryError) {
1874
+ releaseSlot();
1875
+ throw retryError;
1876
+ }
1877
+ }
1878
+ /**
1962
1879
  * Dispatch request to either single-account or multi-account path.
1963
1880
  */
1964
1881
  function dispatchRequest(payload) {
@@ -2078,10 +1995,24 @@ async function createWithMultiAccount(payload) {
2078
1995
  copilotApiEndpoint: account.copilotApiEndpoint,
2079
1996
  accountType: account.accountType,
2080
1997
  githubToken: account.githubToken,
2081
- vsCodeVersion: state.vsCodeVersion
1998
+ vsCodeVersion: state.vsCodeVersion,
1999
+ machineId: account.machineId,
2000
+ sessionId: account.sessionId,
2001
+ proxy: account.proxy
2082
2002
  };
2083
2003
  try {
2084
- const result = await doFetch(payload, tokenSource);
2004
+ if (account.lastRequestAt) {
2005
+ const elapsed = Date.now() - account.lastRequestAt;
2006
+ if (elapsed < MIN_SAME_ACCOUNT_INTERVAL_MS) await new Promise((r) => setTimeout(r, MIN_SAME_ACCOUNT_INTERVAL_MS - elapsed));
2007
+ }
2008
+ if (lastUsedAccountId && lastUsedAccountId !== account.id) {
2009
+ const jitter = ACCOUNT_SWITCH_JITTER_MIN_MS + Math.random() * (ACCOUNT_SWITCH_JITTER_MAX_MS - ACCOUNT_SWITCH_JITTER_MIN_MS);
2010
+ consola.debug(`Account switch jitter: ${Math.round(jitter)}ms (${lastUsedAccountId.slice(0, 8)} → ${account.id.slice(0, 8)})`);
2011
+ await new Promise((r) => setTimeout(r, jitter));
2012
+ }
2013
+ lastUsedAccountId = account.id;
2014
+ const result = await doFetch(payload, tokenSource, account.id);
2015
+ account.lastRequestAt = Date.now();
2085
2016
  accountManager.markAccountSuccess(account.id);
2086
2017
  return result;
2087
2018
  } catch (error) {
@@ -2106,7 +2037,7 @@ async function createWithMultiAccount(payload) {
2106
2037
  * call it with different `TokenSource` objects while keeping all the header
2107
2038
  * construction / retry / error‐surfacing logic in one place.
2108
2039
  */
2109
- async function doFetch(payload, source) {
2040
+ async function doFetch(payload, source, accountId) {
2110
2041
  const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
2111
2042
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
2112
2043
  const buildHeaders = () => ({
@@ -2127,7 +2058,10 @@ async function doFetch(payload, source) {
2127
2058
  method: "POST",
2128
2059
  headers: buildHeaders(),
2129
2060
  body: bodyString
2130
- }));
2061
+ }), {
2062
+ accountId,
2063
+ accountProxy: source.proxy
2064
+ });
2131
2065
  if (!response.ok) {
2132
2066
  const errorBody = await response.text();
2133
2067
  if (response.status === 400) consola.warn(`400: ${errorBody}`);
@@ -3149,7 +3083,7 @@ async function validateGitHubToken(token) {
3149
3083
  state.githubToken = token;
3150
3084
  consola.info("Using provided GitHub token");
3151
3085
  try {
3152
- const { getGitHubUser } = await import("./get-user-DHr540ak.js");
3086
+ const { getGitHubUser } = await import("./get-user-HaNtbtZZ.js");
3153
3087
  const user = await getGitHubUser();
3154
3088
  consola.info(`Logged in as ${user.login}`);
3155
3089
  } catch (error) {
@@ -3200,10 +3134,10 @@ async function runServer(options) {
3200
3134
  try {
3201
3135
  await setupCopilotToken();
3202
3136
  } catch (error) {
3203
- const { HTTPError } = await import("./error-Cc8bY0ph.js");
3137
+ const { HTTPError } = await import("./error-Ci5sfamJ.js");
3204
3138
  if (error instanceof HTTPError && error.response.status === 401) {
3205
3139
  consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
3206
- const { clearGithubToken } = await import("./token-M99mSdhH.js");
3140
+ const { clearGithubToken } = await import("./token-BlHBeJfE.js");
3207
3141
  await clearGithubToken();
3208
3142
  consola.info("Please restart to re-authenticate");
3209
3143
  }