@dianshuv/copilot-api 0.13.1 → 0.15.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/main.mjs +185 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - **Stream repetition detection**: Detects when models get stuck in repetitive output loops using KMP-based pattern matching and logs a warning.
17
17
  - **Stale request reaping**: Automatically force-fails requests that exceed a configurable maximum age (default 600s) to prevent resource leaks.
18
18
  - **PostHog analytics**: Optional PostHog Cloud integration (`--posthog-key`) sends per-request token usage events for long-term trend analysis. Free tier (1M events/month) is more than sufficient for individual use.
19
+ - **GitHub Copilot CLI emulation**: All upstream requests to GitHub — device-flow `login`, Copilot token exchange, and model calls — carry the official GitHub Copilot CLI's (`@github/copilot`) identity: its `copilot-integration-id` (`copilot-developer-cli`), `editor-version`/`user-agent` (`copilot/<version>`), `x-github-api-version`, and a persistent `x-client-machine-id` (stored at `~/.local/share/copilot-api/machine_id`). The `login` flow uses the CLI's own OAuth app, so **new** logins request the `read:user`, `read:org`, `repo`, and `gist` scopes; existing tokens keep working without re-authentication.
19
20
 
20
21
  ## Quick Start
21
22
 
package/dist/main.mjs CHANGED
@@ -3,10 +3,10 @@ import { defineCommand, runMain } from "citty";
3
3
  import consola from "consola";
4
4
  import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
6
7
  import path from "node:path";
7
8
  import { getProxyForUrl } from "proxy-from-env";
8
9
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
9
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
11
  import { serve } from "srvx";
12
12
  import { PostHog } from "posthog-node";
@@ -19,14 +19,31 @@ import { events } from "fetch-event-stream";
19
19
  //#region src/lib/paths.ts
20
20
  const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
21
21
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
22
+ const MACHINE_ID_PATH = path.join(APP_DIR, "machine_id");
22
23
  const PATHS = {
23
24
  APP_DIR,
24
- GITHUB_TOKEN_PATH
25
+ GITHUB_TOKEN_PATH,
26
+ MACHINE_ID_PATH
25
27
  };
26
28
  async function ensurePaths() {
27
29
  await fs.mkdir(PATHS.APP_DIR, { recursive: true });
28
30
  await ensureFile(PATHS.GITHUB_TOKEN_PATH);
29
31
  }
32
+ /**
33
+ * Returns a stable-per-machine UUID for the `x-client-machine-id` header,
34
+ * mirroring the GitHub Copilot CLI which keeps a persistent machine identifier.
35
+ * Self-contained: reads the persisted value, generating and storing one on first
36
+ * use. A missing, unreadable, or corrupted file self-heals by regenerating,
37
+ * so startup can't crash and a bad value can't be sent upstream forever.
38
+ */
39
+ async function getOrCreateMachineId() {
40
+ const existing = await fs.readFile(PATHS.MACHINE_ID_PATH, "utf8").then((content) => content.trim()).catch(() => "");
41
+ if (MACHINE_ID_PATTERN.test(existing)) return existing;
42
+ const machineId = randomUUID();
43
+ await fs.writeFile(PATHS.MACHINE_ID_PATH, machineId, { mode: 384 });
44
+ return machineId;
45
+ }
46
+ const MACHINE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
47
  async function ensureFile(filePath) {
31
48
  const isWindows = process.platform === "win32";
32
49
  try {
@@ -142,10 +159,12 @@ const standardHeaders = () => ({
142
159
  "content-type": "application/json",
143
160
  accept: "application/json"
144
161
  });
145
- const COPILOT_VERSION = "0.26.7";
146
- const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
147
- const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
148
- const API_VERSION = "2025-04-01";
162
+ const CLI_VERSION_FALLBACK = "1.0.67";
163
+ const API_VERSION = "2026-07-01";
164
+ const COPILOT_INTEGRATION_ID = "copilot-developer-cli";
165
+ const cliVersion = (state) => state.copilotCliVersion ?? CLI_VERSION_FALLBACK;
166
+ const editorVersion = (state) => `copilot/${cliVersion(state)}`;
167
+ const userAgent = (state) => `copilot/${cliVersion(state)} (${process.platform} ${process.version}) term/${process.env.TERM_PROGRAM ?? "unknown"}`;
149
168
  const copilotBaseUrl = (state) => state.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state.accountType}.githubcopilot.com`;
150
169
  function hasHeaderKey(headers, key) {
151
170
  const lowerKey = key.toLowerCase();
@@ -157,15 +176,15 @@ function copilotHeaders(state, visionOrOptions) {
157
176
  const options = typeof visionOrOptions === "boolean" ? { vision: visionOrOptions } : visionOrOptions ?? {};
158
177
  const headers = {
159
178
  Authorization: `Bearer ${state.copilotToken}`,
179
+ accept: standardHeaders().accept,
160
180
  "content-type": standardHeaders()["content-type"],
161
- "copilot-integration-id": "vscode-chat",
162
- "editor-version": `vscode/${state.vsCodeVersion}`,
163
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
164
- "user-agent": USER_AGENT,
165
- "openai-intent": options.intent ?? "conversation-panel",
181
+ "copilot-integration-id": COPILOT_INTEGRATION_ID,
182
+ "editor-version": editorVersion(state),
183
+ "user-agent": userAgent(state),
184
+ "openai-intent": options.intent ?? "conversation-agent",
166
185
  "x-github-api-version": API_VERSION,
167
- "x-request-id": randomUUID(),
168
- "x-vscode-user-agent-library-version": "electron-fetch"
186
+ "x-interaction-id": randomUUID(),
187
+ "x-client-machine-id": state.machineId ?? ""
169
188
  };
170
189
  for (const [key, value] of Object.entries(options.modelRequestHeaders ?? {})) if (!hasHeaderKey(headers, key)) headers[key] = value;
171
190
  if (options.vision) headers["copilot-vision-request"] = "true";
@@ -173,17 +192,22 @@ function copilotHeaders(state, visionOrOptions) {
173
192
  }
174
193
  const GITHUB_API_BASE_URL = "https://api.github.com";
175
194
  const githubHeaders = (state) => ({
195
+ accept: standardHeaders().accept,
196
+ authorization: `Bearer ${state.githubToken}`,
197
+ "user-agent": userAgent(state)
198
+ });
199
+ const githubOAuthHeaders = (state) => ({
176
200
  ...standardHeaders(),
177
- authorization: `token ${state.githubToken}`,
178
- "editor-version": `vscode/${state.vsCodeVersion}`,
179
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
180
- "user-agent": USER_AGENT,
181
- "x-github-api-version": API_VERSION,
182
- "x-vscode-user-agent-library-version": "electron-fetch"
201
+ "user-agent": userAgent(state)
183
202
  });
184
203
  const GITHUB_BASE_URL = "https://github.com";
185
- const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
186
- const GITHUB_APP_SCOPES = ["read:user"].join(" ");
204
+ const GITHUB_CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
205
+ const GITHUB_APP_SCOPES = [
206
+ "read:user",
207
+ "read:org",
208
+ "repo",
209
+ "gist"
210
+ ].join(" ");
187
211
 
188
212
  //#endregion
189
213
  //#region src/lib/auto-truncate-common.ts
@@ -454,7 +478,7 @@ const getCopilotToken = async () => {
454
478
  async function getDeviceCode() {
455
479
  const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
456
480
  method: "POST",
457
- headers: standardHeaders(),
481
+ headers: githubOAuthHeaders(state),
458
482
  body: JSON.stringify({
459
483
  client_id: GITHUB_CLIENT_ID,
460
484
  scope: GITHUB_APP_SCOPES
@@ -467,10 +491,7 @@ async function getDeviceCode() {
467
491
  //#endregion
468
492
  //#region src/services/github/get-user.ts
469
493
  async function getGitHubUser() {
470
- const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
471
- authorization: `token ${state.githubToken}`,
472
- ...standardHeaders()
473
- } });
494
+ const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: githubHeaders(state) });
474
495
  if (!response.ok) throw await HTTPError.fromResponse("Failed to get GitHub user", response);
475
496
  return await response.json();
476
497
  }
@@ -655,28 +676,33 @@ const getModels = async () => {
655
676
  };
656
677
 
657
678
  //#endregion
658
- //#region src/services/get-vscode-version.ts
659
- const FALLBACK = "1.104.3";
660
- const GITHUB_API_URL = "https://api.github.com/repos/microsoft/vscode/releases/latest";
661
- async function getVSCodeVersion() {
679
+ //#region src/services/get-copilot-cli-version.ts
680
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@github/copilot/latest";
681
+ /**
682
+ * Fetches the latest published version of the GitHub Copilot CLI (@github/copilot)
683
+ * from the npm registry, so the impersonated `editor-version` / `user-agent`
684
+ * headers track a current CLI release. Falls back to a pinned version on any
685
+ * failure (offline, timeout, malformed response).
686
+ */
687
+ async function getCopilotCliVersion() {
662
688
  const controller = new AbortController();
663
689
  const timeout = setTimeout(() => {
664
690
  controller.abort();
665
691
  }, 5e3);
666
692
  try {
667
- const response = await fetch(GITHUB_API_URL, {
693
+ const response = await fetch(NPM_REGISTRY_URL, {
668
694
  signal: controller.signal,
669
695
  headers: {
670
- Accept: "application/vnd.github.v3+json",
696
+ Accept: "application/json",
671
697
  "User-Agent": "copilot-api"
672
698
  }
673
699
  });
674
- if (!response.ok) return FALLBACK;
675
- const version = (await response.json()).tag_name;
700
+ if (!response.ok) return CLI_VERSION_FALLBACK;
701
+ const version = (await response.json()).version;
676
702
  if (version && /^\d+\.\d+\.\d+$/.test(version)) return version;
677
- return FALLBACK;
703
+ return CLI_VERSION_FALLBACK;
678
704
  } catch {
679
- return FALLBACK;
705
+ return CLI_VERSION_FALLBACK;
680
706
  } finally {
681
707
  clearTimeout(timeout);
682
708
  }
@@ -694,10 +720,21 @@ function findModelById(modelId) {
694
720
  async function cacheModels() {
695
721
  state.models = await getModels();
696
722
  }
697
- const cacheVSCodeVersion = async () => {
698
- const response = await getVSCodeVersion();
699
- state.vsCodeVersion = response;
700
- consola.info(`Using VSCode version: ${response}`);
723
+ const cacheCopilotCliVersion = async () => {
724
+ const response = await getCopilotCliVersion();
725
+ state.copilotCliVersion = response;
726
+ consola.info(`Using Copilot CLI version: ${response}`);
727
+ };
728
+ /**
729
+ * Initializes the GitHub Copilot CLI emulation identity — the persistent
730
+ * machine id and the live CLI version that feed the impersonated headers.
731
+ * Shared by every entrypoint that talks to GitHub (server, login, debug) so no
732
+ * entrypoint can forget a piece of the identity and send a degraded fingerprint.
733
+ * Must run after ensurePaths() and before any request-building call.
734
+ */
735
+ const initCopilotIdentity = async () => {
736
+ state.machineId = await getOrCreateMachineId();
737
+ await cacheCopilotCliVersion();
701
738
  };
702
739
 
703
740
  //#endregion
@@ -709,7 +746,7 @@ async function pollAccessToken(deviceCode) {
709
746
  while (Date.now() < expiresAt) {
710
747
  const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
711
748
  method: "POST",
712
- headers: standardHeaders(),
749
+ headers: githubOAuthHeaders(state),
713
750
  body: JSON.stringify({
714
751
  client_id: GITHUB_CLIENT_ID,
715
752
  device_code: deviceCode.device_code,
@@ -960,6 +997,7 @@ const debugModels = defineCommand({
960
997
  state.accountType = args["account-type"];
961
998
  initProxyFromEnv();
962
999
  await ensurePaths();
1000
+ await initCopilotIdentity();
963
1001
  if (args["github-token"]) {
964
1002
  state.githubToken = args["github-token"];
965
1003
  consola.info("Using provided GitHub token");
@@ -992,6 +1030,7 @@ async function runLogin(options) {
992
1030
  state.showToken = options.showToken;
993
1031
  initProxyFromEnv();
994
1032
  await ensurePaths();
1033
+ await initCopilotIdentity();
995
1034
  await setupGitHubToken({ force: true });
996
1035
  consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
997
1036
  }
@@ -1047,7 +1086,7 @@ const logout = defineCommand({
1047
1086
 
1048
1087
  //#endregion
1049
1088
  //#region package.json
1050
- var version = "0.13.1";
1089
+ var version = "0.15.0";
1051
1090
 
1052
1091
  //#endregion
1053
1092
  //#region src/lib/event-loop-lag.ts
@@ -3860,8 +3899,7 @@ const createChatCompletions = async (payload, options) => {
3860
3899
  const headers = {
3861
3900
  ...copilotHeaders(state, {
3862
3901
  vision: enableVision && modelSupportsVision,
3863
- modelRequestHeaders: options?.resolvedModel?.request_headers,
3864
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3902
+ modelRequestHeaders: options?.resolvedModel?.request_headers
3865
3903
  }),
3866
3904
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3867
3905
  };
@@ -6777,7 +6815,6 @@ async function createAnthropicMessages(payload, options) {
6777
6815
  const headers = {
6778
6816
  ...copilotHeaders(state, {
6779
6817
  vision: enableVision,
6780
- intent: isAgentCall ? "conversation-agent" : "conversation-panel",
6781
6818
  modelRequestHeaders: resolvedModel?.request_headers
6782
6819
  }),
6783
6820
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
@@ -6907,6 +6944,74 @@ function supportsDirectAnthropicApi(modelId) {
6907
6944
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
6908
6945
  }
6909
6946
 
6947
+ //#endregion
6948
+ //#region src/routes/messages/cache-control-injector.ts
6949
+ function blockHasCacheControl(block) {
6950
+ if (typeof block !== "object" || block === null) return false;
6951
+ if (!("cache_control" in block)) return false;
6952
+ const cc = block.cache_control;
6953
+ return cc !== void 0 && cc !== null;
6954
+ }
6955
+ function toolsHaveCacheControl(tools) {
6956
+ if (!Array.isArray(tools)) return false;
6957
+ return tools.some((t) => blockHasCacheControl(t));
6958
+ }
6959
+ function toolResultInnerHasCacheControl(block) {
6960
+ if (!Array.isArray(block.content)) return false;
6961
+ return block.content.some((inner) => blockHasCacheControl(inner));
6962
+ }
6963
+ function messagesHaveCacheControl(messages) {
6964
+ if (!Array.isArray(messages)) return false;
6965
+ for (const message of messages) {
6966
+ if (!Array.isArray(message.content)) continue;
6967
+ for (const block of message.content) {
6968
+ if (blockHasCacheControl(block)) return true;
6969
+ if (block.type === "tool_result" && toolResultInnerHasCacheControl(block)) return true;
6970
+ }
6971
+ }
6972
+ return false;
6973
+ }
6974
+ function systemHasCacheControl(system) {
6975
+ if (!Array.isArray(system)) return false;
6976
+ return system.some((b) => blockHasCacheControl(b));
6977
+ }
6978
+ function hasAnyCacheControl(payload) {
6979
+ return systemHasCacheControl(payload.system) || toolsHaveCacheControl(payload.tools) || messagesHaveCacheControl(payload.messages);
6980
+ }
6981
+ function injectSystemCacheControl(payload) {
6982
+ if (hasAnyCacheControl(payload)) return;
6983
+ if (payload.system === void 0 || payload.system === null) return;
6984
+ if (typeof payload.system === "string") {
6985
+ if (payload.system.length === 0) return;
6986
+ payload.system = [{
6987
+ type: "text",
6988
+ text: payload.system,
6989
+ cache_control: { type: "ephemeral" }
6990
+ }];
6991
+ return;
6992
+ }
6993
+ if (payload.system.length === 0) return;
6994
+ const tail = payload.system.at(-1);
6995
+ if (!tail) return;
6996
+ tail.cache_control = { type: "ephemeral" };
6997
+ }
6998
+
6999
+ //#endregion
7000
+ //#region src/routes/messages/date-normalizer.ts
7001
+ const dateTrackingRegex = /(# currentDate\r?\n)Today['’ʼʹ]s date is (\d{4})[/-](\d{2})[/-](\d{2})\.(\r?\n|$)/g;
7002
+ function normalizeClaudeCodeDate(text) {
7003
+ return text.replaceAll(dateTrackingRegex, "$1Today's date is $2-$3-$4.$5");
7004
+ }
7005
+ function normalizeSystemPromptDate(payload) {
7006
+ if (typeof payload.system === "string") {
7007
+ payload.system = normalizeClaudeCodeDate(payload.system);
7008
+ return;
7009
+ }
7010
+ if (Array.isArray(payload.system)) {
7011
+ for (const block of payload.system) if (typeof block.text === "string") block.text = normalizeClaudeCodeDate(block.text);
7012
+ }
7013
+ }
7014
+
6910
7015
  //#endregion
6911
7016
  //#region src/lib/stream-keepalive.ts
6912
7017
  /** SSE comment line used as a keepalive heartbeat. */
@@ -8768,7 +8873,11 @@ async function handleCompletion(c) {
8768
8873
  const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8769
8874
  const initiatorOverride = subagentMarker ? "agent" : void 0;
8770
8875
  if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8771
- if (supportsDirectAnthropicApi(sanitizedPayload.model)) return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8876
+ if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
8877
+ normalizeSystemPromptDate(sanitizedPayload);
8878
+ injectSystemCacheControl(sanitizedPayload);
8879
+ return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8880
+ }
8772
8881
  return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8773
8882
  }
8774
8883
  /**
@@ -8901,6 +9010,31 @@ modelRoutes.get("/", async (c) => {
8901
9010
  }
8902
9011
  });
8903
9012
 
9013
+ //#endregion
9014
+ //#region src/lib/prompt-cache-key.ts
9015
+ const CLIENT_NAME_PATTERNS = [
9016
+ [/^claude[-_]?(?:code|cli)$/i, "claude-code"],
9017
+ [/^codex$/i, "codex"],
9018
+ [/^cursor$/i, "cursor"],
9019
+ [/^aider$/i, "aider"],
9020
+ [/^copilot$/i, "copilot"]
9021
+ ];
9022
+ const UA_MAIN_TOKEN = /^([\w.-]+)/;
9023
+ function extractClientName(userAgent) {
9024
+ if (!userAgent) return "unknown";
9025
+ const mainToken = UA_MAIN_TOKEN.exec(userAgent)?.[1]?.toLowerCase();
9026
+ if (!mainToken) return "unknown";
9027
+ for (const [pattern, name] of CLIENT_NAME_PATTERNS) if (pattern.test(mainToken)) return name;
9028
+ return mainToken.slice(0, 32);
9029
+ }
9030
+ function buildPromptCacheKey(clientName) {
9031
+ return `copilot-api:${clientName}`;
9032
+ }
9033
+ function injectPromptCacheKey(payload, clientName) {
9034
+ if (payload.prompt_cache_key !== void 0 && payload.prompt_cache_key !== null && payload.prompt_cache_key !== "") return;
9035
+ payload.prompt_cache_key = buildPromptCacheKey(clientName);
9036
+ }
9037
+
8904
9038
  //#endregion
8905
9039
  //#region src/services/copilot/create-responses.ts
8906
9040
  const createResponses = async (payload, { vision, initiator, resolvedModel }) => {
@@ -9158,14 +9292,17 @@ const TERMINAL_EVENTS = new Set([
9158
9292
  "error"
9159
9293
  ]);
9160
9294
  const handleResponses = async (c) => {
9295
+ const rawPayload = await c.req.json();
9296
+ const clientName = extractClientName(c.req.header("user-agent"));
9161
9297
  const { ctx, payload } = createEntryContext({
9162
9298
  c,
9163
- rawPayload: await c.req.json(),
9299
+ rawPayload,
9164
9300
  endpoint: "openai",
9165
9301
  normalizePayload: (p) => {
9166
9302
  const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
9167
9303
  useFunctionApplyPatch(np);
9168
9304
  filterUnsupportedBuiltins(np);
9305
+ injectPromptCacheKey(np, clientName);
9169
9306
  return np;
9170
9307
  },
9171
9308
  buildHistoryRequest: (p) => {
@@ -9467,7 +9604,7 @@ async function runServer(options) {
9467
9604
  initTui({ enabled: true });
9468
9605
  initRequestContextManager(state.staleRequestMaxAge).startReaper();
9469
9606
  await ensurePaths();
9470
- await cacheVSCodeVersion();
9607
+ await initCopilotIdentity();
9471
9608
  if (options.githubToken) {
9472
9609
  state.githubToken = options.githubToken;
9473
9610
  consola.info("Using provided GitHub token");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",