@dianshuv/copilot-api 0.13.1 → 0.14.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/README.md +1 -0
- package/dist/main.mjs +83 -46
- 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
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
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":
|
|
162
|
-
"editor-version":
|
|
163
|
-
"
|
|
164
|
-
"
|
|
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-
|
|
168
|
-
"x-
|
|
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
|
-
|
|
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 = "
|
|
186
|
-
const GITHUB_APP_SCOPES = [
|
|
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:
|
|
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-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
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(
|
|
693
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
668
694
|
signal: controller.signal,
|
|
669
695
|
headers: {
|
|
670
|
-
Accept: "application/
|
|
696
|
+
Accept: "application/json",
|
|
671
697
|
"User-Agent": "copilot-api"
|
|
672
698
|
}
|
|
673
699
|
});
|
|
674
|
-
if (!response.ok) return
|
|
675
|
-
const version = (await response.json()).
|
|
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
|
|
703
|
+
return CLI_VERSION_FALLBACK;
|
|
678
704
|
} catch {
|
|
679
|
-
return
|
|
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
|
|
698
|
-
const response = await
|
|
699
|
-
state.
|
|
700
|
-
consola.info(`Using
|
|
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:
|
|
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.
|
|
1089
|
+
var version = "0.14.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"),
|
|
@@ -9467,7 +9504,7 @@ async function runServer(options) {
|
|
|
9467
9504
|
initTui({ enabled: true });
|
|
9468
9505
|
initRequestContextManager(state.staleRequestMaxAge).startReaper();
|
|
9469
9506
|
await ensurePaths();
|
|
9470
|
-
await
|
|
9507
|
+
await initCopilotIdentity();
|
|
9471
9508
|
if (options.githubToken) {
|
|
9472
9509
|
state.githubToken = options.githubToken;
|
|
9473
9510
|
consola.info("Using provided GitHub token");
|