copilot-api-node20 0.6.0 → 0.7.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/dist/main.js +383 -117
- package/dist/main.js.map +1 -1
- package/package.json +1 -2
package/dist/main.js
CHANGED
|
@@ -4,8 +4,7 @@ import consola from "consola";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
8
|
import clipboard from "clipboardy";
|
|
10
9
|
import process$1 from "node:process";
|
|
11
10
|
import { serve } from "srvx";
|
|
@@ -13,22 +12,26 @@ import invariant from "tiny-invariant";
|
|
|
13
12
|
import { execSync } from "node:child_process";
|
|
14
13
|
import { Hono } from "hono";
|
|
15
14
|
import { cors } from "hono/cors";
|
|
16
|
-
import { logger } from "hono/logger";
|
|
17
15
|
import { streamSSE } from "hono/streaming";
|
|
18
|
-
import { countTokens } from "gpt-tokenizer/model/gpt-4o";
|
|
19
16
|
import { events } from "fetch-event-stream";
|
|
20
17
|
import { request } from "undici";
|
|
21
18
|
|
|
22
19
|
//#region src/lib/paths.ts
|
|
23
20
|
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
|
|
24
21
|
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
22
|
+
const MACHINE_ID_PATH = path.join(APP_DIR, "machine_id");
|
|
23
|
+
const SESSION_ID_PATH = path.join(APP_DIR, "session_id");
|
|
25
24
|
const PATHS = {
|
|
26
25
|
APP_DIR,
|
|
27
|
-
GITHUB_TOKEN_PATH
|
|
26
|
+
GITHUB_TOKEN_PATH,
|
|
27
|
+
MACHINE_ID_PATH,
|
|
28
|
+
SESSION_ID_PATH
|
|
28
29
|
};
|
|
29
30
|
async function ensurePaths() {
|
|
30
31
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
31
32
|
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
33
|
+
await ensureFile(PATHS.MACHINE_ID_PATH);
|
|
34
|
+
await ensureFile(PATHS.SESSION_ID_PATH);
|
|
32
35
|
}
|
|
33
36
|
async function ensureFile(filePath) {
|
|
34
37
|
try {
|
|
@@ -64,7 +67,7 @@ const state = {
|
|
|
64
67
|
|
|
65
68
|
//#endregion
|
|
66
69
|
//#region src/lib/connectivity.ts
|
|
67
|
-
var ConnectivityMonitor = class extends
|
|
70
|
+
var ConnectivityMonitor = class extends EventTarget {
|
|
68
71
|
isOnline = true;
|
|
69
72
|
lastChecked = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
73
|
consecutiveFailures = 0;
|
|
@@ -74,6 +77,14 @@ var ConnectivityMonitor = class extends EventEmitter {
|
|
|
74
77
|
endpointFailureStats = {};
|
|
75
78
|
checkInterval;
|
|
76
79
|
abortController;
|
|
80
|
+
on(event, listener) {
|
|
81
|
+
this.addEventListener(event, listener);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
off(event, listener) {
|
|
85
|
+
this.removeEventListener(event, listener);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
77
88
|
start() {
|
|
78
89
|
if (!state.connectivity.enabled) {
|
|
79
90
|
consola.debug("Connectivity monitoring disabled");
|
|
@@ -108,7 +119,9 @@ var ConnectivityMonitor = class extends EventEmitter {
|
|
|
108
119
|
const jitter = Math.random() * state.connectivity.jitterMaxMs;
|
|
109
120
|
const interval = baseInterval + jitter;
|
|
110
121
|
this.checkInterval = setTimeout(() => {
|
|
111
|
-
this.performConnectivityCheck()
|
|
122
|
+
this.performConnectivityCheck().catch((error) => {
|
|
123
|
+
consola.error("Connectivity check failed:", error);
|
|
124
|
+
});
|
|
112
125
|
}, interval);
|
|
113
126
|
}
|
|
114
127
|
async performConnectivityCheck() {
|
|
@@ -151,12 +164,12 @@ var ConnectivityMonitor = class extends EventEmitter {
|
|
|
151
164
|
this.consecutiveFailures = 0;
|
|
152
165
|
this.lastErrorType = void 0;
|
|
153
166
|
this.lastErrorMessage = void 0;
|
|
154
|
-
if (!wasOnline) this.
|
|
167
|
+
if (!wasOnline) this.dispatchEvent(new CustomEvent("online"));
|
|
155
168
|
} else {
|
|
156
169
|
this.consecutiveFailures++;
|
|
157
170
|
if (wasOnline) {
|
|
158
171
|
consola.warn("Connectivity lost");
|
|
159
|
-
this.
|
|
172
|
+
this.dispatchEvent(new CustomEvent("offline"));
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
}
|
|
@@ -197,24 +210,68 @@ const standardHeaders = () => ({
|
|
|
197
210
|
"content-type": "application/json",
|
|
198
211
|
accept: "application/json"
|
|
199
212
|
});
|
|
200
|
-
const COPILOT_VERSION = "0.32.
|
|
213
|
+
const COPILOT_VERSION = "0.32.2025100203";
|
|
201
214
|
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
202
215
|
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
203
216
|
const API_VERSION = "2025-08-20";
|
|
217
|
+
const generateMachineId = () => {
|
|
218
|
+
const hash = createHash("sha256");
|
|
219
|
+
hash.update(process.platform);
|
|
220
|
+
hash.update(process.arch);
|
|
221
|
+
hash.update(process.env.USER || process.env.USERNAME || "anonymous");
|
|
222
|
+
hash.update(os.hostname());
|
|
223
|
+
hash.update(Date.now().toString());
|
|
224
|
+
hash.update(randomBytes(16));
|
|
225
|
+
return hash.digest("hex");
|
|
226
|
+
};
|
|
227
|
+
const readMachineId = async () => {
|
|
228
|
+
try {
|
|
229
|
+
const machineId = await fs.readFile(PATHS.MACHINE_ID_PATH, "utf8");
|
|
230
|
+
if (machineId.trim()) return machineId.trim();
|
|
231
|
+
} catch {}
|
|
232
|
+
const newMachineId = generateMachineId();
|
|
233
|
+
await fs.writeFile(PATHS.MACHINE_ID_PATH, newMachineId);
|
|
234
|
+
await fs.chmod(PATHS.MACHINE_ID_PATH, 384);
|
|
235
|
+
return newMachineId;
|
|
236
|
+
};
|
|
237
|
+
const readSessionId = async () => {
|
|
238
|
+
try {
|
|
239
|
+
const sessionId = await fs.readFile(PATHS.SESSION_ID_PATH, "utf8");
|
|
240
|
+
if (sessionId.trim()) return sessionId.trim();
|
|
241
|
+
} catch {}
|
|
242
|
+
const newSessionId = randomUUID();
|
|
243
|
+
await fs.writeFile(PATHS.SESSION_ID_PATH, newSessionId);
|
|
244
|
+
await fs.chmod(PATHS.SESSION_ID_PATH, 384);
|
|
245
|
+
return newSessionId;
|
|
246
|
+
};
|
|
247
|
+
const initializeVSCodeIdentifiers = async (state$1) => {
|
|
248
|
+
if (!state$1.machineId) state$1.machineId = await readMachineId();
|
|
249
|
+
if (!state$1.sessionId) state$1.sessionId = await readSessionId();
|
|
250
|
+
};
|
|
204
251
|
const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
|
|
205
252
|
const copilotHeaders = (state$1, vision = false) => {
|
|
253
|
+
if (!state$1.machineId || !state$1.sessionId) throw new Error("VSCode identifiers not initialized. Call initializeVSCodeIdentifiers() during startup.");
|
|
206
254
|
const headers = {
|
|
207
255
|
Authorization: `Bearer ${state$1.copilotToken}`,
|
|
208
|
-
"
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
"
|
|
256
|
+
"Content-Type": "application/json",
|
|
257
|
+
accept: "*/*",
|
|
258
|
+
"accept-encoding": "br, gzip, deflate",
|
|
259
|
+
"accept-language": "*",
|
|
260
|
+
"sec-fetch-mode": "cors",
|
|
261
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
262
|
+
"Editor-Version": `vscode/${state$1.vsCodeVersion}`,
|
|
263
|
+
"Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
|
|
264
|
+
"User-Agent": USER_AGENT,
|
|
265
|
+
"VScode-MachineId": state$1.machineId,
|
|
266
|
+
"VScode-SessionId": state$1.sessionId,
|
|
267
|
+
"OpenAI-Intent": "conversation-agent",
|
|
268
|
+
"X-Interaction-Type": "conversation-agent",
|
|
269
|
+
"X-VSCode-User-Agent-Library-Version": "node-fetch",
|
|
270
|
+
"X-GitHub-Api-Version": API_VERSION,
|
|
271
|
+
"X-Interaction-Id": randomUUID(),
|
|
272
|
+
"X-Request-Id": randomUUID(),
|
|
273
|
+
"openai-intent": "conversation-agent",
|
|
274
|
+
"x-interaction-type": "conversation-agent"
|
|
218
275
|
};
|
|
219
276
|
if (vision) headers["copilot-vision-request"] = "true";
|
|
220
277
|
return headers;
|
|
@@ -227,11 +284,119 @@ const githubHeaders = (state$1) => ({
|
|
|
227
284
|
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
228
285
|
"user-agent": USER_AGENT,
|
|
229
286
|
"x-github-api-version": API_VERSION,
|
|
230
|
-
"x-vscode-user-agent-library-version": "
|
|
287
|
+
"x-vscode-user-agent-library-version": "node-fetch"
|
|
231
288
|
});
|
|
232
289
|
const GITHUB_BASE_URL = "https://github.com";
|
|
233
|
-
const GITHUB_CLIENT_ID = "
|
|
234
|
-
const GITHUB_APP_SCOPES = ["
|
|
290
|
+
const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23";
|
|
291
|
+
const GITHUB_APP_SCOPES = ["user:email"].join(" ");
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/lib/logger/completion-logger.ts
|
|
295
|
+
const humanize = (num) => {
|
|
296
|
+
if (num >= 1e5) return `${Math.round(num / 1e3)}K`;
|
|
297
|
+
if (num >= 1e4) return `${Math.round(num / 1e3)}K`;
|
|
298
|
+
if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
|
|
299
|
+
return num.toString();
|
|
300
|
+
};
|
|
301
|
+
const pad = (str, length) => str.padEnd(length);
|
|
302
|
+
const getContextPercentage = (contextWindow, model) => {
|
|
303
|
+
if (!state.models) return "";
|
|
304
|
+
const selectedModel = state.models.data.find((m) => m.id === model);
|
|
305
|
+
if (!selectedModel) return "";
|
|
306
|
+
const maxContextTokens = selectedModel.capabilities.limits.max_context_window_tokens;
|
|
307
|
+
if (!maxContextTokens) return "";
|
|
308
|
+
const percentage = (contextWindow / maxContextTokens * 100).toFixed(1);
|
|
309
|
+
return ` (${percentage}%)`;
|
|
310
|
+
};
|
|
311
|
+
const formatTokenUsage = (requestData) => {
|
|
312
|
+
const parts = [];
|
|
313
|
+
if (requestData.model) {
|
|
314
|
+
const model = pad(requestData.model, 18);
|
|
315
|
+
parts.push(model);
|
|
316
|
+
}
|
|
317
|
+
if (requestData.tokenUsage) {
|
|
318
|
+
const usage = requestData.tokenUsage;
|
|
319
|
+
const contextWindow = (usage.inputTokens || 0) + (usage.outputTokens || 0);
|
|
320
|
+
const contextPercentage = requestData.model ? getContextPercentage(contextWindow, requestData.model) : "";
|
|
321
|
+
const input = humanize(usage.inputTokens || 0).padStart(5);
|
|
322
|
+
const output = humanize(usage.outputTokens || 0).padStart(5);
|
|
323
|
+
const tokenPart = `↑${input} │ ↓${output}`;
|
|
324
|
+
const tokens = pad(tokenPart, 18);
|
|
325
|
+
const contextNum = contextWindow.toString();
|
|
326
|
+
const contextFormatted = contextPercentage ? `${contextNum}${contextPercentage.padStart(15 - contextNum.length)}` : contextNum.padEnd(15);
|
|
327
|
+
parts.push(`Tokens: ${tokens} | Context: ${contextFormatted}`);
|
|
328
|
+
} else if (requestData.model) {
|
|
329
|
+
const tokens = pad("N/A", 18);
|
|
330
|
+
const context = "N/A".padEnd(15);
|
|
331
|
+
parts.push(`Tokens: ${tokens} | Context: ${context}`);
|
|
332
|
+
}
|
|
333
|
+
if (requestData.copilotDuration) {
|
|
334
|
+
const apiDuration = pad(`${Math.round(requestData.copilotDuration)}ms`, 8);
|
|
335
|
+
parts.push(`API: ${apiDuration}`);
|
|
336
|
+
}
|
|
337
|
+
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
|
338
|
+
};
|
|
339
|
+
const CompletionLogger = {
|
|
340
|
+
completionCallbacks: /* @__PURE__ */ new Map(),
|
|
341
|
+
registerCompletion(requestId, context, startTime) {
|
|
342
|
+
this.completionCallbacks.set(requestId, {
|
|
343
|
+
context,
|
|
344
|
+
startTime,
|
|
345
|
+
requestId
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
executeCompletion(requestId) {
|
|
349
|
+
const data = this.completionCallbacks.get(requestId);
|
|
350
|
+
if (data) {
|
|
351
|
+
this.logCompletion(data);
|
|
352
|
+
this.completionCallbacks.delete(requestId);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
logCompletion(data) {
|
|
356
|
+
const { context: c, startTime } = data;
|
|
357
|
+
const end = Date.now();
|
|
358
|
+
const duration = end - startTime;
|
|
359
|
+
const requestData = c.get("requestData");
|
|
360
|
+
const method = pad(c.req.method, 4);
|
|
361
|
+
const path$1 = pad(c.req.path, 18);
|
|
362
|
+
const status = pad(c.res.status.toString(), 3);
|
|
363
|
+
const durationStr = pad(`${duration}ms`, 8);
|
|
364
|
+
let logLine = ` --> ${method}${path$1}${status} ${durationStr}`;
|
|
365
|
+
if (requestData) logLine += formatTokenUsage(requestData);
|
|
366
|
+
consola.info(logLine);
|
|
367
|
+
},
|
|
368
|
+
logRateLimit(data, response) {
|
|
369
|
+
const { context: c, startTime } = data;
|
|
370
|
+
const end = Date.now();
|
|
371
|
+
const duration = end - startTime;
|
|
372
|
+
const rateLimitExceeded = response.headers.get("x-ratelimit-exceeded") || "";
|
|
373
|
+
const rateLimitType = rateLimitExceeded.split(":")[1] || "unknown";
|
|
374
|
+
const retry = response.headers.get("retry-after") || response.headers.get("x-ratelimit-user-retry-after") || "?";
|
|
375
|
+
const method = pad(c.req.method, 4);
|
|
376
|
+
const path$1 = pad(c.req.path, 18);
|
|
377
|
+
const status = pad("429", 3);
|
|
378
|
+
const durationStr = pad(`${duration}ms`, 8);
|
|
379
|
+
let logLine = `⚠ --> ${method}${path$1}${status} ${durationStr}`;
|
|
380
|
+
logLine += ` | Rate limited (${retry}s retry) | ${rateLimitType}`;
|
|
381
|
+
const requestData = c.get("requestData");
|
|
382
|
+
if (requestData?.copilotDuration) {
|
|
383
|
+
const apiDuration = pad(`${Math.round(requestData.copilotDuration)}ms`, 8);
|
|
384
|
+
logLine += ` | API: ${apiDuration}`;
|
|
385
|
+
}
|
|
386
|
+
consola.info(logLine);
|
|
387
|
+
},
|
|
388
|
+
cleanup() {
|
|
389
|
+
const MAX_CALLBACKS = 1e3;
|
|
390
|
+
const fiveMinutesAgo = Date.now() - 300 * 1e3;
|
|
391
|
+
for (const [requestId, data] of this.completionCallbacks) if (data.startTime < fiveMinutesAgo) this.completionCallbacks.delete(requestId);
|
|
392
|
+
if (this.completionCallbacks.size > MAX_CALLBACKS) {
|
|
393
|
+
const entries = Array.from(this.completionCallbacks.entries()).sort(([, a], [, b]) => a.startTime - b.startTime);
|
|
394
|
+
const toRemove = entries.slice(0, entries.length - MAX_CALLBACKS);
|
|
395
|
+
for (const [requestId] of toRemove) this.completionCallbacks.delete(requestId);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
setInterval(() => CompletionLogger.cleanup(), 60 * 1e3);
|
|
235
400
|
|
|
236
401
|
//#endregion
|
|
237
402
|
//#region src/lib/error.ts
|
|
@@ -243,6 +408,25 @@ var HTTPError = class extends Error {
|
|
|
243
408
|
}
|
|
244
409
|
};
|
|
245
410
|
async function forwardError(c, error) {
|
|
411
|
+
if (error instanceof HTTPError && error.response.status === 429) {
|
|
412
|
+
const requestId = c.get("requestId");
|
|
413
|
+
const completionData = CompletionLogger.completionCallbacks.get(requestId);
|
|
414
|
+
if (completionData) {
|
|
415
|
+
CompletionLogger.logRateLimit(completionData, error.response);
|
|
416
|
+
CompletionLogger.completionCallbacks.delete(requestId);
|
|
417
|
+
}
|
|
418
|
+
const errorText = await error.response.text();
|
|
419
|
+
let errorJson;
|
|
420
|
+
try {
|
|
421
|
+
errorJson = JSON.parse(errorText);
|
|
422
|
+
} catch {
|
|
423
|
+
errorJson = { error: {
|
|
424
|
+
message: errorText,
|
|
425
|
+
type: "error"
|
|
426
|
+
} };
|
|
427
|
+
}
|
|
428
|
+
return c.json(errorJson, 429);
|
|
429
|
+
}
|
|
246
430
|
consola.error("Error occurred:", error);
|
|
247
431
|
if (error instanceof HTTPError) {
|
|
248
432
|
const errorText = await error.response.text();
|
|
@@ -369,8 +553,8 @@ async function cacheModels() {
|
|
|
369
553
|
const models = await getModels();
|
|
370
554
|
state.models = models;
|
|
371
555
|
}
|
|
372
|
-
const cacheVSCodeVersion =
|
|
373
|
-
const response =
|
|
556
|
+
const cacheVSCodeVersion = () => {
|
|
557
|
+
const response = getVSCodeVersion();
|
|
374
558
|
state.vsCodeVersion = response;
|
|
375
559
|
consola.info(`Using VSCode version: ${response}`);
|
|
376
560
|
};
|
|
@@ -627,7 +811,8 @@ const checkUsage = defineCommand({
|
|
|
627
811
|
async function getPackageVersion() {
|
|
628
812
|
try {
|
|
629
813
|
const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
|
|
630
|
-
const
|
|
814
|
+
const packageJsonBuffer = await fs.readFile(packageJsonPath);
|
|
815
|
+
const packageJson = JSON.parse(packageJsonBuffer.toString());
|
|
631
816
|
return packageJson.version;
|
|
632
817
|
} catch {
|
|
633
818
|
return "unknown";
|
|
@@ -756,6 +941,24 @@ function generateEnvScript(envVars, commandToRun = "") {
|
|
|
756
941
|
return commandBlock || commandToRun;
|
|
757
942
|
}
|
|
758
943
|
|
|
944
|
+
//#endregion
|
|
945
|
+
//#region src/lib/logger/enhanced-hono-logger.ts
|
|
946
|
+
function generateRequestId() {
|
|
947
|
+
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
948
|
+
}
|
|
949
|
+
function enhancedLogger() {
|
|
950
|
+
return async (c, next) => {
|
|
951
|
+
const start$1 = Date.now();
|
|
952
|
+
const requestId = generateRequestId();
|
|
953
|
+
c.set("requestId", requestId);
|
|
954
|
+
c.header("x-request-id", requestId);
|
|
955
|
+
CompletionLogger.registerCompletion(requestId, c, start$1);
|
|
956
|
+
await next();
|
|
957
|
+
const requestData = c.get("requestData");
|
|
958
|
+
if (requestData?.tokenUsage) CompletionLogger.executeCompletion(requestId);
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
759
962
|
//#endregion
|
|
760
963
|
//#region src/lib/approval.ts
|
|
761
964
|
const awaitApproval = async () => {
|
|
@@ -763,6 +966,28 @@ const awaitApproval = async () => {
|
|
|
763
966
|
if (!response) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
|
|
764
967
|
};
|
|
765
968
|
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region src/lib/logger/token-tracker.ts
|
|
971
|
+
const TOKEN_PRICING = {
|
|
972
|
+
input: 3e-6,
|
|
973
|
+
output: 15e-6
|
|
974
|
+
};
|
|
975
|
+
function parseGitHubCopilotUsage(usageData) {
|
|
976
|
+
const inputTokens = usageData.prompt_tokens || 0;
|
|
977
|
+
const outputTokens = usageData.completion_tokens || 0;
|
|
978
|
+
const totalTokens = usageData.total_tokens || inputTokens + outputTokens;
|
|
979
|
+
const estimatedCost = inputTokens * TOKEN_PRICING.input + outputTokens * TOKEN_PRICING.output;
|
|
980
|
+
return {
|
|
981
|
+
inputTokens,
|
|
982
|
+
outputTokens,
|
|
983
|
+
totalTokens,
|
|
984
|
+
estimatedCost,
|
|
985
|
+
cachedTokens: usageData.prompt_tokens_details?.cached_tokens,
|
|
986
|
+
acceptedPredictionTokens: usageData.completion_tokens_details?.accepted_prediction_tokens,
|
|
987
|
+
rejectedPredictionTokens: usageData.completion_tokens_details?.rejected_prediction_tokens
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
766
991
|
//#endregion
|
|
767
992
|
//#region src/lib/rate-limit.ts
|
|
768
993
|
async function checkRateLimit(state$1) {
|
|
@@ -789,35 +1014,6 @@ async function checkRateLimit(state$1) {
|
|
|
789
1014
|
consola.info("Rate limit wait completed, proceeding with request");
|
|
790
1015
|
}
|
|
791
1016
|
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region src/lib/tokenizer.ts
|
|
794
|
-
const getTokenCount = (messages) => {
|
|
795
|
-
const simplifiedMessages = messages.map((message) => {
|
|
796
|
-
let content = "";
|
|
797
|
-
if (typeof message.content === "string") content = message.content;
|
|
798
|
-
else if (Array.isArray(message.content)) content = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
799
|
-
return {
|
|
800
|
-
...message,
|
|
801
|
-
content
|
|
802
|
-
};
|
|
803
|
-
});
|
|
804
|
-
let inputMessages = simplifiedMessages.filter((message) => {
|
|
805
|
-
return message.role !== "tool";
|
|
806
|
-
});
|
|
807
|
-
let outputMessages = [];
|
|
808
|
-
const lastMessage = simplifiedMessages.at(-1);
|
|
809
|
-
if (lastMessage?.role === "assistant") {
|
|
810
|
-
inputMessages = simplifiedMessages.slice(0, -1);
|
|
811
|
-
outputMessages = [lastMessage];
|
|
812
|
-
}
|
|
813
|
-
const inputTokens = countTokens(inputMessages);
|
|
814
|
-
const outputTokens = countTokens(outputMessages);
|
|
815
|
-
return {
|
|
816
|
-
input: inputTokens,
|
|
817
|
-
output: outputTokens
|
|
818
|
-
};
|
|
819
|
-
};
|
|
820
|
-
|
|
821
1017
|
//#endregion
|
|
822
1018
|
//#region src/lib/sanitize.ts
|
|
823
1019
|
/**
|
|
@@ -828,14 +1024,28 @@ const getTokenCount = (messages) => {
|
|
|
828
1024
|
* Removes ANSI escape sequences and problematic Unicode characters from a string
|
|
829
1025
|
*/
|
|
830
1026
|
function sanitizeString(str) {
|
|
831
|
-
|
|
1027
|
+
let result = str;
|
|
1028
|
+
const escChar = String.fromCodePoint(27);
|
|
1029
|
+
const parts = result.split(escChar);
|
|
1030
|
+
if (parts.length > 1) {
|
|
1031
|
+
result = parts[0];
|
|
1032
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1033
|
+
const part = parts[i];
|
|
1034
|
+
const match = part.match(/^\[[0-9;]*[a-z]/i);
|
|
1035
|
+
result += match ? part.slice(match[0].length) : escChar + part;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return result.replaceAll(/[\u200B-\u200D\uFEFF]/g, "").replaceAll(/[\u2060-\u2064]/g, "").replaceAll(/[\u206A-\u206F]/g, "").replaceAll(/[\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/g, "").replaceAll(/[\uFFF0-\uFFFF]/g, "").replaceAll(/[\x00-\x08\v\f\x0E-\x1F]/g, "");
|
|
832
1039
|
}
|
|
833
1040
|
/**
|
|
834
1041
|
* Recursively sanitizes all string values in an object or array
|
|
835
1042
|
*/
|
|
836
1043
|
function sanitizePayload(payload) {
|
|
837
1044
|
if (typeof payload === "string") return sanitizeString(payload);
|
|
838
|
-
if (Array.isArray(payload))
|
|
1045
|
+
if (Array.isArray(payload)) {
|
|
1046
|
+
const sanitizedArray = payload.map((item) => sanitizePayload(item));
|
|
1047
|
+
return sanitizedArray;
|
|
1048
|
+
}
|
|
839
1049
|
if (payload && typeof payload === "object") {
|
|
840
1050
|
const sanitized = {};
|
|
841
1051
|
for (const [key, value] of Object.entries(payload)) sanitized[key] = sanitizePayload(value);
|
|
@@ -867,17 +1077,13 @@ const createChatCompletions = async (payload) => {
|
|
|
867
1077
|
body: JSON.stringify(sanitizedPayload),
|
|
868
1078
|
signal: controller.signal,
|
|
869
1079
|
headersTimeout: timeoutMs,
|
|
870
|
-
bodyTimeout: timeoutMs * 3
|
|
871
|
-
connectTimeout: timeoutMs
|
|
1080
|
+
bodyTimeout: timeoutMs * 3
|
|
872
1081
|
});
|
|
873
1082
|
const response = new Response(body, {
|
|
874
1083
|
status: statusCode,
|
|
875
1084
|
headers: responseHeaders
|
|
876
1085
|
});
|
|
877
|
-
if (!response.ok)
|
|
878
|
-
consola.error("Failed to create chat completions", response);
|
|
879
|
-
throw new HTTPError("Failed to create chat completions", response);
|
|
880
|
-
}
|
|
1086
|
+
if (!response.ok) throw new HTTPError("Failed to create chat completions", response);
|
|
881
1087
|
if (sanitizedPayload.stream) return events(response);
|
|
882
1088
|
return await response.json();
|
|
883
1089
|
} finally {
|
|
@@ -891,7 +1097,7 @@ async function handleCompletion$1(c) {
|
|
|
891
1097
|
await checkRateLimit(state);
|
|
892
1098
|
let payload = await c.req.json();
|
|
893
1099
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
894
|
-
|
|
1100
|
+
c.set("requestData", { model: payload.model });
|
|
895
1101
|
if (state.manualApprove) await awaitApproval();
|
|
896
1102
|
if (isNullish(payload.max_tokens)) {
|
|
897
1103
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
@@ -901,17 +1107,42 @@ async function handleCompletion$1(c) {
|
|
|
901
1107
|
};
|
|
902
1108
|
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
903
1109
|
}
|
|
1110
|
+
const copilotStart = performance.now();
|
|
904
1111
|
const response = await createChatCompletions(payload);
|
|
1112
|
+
const copilotDuration = performance.now() - copilotStart;
|
|
905
1113
|
if (isNonStreaming$1(response)) {
|
|
1114
|
+
const requestData = c.get("requestData") || {};
|
|
1115
|
+
if (response.usage) requestData.tokenUsage = parseGitHubCopilotUsage(response.usage);
|
|
1116
|
+
requestData.copilotDuration = copilotDuration;
|
|
1117
|
+
c.set("requestData", requestData);
|
|
906
1118
|
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
907
1119
|
return c.json(response);
|
|
908
1120
|
}
|
|
909
|
-
consola.debug("Streaming response");
|
|
910
1121
|
return streamSSE(c, async (stream) => {
|
|
1122
|
+
let finalUsage = null;
|
|
911
1123
|
for await (const chunk of response) {
|
|
912
|
-
|
|
1124
|
+
if (chunk.data && chunk.data !== "[DONE]") try {
|
|
1125
|
+
const parsed = JSON.parse(chunk.data);
|
|
1126
|
+
if (parsed.usage) {
|
|
1127
|
+
finalUsage = parsed.usage;
|
|
1128
|
+
const requestData = c.get("requestData") || {};
|
|
1129
|
+
requestData.tokenUsage = parseGitHubCopilotUsage(finalUsage);
|
|
1130
|
+
requestData.copilotDuration = copilotDuration;
|
|
1131
|
+
c.set("requestData", requestData);
|
|
1132
|
+
}
|
|
1133
|
+
} catch {}
|
|
913
1134
|
await stream.writeSSE(chunk);
|
|
914
1135
|
}
|
|
1136
|
+
if (finalUsage) {
|
|
1137
|
+
const requestData = c.get("requestData") || {};
|
|
1138
|
+
if (!requestData.tokenUsage) {
|
|
1139
|
+
requestData.tokenUsage = parseGitHubCopilotUsage(finalUsage);
|
|
1140
|
+
requestData.copilotDuration = copilotDuration;
|
|
1141
|
+
c.set("requestData", requestData);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const requestId = c.get("requestId");
|
|
1145
|
+
if (requestId) CompletionLogger.executeCompletion(requestId);
|
|
915
1146
|
});
|
|
916
1147
|
}
|
|
917
1148
|
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
@@ -955,8 +1186,15 @@ const createEmbeddings = async (payload) => {
|
|
|
955
1186
|
const embeddingRoutes = new Hono();
|
|
956
1187
|
embeddingRoutes.post("/", async (c) => {
|
|
957
1188
|
try {
|
|
958
|
-
const
|
|
959
|
-
|
|
1189
|
+
const payload = await c.req.json();
|
|
1190
|
+
c.set("requestData", { model: payload.model });
|
|
1191
|
+
const copilotStart = performance.now();
|
|
1192
|
+
const response = await createEmbeddings(payload);
|
|
1193
|
+
const copilotDuration = performance.now() - copilotStart;
|
|
1194
|
+
const requestData = c.get("requestData") || {};
|
|
1195
|
+
requestData.tokenUsage = parseGitHubCopilotUsage(response.usage);
|
|
1196
|
+
requestData.copilotDuration = copilotDuration;
|
|
1197
|
+
c.set("requestData", requestData);
|
|
960
1198
|
return c.json(response);
|
|
961
1199
|
} catch (error) {
|
|
962
1200
|
return await forwardError(c, error);
|
|
@@ -1293,9 +1531,16 @@ async function handleCompletion(c) {
|
|
|
1293
1531
|
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
|
|
1294
1532
|
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1295
1533
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
|
|
1534
|
+
c.set("requestData", { model: openAIPayload.model });
|
|
1535
|
+
const copilotStart = performance.now();
|
|
1296
1536
|
if (state.manualApprove) await awaitApproval();
|
|
1297
1537
|
const response = await createChatCompletions(openAIPayload);
|
|
1538
|
+
const copilotDuration = performance.now() - copilotStart;
|
|
1298
1539
|
if (isNonStreaming(response)) {
|
|
1540
|
+
const requestData = c.get("requestData") || {};
|
|
1541
|
+
if (response.usage) requestData.tokenUsage = parseGitHubCopilotUsage(response.usage);
|
|
1542
|
+
requestData.copilotDuration = copilotDuration;
|
|
1543
|
+
c.set("requestData", requestData);
|
|
1299
1544
|
consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
1300
1545
|
const anthropicResponse = translateToAnthropic(response);
|
|
1301
1546
|
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
@@ -1303,6 +1548,7 @@ async function handleCompletion(c) {
|
|
|
1303
1548
|
}
|
|
1304
1549
|
consola.debug("Streaming response from Copilot");
|
|
1305
1550
|
return streamSSE(c, async (stream) => {
|
|
1551
|
+
let finalUsage = null;
|
|
1306
1552
|
const streamState = {
|
|
1307
1553
|
messageStartSent: false,
|
|
1308
1554
|
contentBlockIndex: 0,
|
|
@@ -1314,6 +1560,13 @@ async function handleCompletion(c) {
|
|
|
1314
1560
|
if (rawEvent.data === "[DONE]") break;
|
|
1315
1561
|
if (!rawEvent.data) continue;
|
|
1316
1562
|
const chunk = JSON.parse(rawEvent.data);
|
|
1563
|
+
if (chunk.usage) {
|
|
1564
|
+
finalUsage = chunk.usage;
|
|
1565
|
+
const requestData = c.get("requestData") || {};
|
|
1566
|
+
requestData.tokenUsage = parseGitHubCopilotUsage(finalUsage);
|
|
1567
|
+
requestData.copilotDuration = copilotDuration;
|
|
1568
|
+
c.set("requestData", requestData);
|
|
1569
|
+
}
|
|
1317
1570
|
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
1318
1571
|
for (const event of events$1) {
|
|
1319
1572
|
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
@@ -1323,6 +1576,16 @@ async function handleCompletion(c) {
|
|
|
1323
1576
|
});
|
|
1324
1577
|
}
|
|
1325
1578
|
}
|
|
1579
|
+
if (finalUsage) {
|
|
1580
|
+
const requestData = c.get("requestData") || {};
|
|
1581
|
+
if (!requestData.tokenUsage) {
|
|
1582
|
+
requestData.tokenUsage = parseGitHubCopilotUsage(finalUsage);
|
|
1583
|
+
requestData.copilotDuration = copilotDuration;
|
|
1584
|
+
c.set("requestData", requestData);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const requestId = c.get("requestId");
|
|
1588
|
+
if (requestId) CompletionLogger.executeCompletion(requestId);
|
|
1326
1589
|
});
|
|
1327
1590
|
}
|
|
1328
1591
|
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
@@ -1394,7 +1657,7 @@ usageRoute.get("/", async (c) => {
|
|
|
1394
1657
|
//#endregion
|
|
1395
1658
|
//#region src/server.ts
|
|
1396
1659
|
const server = new Hono();
|
|
1397
|
-
server.use(
|
|
1660
|
+
server.use(enhancedLogger());
|
|
1398
1661
|
server.use(cors());
|
|
1399
1662
|
server.get("/", (c) => c.text("Server running"));
|
|
1400
1663
|
server.route("/chat/completions", completionRoutes);
|
|
@@ -1433,6 +1696,54 @@ function setupGracefulShutdown() {
|
|
|
1433
1696
|
cleanup().finally(() => process$1.exit(1));
|
|
1434
1697
|
});
|
|
1435
1698
|
}
|
|
1699
|
+
async function setupClaudeCodeConfiguration(options, serverUrl) {
|
|
1700
|
+
if (!options.claudeCode) return;
|
|
1701
|
+
invariant(state.models, "Models should be loaded by now");
|
|
1702
|
+
let selectedModel;
|
|
1703
|
+
let selectedSmallModel;
|
|
1704
|
+
if (options.model && options.smallModel) {
|
|
1705
|
+
const availableModelIds = state.models.data.map((model) => model.id);
|
|
1706
|
+
if (!availableModelIds.includes(options.model)) {
|
|
1707
|
+
consola.error(`Invalid model: ${options.model}`);
|
|
1708
|
+
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1709
|
+
process$1.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
if (!availableModelIds.includes(options.smallModel)) {
|
|
1712
|
+
consola.error(`Invalid small model: ${options.smallModel}`);
|
|
1713
|
+
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1714
|
+
process$1.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
selectedModel = options.model;
|
|
1717
|
+
selectedSmallModel = options.smallModel;
|
|
1718
|
+
consola.info(`Using model: ${selectedModel}`);
|
|
1719
|
+
consola.info(`Using small model: ${selectedSmallModel}`);
|
|
1720
|
+
} else if (options.model || options.smallModel) {
|
|
1721
|
+
consola.error("Both --model and --small-model must be specified when using command-line model selection");
|
|
1722
|
+
process$1.exit(1);
|
|
1723
|
+
} else {
|
|
1724
|
+
selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1725
|
+
type: "select",
|
|
1726
|
+
options: state.models.data.map((model) => model.id)
|
|
1727
|
+
});
|
|
1728
|
+
selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1729
|
+
type: "select",
|
|
1730
|
+
options: state.models.data.map((model) => model.id)
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
const command = generateEnvScript({
|
|
1734
|
+
ANTHROPIC_BASE_URL: serverUrl,
|
|
1735
|
+
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1736
|
+
ANTHROPIC_MODEL: selectedModel,
|
|
1737
|
+
ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel
|
|
1738
|
+
}, "claude");
|
|
1739
|
+
try {
|
|
1740
|
+
clipboard.writeSync(command);
|
|
1741
|
+
consola.success("Copied Claude Code command to clipboard!");
|
|
1742
|
+
} catch {
|
|
1743
|
+
consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
|
|
1744
|
+
consola.log(command);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1436
1747
|
async function runServer(options) {
|
|
1437
1748
|
setupGracefulShutdown();
|
|
1438
1749
|
cleanupFunctions.push(() => {
|
|
@@ -1455,7 +1766,8 @@ async function runServer(options) {
|
|
|
1455
1766
|
state.timeoutMs = options.timeout;
|
|
1456
1767
|
state.connectivity.enabled = !options.disableConnectivityMonitoring;
|
|
1457
1768
|
await ensurePaths();
|
|
1458
|
-
|
|
1769
|
+
cacheVSCodeVersion();
|
|
1770
|
+
await initializeVSCodeIdentifiers(state);
|
|
1459
1771
|
if (options.githubToken) {
|
|
1460
1772
|
state.githubToken = options.githubToken;
|
|
1461
1773
|
consola.info("Using provided GitHub token");
|
|
@@ -1464,53 +1776,7 @@ async function runServer(options) {
|
|
|
1464
1776
|
await cacheModels();
|
|
1465
1777
|
consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
|
|
1466
1778
|
const serverUrl = `http://localhost:${options.port}`;
|
|
1467
|
-
|
|
1468
|
-
invariant(state.models, "Models should be loaded by now");
|
|
1469
|
-
let selectedModel;
|
|
1470
|
-
let selectedSmallModel;
|
|
1471
|
-
if (options.model && options.smallModel) {
|
|
1472
|
-
const availableModelIds = state.models.data.map((model) => model.id);
|
|
1473
|
-
if (!availableModelIds.includes(options.model)) {
|
|
1474
|
-
consola.error(`Invalid model: ${options.model}`);
|
|
1475
|
-
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1476
|
-
process$1.exit(1);
|
|
1477
|
-
}
|
|
1478
|
-
if (!availableModelIds.includes(options.smallModel)) {
|
|
1479
|
-
consola.error(`Invalid small model: ${options.smallModel}`);
|
|
1480
|
-
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1481
|
-
process$1.exit(1);
|
|
1482
|
-
}
|
|
1483
|
-
selectedModel = options.model;
|
|
1484
|
-
selectedSmallModel = options.smallModel;
|
|
1485
|
-
consola.info(`Using model: ${selectedModel}`);
|
|
1486
|
-
consola.info(`Using small model: ${selectedSmallModel}`);
|
|
1487
|
-
} else if (options.model || options.smallModel) {
|
|
1488
|
-
consola.error("Both --model and --small-model must be specified when using command-line model selection");
|
|
1489
|
-
process$1.exit(1);
|
|
1490
|
-
} else {
|
|
1491
|
-
selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1492
|
-
type: "select",
|
|
1493
|
-
options: state.models.data.map((model) => model.id)
|
|
1494
|
-
});
|
|
1495
|
-
selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1496
|
-
type: "select",
|
|
1497
|
-
options: state.models.data.map((model) => model.id)
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
const command = generateEnvScript({
|
|
1501
|
-
ANTHROPIC_BASE_URL: serverUrl,
|
|
1502
|
-
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1503
|
-
ANTHROPIC_MODEL: selectedModel,
|
|
1504
|
-
ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel
|
|
1505
|
-
}, "claude");
|
|
1506
|
-
try {
|
|
1507
|
-
clipboard.writeSync(command);
|
|
1508
|
-
consola.success("Copied Claude Code command to clipboard!");
|
|
1509
|
-
} catch {
|
|
1510
|
-
consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
|
|
1511
|
-
consola.log(command);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1779
|
+
await setupClaudeCodeConfiguration(options, serverUrl);
|
|
1514
1780
|
consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
|
|
1515
1781
|
serve({
|
|
1516
1782
|
fetch: server.fetch,
|