copilot-api-node20 0.5.14-node20 → 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 +664 -143
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -4,7 +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 { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import clipboard from "clipboardy";
|
|
9
9
|
import process$1 from "node:process";
|
|
10
10
|
import { serve } from "srvx";
|
|
@@ -12,21 +12,26 @@ import invariant from "tiny-invariant";
|
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { Hono } from "hono";
|
|
14
14
|
import { cors } from "hono/cors";
|
|
15
|
-
import { logger } from "hono/logger";
|
|
16
15
|
import { streamSSE } from "hono/streaming";
|
|
17
|
-
import { countTokens } from "gpt-tokenizer/model/gpt-4o";
|
|
18
16
|
import { events } from "fetch-event-stream";
|
|
17
|
+
import { request } from "undici";
|
|
19
18
|
|
|
20
19
|
//#region src/lib/paths.ts
|
|
21
20
|
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
|
|
22
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");
|
|
23
24
|
const PATHS = {
|
|
24
25
|
APP_DIR,
|
|
25
|
-
GITHUB_TOKEN_PATH
|
|
26
|
+
GITHUB_TOKEN_PATH,
|
|
27
|
+
MACHINE_ID_PATH,
|
|
28
|
+
SESSION_ID_PATH
|
|
26
29
|
};
|
|
27
30
|
async function ensurePaths() {
|
|
28
31
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
29
32
|
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
33
|
+
await ensureFile(PATHS.MACHINE_ID_PATH);
|
|
34
|
+
await ensureFile(PATHS.SESSION_ID_PATH);
|
|
30
35
|
}
|
|
31
36
|
async function ensureFile(filePath) {
|
|
32
37
|
try {
|
|
@@ -43,32 +48,230 @@ const state = {
|
|
|
43
48
|
accountType: "individual",
|
|
44
49
|
manualApprove: false,
|
|
45
50
|
rateLimitWait: false,
|
|
46
|
-
showToken: false
|
|
51
|
+
showToken: false,
|
|
52
|
+
connectivity: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
probeEndpoints: [
|
|
55
|
+
"https://api.github.com",
|
|
56
|
+
"https://www.google.com",
|
|
57
|
+
"https://1.1.1.1"
|
|
58
|
+
],
|
|
59
|
+
fastProbeInterval: 5e3,
|
|
60
|
+
slowProbeInterval: 6e4,
|
|
61
|
+
timeoutMs: 5e3,
|
|
62
|
+
jitterMaxMs: 1e3,
|
|
63
|
+
connectionPooling: true,
|
|
64
|
+
dnsCache: true
|
|
65
|
+
}
|
|
47
66
|
};
|
|
48
67
|
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/lib/connectivity.ts
|
|
70
|
+
var ConnectivityMonitor = class extends EventTarget {
|
|
71
|
+
isOnline = true;
|
|
72
|
+
lastChecked = (/* @__PURE__ */ new Date()).toISOString();
|
|
73
|
+
consecutiveFailures = 0;
|
|
74
|
+
lastErrorType;
|
|
75
|
+
lastErrorMessage;
|
|
76
|
+
lastSuccessfulEndpoint;
|
|
77
|
+
endpointFailureStats = {};
|
|
78
|
+
checkInterval;
|
|
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
|
+
}
|
|
88
|
+
start() {
|
|
89
|
+
if (!state.connectivity.enabled) {
|
|
90
|
+
consola.debug("Connectivity monitoring disabled");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
consola.info("Starting connectivity monitor", {
|
|
94
|
+
probeEndpoints: state.connectivity.probeEndpoints,
|
|
95
|
+
fastInterval: state.connectivity.fastProbeInterval
|
|
96
|
+
});
|
|
97
|
+
this.scheduleNextCheck();
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
this.stop();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
};
|
|
102
|
+
process.on("SIGINT", cleanup);
|
|
103
|
+
process.on("SIGTERM", cleanup);
|
|
104
|
+
}
|
|
105
|
+
stop() {
|
|
106
|
+
consola.debug("Stopping connectivity monitor");
|
|
107
|
+
if (this.checkInterval) {
|
|
108
|
+
clearTimeout(this.checkInterval);
|
|
109
|
+
this.checkInterval = void 0;
|
|
110
|
+
}
|
|
111
|
+
if (this.abortController) {
|
|
112
|
+
this.abortController.abort();
|
|
113
|
+
this.abortController = void 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
scheduleNextCheck() {
|
|
117
|
+
if (this.checkInterval) clearTimeout(this.checkInterval);
|
|
118
|
+
const baseInterval = this.isOnline ? state.connectivity.slowProbeInterval : state.connectivity.fastProbeInterval;
|
|
119
|
+
const jitter = Math.random() * state.connectivity.jitterMaxMs;
|
|
120
|
+
const interval = baseInterval + jitter;
|
|
121
|
+
this.checkInterval = setTimeout(() => {
|
|
122
|
+
this.performConnectivityCheck().catch((error) => {
|
|
123
|
+
consola.error("Connectivity check failed:", error);
|
|
124
|
+
});
|
|
125
|
+
}, interval);
|
|
126
|
+
}
|
|
127
|
+
async performConnectivityCheck() {
|
|
128
|
+
this.abortController = new AbortController();
|
|
129
|
+
const timeoutId = setTimeout(() => this.abortController?.abort(), state.connectivity.timeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
let success = false;
|
|
132
|
+
for (const endpoint of state.connectivity.probeEndpoints) try {
|
|
133
|
+
const response = await fetch(endpoint, {
|
|
134
|
+
method: "HEAD",
|
|
135
|
+
signal: this.abortController.signal,
|
|
136
|
+
headers: {
|
|
137
|
+
"User-Agent": "copilot-api-connectivity-monitor/1.0",
|
|
138
|
+
...state.connectivity.dnsCache && { "Cache-Control": "max-age=300" }
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
success = true;
|
|
143
|
+
this.lastSuccessfulEndpoint = endpoint;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.endpointFailureStats[endpoint] = (this.endpointFailureStats[endpoint] || 0) + 1;
|
|
148
|
+
consola.debug(`Probe failed for ${endpoint}:`, error);
|
|
149
|
+
}
|
|
150
|
+
this.updateConnectivityState(success);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.handleConnectivityError(error);
|
|
153
|
+
} finally {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
this.scheduleNextCheck();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
updateConnectivityState(isOnline) {
|
|
159
|
+
const wasOnline = this.isOnline;
|
|
160
|
+
this.isOnline = isOnline;
|
|
161
|
+
this.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
|
|
162
|
+
if (isOnline) {
|
|
163
|
+
if (this.consecutiveFailures > 0) consola.info(`Connectivity restored after ${this.consecutiveFailures} failures`);
|
|
164
|
+
this.consecutiveFailures = 0;
|
|
165
|
+
this.lastErrorType = void 0;
|
|
166
|
+
this.lastErrorMessage = void 0;
|
|
167
|
+
if (!wasOnline) this.dispatchEvent(new CustomEvent("online"));
|
|
168
|
+
} else {
|
|
169
|
+
this.consecutiveFailures++;
|
|
170
|
+
if (wasOnline) {
|
|
171
|
+
consola.warn("Connectivity lost");
|
|
172
|
+
this.dispatchEvent(new CustomEvent("offline"));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
handleConnectivityError(error) {
|
|
177
|
+
this.lastErrorType = error.name;
|
|
178
|
+
this.lastErrorMessage = error.message;
|
|
179
|
+
consola.error("Connectivity check failed:", error);
|
|
180
|
+
this.updateConnectivityState(false);
|
|
181
|
+
}
|
|
182
|
+
getConnectivityStats() {
|
|
183
|
+
return {
|
|
184
|
+
isOnline: this.isOnline,
|
|
185
|
+
lastChecked: this.lastChecked,
|
|
186
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
187
|
+
lastErrorType: this.lastErrorType,
|
|
188
|
+
lastErrorMessage: this.lastErrorMessage
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
getPerformanceStats() {
|
|
192
|
+
const currentInterval = this.isOnline ? state.connectivity.slowProbeInterval : state.connectivity.fastProbeInterval;
|
|
193
|
+
const nextCheckEstimate = new Date(Date.now() + currentInterval + Math.random() * state.connectivity.jitterMaxMs).toISOString();
|
|
194
|
+
return {
|
|
195
|
+
currentInterval,
|
|
196
|
+
nextCheckEstimate,
|
|
197
|
+
lastSuccessfulEndpoint: this.lastSuccessfulEndpoint,
|
|
198
|
+
endpointFailureStats: { ...this.endpointFailureStats },
|
|
199
|
+
jitterEnabled: state.connectivity.jitterMaxMs > 0,
|
|
200
|
+
connectionPooling: state.connectivity.connectionPooling,
|
|
201
|
+
dnsCache: state.connectivity.dnsCache
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const connectivityMonitor = new ConnectivityMonitor();
|
|
206
|
+
|
|
49
207
|
//#endregion
|
|
50
208
|
//#region src/lib/api-config.ts
|
|
51
209
|
const standardHeaders = () => ({
|
|
52
210
|
"content-type": "application/json",
|
|
53
211
|
accept: "application/json"
|
|
54
212
|
});
|
|
55
|
-
const COPILOT_VERSION = "0.
|
|
213
|
+
const COPILOT_VERSION = "0.32.2025100203";
|
|
56
214
|
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
57
215
|
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
58
|
-
const API_VERSION = "2025-
|
|
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
|
+
};
|
|
59
251
|
const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
|
|
60
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.");
|
|
61
254
|
const headers = {
|
|
62
255
|
Authorization: `Bearer ${state$1.copilotToken}`,
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
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"
|
|
72
275
|
};
|
|
73
276
|
if (vision) headers["copilot-vision-request"] = "true";
|
|
74
277
|
return headers;
|
|
@@ -81,11 +284,119 @@ const githubHeaders = (state$1) => ({
|
|
|
81
284
|
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
82
285
|
"user-agent": USER_AGENT,
|
|
83
286
|
"x-github-api-version": API_VERSION,
|
|
84
|
-
"x-vscode-user-agent-library-version": "
|
|
287
|
+
"x-vscode-user-agent-library-version": "node-fetch"
|
|
85
288
|
});
|
|
86
289
|
const GITHUB_BASE_URL = "https://github.com";
|
|
87
|
-
const GITHUB_CLIENT_ID = "
|
|
88
|
-
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);
|
|
89
400
|
|
|
90
401
|
//#endregion
|
|
91
402
|
//#region src/lib/error.ts
|
|
@@ -97,6 +408,25 @@ var HTTPError = class extends Error {
|
|
|
97
408
|
}
|
|
98
409
|
};
|
|
99
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
|
+
}
|
|
100
430
|
consola.error("Error occurred:", error);
|
|
101
431
|
if (error instanceof HTTPError) {
|
|
102
432
|
const errorText = await error.response.text();
|
|
@@ -208,26 +538,10 @@ const getModels = async () => {
|
|
|
208
538
|
|
|
209
539
|
//#endregion
|
|
210
540
|
//#region src/services/get-vscode-version.ts
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const controller = new AbortController();
|
|
214
|
-
const timeout = setTimeout(() => {
|
|
215
|
-
controller.abort();
|
|
216
|
-
}, 5e3);
|
|
217
|
-
try {
|
|
218
|
-
const response = await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal });
|
|
219
|
-
const pkgbuild = await response.text();
|
|
220
|
-
const pkgverRegex = /pkgver=([0-9.]+)/;
|
|
221
|
-
const match = pkgbuild.match(pkgverRegex);
|
|
222
|
-
if (match) return match[1];
|
|
223
|
-
return FALLBACK;
|
|
224
|
-
} catch {
|
|
225
|
-
return FALLBACK;
|
|
226
|
-
} finally {
|
|
227
|
-
clearTimeout(timeout);
|
|
228
|
-
}
|
|
541
|
+
function getVSCodeVersion() {
|
|
542
|
+
return "1.105.0-insider";
|
|
229
543
|
}
|
|
230
|
-
|
|
544
|
+
getVSCodeVersion();
|
|
231
545
|
|
|
232
546
|
//#endregion
|
|
233
547
|
//#region src/lib/utils.ts
|
|
@@ -239,8 +553,8 @@ async function cacheModels() {
|
|
|
239
553
|
const models = await getModels();
|
|
240
554
|
state.models = models;
|
|
241
555
|
}
|
|
242
|
-
const cacheVSCodeVersion =
|
|
243
|
-
const response =
|
|
556
|
+
const cacheVSCodeVersion = () => {
|
|
557
|
+
const response = getVSCodeVersion();
|
|
244
558
|
state.vsCodeVersion = response;
|
|
245
559
|
consola.info(`Using VSCode version: ${response}`);
|
|
246
560
|
};
|
|
@@ -292,6 +606,25 @@ async function pollAccessToken(deviceCode) {
|
|
|
292
606
|
|
|
293
607
|
//#endregion
|
|
294
608
|
//#region src/lib/token.ts
|
|
609
|
+
let tokenRefreshInterval;
|
|
610
|
+
let isRefreshPending = false;
|
|
611
|
+
let onlineHandler;
|
|
612
|
+
let offlineHandler;
|
|
613
|
+
function cleanupTokenManagement() {
|
|
614
|
+
consola.debug("Cleaning up token management");
|
|
615
|
+
if (tokenRefreshInterval) {
|
|
616
|
+
clearInterval(tokenRefreshInterval);
|
|
617
|
+
tokenRefreshInterval = void 0;
|
|
618
|
+
}
|
|
619
|
+
if (onlineHandler) {
|
|
620
|
+
connectivityMonitor.off("online", onlineHandler);
|
|
621
|
+
onlineHandler = void 0;
|
|
622
|
+
}
|
|
623
|
+
if (offlineHandler) {
|
|
624
|
+
connectivityMonitor.off("offline", offlineHandler);
|
|
625
|
+
offlineHandler = void 0;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
295
628
|
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
|
|
296
629
|
const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
|
|
297
630
|
const setupCopilotToken = async () => {
|
|
@@ -300,18 +633,52 @@ const setupCopilotToken = async () => {
|
|
|
300
633
|
consola.debug("GitHub Copilot Token fetched successfully!");
|
|
301
634
|
if (state.showToken) consola.info("Copilot token:", token);
|
|
302
635
|
const refreshInterval = (refresh_in - 60) * 1e3;
|
|
303
|
-
|
|
304
|
-
|
|
636
|
+
const refreshTokenWithRetry = async (maxRetries = 5, baseDelay = 1e3, reason = "scheduled") => {
|
|
637
|
+
if (isRefreshPending) {
|
|
638
|
+
consola.debug("Token refresh already in progress, skipping");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
isRefreshPending = true;
|
|
305
642
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
643
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) try {
|
|
644
|
+
consola.debug(`Refreshing Copilot token (${reason}, attempt ${attempt}/${maxRetries})`);
|
|
645
|
+
const { token: token$1 } = await getCopilotToken();
|
|
646
|
+
state.copilotToken = token$1;
|
|
647
|
+
consola.debug("Copilot token refreshed successfully");
|
|
648
|
+
if (state.showToken) consola.info("Refreshed Copilot token:", token$1);
|
|
649
|
+
return;
|
|
650
|
+
} catch (error) {
|
|
651
|
+
const isLastAttempt = attempt === maxRetries;
|
|
652
|
+
consola.error(`Failed to refresh Copilot token (attempt ${attempt}/${maxRetries}):`, error);
|
|
653
|
+
if (isLastAttempt) {
|
|
654
|
+
consola.error("All token refresh attempts failed. Service may be unavailable until next scheduled refresh.");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
658
|
+
consola.debug(`Retrying token refresh in ${delay}ms...`);
|
|
659
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
660
|
+
}
|
|
661
|
+
} finally {
|
|
662
|
+
isRefreshPending = false;
|
|
313
663
|
}
|
|
664
|
+
};
|
|
665
|
+
tokenRefreshInterval = setInterval(() => {
|
|
666
|
+
refreshTokenWithRetry(5, 1e3, "scheduled").catch((error) => {
|
|
667
|
+
consola.error("Unexpected error in scheduled token refresh:", error);
|
|
668
|
+
});
|
|
314
669
|
}, refreshInterval);
|
|
670
|
+
connectivityMonitor.start();
|
|
671
|
+
onlineHandler = () => {
|
|
672
|
+
consola.debug("Network connectivity restored, attempting immediate token refresh");
|
|
673
|
+
refreshTokenWithRetry(3, 500, "network-restored").catch((error) => {
|
|
674
|
+
consola.error("Unexpected error in network-restored token refresh:", error);
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
offlineHandler = () => {
|
|
678
|
+
consola.debug("Network connectivity lost");
|
|
679
|
+
};
|
|
680
|
+
connectivityMonitor.on("online", onlineHandler);
|
|
681
|
+
connectivityMonitor.on("offline", offlineHandler);
|
|
315
682
|
};
|
|
316
683
|
async function setupGitHubToken(options) {
|
|
317
684
|
try {
|
|
@@ -444,7 +811,8 @@ const checkUsage = defineCommand({
|
|
|
444
811
|
async function getPackageVersion() {
|
|
445
812
|
try {
|
|
446
813
|
const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
|
|
447
|
-
const
|
|
814
|
+
const packageJsonBuffer = await fs.readFile(packageJsonPath);
|
|
815
|
+
const packageJson = JSON.parse(packageJsonBuffer.toString());
|
|
448
816
|
return packageJson.version;
|
|
449
817
|
} catch {
|
|
450
818
|
return "unknown";
|
|
@@ -573,6 +941,24 @@ function generateEnvScript(envVars, commandToRun = "") {
|
|
|
573
941
|
return commandBlock || commandToRun;
|
|
574
942
|
}
|
|
575
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
|
+
|
|
576
962
|
//#endregion
|
|
577
963
|
//#region src/lib/approval.ts
|
|
578
964
|
const awaitApproval = async () => {
|
|
@@ -580,6 +966,28 @@ const awaitApproval = async () => {
|
|
|
580
966
|
if (!response) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
|
|
581
967
|
};
|
|
582
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
|
+
|
|
583
991
|
//#endregion
|
|
584
992
|
//#region src/lib/rate-limit.ts
|
|
585
993
|
async function checkRateLimit(state$1) {
|
|
@@ -607,40 +1015,52 @@ async function checkRateLimit(state$1) {
|
|
|
607
1015
|
}
|
|
608
1016
|
|
|
609
1017
|
//#endregion
|
|
610
|
-
//#region src/lib/
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1018
|
+
//#region src/lib/sanitize.ts
|
|
1019
|
+
/**
|
|
1020
|
+
* Sanitizes JSON payloads by removing ANSI escape sequences and invisible Unicode characters
|
|
1021
|
+
* that can cause GitHub Copilot API to return 400 Bad Request errors.
|
|
1022
|
+
*/
|
|
1023
|
+
/**
|
|
1024
|
+
* Removes ANSI escape sequences and problematic Unicode characters from a string
|
|
1025
|
+
*/
|
|
1026
|
+
function sanitizeString(str) {
|
|
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, "");
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Recursively sanitizes all string values in an object or array
|
|
1042
|
+
*/
|
|
1043
|
+
function sanitizePayload(payload) {
|
|
1044
|
+
if (typeof payload === "string") return sanitizeString(payload);
|
|
1045
|
+
if (Array.isArray(payload)) {
|
|
1046
|
+
const sanitizedArray = payload.map((item) => sanitizePayload(item));
|
|
1047
|
+
return sanitizedArray;
|
|
1048
|
+
}
|
|
1049
|
+
if (payload && typeof payload === "object") {
|
|
1050
|
+
const sanitized = {};
|
|
1051
|
+
for (const [key, value] of Object.entries(payload)) sanitized[key] = sanitizePayload(value);
|
|
1052
|
+
return sanitized;
|
|
1053
|
+
}
|
|
1054
|
+
return payload;
|
|
1055
|
+
}
|
|
637
1056
|
|
|
638
1057
|
//#endregion
|
|
639
1058
|
//#region src/services/copilot/create-chat-completions.ts
|
|
640
1059
|
const createChatCompletions = async (payload) => {
|
|
641
1060
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
642
|
-
const
|
|
643
|
-
const
|
|
1061
|
+
const sanitizedPayload = sanitizePayload(payload);
|
|
1062
|
+
const enableVision = sanitizedPayload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x$1) => x$1.type === "image_url"));
|
|
1063
|
+
const isAgentCall = sanitizedPayload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
644
1064
|
const headers = {
|
|
645
1065
|
...copilotHeaders(state, enableVision),
|
|
646
1066
|
"X-Initiator": isAgentCall ? "agent" : "user"
|
|
@@ -651,17 +1071,20 @@ const createChatCompletions = async (payload) => {
|
|
|
651
1071
|
controller.abort();
|
|
652
1072
|
}, timeoutMs);
|
|
653
1073
|
try {
|
|
654
|
-
const
|
|
1074
|
+
const { statusCode, headers: responseHeaders, body } = await request(`${copilotBaseUrl(state)}/chat/completions`, {
|
|
655
1075
|
method: "POST",
|
|
656
1076
|
headers,
|
|
657
|
-
body: JSON.stringify(
|
|
658
|
-
signal: controller.signal
|
|
1077
|
+
body: JSON.stringify(sanitizedPayload),
|
|
1078
|
+
signal: controller.signal,
|
|
1079
|
+
headersTimeout: timeoutMs,
|
|
1080
|
+
bodyTimeout: timeoutMs * 3
|
|
659
1081
|
});
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
if (
|
|
1082
|
+
const response = new Response(body, {
|
|
1083
|
+
status: statusCode,
|
|
1084
|
+
headers: responseHeaders
|
|
1085
|
+
});
|
|
1086
|
+
if (!response.ok) throw new HTTPError("Failed to create chat completions", response);
|
|
1087
|
+
if (sanitizedPayload.stream) return events(response);
|
|
665
1088
|
return await response.json();
|
|
666
1089
|
} finally {
|
|
667
1090
|
clearTimeout(timeout);
|
|
@@ -674,7 +1097,7 @@ async function handleCompletion$1(c) {
|
|
|
674
1097
|
await checkRateLimit(state);
|
|
675
1098
|
let payload = await c.req.json();
|
|
676
1099
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
677
|
-
|
|
1100
|
+
c.set("requestData", { model: payload.model });
|
|
678
1101
|
if (state.manualApprove) await awaitApproval();
|
|
679
1102
|
if (isNullish(payload.max_tokens)) {
|
|
680
1103
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
@@ -684,17 +1107,42 @@ async function handleCompletion$1(c) {
|
|
|
684
1107
|
};
|
|
685
1108
|
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
686
1109
|
}
|
|
1110
|
+
const copilotStart = performance.now();
|
|
687
1111
|
const response = await createChatCompletions(payload);
|
|
1112
|
+
const copilotDuration = performance.now() - copilotStart;
|
|
688
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);
|
|
689
1118
|
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
690
1119
|
return c.json(response);
|
|
691
1120
|
}
|
|
692
|
-
consola.debug("Streaming response");
|
|
693
1121
|
return streamSSE(c, async (stream) => {
|
|
1122
|
+
let finalUsage = null;
|
|
694
1123
|
for await (const chunk of response) {
|
|
695
|
-
|
|
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 {}
|
|
696
1134
|
await stream.writeSSE(chunk);
|
|
697
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);
|
|
698
1146
|
});
|
|
699
1147
|
}
|
|
700
1148
|
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
@@ -738,8 +1186,15 @@ const createEmbeddings = async (payload) => {
|
|
|
738
1186
|
const embeddingRoutes = new Hono();
|
|
739
1187
|
embeddingRoutes.post("/", async (c) => {
|
|
740
1188
|
try {
|
|
741
|
-
const
|
|
742
|
-
|
|
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);
|
|
743
1198
|
return c.json(response);
|
|
744
1199
|
} catch (error) {
|
|
745
1200
|
return await forwardError(c, error);
|
|
@@ -807,7 +1262,7 @@ function handleUserMessage(message) {
|
|
|
807
1262
|
for (const block of toolResultBlocks) newMessages.push({
|
|
808
1263
|
role: "tool",
|
|
809
1264
|
tool_call_id: block.tool_use_id,
|
|
810
|
-
content: block.content
|
|
1265
|
+
content: mapContent(block.content)
|
|
811
1266
|
});
|
|
812
1267
|
if (otherBlocks.length > 0) newMessages.push({
|
|
813
1268
|
role: "user",
|
|
@@ -1076,9 +1531,16 @@ async function handleCompletion(c) {
|
|
|
1076
1531
|
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
|
|
1077
1532
|
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1078
1533
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
|
|
1534
|
+
c.set("requestData", { model: openAIPayload.model });
|
|
1535
|
+
const copilotStart = performance.now();
|
|
1079
1536
|
if (state.manualApprove) await awaitApproval();
|
|
1080
1537
|
const response = await createChatCompletions(openAIPayload);
|
|
1538
|
+
const copilotDuration = performance.now() - copilotStart;
|
|
1081
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);
|
|
1082
1544
|
consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
1083
1545
|
const anthropicResponse = translateToAnthropic(response);
|
|
1084
1546
|
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
@@ -1086,6 +1548,7 @@ async function handleCompletion(c) {
|
|
|
1086
1548
|
}
|
|
1087
1549
|
consola.debug("Streaming response from Copilot");
|
|
1088
1550
|
return streamSSE(c, async (stream) => {
|
|
1551
|
+
let finalUsage = null;
|
|
1089
1552
|
const streamState = {
|
|
1090
1553
|
messageStartSent: false,
|
|
1091
1554
|
contentBlockIndex: 0,
|
|
@@ -1097,6 +1560,13 @@ async function handleCompletion(c) {
|
|
|
1097
1560
|
if (rawEvent.data === "[DONE]") break;
|
|
1098
1561
|
if (!rawEvent.data) continue;
|
|
1099
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
|
+
}
|
|
1100
1570
|
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
1101
1571
|
for (const event of events$1) {
|
|
1102
1572
|
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
@@ -1106,6 +1576,16 @@ async function handleCompletion(c) {
|
|
|
1106
1576
|
});
|
|
1107
1577
|
}
|
|
1108
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);
|
|
1109
1589
|
});
|
|
1110
1590
|
}
|
|
1111
1591
|
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
@@ -1177,7 +1657,7 @@ usageRoute.get("/", async (c) => {
|
|
|
1177
1657
|
//#endregion
|
|
1178
1658
|
//#region src/server.ts
|
|
1179
1659
|
const server = new Hono();
|
|
1180
|
-
server.use(
|
|
1660
|
+
server.use(enhancedLogger());
|
|
1181
1661
|
server.use(cors());
|
|
1182
1662
|
server.get("/", (c) => c.text("Server running"));
|
|
1183
1663
|
server.route("/chat/completions", completionRoutes);
|
|
@@ -1193,7 +1673,86 @@ server.post("/v1/messages/count_tokens", (c) => c.json({ input_tokens: 1 }));
|
|
|
1193
1673
|
|
|
1194
1674
|
//#endregion
|
|
1195
1675
|
//#region src/start.ts
|
|
1676
|
+
const cleanupFunctions = [];
|
|
1677
|
+
function setupGracefulShutdown() {
|
|
1678
|
+
const cleanup = async () => {
|
|
1679
|
+
consola.info("Gracefully shutting down...");
|
|
1680
|
+
for (const cleanupFn of cleanupFunctions) try {
|
|
1681
|
+
await cleanupFn();
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
consola.error("Error during cleanup:", error);
|
|
1684
|
+
}
|
|
1685
|
+
consola.info("Shutdown complete");
|
|
1686
|
+
process$1.exit(0);
|
|
1687
|
+
};
|
|
1688
|
+
process$1.on("SIGINT", cleanup);
|
|
1689
|
+
process$1.on("SIGTERM", cleanup);
|
|
1690
|
+
process$1.on("uncaughtException", (error) => {
|
|
1691
|
+
consola.error("Uncaught exception:", error);
|
|
1692
|
+
cleanup().finally(() => process$1.exit(1));
|
|
1693
|
+
});
|
|
1694
|
+
process$1.on("unhandledRejection", (reason, promise) => {
|
|
1695
|
+
consola.error("Unhandled promise rejection at:", promise, "reason:", reason);
|
|
1696
|
+
cleanup().finally(() => process$1.exit(1));
|
|
1697
|
+
});
|
|
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
|
+
}
|
|
1196
1747
|
async function runServer(options) {
|
|
1748
|
+
setupGracefulShutdown();
|
|
1749
|
+
cleanupFunctions.push(() => {
|
|
1750
|
+
consola.debug("Cleaning up connectivity monitor");
|
|
1751
|
+
connectivityMonitor.stop();
|
|
1752
|
+
}, () => {
|
|
1753
|
+
consola.debug("Cleaning up token management");
|
|
1754
|
+
cleanupTokenManagement();
|
|
1755
|
+
});
|
|
1197
1756
|
if (options.verbose) {
|
|
1198
1757
|
consola.level = 5;
|
|
1199
1758
|
consola.info("Verbose logging enabled");
|
|
@@ -1205,8 +1764,10 @@ async function runServer(options) {
|
|
|
1205
1764
|
state.rateLimitWait = options.rateLimitWait;
|
|
1206
1765
|
state.showToken = options.showToken;
|
|
1207
1766
|
state.timeoutMs = options.timeout;
|
|
1767
|
+
state.connectivity.enabled = !options.disableConnectivityMonitoring;
|
|
1208
1768
|
await ensurePaths();
|
|
1209
|
-
|
|
1769
|
+
cacheVSCodeVersion();
|
|
1770
|
+
await initializeVSCodeIdentifiers(state);
|
|
1210
1771
|
if (options.githubToken) {
|
|
1211
1772
|
state.githubToken = options.githubToken;
|
|
1212
1773
|
consola.info("Using provided GitHub token");
|
|
@@ -1215,53 +1776,7 @@ async function runServer(options) {
|
|
|
1215
1776
|
await cacheModels();
|
|
1216
1777
|
consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
|
|
1217
1778
|
const serverUrl = `http://localhost:${options.port}`;
|
|
1218
|
-
|
|
1219
|
-
invariant(state.models, "Models should be loaded by now");
|
|
1220
|
-
let selectedModel;
|
|
1221
|
-
let selectedSmallModel;
|
|
1222
|
-
if (options.model && options.smallModel) {
|
|
1223
|
-
const availableModelIds = state.models.data.map((model) => model.id);
|
|
1224
|
-
if (!availableModelIds.includes(options.model)) {
|
|
1225
|
-
consola.error(`Invalid model: ${options.model}`);
|
|
1226
|
-
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1227
|
-
process$1.exit(1);
|
|
1228
|
-
}
|
|
1229
|
-
if (!availableModelIds.includes(options.smallModel)) {
|
|
1230
|
-
consola.error(`Invalid small model: ${options.smallModel}`);
|
|
1231
|
-
consola.info(`Available models: \n${availableModelIds.join("\n")}`);
|
|
1232
|
-
process$1.exit(1);
|
|
1233
|
-
}
|
|
1234
|
-
selectedModel = options.model;
|
|
1235
|
-
selectedSmallModel = options.smallModel;
|
|
1236
|
-
consola.info(`Using model: ${selectedModel}`);
|
|
1237
|
-
consola.info(`Using small model: ${selectedSmallModel}`);
|
|
1238
|
-
} else if (options.model || options.smallModel) {
|
|
1239
|
-
consola.error("Both --model and --small-model must be specified when using command-line model selection");
|
|
1240
|
-
process$1.exit(1);
|
|
1241
|
-
} else {
|
|
1242
|
-
selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1243
|
-
type: "select",
|
|
1244
|
-
options: state.models.data.map((model) => model.id)
|
|
1245
|
-
});
|
|
1246
|
-
selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1247
|
-
type: "select",
|
|
1248
|
-
options: state.models.data.map((model) => model.id)
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
const command = generateEnvScript({
|
|
1252
|
-
ANTHROPIC_BASE_URL: serverUrl,
|
|
1253
|
-
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1254
|
-
ANTHROPIC_MODEL: selectedModel,
|
|
1255
|
-
ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel
|
|
1256
|
-
}, "claude");
|
|
1257
|
-
try {
|
|
1258
|
-
clipboard.writeSync(command);
|
|
1259
|
-
consola.success("Copied Claude Code command to clipboard!");
|
|
1260
|
-
} catch {
|
|
1261
|
-
consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
|
|
1262
|
-
consola.log(command);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1779
|
+
await setupClaudeCodeConfiguration(options, serverUrl);
|
|
1265
1780
|
consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
|
|
1266
1781
|
serve({
|
|
1267
1782
|
fetch: server.fetch,
|
|
@@ -1338,6 +1853,11 @@ const start = defineCommand({
|
|
|
1338
1853
|
alias: "t",
|
|
1339
1854
|
type: "string",
|
|
1340
1855
|
description: "API timeout in milliseconds (default: 120000)"
|
|
1856
|
+
},
|
|
1857
|
+
"disable-connectivity-monitoring": {
|
|
1858
|
+
type: "boolean",
|
|
1859
|
+
default: false,
|
|
1860
|
+
description: "Disable automatic network connectivity monitoring for token refresh"
|
|
1341
1861
|
}
|
|
1342
1862
|
},
|
|
1343
1863
|
run({ args }) {
|
|
@@ -1357,7 +1877,8 @@ const start = defineCommand({
|
|
|
1357
1877
|
model: args.model,
|
|
1358
1878
|
smallModel: args["small-model"],
|
|
1359
1879
|
showToken: args["show-token"],
|
|
1360
|
-
timeout
|
|
1880
|
+
timeout,
|
|
1881
|
+
disableConnectivityMonitoring: args["disable-connectivity-monitoring"]
|
|
1361
1882
|
});
|
|
1362
1883
|
}
|
|
1363
1884
|
});
|