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 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 { EventEmitter } from "node:events";
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 EventEmitter {
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.emit("online");
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.emit("offline");
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.2025093001";
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
- "content-type": standardHeaders()["content-type"],
209
- "copilot-integration-id": "vscode-chat",
210
- "editor-version": `vscode/${state$1.vsCodeVersion}`,
211
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
212
- "user-agent": USER_AGENT,
213
- "openai-intent": "model-access",
214
- "x-github-api-version": API_VERSION,
215
- "x-interaction-type": "model-access",
216
- "x-request-id": randomUUID(),
217
- "x-vscode-user-agent-library-version": "electron-fetch"
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": "electron-fetch"
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 = "Iv1.b507a08c87ecfe98";
234
- const GITHUB_APP_SCOPES = ["read:user"].join(" ");
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 = async () => {
373
- const response = await getVSCodeVersion();
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 packageJson = JSON.parse(await fs.readFile(packageJsonPath));
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
- return str.replaceAll(/\x1b\[[0-9;]*[a-z]/gi, "").replaceAll(/\x1b\[[0-9;]*m/g, "").replaceAll(/\x1b\[[\d;]*[HfA-DsuJKmhlp]/g, "").replaceAll(/[\x00-\x08\v\f\x0E-\x1F\x7F]/g, "").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, "");
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)) return payload.map((item) => sanitizePayload(item));
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
- consola.info("Current token count:", getTokenCount(payload.messages));
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
- consola.debug("Streaming chunk:", JSON.stringify(chunk));
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 paylod = await c.req.json();
959
- const response = await createEmbeddings(paylod);
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(logger());
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
- await cacheVSCodeVersion();
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
- if (options.claudeCode) {
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,