ghc-proxy 0.1.1
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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/main.js +1816 -0
- package/dist/main.js.map +1 -0
- package/package.json +68 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,1816 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { events } from "fetch-event-stream";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import clipboard from "clipboardy";
|
|
11
|
+
import { serve } from "srvx";
|
|
12
|
+
import invariant from "tiny-invariant";
|
|
13
|
+
import { getProxyForUrl } from "proxy-from-env";
|
|
14
|
+
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
15
|
+
import { execSync } from "node:child_process";
|
|
16
|
+
import { Hono } from "hono";
|
|
17
|
+
import { cors } from "hono/cors";
|
|
18
|
+
import { colorize } from "consola/utils";
|
|
19
|
+
import { streamSSE } from "hono/streaming";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
//#region src/lib/paths.ts
|
|
23
|
+
const APP_DIR = path.join(os.homedir(), ".local", "share", "ghc-proxy");
|
|
24
|
+
const CONFIG_PATH = path.join(APP_DIR, "config.json");
|
|
25
|
+
const PATHS = {
|
|
26
|
+
APP_DIR,
|
|
27
|
+
CONFIG_PATH
|
|
28
|
+
};
|
|
29
|
+
async function ensurePaths() {
|
|
30
|
+
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/lib/config.ts
|
|
35
|
+
let cachedConfig = {};
|
|
36
|
+
async function readConfig() {
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(PATHS.CONFIG_PATH, "utf8");
|
|
39
|
+
if (!content.trim()) {
|
|
40
|
+
cachedConfig = {};
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
const parsed = JSON.parse(content);
|
|
44
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
45
|
+
consola.warn("config.json is not a valid object. Using defaults.");
|
|
46
|
+
cachedConfig = {};
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
cachedConfig = parsed;
|
|
50
|
+
return cachedConfig;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === "ENOENT") {
|
|
53
|
+
cachedConfig = {};
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
consola.warn(`Failed to parse config.json: ${error.message}. Using defaults.`);
|
|
57
|
+
cachedConfig = {};
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getCachedConfig() {
|
|
62
|
+
return cachedConfig;
|
|
63
|
+
}
|
|
64
|
+
async function writeConfigField(field, value) {
|
|
65
|
+
try {
|
|
66
|
+
let existing = {};
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(PATHS.CONFIG_PATH, "utf8");
|
|
69
|
+
if (content.trim()) existing = JSON.parse(content);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.code !== "ENOENT") consola.warn(`Could not read existing config.json: ${error.message}. Starting fresh.`);
|
|
72
|
+
}
|
|
73
|
+
const merged = {
|
|
74
|
+
...existing,
|
|
75
|
+
[field]: value
|
|
76
|
+
};
|
|
77
|
+
await fs.writeFile(PATHS.CONFIG_PATH, JSON.stringify(merged, null, 2), "utf8");
|
|
78
|
+
await fs.chmod(PATHS.CONFIG_PATH, 384);
|
|
79
|
+
cachedConfig = merged;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
consola.error(`Failed to write config.json: ${error.message}`);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/lib/state.ts
|
|
88
|
+
const state = {
|
|
89
|
+
auth: {},
|
|
90
|
+
config: {
|
|
91
|
+
accountType: "individual",
|
|
92
|
+
manualApprove: false,
|
|
93
|
+
rateLimitWait: false,
|
|
94
|
+
showToken: false
|
|
95
|
+
},
|
|
96
|
+
cache: {},
|
|
97
|
+
rateLimit: {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/lib/api-config.ts
|
|
102
|
+
function standardHeaders() {
|
|
103
|
+
return {
|
|
104
|
+
"content-type": "application/json",
|
|
105
|
+
"accept": "application/json"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const COPILOT_VERSION = "0.26.7";
|
|
109
|
+
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
110
|
+
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
111
|
+
const API_VERSION = "2025-04-01";
|
|
112
|
+
function copilotBaseUrl(config) {
|
|
113
|
+
return config.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${config.accountType}.githubcopilot.com`;
|
|
114
|
+
}
|
|
115
|
+
function copilotHeaders(auth$1, config, vision = false) {
|
|
116
|
+
const headers = {
|
|
117
|
+
"Authorization": `Bearer ${auth$1.copilotToken}`,
|
|
118
|
+
"content-type": standardHeaders()["content-type"],
|
|
119
|
+
"copilot-integration-id": "vscode-chat",
|
|
120
|
+
"editor-version": `vscode/${config.vsCodeVersion ?? "unknown"}`,
|
|
121
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
122
|
+
"user-agent": USER_AGENT,
|
|
123
|
+
"openai-intent": "conversation-panel",
|
|
124
|
+
"x-github-api-version": API_VERSION,
|
|
125
|
+
"x-request-id": randomUUID(),
|
|
126
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
127
|
+
};
|
|
128
|
+
if (vision) headers["copilot-vision-request"] = "true";
|
|
129
|
+
return headers;
|
|
130
|
+
}
|
|
131
|
+
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
132
|
+
function githubHeaders(auth$1, config) {
|
|
133
|
+
return {
|
|
134
|
+
...standardHeaders(),
|
|
135
|
+
"authorization": `token ${auth$1.githubToken}`,
|
|
136
|
+
"editor-version": `vscode/${config.vsCodeVersion ?? "unknown"}`,
|
|
137
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
138
|
+
"user-agent": USER_AGENT,
|
|
139
|
+
"x-github-api-version": API_VERSION,
|
|
140
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const GITHUB_BASE_URL = "https://github.com";
|
|
144
|
+
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
145
|
+
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/lib/error.ts
|
|
149
|
+
var HTTPError = class extends Error {
|
|
150
|
+
response;
|
|
151
|
+
constructor(message, response) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.response = response;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
async function forwardError(c, error) {
|
|
157
|
+
consola.error("Error occurred:", error);
|
|
158
|
+
if (error instanceof HTTPError) {
|
|
159
|
+
const errorText = await error.response.text();
|
|
160
|
+
let errorJson;
|
|
161
|
+
try {
|
|
162
|
+
errorJson = JSON.parse(errorText);
|
|
163
|
+
} catch {
|
|
164
|
+
errorJson = errorText;
|
|
165
|
+
}
|
|
166
|
+
consola.error("HTTP error:", errorJson);
|
|
167
|
+
return c.json({ error: {
|
|
168
|
+
message: errorText,
|
|
169
|
+
type: "error"
|
|
170
|
+
} }, error.response.status);
|
|
171
|
+
}
|
|
172
|
+
return c.json({ error: {
|
|
173
|
+
message: error.message,
|
|
174
|
+
type: "error"
|
|
175
|
+
} }, 500);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/clients/copilot-client.ts
|
|
180
|
+
var CopilotClient = class {
|
|
181
|
+
auth;
|
|
182
|
+
config;
|
|
183
|
+
fetchImpl;
|
|
184
|
+
constructor(auth$1, config, deps) {
|
|
185
|
+
this.auth = auth$1;
|
|
186
|
+
this.config = config;
|
|
187
|
+
this.fetchImpl = deps?.fetch ?? fetch;
|
|
188
|
+
}
|
|
189
|
+
async createChatCompletions(payload, options) {
|
|
190
|
+
if (!this.auth.copilotToken) throw new Error("Copilot token not found");
|
|
191
|
+
const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((content) => content.type === "image_url"));
|
|
192
|
+
const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
193
|
+
const headers = {
|
|
194
|
+
...copilotHeaders(this.auth, this.config, enableVision),
|
|
195
|
+
"X-Initiator": isAgentCall ? "agent" : "user"
|
|
196
|
+
};
|
|
197
|
+
const response = await this.fetchImpl(`${copilotBaseUrl(this.config)}/chat/completions`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers,
|
|
200
|
+
body: JSON.stringify(payload),
|
|
201
|
+
signal: options?.signal
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
consola.error("Failed to create chat completions", response);
|
|
205
|
+
throw new HTTPError("Failed to create chat completions", response);
|
|
206
|
+
}
|
|
207
|
+
if (payload.stream) return events(response);
|
|
208
|
+
return await response.json();
|
|
209
|
+
}
|
|
210
|
+
async createEmbeddings(payload) {
|
|
211
|
+
if (!this.auth.copilotToken) throw new Error("Copilot token not found");
|
|
212
|
+
const response = await this.fetchImpl(`${copilotBaseUrl(this.config)}/embeddings`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: copilotHeaders(this.auth, this.config),
|
|
215
|
+
body: JSON.stringify(payload)
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
|
|
218
|
+
return await response.json();
|
|
219
|
+
}
|
|
220
|
+
async getModels() {
|
|
221
|
+
const response = await this.fetchImpl(`${copilotBaseUrl(this.config)}/models`, { headers: copilotHeaders(this.auth, this.config) });
|
|
222
|
+
if (!response.ok) throw new HTTPError("Failed to get models", response);
|
|
223
|
+
return await response.json();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/lib/client-config.ts
|
|
229
|
+
function getClientConfig(appState) {
|
|
230
|
+
return {
|
|
231
|
+
accountType: appState.config.accountType,
|
|
232
|
+
vsCodeVersion: appState.cache.vsCodeVersion
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/lib/utils.ts
|
|
238
|
+
function sleep(ms) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
setTimeout(resolve, ms);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function isNullish(value) {
|
|
244
|
+
return value === null || value === void 0;
|
|
245
|
+
}
|
|
246
|
+
async function cacheModels(client) {
|
|
247
|
+
const models = await (client ?? new CopilotClient(state.auth, getClientConfig(state))).getModels();
|
|
248
|
+
state.cache.models = models;
|
|
249
|
+
}
|
|
250
|
+
async function cacheVSCodeVersion() {
|
|
251
|
+
const response = await getVSCodeVersion();
|
|
252
|
+
state.cache.vsCodeVersion = response;
|
|
253
|
+
consola.info(`Using VSCode version: ${response}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/clients/github-client.ts
|
|
258
|
+
var GitHubClient = class {
|
|
259
|
+
auth;
|
|
260
|
+
config;
|
|
261
|
+
fetchImpl;
|
|
262
|
+
constructor(auth$1, config, deps) {
|
|
263
|
+
this.auth = auth$1;
|
|
264
|
+
this.config = config;
|
|
265
|
+
this.fetchImpl = deps?.fetch ?? fetch;
|
|
266
|
+
}
|
|
267
|
+
async getCopilotUsage() {
|
|
268
|
+
const response = await this.fetchImpl(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(this.auth, this.config) });
|
|
269
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot usage", response);
|
|
270
|
+
return await response.json();
|
|
271
|
+
}
|
|
272
|
+
async getCopilotToken() {
|
|
273
|
+
const response = await this.fetchImpl(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(this.auth, this.config) });
|
|
274
|
+
if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
|
|
275
|
+
return await response.json();
|
|
276
|
+
}
|
|
277
|
+
async getDeviceCode() {
|
|
278
|
+
const response = await this.fetchImpl(`${GITHUB_BASE_URL}/login/device/code`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: standardHeaders(),
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
client_id: GITHUB_CLIENT_ID,
|
|
283
|
+
scope: GITHUB_APP_SCOPES
|
|
284
|
+
})
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok) throw new HTTPError("Failed to get device code", response);
|
|
287
|
+
return await response.json();
|
|
288
|
+
}
|
|
289
|
+
async pollAccessToken(deviceCode) {
|
|
290
|
+
const sleepDuration = (deviceCode.interval + 1) * 1e3;
|
|
291
|
+
consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
|
|
292
|
+
while (true) {
|
|
293
|
+
const response = await this.fetchImpl(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: standardHeaders(),
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
client_id: GITHUB_CLIENT_ID,
|
|
298
|
+
device_code: deviceCode.device_code,
|
|
299
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
await sleep(sleepDuration);
|
|
304
|
+
consola.error("Failed to poll access token:", await response.text());
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const json = await response.json();
|
|
308
|
+
consola.debug("Polling access token response:", json);
|
|
309
|
+
if (json.access_token) return json.access_token;
|
|
310
|
+
await sleep(sleepDuration);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async getGitHubUser() {
|
|
314
|
+
const response = await this.fetchImpl(`${GITHUB_API_BASE_URL}/user`, { headers: {
|
|
315
|
+
authorization: `token ${this.auth.githubToken}`,
|
|
316
|
+
...standardHeaders()
|
|
317
|
+
} });
|
|
318
|
+
if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
|
|
319
|
+
return await response.json();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/clients/vscode-client.ts
|
|
325
|
+
const FALLBACK = "1.104.3";
|
|
326
|
+
async function getVSCodeVersion() {
|
|
327
|
+
const controller = new AbortController();
|
|
328
|
+
const timeout = setTimeout(() => {
|
|
329
|
+
controller.abort();
|
|
330
|
+
}, 5e3);
|
|
331
|
+
try {
|
|
332
|
+
const match = (await (await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal })).text()).match(/pkgver=([0-9.]+)/);
|
|
333
|
+
if (match) return match[1];
|
|
334
|
+
return FALLBACK;
|
|
335
|
+
} catch {
|
|
336
|
+
return FALLBACK;
|
|
337
|
+
} finally {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/lib/token.ts
|
|
344
|
+
async function writeGithubToken(token) {
|
|
345
|
+
await writeConfigField("githubToken", token);
|
|
346
|
+
}
|
|
347
|
+
async function setupCopilotToken() {
|
|
348
|
+
await ensureVSCodeVersion();
|
|
349
|
+
const githubClient = createGitHubClient();
|
|
350
|
+
const { token, refresh_in } = await githubClient.getCopilotToken();
|
|
351
|
+
state.auth.copilotToken = token;
|
|
352
|
+
consola.debug("GitHub Copilot Token fetched successfully!");
|
|
353
|
+
if (state.config.showToken) consola.info("Copilot token:", token);
|
|
354
|
+
const refreshInterval = (refresh_in - 60) * 1e3;
|
|
355
|
+
const refreshCopilotToken = async () => {
|
|
356
|
+
consola.debug("Refreshing Copilot token");
|
|
357
|
+
try {
|
|
358
|
+
const { token: token$1 } = await githubClient.getCopilotToken();
|
|
359
|
+
state.auth.copilotToken = token$1;
|
|
360
|
+
consola.debug("Copilot token refreshed");
|
|
361
|
+
if (state.config.showToken) consola.info("Refreshed Copilot token:", token$1);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
consola.error("Failed to refresh Copilot token:", error);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
setInterval(() => {
|
|
367
|
+
refreshCopilotToken();
|
|
368
|
+
}, refreshInterval);
|
|
369
|
+
}
|
|
370
|
+
async function setupGitHubToken(options) {
|
|
371
|
+
try {
|
|
372
|
+
await ensureVSCodeVersion();
|
|
373
|
+
const githubToken = getCachedConfig().githubToken?.trim() || "";
|
|
374
|
+
if (githubToken && !options?.force) {
|
|
375
|
+
state.auth.githubToken = githubToken;
|
|
376
|
+
if (state.config.showToken) consola.info("GitHub token:", githubToken);
|
|
377
|
+
try {
|
|
378
|
+
await logUser();
|
|
379
|
+
return;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (isAuthError(error) && !options?.force) {
|
|
382
|
+
consola.warn("Stored GitHub token invalid or expired. Re-authenticating...");
|
|
383
|
+
await setupGitHubToken({ force: true });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
consola.info("Not logged in, getting new access token");
|
|
390
|
+
const githubClient = createGitHubClient();
|
|
391
|
+
const response = await githubClient.getDeviceCode();
|
|
392
|
+
consola.debug("Device code response:", response);
|
|
393
|
+
consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
|
|
394
|
+
const token = await githubClient.pollAccessToken(response);
|
|
395
|
+
await writeGithubToken(token);
|
|
396
|
+
state.auth.githubToken = token;
|
|
397
|
+
if (state.config.showToken) consola.info("GitHub token:", token);
|
|
398
|
+
await logUser();
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof HTTPError) {
|
|
401
|
+
consola.error("Failed to get GitHub token:", await error.response.json());
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
consola.error("Failed to get GitHub token:", error);
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function isAuthError(error) {
|
|
409
|
+
return error instanceof HTTPError && (error.response.status === 401 || error.response.status === 403);
|
|
410
|
+
}
|
|
411
|
+
async function logUser() {
|
|
412
|
+
const user = await createGitHubClient().getGitHubUser();
|
|
413
|
+
consola.info(`Logged in as ${user.login}`);
|
|
414
|
+
}
|
|
415
|
+
function createGitHubClient() {
|
|
416
|
+
return new GitHubClient(state.auth, getClientConfig(state));
|
|
417
|
+
}
|
|
418
|
+
async function ensureVSCodeVersion() {
|
|
419
|
+
if (!state.cache.vsCodeVersion) await cacheVSCodeVersion();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/auth.ts
|
|
424
|
+
async function runAuth(options) {
|
|
425
|
+
if (options.verbose) {
|
|
426
|
+
consola.level = 5;
|
|
427
|
+
consola.info("Verbose logging enabled");
|
|
428
|
+
}
|
|
429
|
+
state.config.showToken = options.showToken;
|
|
430
|
+
await ensurePaths();
|
|
431
|
+
await readConfig();
|
|
432
|
+
await cacheVSCodeVersion();
|
|
433
|
+
await setupGitHubToken({ force: true });
|
|
434
|
+
consola.success("GitHub token written to config.json");
|
|
435
|
+
}
|
|
436
|
+
const auth = defineCommand({
|
|
437
|
+
meta: {
|
|
438
|
+
name: "auth",
|
|
439
|
+
description: "Run GitHub auth flow without running the server"
|
|
440
|
+
},
|
|
441
|
+
args: {
|
|
442
|
+
"verbose": {
|
|
443
|
+
alias: "v",
|
|
444
|
+
type: "boolean",
|
|
445
|
+
default: false,
|
|
446
|
+
description: "Enable verbose logging"
|
|
447
|
+
},
|
|
448
|
+
"show-token": {
|
|
449
|
+
type: "boolean",
|
|
450
|
+
default: false,
|
|
451
|
+
description: "Show GitHub token on auth"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
run({ args }) {
|
|
455
|
+
return runAuth({
|
|
456
|
+
verbose: args.verbose,
|
|
457
|
+
showToken: args["show-token"]
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/check-usage.ts
|
|
464
|
+
const checkUsage = defineCommand({
|
|
465
|
+
meta: {
|
|
466
|
+
name: "check-usage",
|
|
467
|
+
description: "Show current GitHub Copilot usage/quota information"
|
|
468
|
+
},
|
|
469
|
+
async run() {
|
|
470
|
+
await ensurePaths();
|
|
471
|
+
await readConfig();
|
|
472
|
+
await cacheVSCodeVersion();
|
|
473
|
+
await setupGitHubToken();
|
|
474
|
+
try {
|
|
475
|
+
const usage = await new GitHubClient(state.auth, getClientConfig(state)).getCopilotUsage();
|
|
476
|
+
const premium = usage.quota_snapshots.premium_interactions;
|
|
477
|
+
const premiumTotal = premium.entitlement;
|
|
478
|
+
const premiumUsed = premiumTotal - premium.remaining;
|
|
479
|
+
const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
|
|
480
|
+
const premiumPercentRemaining = premium.percent_remaining;
|
|
481
|
+
function summarizeQuota(name, snap) {
|
|
482
|
+
if (!snap) return `${name}: N/A`;
|
|
483
|
+
const total = snap.entitlement;
|
|
484
|
+
const used = total - snap.remaining;
|
|
485
|
+
const percentUsed = total > 0 ? used / total * 100 : 0;
|
|
486
|
+
const percentRemaining = snap.percent_remaining;
|
|
487
|
+
return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
|
|
488
|
+
}
|
|
489
|
+
const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
|
|
490
|
+
const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
|
|
491
|
+
const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
|
|
492
|
+
consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
consola.error("Failed to fetch Copilot usage:", err);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/debug.ts
|
|
502
|
+
async function getPackageVersion() {
|
|
503
|
+
try {
|
|
504
|
+
const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
|
|
505
|
+
return JSON.parse(await fs.readFile(packageJsonPath)).version;
|
|
506
|
+
} catch {
|
|
507
|
+
return "unknown";
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function getRuntimeInfo() {
|
|
511
|
+
return {
|
|
512
|
+
name: "bun",
|
|
513
|
+
version: Bun.version,
|
|
514
|
+
platform: os.platform(),
|
|
515
|
+
arch: os.arch()
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function hasToken() {
|
|
519
|
+
try {
|
|
520
|
+
const config = getCachedConfig();
|
|
521
|
+
return Boolean(config.githubToken?.trim());
|
|
522
|
+
} catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function checkConfigExists() {
|
|
527
|
+
try {
|
|
528
|
+
if (!(await fs.stat(PATHS.CONFIG_PATH)).isFile()) return false;
|
|
529
|
+
return (await fs.readFile(PATHS.CONFIG_PATH, "utf8")).trim().length > 0;
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function getDebugInfo() {
|
|
535
|
+
const [version, configExists] = await Promise.all([getPackageVersion(), checkConfigExists()]);
|
|
536
|
+
return {
|
|
537
|
+
version,
|
|
538
|
+
runtime: getRuntimeInfo(),
|
|
539
|
+
paths: {
|
|
540
|
+
APP_DIR: PATHS.APP_DIR,
|
|
541
|
+
CONFIG_PATH: PATHS.CONFIG_PATH
|
|
542
|
+
},
|
|
543
|
+
configExists,
|
|
544
|
+
tokenExists: hasToken()
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function printDebugInfoPlain(info) {
|
|
548
|
+
consola.info(`ghc-proxy debug
|
|
549
|
+
|
|
550
|
+
Version: ${info.version}
|
|
551
|
+
Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
|
|
552
|
+
|
|
553
|
+
Paths:
|
|
554
|
+
- APP_DIR: ${info.paths.APP_DIR}
|
|
555
|
+
- CONFIG_PATH: ${info.paths.CONFIG_PATH}
|
|
556
|
+
|
|
557
|
+
Config exists: ${info.configExists ? "Yes" : "No"}
|
|
558
|
+
Token exists: ${info.tokenExists ? "Yes" : "No"}`);
|
|
559
|
+
}
|
|
560
|
+
async function runDebug(options) {
|
|
561
|
+
const debugInfo = await getDebugInfo();
|
|
562
|
+
if (options.json) await Bun.write(Bun.stdout, `${JSON.stringify(debugInfo, null, 2)}\n`);
|
|
563
|
+
else printDebugInfoPlain(debugInfo);
|
|
564
|
+
}
|
|
565
|
+
const debug = defineCommand({
|
|
566
|
+
meta: {
|
|
567
|
+
name: "debug",
|
|
568
|
+
description: "Print debug information about the application"
|
|
569
|
+
},
|
|
570
|
+
args: { json: {
|
|
571
|
+
type: "boolean",
|
|
572
|
+
default: false,
|
|
573
|
+
description: "Output debug information as JSON"
|
|
574
|
+
} },
|
|
575
|
+
run({ args }) {
|
|
576
|
+
return runDebug({ json: args.json });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/lib/proxy.ts
|
|
582
|
+
function initProxyFromEnv() {
|
|
583
|
+
if (typeof Bun !== "undefined") return;
|
|
584
|
+
try {
|
|
585
|
+
const direct = new Agent();
|
|
586
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
587
|
+
setGlobalDispatcher({
|
|
588
|
+
dispatch(options, handler) {
|
|
589
|
+
try {
|
|
590
|
+
const origin = typeof options.origin === "string" ? new URL(options.origin) : options.origin;
|
|
591
|
+
const raw = getProxyForUrl(origin.toString());
|
|
592
|
+
const proxyUrl = raw && raw.length > 0 ? raw : void 0;
|
|
593
|
+
if (!proxyUrl) {
|
|
594
|
+
consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
|
|
595
|
+
return direct.dispatch(options, handler);
|
|
596
|
+
}
|
|
597
|
+
let agent = proxies.get(proxyUrl);
|
|
598
|
+
if (!agent) {
|
|
599
|
+
agent = new ProxyAgent(proxyUrl);
|
|
600
|
+
proxies.set(proxyUrl, agent);
|
|
601
|
+
}
|
|
602
|
+
let label = proxyUrl;
|
|
603
|
+
try {
|
|
604
|
+
const u = new URL(proxyUrl);
|
|
605
|
+
label = `${u.protocol}//${u.host}`;
|
|
606
|
+
} catch {}
|
|
607
|
+
consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`);
|
|
608
|
+
return agent.dispatch(options, handler);
|
|
609
|
+
} catch {
|
|
610
|
+
return direct.dispatch(options, handler);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
close() {
|
|
614
|
+
return direct.close();
|
|
615
|
+
},
|
|
616
|
+
destroy() {
|
|
617
|
+
return direct.destroy();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
consola.debug("HTTP proxy configured from environment (per-URL)");
|
|
621
|
+
} catch (err) {
|
|
622
|
+
consola.debug("Proxy setup skipped:", err);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/lib/shell.ts
|
|
628
|
+
function getShell() {
|
|
629
|
+
const { platform, ppid, env } = process;
|
|
630
|
+
if (platform === "win32") {
|
|
631
|
+
try {
|
|
632
|
+
const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
|
|
633
|
+
if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
|
|
634
|
+
} catch {
|
|
635
|
+
return "cmd";
|
|
636
|
+
}
|
|
637
|
+
return "cmd";
|
|
638
|
+
} else {
|
|
639
|
+
const shellPath = env.SHELL;
|
|
640
|
+
if (shellPath) {
|
|
641
|
+
if (shellPath.endsWith("zsh")) return "zsh";
|
|
642
|
+
if (shellPath.endsWith("fish")) return "fish";
|
|
643
|
+
if (shellPath.endsWith("bash")) return "bash";
|
|
644
|
+
}
|
|
645
|
+
return "sh";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Generates a copy-pasteable script to set multiple environment variables
|
|
650
|
+
* and run a subsequent command.
|
|
651
|
+
* @param {EnvVars} envVars - An object of environment variables to set.
|
|
652
|
+
* @param {string} commandToRun - The command to run after setting the variables.
|
|
653
|
+
* @returns {string} The formatted script string.
|
|
654
|
+
*/
|
|
655
|
+
function generateEnvScript(envVars, commandToRun = "") {
|
|
656
|
+
const shell = getShell();
|
|
657
|
+
const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
|
|
658
|
+
let commandBlock;
|
|
659
|
+
switch (shell) {
|
|
660
|
+
case "powershell":
|
|
661
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${value}`).join("; ");
|
|
662
|
+
break;
|
|
663
|
+
case "cmd":
|
|
664
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
|
|
665
|
+
break;
|
|
666
|
+
case "fish":
|
|
667
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${value}`).join("; ");
|
|
668
|
+
break;
|
|
669
|
+
default: {
|
|
670
|
+
const assignments = filteredEnvVars.map(([key, value]) => `${key}=${value}`).join(" ");
|
|
671
|
+
commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : " && "}${commandToRun}`;
|
|
676
|
+
return commandBlock || commandToRun;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/lib/request-logger.ts
|
|
681
|
+
function formatElapsed(start$1) {
|
|
682
|
+
const delta = Date.now() - start$1;
|
|
683
|
+
return delta < 1e3 ? `${delta}ms` : `${Math.round(delta / 1e3)}s`;
|
|
684
|
+
}
|
|
685
|
+
function formatPath(rawUrl) {
|
|
686
|
+
try {
|
|
687
|
+
const url = new URL(rawUrl);
|
|
688
|
+
return `${url.pathname}${url.search}`;
|
|
689
|
+
} catch {
|
|
690
|
+
return rawUrl;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function colorizeStatus(status) {
|
|
694
|
+
if (status >= 500) return colorize("red", status);
|
|
695
|
+
if (status >= 400) return colorize("yellow", status);
|
|
696
|
+
if (status >= 300) return colorize("cyan", status);
|
|
697
|
+
return colorize("green", status);
|
|
698
|
+
}
|
|
699
|
+
const methodColors = {
|
|
700
|
+
GET: "cyan",
|
|
701
|
+
POST: "magenta",
|
|
702
|
+
PUT: "yellow",
|
|
703
|
+
PATCH: "yellow",
|
|
704
|
+
DELETE: "red"
|
|
705
|
+
};
|
|
706
|
+
function colorizeMethod(method) {
|
|
707
|
+
return colorize(methodColors[method] ?? "white", method);
|
|
708
|
+
}
|
|
709
|
+
function formatModelMapping(info) {
|
|
710
|
+
if (!info) return "";
|
|
711
|
+
const { originalModel, mappedModel } = info;
|
|
712
|
+
if (!originalModel && !mappedModel) return "";
|
|
713
|
+
const original = originalModel ?? "-";
|
|
714
|
+
const mapped = mappedModel ?? "-";
|
|
715
|
+
if (original === mapped) return ` ${colorize("dim", "model=")}${colorize("blueBright", original)}`;
|
|
716
|
+
return ` ${colorize("dim", "model=")}${colorize("blueBright", original)} ${colorize("dim", "→")} ${colorize("greenBright", mapped)}`;
|
|
717
|
+
}
|
|
718
|
+
const requestLogger = async (c, next) => {
|
|
719
|
+
const { method, url } = c.req;
|
|
720
|
+
const path$1 = formatPath(url);
|
|
721
|
+
const start$1 = Date.now();
|
|
722
|
+
try {
|
|
723
|
+
await next();
|
|
724
|
+
} finally {
|
|
725
|
+
const elapsed = formatElapsed(start$1);
|
|
726
|
+
const status = c.res.status;
|
|
727
|
+
const modelInfo = c.get("modelMappingInfo");
|
|
728
|
+
const line = [
|
|
729
|
+
colorizeMethod(method),
|
|
730
|
+
colorize("white", path$1),
|
|
731
|
+
colorizeStatus(status),
|
|
732
|
+
colorize("dim", elapsed)
|
|
733
|
+
].join(" ");
|
|
734
|
+
consola.info(`${line}${formatModelMapping(modelInfo)}`);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
function setModelMappingInfo(c, info) {
|
|
738
|
+
c.set("modelMappingInfo", info);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/lib/approval.ts
|
|
743
|
+
async function awaitApproval() {
|
|
744
|
+
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/lib/rate-limit.ts
|
|
749
|
+
async function checkRateLimit(state$1) {
|
|
750
|
+
if (state$1.config.rateLimitSeconds === void 0) return;
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
if (!state$1.rateLimit.lastRequestTimestamp) {
|
|
753
|
+
state$1.rateLimit.lastRequestTimestamp = now;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const elapsedSeconds = (now - state$1.rateLimit.lastRequestTimestamp) / 1e3;
|
|
757
|
+
if (elapsedSeconds > state$1.config.rateLimitSeconds) {
|
|
758
|
+
state$1.rateLimit.lastRequestTimestamp = now;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const waitTimeSeconds = Math.ceil(state$1.config.rateLimitSeconds - elapsedSeconds);
|
|
762
|
+
if (!state$1.config.rateLimitWait) {
|
|
763
|
+
consola.warn(`Rate limit exceeded. Need to wait ${waitTimeSeconds} more seconds.`);
|
|
764
|
+
throw new HTTPError("Rate limit exceeded", Response.json({ message: "Rate limit exceeded" }, { status: 429 }));
|
|
765
|
+
}
|
|
766
|
+
const waitTimeMs = waitTimeSeconds * 1e3;
|
|
767
|
+
consola.warn(`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`);
|
|
768
|
+
await sleep(waitTimeMs);
|
|
769
|
+
state$1.rateLimit.lastRequestTimestamp = now;
|
|
770
|
+
consola.info("Rate limit wait completed, proceeding with request");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/routes/middleware/request-guard.ts
|
|
775
|
+
const requestGuard = async (c, next) => {
|
|
776
|
+
await checkRateLimit(state);
|
|
777
|
+
if (state.config.manualApprove) await awaitApproval();
|
|
778
|
+
await next();
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/lib/tokenizer.ts
|
|
783
|
+
const ENCODING_MAP = {
|
|
784
|
+
o200k_base: () => import("gpt-tokenizer/encoding/o200k_base"),
|
|
785
|
+
cl100k_base: () => import("gpt-tokenizer/encoding/cl100k_base"),
|
|
786
|
+
p50k_base: () => import("gpt-tokenizer/encoding/p50k_base"),
|
|
787
|
+
p50k_edit: () => import("gpt-tokenizer/encoding/p50k_edit"),
|
|
788
|
+
r50k_base: () => import("gpt-tokenizer/encoding/r50k_base")
|
|
789
|
+
};
|
|
790
|
+
const encodingCache = /* @__PURE__ */ new Map();
|
|
791
|
+
/**
|
|
792
|
+
* Calculate tokens for tool calls
|
|
793
|
+
*/
|
|
794
|
+
function calculateToolCallsTokens(toolCalls, encoder, constants) {
|
|
795
|
+
let tokens = 0;
|
|
796
|
+
for (const toolCall of toolCalls) {
|
|
797
|
+
tokens += constants.funcInit;
|
|
798
|
+
tokens += encoder.encode(JSON.stringify(toolCall)).length;
|
|
799
|
+
}
|
|
800
|
+
tokens += constants.funcEnd;
|
|
801
|
+
return tokens;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Calculate tokens for content parts
|
|
805
|
+
*/
|
|
806
|
+
function calculateContentPartsTokens(contentParts, encoder) {
|
|
807
|
+
let tokens = 0;
|
|
808
|
+
for (const part of contentParts) if (part.type === "image_url") tokens += encoder.encode(part.image_url.url).length + 85;
|
|
809
|
+
else if (part.text) tokens += encoder.encode(part.text).length;
|
|
810
|
+
return tokens;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Calculate tokens for a single message
|
|
814
|
+
*/
|
|
815
|
+
function calculateMessageTokens(message, encoder, constants) {
|
|
816
|
+
const tokensPerMessage = 3;
|
|
817
|
+
const tokensPerName = 1;
|
|
818
|
+
let tokens = tokensPerMessage;
|
|
819
|
+
for (const [key, value] of Object.entries(message)) {
|
|
820
|
+
if (typeof value === "string") tokens += encoder.encode(value).length;
|
|
821
|
+
if (key === "name") tokens += tokensPerName;
|
|
822
|
+
if (key === "tool_calls") tokens += calculateToolCallsTokens(value, encoder, constants);
|
|
823
|
+
if (key === "content" && Array.isArray(value)) tokens += calculateContentPartsTokens(value, encoder);
|
|
824
|
+
}
|
|
825
|
+
return tokens;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Calculate tokens using custom algorithm
|
|
829
|
+
*/
|
|
830
|
+
function calculateTokens(messages, encoder, constants) {
|
|
831
|
+
if (messages.length === 0) return 0;
|
|
832
|
+
let numTokens = 0;
|
|
833
|
+
for (const message of messages) numTokens += calculateMessageTokens(message, encoder, constants);
|
|
834
|
+
numTokens += 3;
|
|
835
|
+
return numTokens;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Get the corresponding encoder module based on encoding type
|
|
839
|
+
*/
|
|
840
|
+
async function getEncodeChatFunction(encoding) {
|
|
841
|
+
if (encodingCache.has(encoding)) {
|
|
842
|
+
const cached = encodingCache.get(encoding);
|
|
843
|
+
if (cached) return cached;
|
|
844
|
+
}
|
|
845
|
+
const supportedEncoding = encoding;
|
|
846
|
+
if (!(supportedEncoding in ENCODING_MAP)) {
|
|
847
|
+
const fallbackModule = await ENCODING_MAP.o200k_base();
|
|
848
|
+
encodingCache.set(encoding, fallbackModule);
|
|
849
|
+
return fallbackModule;
|
|
850
|
+
}
|
|
851
|
+
const encodingModule = await ENCODING_MAP[supportedEncoding]();
|
|
852
|
+
encodingCache.set(encoding, encodingModule);
|
|
853
|
+
return encodingModule;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Get tokenizer type from model information
|
|
857
|
+
*/
|
|
858
|
+
function getTokenizerFromModel(model) {
|
|
859
|
+
return model.capabilities.tokenizer || "o200k_base";
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get model-specific constants for token calculation
|
|
863
|
+
*/
|
|
864
|
+
function getModelConstants(model) {
|
|
865
|
+
return model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
|
|
866
|
+
funcInit: 10,
|
|
867
|
+
propInit: 3,
|
|
868
|
+
propKey: 3,
|
|
869
|
+
enumInit: -3,
|
|
870
|
+
enumItem: 3,
|
|
871
|
+
funcEnd: 12
|
|
872
|
+
} : {
|
|
873
|
+
funcInit: 7,
|
|
874
|
+
propInit: 3,
|
|
875
|
+
propKey: 3,
|
|
876
|
+
enumInit: -3,
|
|
877
|
+
enumItem: 3,
|
|
878
|
+
funcEnd: 12
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Calculate tokens for a single parameter
|
|
883
|
+
*/
|
|
884
|
+
function calculateParameterTokens(key, prop, context) {
|
|
885
|
+
const { encoder, constants } = context;
|
|
886
|
+
let tokens = constants.propKey;
|
|
887
|
+
if (typeof prop !== "object" || prop === null) return tokens;
|
|
888
|
+
const param = prop;
|
|
889
|
+
const paramName = key;
|
|
890
|
+
const paramType = param.type || "string";
|
|
891
|
+
let paramDesc = param.description || "";
|
|
892
|
+
if (param.enum && Array.isArray(param.enum)) {
|
|
893
|
+
tokens += constants.enumInit;
|
|
894
|
+
for (const item of param.enum) {
|
|
895
|
+
tokens += constants.enumItem;
|
|
896
|
+
tokens += encoder.encode(String(item)).length;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (paramDesc.endsWith(".")) paramDesc = paramDesc.slice(0, -1);
|
|
900
|
+
const line = `${paramName}:${paramType}:${paramDesc}`;
|
|
901
|
+
tokens += encoder.encode(line).length;
|
|
902
|
+
const excludedKeys = new Set([
|
|
903
|
+
"type",
|
|
904
|
+
"description",
|
|
905
|
+
"enum"
|
|
906
|
+
]);
|
|
907
|
+
for (const propertyName of Object.keys(param)) if (!excludedKeys.has(propertyName)) {
|
|
908
|
+
const propertyValue = param[propertyName];
|
|
909
|
+
const propertyText = typeof propertyValue === "string" ? propertyValue : JSON.stringify(propertyValue);
|
|
910
|
+
tokens += encoder.encode(`${propertyName}:${propertyText}`).length;
|
|
911
|
+
}
|
|
912
|
+
return tokens;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Calculate tokens for function parameters
|
|
916
|
+
*/
|
|
917
|
+
function calculateParametersTokens(parameters, encoder, constants) {
|
|
918
|
+
if (!parameters || typeof parameters !== "object") return 0;
|
|
919
|
+
const params = parameters;
|
|
920
|
+
let tokens = 0;
|
|
921
|
+
for (const [key, value] of Object.entries(params)) if (key === "properties") {
|
|
922
|
+
const properties = value;
|
|
923
|
+
if (Object.keys(properties).length > 0) {
|
|
924
|
+
tokens += constants.propInit;
|
|
925
|
+
for (const propKey of Object.keys(properties)) tokens += calculateParameterTokens(propKey, properties[propKey], {
|
|
926
|
+
encoder,
|
|
927
|
+
constants
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
const paramText = typeof value === "string" ? value : JSON.stringify(value);
|
|
932
|
+
tokens += encoder.encode(`${key}:${paramText}`).length;
|
|
933
|
+
}
|
|
934
|
+
return tokens;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Calculate tokens for a single tool
|
|
938
|
+
*/
|
|
939
|
+
function calculateToolTokens(tool, encoder, constants) {
|
|
940
|
+
let tokens = constants.funcInit;
|
|
941
|
+
const func = tool.function;
|
|
942
|
+
const fName = func.name;
|
|
943
|
+
let fDesc = func.description || "";
|
|
944
|
+
if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
|
|
945
|
+
const line = `${fName}:${fDesc}`;
|
|
946
|
+
tokens += encoder.encode(line).length;
|
|
947
|
+
if (typeof func.parameters === "object" && func.parameters !== null) tokens += calculateParametersTokens(func.parameters, encoder, constants);
|
|
948
|
+
return tokens;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Calculate token count for tools based on model
|
|
952
|
+
*/
|
|
953
|
+
function numTokensForTools(tools, encoder, constants) {
|
|
954
|
+
let funcTokenCount = 0;
|
|
955
|
+
for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
|
|
956
|
+
funcTokenCount += constants.funcEnd;
|
|
957
|
+
return funcTokenCount;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Calculate the token count of messages, supporting multiple GPT encoders
|
|
961
|
+
*/
|
|
962
|
+
async function getTokenCount(payload, model) {
|
|
963
|
+
const tokenizer = getTokenizerFromModel(model);
|
|
964
|
+
const encoder = await getEncodeChatFunction(tokenizer);
|
|
965
|
+
const simplifiedMessages = payload.messages;
|
|
966
|
+
const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
|
|
967
|
+
const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
|
|
968
|
+
const constants = getModelConstants(model);
|
|
969
|
+
let inputTokens = calculateTokens(inputMessages, encoder, constants);
|
|
970
|
+
if (payload.tools && payload.tools.length > 0) inputTokens += numTokensForTools(payload.tools, encoder, constants);
|
|
971
|
+
const outputTokens = calculateTokens(outputMessages, encoder, constants);
|
|
972
|
+
return {
|
|
973
|
+
input: inputTokens,
|
|
974
|
+
output: outputTokens
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/lib/validation.ts
|
|
980
|
+
const openAIMessageSchema = z.object({
|
|
981
|
+
role: z.string(),
|
|
982
|
+
content: z.union([
|
|
983
|
+
z.string(),
|
|
984
|
+
z.array(z.any()),
|
|
985
|
+
z.null()
|
|
986
|
+
]),
|
|
987
|
+
name: z.string().optional(),
|
|
988
|
+
tool_calls: z.array(z.any()).optional(),
|
|
989
|
+
tool_call_id: z.string().optional()
|
|
990
|
+
}).loose();
|
|
991
|
+
const openAIChatPayloadSchema = z.object({
|
|
992
|
+
model: z.string(),
|
|
993
|
+
messages: z.array(openAIMessageSchema).min(1)
|
|
994
|
+
}).loose();
|
|
995
|
+
const anthropicMessageSchema = z.object({
|
|
996
|
+
role: z.enum(["user", "assistant"]),
|
|
997
|
+
content: z.union([z.string(), z.array(z.any())])
|
|
998
|
+
}).loose();
|
|
999
|
+
const anthropicMessagesBasePayloadSchema = z.object({
|
|
1000
|
+
model: z.string(),
|
|
1001
|
+
messages: z.array(anthropicMessageSchema).min(1)
|
|
1002
|
+
}).loose();
|
|
1003
|
+
const anthropicMessagesPayloadSchema = anthropicMessagesBasePayloadSchema.extend({ max_tokens: z.number() }).loose();
|
|
1004
|
+
const anthropicCountTokensPayloadSchema = anthropicMessagesBasePayloadSchema.extend({ max_tokens: z.number().optional() }).loose();
|
|
1005
|
+
const embeddingRequestSchema = z.object({
|
|
1006
|
+
input: z.union([z.string(), z.array(z.string())]),
|
|
1007
|
+
model: z.string()
|
|
1008
|
+
}).loose();
|
|
1009
|
+
function throwInvalidPayload(context, issues) {
|
|
1010
|
+
consola.warn("Invalid request payload", {
|
|
1011
|
+
context,
|
|
1012
|
+
issues
|
|
1013
|
+
});
|
|
1014
|
+
throw new HTTPError("Invalid request payload", new Response("Invalid request payload", { status: 400 }));
|
|
1015
|
+
}
|
|
1016
|
+
function parseOpenAIChatPayload(payload) {
|
|
1017
|
+
const result = openAIChatPayloadSchema.safeParse(payload);
|
|
1018
|
+
if (!result.success) throwInvalidPayload("openai.chat", result.error.issues);
|
|
1019
|
+
return result.data;
|
|
1020
|
+
}
|
|
1021
|
+
function parseAnthropicMessagesPayload(payload) {
|
|
1022
|
+
const result = anthropicMessagesPayloadSchema.safeParse(payload);
|
|
1023
|
+
if (!result.success) throwInvalidPayload("anthropic.messages", result.error.issues);
|
|
1024
|
+
return result.data;
|
|
1025
|
+
}
|
|
1026
|
+
function parseAnthropicCountTokensPayload(payload) {
|
|
1027
|
+
const result = anthropicCountTokensPayloadSchema.safeParse(payload);
|
|
1028
|
+
if (!result.success) throwInvalidPayload("anthropic.messages.count_tokens", result.error.issues);
|
|
1029
|
+
return result.data;
|
|
1030
|
+
}
|
|
1031
|
+
function parseEmbeddingRequest(payload) {
|
|
1032
|
+
const result = embeddingRequestSchema.safeParse(payload);
|
|
1033
|
+
if (!result.success) throwInvalidPayload("openai.embeddings", result.error.issues);
|
|
1034
|
+
return result.data;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/routes/chat-completions/handler.ts
|
|
1039
|
+
async function handleCompletion$1(c) {
|
|
1040
|
+
let payload = parseOpenAIChatPayload(await c.req.json());
|
|
1041
|
+
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
1042
|
+
const selectedModel = state.cache.models?.data.find((model) => model.id === payload.model);
|
|
1043
|
+
try {
|
|
1044
|
+
if (selectedModel) {
|
|
1045
|
+
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
1046
|
+
consola.info("Current token count:", tokenCount);
|
|
1047
|
+
} else consola.warn("No model selected, skipping token count calculation");
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
consola.warn("Failed to calculate token count:", error);
|
|
1050
|
+
}
|
|
1051
|
+
if (isNullish(payload.max_tokens)) {
|
|
1052
|
+
payload = {
|
|
1053
|
+
...payload,
|
|
1054
|
+
max_tokens: selectedModel?.capabilities.limits.max_output_tokens
|
|
1055
|
+
};
|
|
1056
|
+
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
1057
|
+
}
|
|
1058
|
+
const response = await new CopilotClient(state.auth, getClientConfig(state)).createChatCompletions(payload, { signal: c.req.raw.signal });
|
|
1059
|
+
if (isNonStreaming$1(response)) {
|
|
1060
|
+
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1061
|
+
return c.json(response);
|
|
1062
|
+
}
|
|
1063
|
+
consola.debug("Streaming response");
|
|
1064
|
+
return streamSSE(c, async (stream) => {
|
|
1065
|
+
try {
|
|
1066
|
+
for await (const chunk of response) {
|
|
1067
|
+
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1068
|
+
await stream.writeSSE(chunk);
|
|
1069
|
+
}
|
|
1070
|
+
} finally {}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function isNonStreaming$1(response) {
|
|
1074
|
+
return Object.hasOwn(response, "choices");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/routes/chat-completions/route.ts
|
|
1079
|
+
const completionRoutes = new Hono();
|
|
1080
|
+
completionRoutes.post("/", requestGuard, (c) => handleCompletion$1(c));
|
|
1081
|
+
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/routes/embeddings/route.ts
|
|
1084
|
+
const embeddingRoutes = new Hono();
|
|
1085
|
+
embeddingRoutes.post("/", async (c) => {
|
|
1086
|
+
const payload = parseEmbeddingRequest(await c.req.json());
|
|
1087
|
+
const response = await new CopilotClient(state.auth, getClientConfig(state)).createEmbeddings(payload);
|
|
1088
|
+
return c.json(response);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
//#endregion
|
|
1092
|
+
//#region src/translator/anthropic/shared.ts
|
|
1093
|
+
function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
1094
|
+
if (finishReason === null) return null;
|
|
1095
|
+
return {
|
|
1096
|
+
stop: "end_turn",
|
|
1097
|
+
length: "max_tokens",
|
|
1098
|
+
tool_calls: "tool_use",
|
|
1099
|
+
content_filter: "end_turn"
|
|
1100
|
+
}[finishReason];
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/translator/anthropic/anthropic-stream-translator.ts
|
|
1105
|
+
var AnthropicStreamTranslator = class {
|
|
1106
|
+
state;
|
|
1107
|
+
constructor() {
|
|
1108
|
+
this.state = {
|
|
1109
|
+
messageStartSent: false,
|
|
1110
|
+
contentBlockIndex: 0,
|
|
1111
|
+
contentBlockOpen: false,
|
|
1112
|
+
toolCalls: {}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
onChunk(chunk) {
|
|
1116
|
+
if (chunk.choices.length === 0) return [];
|
|
1117
|
+
const events$1 = [];
|
|
1118
|
+
const choice = chunk.choices[0];
|
|
1119
|
+
const { delta } = choice;
|
|
1120
|
+
this.appendMessageStart(events$1, chunk);
|
|
1121
|
+
this.appendContentDelta(events$1, delta.content);
|
|
1122
|
+
this.appendToolCalls(events$1, delta.tool_calls);
|
|
1123
|
+
this.appendFinish(events$1, chunk, choice.finish_reason);
|
|
1124
|
+
return events$1;
|
|
1125
|
+
}
|
|
1126
|
+
onError(error) {
|
|
1127
|
+
return [{
|
|
1128
|
+
type: "error",
|
|
1129
|
+
error: {
|
|
1130
|
+
type: "api_error",
|
|
1131
|
+
message: this.getErrorMessage(error)
|
|
1132
|
+
}
|
|
1133
|
+
}];
|
|
1134
|
+
}
|
|
1135
|
+
getErrorMessage(error) {
|
|
1136
|
+
if (this.isTimeoutError(error)) return "Upstream streaming request timed out. Please retry.";
|
|
1137
|
+
return "An unexpected error occurred during streaming.";
|
|
1138
|
+
}
|
|
1139
|
+
isTimeoutError(error) {
|
|
1140
|
+
if (error instanceof DOMException) return error.name === "TimeoutError";
|
|
1141
|
+
if (error instanceof Error) return error.name === "TimeoutError";
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
isToolBlockOpen() {
|
|
1145
|
+
if (!this.state.contentBlockOpen) return false;
|
|
1146
|
+
return Object.values(this.state.toolCalls).some((tc) => {
|
|
1147
|
+
return tc !== void 0 && tc.anthropicBlockIndex === this.state.contentBlockIndex;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
appendMessageStart(events$1, chunk) {
|
|
1151
|
+
if (this.state.messageStartSent) return;
|
|
1152
|
+
events$1.push({
|
|
1153
|
+
type: "message_start",
|
|
1154
|
+
message: {
|
|
1155
|
+
id: chunk.id,
|
|
1156
|
+
type: "message",
|
|
1157
|
+
role: "assistant",
|
|
1158
|
+
content: [],
|
|
1159
|
+
model: chunk.model,
|
|
1160
|
+
stop_reason: null,
|
|
1161
|
+
stop_sequence: null,
|
|
1162
|
+
usage: {
|
|
1163
|
+
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1164
|
+
output_tokens: 0,
|
|
1165
|
+
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
this.state.messageStartSent = true;
|
|
1170
|
+
}
|
|
1171
|
+
appendContentDelta(events$1, content) {
|
|
1172
|
+
if (!content) return;
|
|
1173
|
+
if (this.isToolBlockOpen()) {
|
|
1174
|
+
events$1.push({
|
|
1175
|
+
type: "content_block_stop",
|
|
1176
|
+
index: this.state.contentBlockIndex
|
|
1177
|
+
});
|
|
1178
|
+
this.state.contentBlockIndex++;
|
|
1179
|
+
this.state.contentBlockOpen = false;
|
|
1180
|
+
}
|
|
1181
|
+
if (!this.state.contentBlockOpen) {
|
|
1182
|
+
events$1.push({
|
|
1183
|
+
type: "content_block_start",
|
|
1184
|
+
index: this.state.contentBlockIndex,
|
|
1185
|
+
content_block: {
|
|
1186
|
+
type: "text",
|
|
1187
|
+
text: ""
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
this.state.contentBlockOpen = true;
|
|
1191
|
+
}
|
|
1192
|
+
events$1.push({
|
|
1193
|
+
type: "content_block_delta",
|
|
1194
|
+
index: this.state.contentBlockIndex,
|
|
1195
|
+
delta: {
|
|
1196
|
+
type: "text_delta",
|
|
1197
|
+
text: content
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
appendToolCalls(events$1, toolCalls) {
|
|
1202
|
+
if (!toolCalls || toolCalls.length === 0) return;
|
|
1203
|
+
for (const toolCall of toolCalls) {
|
|
1204
|
+
if (toolCall.id && toolCall.function?.name) {
|
|
1205
|
+
if (this.state.contentBlockOpen) {
|
|
1206
|
+
events$1.push({
|
|
1207
|
+
type: "content_block_stop",
|
|
1208
|
+
index: this.state.contentBlockIndex
|
|
1209
|
+
});
|
|
1210
|
+
this.state.contentBlockIndex++;
|
|
1211
|
+
this.state.contentBlockOpen = false;
|
|
1212
|
+
}
|
|
1213
|
+
const anthropicBlockIndex = this.state.contentBlockIndex;
|
|
1214
|
+
this.state.toolCalls[toolCall.index] = {
|
|
1215
|
+
id: toolCall.id,
|
|
1216
|
+
name: toolCall.function.name,
|
|
1217
|
+
anthropicBlockIndex
|
|
1218
|
+
};
|
|
1219
|
+
events$1.push({
|
|
1220
|
+
type: "content_block_start",
|
|
1221
|
+
index: anthropicBlockIndex,
|
|
1222
|
+
content_block: {
|
|
1223
|
+
type: "tool_use",
|
|
1224
|
+
id: toolCall.id,
|
|
1225
|
+
name: toolCall.function.name,
|
|
1226
|
+
input: {}
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
this.state.contentBlockOpen = true;
|
|
1230
|
+
}
|
|
1231
|
+
if (toolCall.function?.arguments) {
|
|
1232
|
+
const toolCallInfo = this.state.toolCalls[toolCall.index];
|
|
1233
|
+
if (!toolCallInfo) continue;
|
|
1234
|
+
events$1.push({
|
|
1235
|
+
type: "content_block_delta",
|
|
1236
|
+
index: toolCallInfo.anthropicBlockIndex,
|
|
1237
|
+
delta: {
|
|
1238
|
+
type: "input_json_delta",
|
|
1239
|
+
partial_json: toolCall.function.arguments
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
appendFinish(events$1, chunk, finishReason) {
|
|
1246
|
+
if (!finishReason) return;
|
|
1247
|
+
if (this.state.contentBlockOpen) {
|
|
1248
|
+
events$1.push({
|
|
1249
|
+
type: "content_block_stop",
|
|
1250
|
+
index: this.state.contentBlockIndex
|
|
1251
|
+
});
|
|
1252
|
+
this.state.contentBlockOpen = false;
|
|
1253
|
+
}
|
|
1254
|
+
events$1.push({
|
|
1255
|
+
type: "message_delta",
|
|
1256
|
+
delta: {
|
|
1257
|
+
stop_reason: mapOpenAIStopReasonToAnthropic(finishReason),
|
|
1258
|
+
stop_sequence: null
|
|
1259
|
+
},
|
|
1260
|
+
usage: {
|
|
1261
|
+
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1262
|
+
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
1263
|
+
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1264
|
+
}
|
|
1265
|
+
}, { type: "message_stop" });
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region src/lib/model-resolver.ts
|
|
1271
|
+
const DEFAULT_FALLBACKS = {
|
|
1272
|
+
claudeOpus: "claude-opus-4.6",
|
|
1273
|
+
claudeSonnet: "claude-sonnet-4.5",
|
|
1274
|
+
claudeHaiku: "claude-haiku-4.5"
|
|
1275
|
+
};
|
|
1276
|
+
function getModelFallbackConfig() {
|
|
1277
|
+
const cachedConfig$1 = getCachedConfig();
|
|
1278
|
+
return {
|
|
1279
|
+
claudeOpus: process.env.MODEL_FALLBACK_CLAUDE_OPUS || cachedConfig$1.modelFallback?.claudeOpus || DEFAULT_FALLBACKS.claudeOpus,
|
|
1280
|
+
claudeSonnet: process.env.MODEL_FALLBACK_CLAUDE_SONNET || cachedConfig$1.modelFallback?.claudeSonnet || DEFAULT_FALLBACKS.claudeSonnet,
|
|
1281
|
+
claudeHaiku: process.env.MODEL_FALLBACK_CLAUDE_HAIKU || cachedConfig$1.modelFallback?.claudeHaiku || DEFAULT_FALLBACKS.claudeHaiku
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function resolveModel(modelId, knownModelIds, config) {
|
|
1285
|
+
if (knownModelIds?.has(modelId)) return modelId;
|
|
1286
|
+
if (modelId.startsWith("claude-opus-")) return config.claudeOpus;
|
|
1287
|
+
if (modelId.startsWith("claude-sonnet-")) return config.claudeSonnet;
|
|
1288
|
+
if (modelId.startsWith("claude-haiku-")) return config.claudeHaiku;
|
|
1289
|
+
return modelId;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region src/translator/anthropic/anthropic-translator.ts
|
|
1294
|
+
var AnthropicTranslator = class {
|
|
1295
|
+
toOpenAI(payload) {
|
|
1296
|
+
return {
|
|
1297
|
+
model: this.translateModelName(payload.model),
|
|
1298
|
+
messages: this.translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1299
|
+
max_tokens: payload.max_tokens,
|
|
1300
|
+
stop: payload.stop_sequences,
|
|
1301
|
+
stream: payload.stream,
|
|
1302
|
+
temperature: payload.temperature,
|
|
1303
|
+
top_p: payload.top_p,
|
|
1304
|
+
user: payload.metadata?.user_id,
|
|
1305
|
+
tools: this.translateAnthropicToolsToOpenAI(payload.tools),
|
|
1306
|
+
tool_choice: this.translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
fromOpenAI(response) {
|
|
1310
|
+
const allTextBlocks = [];
|
|
1311
|
+
const allToolUseBlocks = [];
|
|
1312
|
+
let stopReason = null;
|
|
1313
|
+
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
1314
|
+
for (const choice of response.choices) {
|
|
1315
|
+
const textBlocks = this.getAnthropicTextBlocks(choice.message.content);
|
|
1316
|
+
const toolUseBlocks = this.getAnthropicToolUseBlocks(choice.message.tool_calls);
|
|
1317
|
+
allTextBlocks.push(...textBlocks);
|
|
1318
|
+
allToolUseBlocks.push(...toolUseBlocks);
|
|
1319
|
+
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
id: response.id,
|
|
1323
|
+
type: "message",
|
|
1324
|
+
role: "assistant",
|
|
1325
|
+
model: response.model,
|
|
1326
|
+
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
1327
|
+
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
1328
|
+
stop_sequence: null,
|
|
1329
|
+
usage: {
|
|
1330
|
+
input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1331
|
+
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
1332
|
+
...response.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.prompt_tokens_details.cached_tokens }
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
createStreamTranslator() {
|
|
1337
|
+
return new AnthropicStreamTranslator();
|
|
1338
|
+
}
|
|
1339
|
+
translateModelName(model) {
|
|
1340
|
+
const knownModelIds = state.cache.models ? new Set(state.cache.models.data.map((m) => m.id)) : void 0;
|
|
1341
|
+
const config = getModelFallbackConfig();
|
|
1342
|
+
return resolveModel(model, knownModelIds, config);
|
|
1343
|
+
}
|
|
1344
|
+
translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
|
|
1345
|
+
const systemMessages = this.handleSystemPrompt(system);
|
|
1346
|
+
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? this.handleUserMessage(message) : this.handleAssistantMessage(message));
|
|
1347
|
+
return [...systemMessages, ...otherMessages];
|
|
1348
|
+
}
|
|
1349
|
+
handleSystemPrompt(system) {
|
|
1350
|
+
if (!system) return [];
|
|
1351
|
+
if (typeof system === "string") return [{
|
|
1352
|
+
role: "system",
|
|
1353
|
+
content: system
|
|
1354
|
+
}];
|
|
1355
|
+
return [{
|
|
1356
|
+
role: "system",
|
|
1357
|
+
content: system.map((block) => block.text).join("\n\n")
|
|
1358
|
+
}];
|
|
1359
|
+
}
|
|
1360
|
+
handleUserMessage(message) {
|
|
1361
|
+
const newMessages = [];
|
|
1362
|
+
if (Array.isArray(message.content)) {
|
|
1363
|
+
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
1364
|
+
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
1365
|
+
for (const block of toolResultBlocks) newMessages.push({
|
|
1366
|
+
role: "tool",
|
|
1367
|
+
tool_call_id: block.tool_use_id,
|
|
1368
|
+
content: this.mapContent(block.content)
|
|
1369
|
+
});
|
|
1370
|
+
if (otherBlocks.length > 0) newMessages.push({
|
|
1371
|
+
role: "user",
|
|
1372
|
+
content: this.mapContent(otherBlocks)
|
|
1373
|
+
});
|
|
1374
|
+
} else newMessages.push({
|
|
1375
|
+
role: "user",
|
|
1376
|
+
content: this.mapContent(message.content)
|
|
1377
|
+
});
|
|
1378
|
+
return newMessages;
|
|
1379
|
+
}
|
|
1380
|
+
handleAssistantMessage(message) {
|
|
1381
|
+
if (!Array.isArray(message.content)) return [{
|
|
1382
|
+
role: "assistant",
|
|
1383
|
+
content: this.mapContent(message.content)
|
|
1384
|
+
}];
|
|
1385
|
+
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
1386
|
+
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
1387
|
+
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
1388
|
+
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
1389
|
+
return toolUseBlocks.length > 0 ? [{
|
|
1390
|
+
role: "assistant",
|
|
1391
|
+
content: allTextContent || null,
|
|
1392
|
+
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
1393
|
+
id: toolUse.id,
|
|
1394
|
+
type: "function",
|
|
1395
|
+
function: {
|
|
1396
|
+
name: toolUse.name,
|
|
1397
|
+
arguments: JSON.stringify(toolUse.input)
|
|
1398
|
+
}
|
|
1399
|
+
}))
|
|
1400
|
+
}] : [{
|
|
1401
|
+
role: "assistant",
|
|
1402
|
+
content: this.mapContent(message.content)
|
|
1403
|
+
}];
|
|
1404
|
+
}
|
|
1405
|
+
mapContent(content) {
|
|
1406
|
+
if (typeof content === "string") return content;
|
|
1407
|
+
if (!Array.isArray(content)) return null;
|
|
1408
|
+
if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
|
|
1409
|
+
const contentParts = [];
|
|
1410
|
+
for (const block of content) switch (block.type) {
|
|
1411
|
+
case "text":
|
|
1412
|
+
contentParts.push({
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: block.text
|
|
1415
|
+
});
|
|
1416
|
+
break;
|
|
1417
|
+
case "thinking":
|
|
1418
|
+
contentParts.push({
|
|
1419
|
+
type: "text",
|
|
1420
|
+
text: block.thinking
|
|
1421
|
+
});
|
|
1422
|
+
break;
|
|
1423
|
+
case "image":
|
|
1424
|
+
contentParts.push({
|
|
1425
|
+
type: "image_url",
|
|
1426
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
1427
|
+
});
|
|
1428
|
+
break;
|
|
1429
|
+
default: break;
|
|
1430
|
+
}
|
|
1431
|
+
return contentParts;
|
|
1432
|
+
}
|
|
1433
|
+
translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1434
|
+
if (!anthropicTools) return;
|
|
1435
|
+
return anthropicTools.map((tool) => ({
|
|
1436
|
+
type: "function",
|
|
1437
|
+
function: {
|
|
1438
|
+
name: tool.name,
|
|
1439
|
+
description: tool.description,
|
|
1440
|
+
parameters: tool.input_schema
|
|
1441
|
+
}
|
|
1442
|
+
}));
|
|
1443
|
+
}
|
|
1444
|
+
translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
|
|
1445
|
+
if (!anthropicToolChoice) return;
|
|
1446
|
+
switch (anthropicToolChoice.type) {
|
|
1447
|
+
case "auto": return "auto";
|
|
1448
|
+
case "any": return "required";
|
|
1449
|
+
case "tool":
|
|
1450
|
+
if (anthropicToolChoice.name) return {
|
|
1451
|
+
type: "function",
|
|
1452
|
+
function: { name: anthropicToolChoice.name }
|
|
1453
|
+
};
|
|
1454
|
+
return;
|
|
1455
|
+
case "none": return "none";
|
|
1456
|
+
default: return;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
getAnthropicTextBlocks(messageContent) {
|
|
1460
|
+
if (typeof messageContent === "string") return [{
|
|
1461
|
+
type: "text",
|
|
1462
|
+
text: messageContent
|
|
1463
|
+
}];
|
|
1464
|
+
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
1465
|
+
type: "text",
|
|
1466
|
+
text: part.text
|
|
1467
|
+
}));
|
|
1468
|
+
return [];
|
|
1469
|
+
}
|
|
1470
|
+
getAnthropicToolUseBlocks(toolCalls) {
|
|
1471
|
+
if (!toolCalls) return [];
|
|
1472
|
+
return toolCalls.map((toolCall) => ({
|
|
1473
|
+
type: "tool_use",
|
|
1474
|
+
id: toolCall.id,
|
|
1475
|
+
name: toolCall.function.name,
|
|
1476
|
+
input: JSON.parse(toolCall.function.arguments)
|
|
1477
|
+
}));
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
//#endregion
|
|
1482
|
+
//#region src/routes/messages/count-tokens-handler.ts
|
|
1483
|
+
/**
|
|
1484
|
+
* Handles token counting for Anthropic messages
|
|
1485
|
+
*/
|
|
1486
|
+
async function handleCountTokens(c) {
|
|
1487
|
+
const anthropicBeta = c.req.header("anthropic-beta");
|
|
1488
|
+
const anthropicPayload = parseAnthropicCountTokensPayload(await c.req.json());
|
|
1489
|
+
const normalizedPayload = {
|
|
1490
|
+
...anthropicPayload,
|
|
1491
|
+
max_tokens: anthropicPayload.max_tokens ?? 0
|
|
1492
|
+
};
|
|
1493
|
+
const openAIPayload = new AnthropicTranslator().toOpenAI(normalizedPayload);
|
|
1494
|
+
const selectedModel = state.cache.models?.data.find((model) => model.id === openAIPayload.model);
|
|
1495
|
+
if (!selectedModel) throw new HTTPError(`Model not found for token counting: "${openAIPayload.model}"`, new Response(`Model not found for token counting: "${openAIPayload.model}"`, { status: 400 }));
|
|
1496
|
+
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
1497
|
+
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
1498
|
+
let mcpToolExist = false;
|
|
1499
|
+
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
1500
|
+
if (!mcpToolExist) {
|
|
1501
|
+
if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
|
|
1502
|
+
else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
1506
|
+
if (anthropicPayload.model.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
|
|
1507
|
+
else if (anthropicPayload.model.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
|
|
1508
|
+
consola.info("Token count:", finalTokenCount);
|
|
1509
|
+
return c.json({ input_tokens: finalTokenCount });
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/routes/messages/handler.ts
|
|
1514
|
+
async function handleCompletion(c) {
|
|
1515
|
+
const anthropicPayload = parseAnthropicMessagesPayload(await c.req.json());
|
|
1516
|
+
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
|
|
1517
|
+
const translator = new AnthropicTranslator();
|
|
1518
|
+
const openAIPayload = translator.toOpenAI(anthropicPayload);
|
|
1519
|
+
setModelMappingInfo(c, {
|
|
1520
|
+
originalModel: anthropicPayload.model,
|
|
1521
|
+
mappedModel: openAIPayload.model
|
|
1522
|
+
});
|
|
1523
|
+
consola.debug("Claude Code requested model:", anthropicPayload.model, "-> Copilot model:", openAIPayload.model);
|
|
1524
|
+
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
|
|
1525
|
+
const response = await new CopilotClient(state.auth, getClientConfig(state)).createChatCompletions(openAIPayload, { signal: c.req.raw.signal });
|
|
1526
|
+
if (isNonStreaming(response)) {
|
|
1527
|
+
consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
1528
|
+
const anthropicResponse = translator.fromOpenAI(response);
|
|
1529
|
+
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
1530
|
+
return c.json(anthropicResponse);
|
|
1531
|
+
}
|
|
1532
|
+
consola.debug("Streaming response from Copilot");
|
|
1533
|
+
return streamSSE(c, async (stream) => {
|
|
1534
|
+
const streamTranslator = translator.createStreamTranslator();
|
|
1535
|
+
try {
|
|
1536
|
+
for await (const rawEvent of response) {
|
|
1537
|
+
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
1538
|
+
if (rawEvent.data === "[DONE]") break;
|
|
1539
|
+
if (!rawEvent.data) continue;
|
|
1540
|
+
const chunk = JSON.parse(rawEvent.data);
|
|
1541
|
+
const events$1 = streamTranslator.onChunk(chunk);
|
|
1542
|
+
for (const event of events$1) {
|
|
1543
|
+
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
1544
|
+
await stream.writeSSE({
|
|
1545
|
+
event: event.type,
|
|
1546
|
+
data: JSON.stringify(event)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
if (c.req.raw.signal.aborted) {
|
|
1552
|
+
consola.debug("Client disconnected during Anthropic stream");
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
consola.error("Error streaming Anthropic response:", error);
|
|
1556
|
+
const errorEvents = streamTranslator.onError(error);
|
|
1557
|
+
for (const event of errorEvents) await stream.writeSSE({
|
|
1558
|
+
event: event.type,
|
|
1559
|
+
data: JSON.stringify(event)
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function isNonStreaming(response) {
|
|
1565
|
+
return Object.hasOwn(response, "choices");
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
//#endregion
|
|
1569
|
+
//#region src/routes/messages/route.ts
|
|
1570
|
+
const messageRoutes = new Hono();
|
|
1571
|
+
messageRoutes.post("/", requestGuard, (c) => handleCompletion(c));
|
|
1572
|
+
messageRoutes.post("/count_tokens", (c) => handleCountTokens(c));
|
|
1573
|
+
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region src/routes/models/route.ts
|
|
1576
|
+
const modelRoutes = new Hono();
|
|
1577
|
+
modelRoutes.get("/", async (c) => {
|
|
1578
|
+
if (!state.cache.models) {
|
|
1579
|
+
const copilotClient = new CopilotClient(state.auth, getClientConfig(state));
|
|
1580
|
+
await cacheModels(copilotClient);
|
|
1581
|
+
}
|
|
1582
|
+
const models = state.cache.models?.data.map((model) => ({
|
|
1583
|
+
id: model.id,
|
|
1584
|
+
object: "model",
|
|
1585
|
+
type: "model",
|
|
1586
|
+
created: 0,
|
|
1587
|
+
created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1588
|
+
owned_by: model.vendor,
|
|
1589
|
+
display_name: model.name
|
|
1590
|
+
}));
|
|
1591
|
+
return c.json({
|
|
1592
|
+
object: "list",
|
|
1593
|
+
data: models,
|
|
1594
|
+
has_more: false
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region src/routes/token/route.ts
|
|
1600
|
+
const tokenRoute = new Hono();
|
|
1601
|
+
tokenRoute.get("/", (c) => {
|
|
1602
|
+
return c.json({ token: state.auth.copilotToken });
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/routes/usage/route.ts
|
|
1607
|
+
const usageRoute = new Hono();
|
|
1608
|
+
usageRoute.get("/", async (c) => {
|
|
1609
|
+
const usage = await new GitHubClient(state.auth, getClientConfig(state)).getCopilotUsage();
|
|
1610
|
+
return c.json(usage);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
//#endregion
|
|
1614
|
+
//#region src/server.ts
|
|
1615
|
+
const server = new Hono();
|
|
1616
|
+
server.use(requestLogger);
|
|
1617
|
+
server.use(cors());
|
|
1618
|
+
server.onError((error, c) => forwardError(c, error));
|
|
1619
|
+
server.get("/", (c) => c.text("Server running"));
|
|
1620
|
+
server.route("/chat/completions", completionRoutes);
|
|
1621
|
+
server.route("/models", modelRoutes);
|
|
1622
|
+
server.route("/embeddings", embeddingRoutes);
|
|
1623
|
+
server.route("/usage", usageRoute);
|
|
1624
|
+
server.route("/token", tokenRoute);
|
|
1625
|
+
server.route("/v1/chat/completions", completionRoutes);
|
|
1626
|
+
server.route("/v1/models", modelRoutes);
|
|
1627
|
+
server.route("/v1/embeddings", embeddingRoutes);
|
|
1628
|
+
server.route("/v1/messages", messageRoutes);
|
|
1629
|
+
|
|
1630
|
+
//#endregion
|
|
1631
|
+
//#region src/start.ts
|
|
1632
|
+
async function maybeCopyClaudeCodeCommand(serverUrl) {
|
|
1633
|
+
if (!state.cache.models) return;
|
|
1634
|
+
const selectableModels = state.cache.models.data.filter((model) => model.model_picker_enabled);
|
|
1635
|
+
const modelOptions = selectableModels.length > 0 ? selectableModels : state.cache.models.data;
|
|
1636
|
+
const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1637
|
+
type: "select",
|
|
1638
|
+
options: modelOptions.map((model) => model.id)
|
|
1639
|
+
});
|
|
1640
|
+
const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1641
|
+
type: "select",
|
|
1642
|
+
options: modelOptions.map((model) => model.id)
|
|
1643
|
+
});
|
|
1644
|
+
const command = generateEnvScript({
|
|
1645
|
+
ANTHROPIC_BASE_URL: serverUrl,
|
|
1646
|
+
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1647
|
+
ANTHROPIC_MODEL: selectedModel,
|
|
1648
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
|
|
1649
|
+
ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
|
|
1650
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
|
|
1651
|
+
DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
|
|
1652
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
1653
|
+
}, "claude");
|
|
1654
|
+
try {
|
|
1655
|
+
clipboard.writeSync(command);
|
|
1656
|
+
consola.success("Copied Claude Code command to clipboard!");
|
|
1657
|
+
} catch {
|
|
1658
|
+
consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
|
|
1659
|
+
consola.log(command);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function runServer(options) {
|
|
1663
|
+
const accountType = options.accountType === "individual" || options.accountType === "business" || options.accountType === "enterprise" ? options.accountType : "individual";
|
|
1664
|
+
if (accountType !== options.accountType) consola.warn(`Unknown account type "${options.accountType}". Falling back to "individual".`);
|
|
1665
|
+
if (options.proxyEnv) initProxyFromEnv();
|
|
1666
|
+
if (options.verbose) {
|
|
1667
|
+
consola.level = 5;
|
|
1668
|
+
consola.info("Verbose logging enabled");
|
|
1669
|
+
}
|
|
1670
|
+
state.config.accountType = accountType;
|
|
1671
|
+
if (accountType !== "individual") consola.info(`Using ${accountType} plan GitHub account`);
|
|
1672
|
+
if (options.githubToken) {
|
|
1673
|
+
state.auth.githubToken = options.githubToken;
|
|
1674
|
+
consola.info("Using provided GitHub token");
|
|
1675
|
+
}
|
|
1676
|
+
state.config.manualApprove = options.manual;
|
|
1677
|
+
state.config.rateLimitSeconds = options.rateLimit;
|
|
1678
|
+
state.config.rateLimitWait = options.rateLimitWait;
|
|
1679
|
+
state.config.showToken = options.showToken;
|
|
1680
|
+
await ensurePaths();
|
|
1681
|
+
await readConfig();
|
|
1682
|
+
await cacheVSCodeVersion();
|
|
1683
|
+
if (!options.githubToken) await setupGitHubToken();
|
|
1684
|
+
await setupCopilotToken();
|
|
1685
|
+
const clientConfig = {
|
|
1686
|
+
...getClientConfig(state),
|
|
1687
|
+
accountType
|
|
1688
|
+
};
|
|
1689
|
+
const copilotClient = new CopilotClient(state.auth, clientConfig);
|
|
1690
|
+
await cacheModels(copilotClient);
|
|
1691
|
+
consola.info(`Available models: \n${state.cache.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
|
|
1692
|
+
const serverUrl = `http://localhost:${options.port}`;
|
|
1693
|
+
if (options.claudeCode) {
|
|
1694
|
+
invariant(state.cache.models, "Models should be loaded by now");
|
|
1695
|
+
await maybeCopyClaudeCodeCommand(serverUrl);
|
|
1696
|
+
}
|
|
1697
|
+
serve({
|
|
1698
|
+
fetch: server.fetch,
|
|
1699
|
+
port: options.port,
|
|
1700
|
+
bun: options.idleTimeoutSeconds === void 0 ? void 0 : { idleTimeout: options.idleTimeoutSeconds }
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
const start = defineCommand({
|
|
1704
|
+
meta: {
|
|
1705
|
+
name: "start",
|
|
1706
|
+
description: "Start the Copilot API server"
|
|
1707
|
+
},
|
|
1708
|
+
args: {
|
|
1709
|
+
"port": {
|
|
1710
|
+
alias: "p",
|
|
1711
|
+
type: "string",
|
|
1712
|
+
default: "4141",
|
|
1713
|
+
description: "Port to listen on"
|
|
1714
|
+
},
|
|
1715
|
+
"verbose": {
|
|
1716
|
+
alias: "v",
|
|
1717
|
+
type: "boolean",
|
|
1718
|
+
default: false,
|
|
1719
|
+
description: "Enable verbose logging"
|
|
1720
|
+
},
|
|
1721
|
+
"account-type": {
|
|
1722
|
+
alias: "a",
|
|
1723
|
+
type: "string",
|
|
1724
|
+
default: "individual",
|
|
1725
|
+
description: "Account type to use (individual, business, enterprise)"
|
|
1726
|
+
},
|
|
1727
|
+
"manual": {
|
|
1728
|
+
type: "boolean",
|
|
1729
|
+
default: false,
|
|
1730
|
+
description: "Enable manual request approval"
|
|
1731
|
+
},
|
|
1732
|
+
"rate-limit": {
|
|
1733
|
+
alias: "r",
|
|
1734
|
+
type: "string",
|
|
1735
|
+
description: "Rate limit in seconds between requests"
|
|
1736
|
+
},
|
|
1737
|
+
"wait": {
|
|
1738
|
+
alias: "w",
|
|
1739
|
+
type: "boolean",
|
|
1740
|
+
default: false,
|
|
1741
|
+
description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
|
|
1742
|
+
},
|
|
1743
|
+
"github-token": {
|
|
1744
|
+
alias: "g",
|
|
1745
|
+
type: "string",
|
|
1746
|
+
description: "Provide GitHub token directly (must be generated using the `auth` subcommand)"
|
|
1747
|
+
},
|
|
1748
|
+
"claude-code": {
|
|
1749
|
+
alias: "c",
|
|
1750
|
+
type: "boolean",
|
|
1751
|
+
default: false,
|
|
1752
|
+
description: "Generate a command to launch Claude Code with Copilot API config"
|
|
1753
|
+
},
|
|
1754
|
+
"show-token": {
|
|
1755
|
+
type: "boolean",
|
|
1756
|
+
default: false,
|
|
1757
|
+
description: "Show GitHub and Copilot tokens on fetch and refresh"
|
|
1758
|
+
},
|
|
1759
|
+
"proxy-env": {
|
|
1760
|
+
type: "boolean",
|
|
1761
|
+
default: false,
|
|
1762
|
+
description: "Initialize proxy from environment variables"
|
|
1763
|
+
},
|
|
1764
|
+
"idle-timeout": {
|
|
1765
|
+
type: "string",
|
|
1766
|
+
default: "120",
|
|
1767
|
+
description: "Bun server idle timeout in seconds"
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
run({ args }) {
|
|
1771
|
+
const rateLimitRaw = args["rate-limit"];
|
|
1772
|
+
const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
|
|
1773
|
+
const idleTimeoutRaw = args["idle-timeout"];
|
|
1774
|
+
let idleTimeoutSeconds = idleTimeoutRaw === void 0 ? void 0 : Number.parseInt(idleTimeoutRaw, 10);
|
|
1775
|
+
if (idleTimeoutSeconds !== void 0 && (Number.isNaN(idleTimeoutSeconds) || idleTimeoutSeconds < 0)) {
|
|
1776
|
+
consola.warn(`Invalid --idle-timeout value "${idleTimeoutRaw}". Falling back to Bun default.`);
|
|
1777
|
+
idleTimeoutSeconds = void 0;
|
|
1778
|
+
}
|
|
1779
|
+
return runServer({
|
|
1780
|
+
port: Number.parseInt(args.port, 10),
|
|
1781
|
+
verbose: args.verbose,
|
|
1782
|
+
accountType: args["account-type"],
|
|
1783
|
+
manual: args.manual,
|
|
1784
|
+
rateLimit,
|
|
1785
|
+
rateLimitWait: args.wait,
|
|
1786
|
+
githubToken: args["github-token"],
|
|
1787
|
+
claudeCode: args["claude-code"],
|
|
1788
|
+
showToken: args["show-token"],
|
|
1789
|
+
proxyEnv: args["proxy-env"],
|
|
1790
|
+
idleTimeoutSeconds
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region src/main.ts
|
|
1797
|
+
const main = defineCommand({
|
|
1798
|
+
meta: {
|
|
1799
|
+
name: "ghc-proxy",
|
|
1800
|
+
description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
|
|
1801
|
+
},
|
|
1802
|
+
subCommands: {
|
|
1803
|
+
auth,
|
|
1804
|
+
start,
|
|
1805
|
+
"check-usage": checkUsage,
|
|
1806
|
+
debug
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
runMain(main).catch((error) => {
|
|
1810
|
+
consola.error("Failed to start CLI:", error);
|
|
1811
|
+
process.exitCode = 1;
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
//#endregion
|
|
1815
|
+
export { };
|
|
1816
|
+
//# sourceMappingURL=main.js.map
|