@ashsec/copilot-api 0.8.0 → 0.9.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 +159 -227
- package/dist/main.js.map +1 -1
- package/package.json +69 -69
package/dist/main.js
CHANGED
|
@@ -5,7 +5,6 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { events } from "fetch-event-stream";
|
|
9
8
|
import clipboard from "clipboardy";
|
|
10
9
|
import { serve } from "srvx";
|
|
11
10
|
import invariant from "tiny-invariant";
|
|
@@ -17,28 +16,102 @@ import process$1 from "node:process";
|
|
|
17
16
|
import { Hono } from "hono";
|
|
18
17
|
import { cors } from "hono/cors";
|
|
19
18
|
import { streamSSE } from "hono/streaming";
|
|
19
|
+
import { events } from "fetch-event-stream";
|
|
20
20
|
import util from "node:util";
|
|
21
21
|
|
|
22
22
|
//#region package.json
|
|
23
|
-
var
|
|
23
|
+
var name = "@ashsec/copilot-api";
|
|
24
|
+
var version = "0.9.0";
|
|
25
|
+
var description = "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!";
|
|
26
|
+
var keywords = [
|
|
27
|
+
"proxy",
|
|
28
|
+
"github-copilot",
|
|
29
|
+
"openai-compatible"
|
|
30
|
+
];
|
|
31
|
+
var homepage = "https://github.com/ericc-ch/copilot-api";
|
|
32
|
+
var bugs = "https://github.com/ericc-ch/copilot-api/issues";
|
|
33
|
+
var repository = {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ericc-ch/copilot-api.git"
|
|
36
|
+
};
|
|
37
|
+
var author = "Erick Christian <erickchristian48@gmail.com>";
|
|
38
|
+
var type = "module";
|
|
39
|
+
var bin = { "copilot-api": "./dist/main.js" };
|
|
40
|
+
var files = ["dist"];
|
|
41
|
+
var scripts = {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "bun run --watch ./src/main.ts",
|
|
44
|
+
"knip": "knip-bun",
|
|
45
|
+
"lint": "eslint --cache",
|
|
46
|
+
"lint:all": "eslint --cache .",
|
|
47
|
+
"prepack": "bun run build",
|
|
48
|
+
"prepare": "simple-git-hooks",
|
|
49
|
+
"release": "bumpp && bun publish --access public",
|
|
50
|
+
"start": "NODE_ENV=production bun run ./src/main.ts",
|
|
51
|
+
"typecheck": "tsc"
|
|
52
|
+
};
|
|
53
|
+
var simple_git_hooks = { "pre-commit": "bunx lint-staged" };
|
|
54
|
+
var lint_staged = { "*": "bun run lint --fix" };
|
|
55
|
+
var dependencies = {
|
|
56
|
+
"citty": "^0.1.6",
|
|
57
|
+
"clipboardy": "^5.0.0",
|
|
58
|
+
"consola": "^3.4.2",
|
|
59
|
+
"fetch-event-stream": "^0.1.5",
|
|
60
|
+
"gpt-tokenizer": "^3.0.1",
|
|
61
|
+
"hono": "^4.9.9",
|
|
62
|
+
"ms": "^2.1.3",
|
|
63
|
+
"proxy-from-env": "^1.1.0",
|
|
64
|
+
"srvx": "^0.8.9",
|
|
65
|
+
"tiny-invariant": "^1.3.3",
|
|
66
|
+
"undici": "^7.16.0",
|
|
67
|
+
"zod": "^4.1.11"
|
|
68
|
+
};
|
|
69
|
+
var devDependencies = {
|
|
70
|
+
"@echristian/eslint-config": "^0.0.54",
|
|
71
|
+
"@types/bun": "^1.2.23",
|
|
72
|
+
"@types/proxy-from-env": "^1.0.4",
|
|
73
|
+
"bumpp": "^10.2.3",
|
|
74
|
+
"eslint": "^9.37.0",
|
|
75
|
+
"knip": "^5.64.1",
|
|
76
|
+
"lint-staged": "^16.2.3",
|
|
77
|
+
"prettier-plugin-packagejson": "^2.5.19",
|
|
78
|
+
"simple-git-hooks": "^2.13.1",
|
|
79
|
+
"tsdown": "^0.15.6",
|
|
80
|
+
"typescript": "^5.9.3"
|
|
81
|
+
};
|
|
82
|
+
var package_default = {
|
|
83
|
+
name,
|
|
84
|
+
version,
|
|
85
|
+
description,
|
|
86
|
+
keywords,
|
|
87
|
+
homepage,
|
|
88
|
+
bugs,
|
|
89
|
+
repository,
|
|
90
|
+
author,
|
|
91
|
+
type,
|
|
92
|
+
bin,
|
|
93
|
+
files,
|
|
94
|
+
scripts,
|
|
95
|
+
"simple-git-hooks": simple_git_hooks,
|
|
96
|
+
"lint-staged": lint_staged,
|
|
97
|
+
dependencies,
|
|
98
|
+
devDependencies
|
|
99
|
+
};
|
|
24
100
|
|
|
25
101
|
//#endregion
|
|
26
102
|
//#region src/lib/paths.ts
|
|
27
103
|
const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
|
|
28
104
|
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
29
|
-
const AZURE_OPENAI_CONFIG_PATH = path.join(APP_DIR, "azure_openai_config");
|
|
30
105
|
const REPLACEMENTS_CONFIG_PATH = path.join(APP_DIR, "replacements.json");
|
|
31
106
|
const PATHS = {
|
|
32
107
|
APP_DIR,
|
|
33
108
|
CONFIG_PATH: path.join(APP_DIR, "config.json"),
|
|
34
109
|
GITHUB_TOKEN_PATH,
|
|
35
|
-
AZURE_OPENAI_CONFIG_PATH,
|
|
36
110
|
REPLACEMENTS_CONFIG_PATH
|
|
37
111
|
};
|
|
38
112
|
async function ensurePaths() {
|
|
39
113
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
40
114
|
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
41
|
-
await ensureFile(PATHS.AZURE_OPENAI_CONFIG_PATH);
|
|
42
115
|
}
|
|
43
116
|
async function ensureFile(filePath) {
|
|
44
117
|
try {
|
|
@@ -180,56 +253,6 @@ async function getGitHubUser() {
|
|
|
180
253
|
return await response.json();
|
|
181
254
|
}
|
|
182
255
|
|
|
183
|
-
//#endregion
|
|
184
|
-
//#region src/services/azure-openai/config.ts
|
|
185
|
-
const AZURE_OPENAI_MODEL_PREFIX = "azure_openai_";
|
|
186
|
-
async function loadAzureOpenAIConfig() {
|
|
187
|
-
try {
|
|
188
|
-
const content = await fs.readFile(PATHS.AZURE_OPENAI_CONFIG_PATH, "utf8");
|
|
189
|
-
if (!content.trim()) return null;
|
|
190
|
-
const decoded = Buffer.from(content.trim(), "base64").toString("utf8");
|
|
191
|
-
const config$1 = JSON.parse(decoded);
|
|
192
|
-
if (!config$1.endpoint || !config$1.apiKey) return null;
|
|
193
|
-
return config$1;
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
async function saveAzureOpenAIConfig(config$1) {
|
|
199
|
-
const encoded = Buffer.from(JSON.stringify(config$1)).toString("base64");
|
|
200
|
-
await fs.writeFile(PATHS.AZURE_OPENAI_CONFIG_PATH, encoded, "utf8");
|
|
201
|
-
await fs.chmod(PATHS.AZURE_OPENAI_CONFIG_PATH, 384);
|
|
202
|
-
consola.success("Azure OpenAI configuration saved");
|
|
203
|
-
}
|
|
204
|
-
async function promptAzureOpenAISetup() {
|
|
205
|
-
if (!await consola.prompt("Would you like to add a custom Azure OpenAI endpoint?", {
|
|
206
|
-
type: "confirm",
|
|
207
|
-
initial: false
|
|
208
|
-
})) return null;
|
|
209
|
-
const endpoint = await consola.prompt("Enter your Azure OpenAI endpoint URL (e.g., https://your-resource.openai.azure.com):", { type: "text" });
|
|
210
|
-
if (!endpoint || typeof endpoint !== "string" || !endpoint.trim()) {
|
|
211
|
-
consola.warn("No endpoint provided, skipping Azure OpenAI setup");
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
const apiKey = await consola.prompt("Enter your Azure OpenAI API key:", { type: "text" });
|
|
215
|
-
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
|
216
|
-
consola.warn("No API key provided, skipping Azure OpenAI setup");
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
const config$1 = {
|
|
220
|
-
endpoint: endpoint.trim().replace(/\/$/, ""),
|
|
221
|
-
apiKey: apiKey.trim()
|
|
222
|
-
};
|
|
223
|
-
await saveAzureOpenAIConfig(config$1);
|
|
224
|
-
return config$1;
|
|
225
|
-
}
|
|
226
|
-
function isAzureOpenAIModel(modelId) {
|
|
227
|
-
return modelId.startsWith(AZURE_OPENAI_MODEL_PREFIX);
|
|
228
|
-
}
|
|
229
|
-
function getAzureDeploymentName(modelId) {
|
|
230
|
-
return modelId.slice(13);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
256
|
//#endregion
|
|
234
257
|
//#region src/lib/retry-fetch.ts
|
|
235
258
|
const RETRY_DELAYS_MS = [
|
|
@@ -299,62 +322,6 @@ async function fetchWithRetry(input, init) {
|
|
|
299
322
|
throw lastError;
|
|
300
323
|
}
|
|
301
324
|
|
|
302
|
-
//#endregion
|
|
303
|
-
//#region src/services/azure-openai/create-chat-completions.ts
|
|
304
|
-
const AZURE_API_VERSION = "2024-10-21";
|
|
305
|
-
async function createAzureOpenAIChatCompletions(config$1, payload) {
|
|
306
|
-
const deploymentName = getAzureDeploymentName(payload.model);
|
|
307
|
-
const { max_tokens,...restPayload } = payload;
|
|
308
|
-
const azurePayload = {
|
|
309
|
-
...restPayload,
|
|
310
|
-
model: deploymentName,
|
|
311
|
-
...max_tokens != null && { max_completion_tokens: max_tokens }
|
|
312
|
-
};
|
|
313
|
-
const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
|
|
314
|
-
method: "POST",
|
|
315
|
-
headers: {
|
|
316
|
-
"api-key": config$1.apiKey,
|
|
317
|
-
"Content-Type": "application/json"
|
|
318
|
-
},
|
|
319
|
-
body: JSON.stringify(azurePayload)
|
|
320
|
-
});
|
|
321
|
-
if (!response.ok) {
|
|
322
|
-
consola.error("Failed to create Azure OpenAI chat completions:", response);
|
|
323
|
-
throw new HTTPError("Failed to create Azure OpenAI chat completions", response, payload);
|
|
324
|
-
}
|
|
325
|
-
if (payload.stream) return events(response);
|
|
326
|
-
return await response.json();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
//#endregion
|
|
330
|
-
//#region src/services/azure-openai/get-models.ts
|
|
331
|
-
const AZURE_DEPLOYMENTS_API_VERSION = "2022-12-01";
|
|
332
|
-
async function getAzureOpenAIDeployments(config$1) {
|
|
333
|
-
try {
|
|
334
|
-
const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
|
|
335
|
-
"api-key": config$1.apiKey,
|
|
336
|
-
"Content-Type": "application/json"
|
|
337
|
-
} });
|
|
338
|
-
if (!response.ok) {
|
|
339
|
-
const errorText = await response.text().catch(() => "");
|
|
340
|
-
consola.error(`Failed to fetch Azure OpenAI deployments: ${response.status}`, errorText);
|
|
341
|
-
throw new HTTPError("Failed to fetch Azure OpenAI deployments", response);
|
|
342
|
-
}
|
|
343
|
-
return (await response.json()).data.filter((deployment) => deployment.status === "succeeded").map((deployment) => ({
|
|
344
|
-
id: `${AZURE_OPENAI_MODEL_PREFIX}${deployment.id}`,
|
|
345
|
-
deploymentName: deployment.id,
|
|
346
|
-
model: deployment.model,
|
|
347
|
-
created: deployment.created_at,
|
|
348
|
-
object: "deployment",
|
|
349
|
-
owned_by: deployment.owner || "azure-openai"
|
|
350
|
-
}));
|
|
351
|
-
} catch (error) {
|
|
352
|
-
if (error instanceof HTTPError) throw error;
|
|
353
|
-
consola.error("Failed to fetch Azure OpenAI deployments:", error);
|
|
354
|
-
return [];
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
325
|
//#endregion
|
|
359
326
|
//#region src/services/copilot/get-models.ts
|
|
360
327
|
const getModels = async () => {
|
|
@@ -418,24 +385,6 @@ const cacheVSCodeVersion = async () => {
|
|
|
418
385
|
state.vsCodeVersion = response;
|
|
419
386
|
consola.info(`Using VSCode version: ${response}`);
|
|
420
387
|
};
|
|
421
|
-
async function setupAzureOpenAI() {
|
|
422
|
-
let config$1 = await loadAzureOpenAIConfig();
|
|
423
|
-
if (!config$1) config$1 = await promptAzureOpenAISetup();
|
|
424
|
-
if (!config$1) {
|
|
425
|
-
consola.info("Azure OpenAI not configured");
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
state.azureOpenAIConfig = config$1;
|
|
429
|
-
consola.info("Azure OpenAI configuration loaded");
|
|
430
|
-
try {
|
|
431
|
-
const deployments = await getAzureOpenAIDeployments(config$1);
|
|
432
|
-
state.azureOpenAIDeployments = deployments;
|
|
433
|
-
if (deployments.length > 0) consola.info(`Loaded ${deployments.length} Azure OpenAI deployment(s):\n${deployments.map((d) => `- ${d.id} (${d.model})`).join("\n")}`);
|
|
434
|
-
else consola.warn("No Azure OpenAI deployments found");
|
|
435
|
-
} catch (error) {
|
|
436
|
-
consola.warn("Failed to fetch Azure OpenAI deployments:", error);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
388
|
|
|
440
389
|
//#endregion
|
|
441
390
|
//#region src/services/github/poll-access-token.ts
|
|
@@ -583,13 +532,13 @@ const checkUsage = defineCommand({
|
|
|
583
532
|
const premiumUsed = premiumTotal - premium.remaining;
|
|
584
533
|
const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
|
|
585
534
|
const premiumPercentRemaining = premium.percent_remaining;
|
|
586
|
-
function summarizeQuota(name, snap) {
|
|
587
|
-
if (!snap) return `${name}: N/A`;
|
|
535
|
+
function summarizeQuota(name$1, snap) {
|
|
536
|
+
if (!snap) return `${name$1}: N/A`;
|
|
588
537
|
const total = snap.entitlement;
|
|
589
538
|
const used = total - snap.remaining;
|
|
590
539
|
const percentUsed = total > 0 ? used / total * 100 : 0;
|
|
591
540
|
const percentRemaining = snap.percent_remaining;
|
|
592
|
-
return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
|
|
541
|
+
return `${name$1}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
|
|
593
542
|
}
|
|
594
543
|
const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
|
|
595
544
|
const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
|
|
@@ -665,11 +614,11 @@ async function getUserReplacements() {
|
|
|
665
614
|
* Add a new user replacement rule
|
|
666
615
|
*/
|
|
667
616
|
async function addReplacement(pattern, replacement, options) {
|
|
668
|
-
const { isRegex = false, name } = options ?? {};
|
|
617
|
+
const { isRegex = false, name: name$1 } = options ?? {};
|
|
669
618
|
await ensureLoaded();
|
|
670
619
|
const rule = {
|
|
671
620
|
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
672
|
-
name,
|
|
621
|
+
name: name$1,
|
|
673
622
|
pattern,
|
|
674
623
|
replacement,
|
|
675
624
|
isRegex,
|
|
@@ -820,11 +769,11 @@ async function applyReplacementsToPayload(payload) {
|
|
|
820
769
|
//#region src/config.ts
|
|
821
770
|
function formatRule(rule, index) {
|
|
822
771
|
const status = rule.enabled ? "✓" : "✗";
|
|
823
|
-
const type = rule.isRegex ? "regex" : "string";
|
|
772
|
+
const type$1 = rule.isRegex ? "regex" : "string";
|
|
824
773
|
const system = rule.isSystem ? " [system]" : "";
|
|
825
|
-
const name = rule.name ? ` "${rule.name}"` : "";
|
|
774
|
+
const name$1 = rule.name ? ` "${rule.name}"` : "";
|
|
826
775
|
const replacement = rule.replacement || "(empty)";
|
|
827
|
-
return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
|
|
776
|
+
return `${index + 1}. [${status}] (${type$1})${system}${name$1} "${rule.pattern}" → "${replacement}"`;
|
|
828
777
|
}
|
|
829
778
|
async function listReplacements() {
|
|
830
779
|
const all = await getAllReplacements();
|
|
@@ -837,11 +786,11 @@ async function listReplacements() {
|
|
|
837
786
|
console.log();
|
|
838
787
|
}
|
|
839
788
|
async function addNewReplacement() {
|
|
840
|
-
const name = await consola.prompt("Name (optional, short description):", {
|
|
789
|
+
const name$1 = await consola.prompt("Name (optional, short description):", {
|
|
841
790
|
type: "text",
|
|
842
791
|
default: ""
|
|
843
792
|
});
|
|
844
|
-
if (typeof name === "symbol") {
|
|
793
|
+
if (typeof name$1 === "symbol") {
|
|
845
794
|
consola.info("Cancelled.");
|
|
846
795
|
return;
|
|
847
796
|
}
|
|
@@ -878,7 +827,7 @@ async function addNewReplacement() {
|
|
|
878
827
|
consola.info("Cancelled.");
|
|
879
828
|
return;
|
|
880
829
|
}
|
|
881
|
-
const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
|
|
830
|
+
const rule = await addReplacement(pattern, replacement, matchType === "regex", name$1 || void 0);
|
|
882
831
|
consola.success(`Added rule: ${rule.name || rule.id}`);
|
|
883
832
|
}
|
|
884
833
|
async function editExistingReplacement() {
|
|
@@ -906,11 +855,11 @@ async function editExistingReplacement() {
|
|
|
906
855
|
}
|
|
907
856
|
consola.info(`\nEditing rule: ${rule.name || rule.id}`);
|
|
908
857
|
consola.info("Press Enter to keep current value.\n");
|
|
909
|
-
const name = await consola.prompt("Name:", {
|
|
858
|
+
const name$1 = await consola.prompt("Name:", {
|
|
910
859
|
type: "text",
|
|
911
860
|
default: rule.name || ""
|
|
912
861
|
});
|
|
913
|
-
if (typeof name === "symbol") {
|
|
862
|
+
if (typeof name$1 === "symbol") {
|
|
914
863
|
consola.info("Cancelled.");
|
|
915
864
|
return;
|
|
916
865
|
}
|
|
@@ -952,7 +901,7 @@ async function editExistingReplacement() {
|
|
|
952
901
|
return;
|
|
953
902
|
}
|
|
954
903
|
const updated = await updateReplacement(selected, {
|
|
955
|
-
name: name || void 0,
|
|
904
|
+
name: name$1 || void 0,
|
|
956
905
|
pattern,
|
|
957
906
|
replacement,
|
|
958
907
|
isRegex: matchType === "regex"
|
|
@@ -1269,7 +1218,8 @@ function mergeDefaultExtraPrompts(config$1) {
|
|
|
1269
1218
|
};
|
|
1270
1219
|
}
|
|
1271
1220
|
function mergeConfigWithDefaults() {
|
|
1272
|
-
const
|
|
1221
|
+
const config$1 = readConfigFromDisk();
|
|
1222
|
+
const { mergedConfig, changed } = mergeDefaultExtraPrompts(config$1);
|
|
1273
1223
|
if (changed) try {
|
|
1274
1224
|
fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
|
|
1275
1225
|
} catch (writeError) {
|
|
@@ -1347,7 +1297,8 @@ function getShell() {
|
|
|
1347
1297
|
const { platform, ppid, env } = process$1;
|
|
1348
1298
|
if (platform === "win32") {
|
|
1349
1299
|
try {
|
|
1350
|
-
|
|
1300
|
+
const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
|
|
1301
|
+
if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
|
|
1351
1302
|
} catch {
|
|
1352
1303
|
return "cmd";
|
|
1353
1304
|
}
|
|
@@ -1405,7 +1356,8 @@ function normalizeApiKeys(apiKeys) {
|
|
|
1405
1356
|
return [...new Set(normalizedKeys)];
|
|
1406
1357
|
}
|
|
1407
1358
|
function getConfiguredApiKeys() {
|
|
1408
|
-
|
|
1359
|
+
const config$1 = getConfig();
|
|
1360
|
+
return normalizeApiKeys(config$1.auth?.apiKeys);
|
|
1409
1361
|
}
|
|
1410
1362
|
function extractRequestApiKey(c) {
|
|
1411
1363
|
const xApiKey = c.req.header("x-api-key")?.trim();
|
|
@@ -1498,7 +1450,8 @@ async function logRawRequest(c) {
|
|
|
1498
1450
|
if (method !== "GET" && method !== "HEAD") try {
|
|
1499
1451
|
const body = await c.req.raw.clone().text();
|
|
1500
1452
|
if (body) try {
|
|
1501
|
-
const
|
|
1453
|
+
const parsed = JSON.parse(body);
|
|
1454
|
+
const sanitized = sanitizeRequestBody(parsed);
|
|
1502
1455
|
lines.push(`${colors.dim}Body (sanitized):${colors.reset}`, ` ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`);
|
|
1503
1456
|
} catch {
|
|
1504
1457
|
lines.push(`${colors.dim}Body:${colors.reset} [${body.length} bytes]`);
|
|
@@ -1539,7 +1492,7 @@ async function requestLogger(c, next) {
|
|
|
1539
1492
|
const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
|
|
1540
1493
|
lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
|
|
1541
1494
|
if (ctx?.provider && ctx.model) {
|
|
1542
|
-
const providerColor =
|
|
1495
|
+
const providerColor = colors.magenta;
|
|
1543
1496
|
lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
|
|
1544
1497
|
}
|
|
1545
1498
|
if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
|
|
@@ -1778,7 +1731,8 @@ const numTokensForTools = (tools, encoder, constants) => {
|
|
|
1778
1731
|
* Calculate the token count of messages, supporting multiple GPT encoders
|
|
1779
1732
|
*/
|
|
1780
1733
|
const getTokenCount = async (payload, model) => {
|
|
1781
|
-
const
|
|
1734
|
+
const tokenizer = getTokenizerFromModel(model);
|
|
1735
|
+
const encoder = await getEncodeChatFunction(tokenizer);
|
|
1782
1736
|
const simplifiedMessages = payload.messages;
|
|
1783
1737
|
const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
|
|
1784
1738
|
const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
|
|
@@ -1823,50 +1777,23 @@ const createChatCompletions = async (payload, options) => {
|
|
|
1823
1777
|
//#region src/routes/chat-completions/handler.ts
|
|
1824
1778
|
async function handleCompletion$1(c) {
|
|
1825
1779
|
await checkRateLimit(state);
|
|
1826
|
-
|
|
1780
|
+
const rawPayload = await c.req.json();
|
|
1781
|
+
let payload = await applyReplacementsToPayload(rawPayload);
|
|
1827
1782
|
payload = {
|
|
1828
1783
|
...payload,
|
|
1829
1784
|
model: normalizeModelName(payload.model)
|
|
1830
1785
|
};
|
|
1831
1786
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
1832
|
-
if (isAzureOpenAIModel(payload.model)) {
|
|
1833
|
-
if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
|
|
1834
|
-
setRequestContext(c, {
|
|
1835
|
-
provider: "Azure OpenAI",
|
|
1836
|
-
model: payload.model
|
|
1837
|
-
});
|
|
1838
|
-
if (state.manualApprove) await awaitApproval();
|
|
1839
|
-
const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
|
|
1840
|
-
if (isNonStreaming$1(response$1)) {
|
|
1841
|
-
consola.debug("Non-streaming response:", JSON.stringify(response$1));
|
|
1842
|
-
if (response$1.usage) setRequestContext(c, {
|
|
1843
|
-
inputTokens: response$1.usage.prompt_tokens,
|
|
1844
|
-
outputTokens: response$1.usage.completion_tokens
|
|
1845
|
-
});
|
|
1846
|
-
return c.json(response$1);
|
|
1847
|
-
}
|
|
1848
|
-
consola.debug("Streaming response");
|
|
1849
|
-
return streamSSE(c, async (stream) => {
|
|
1850
|
-
for await (const chunk of response$1) {
|
|
1851
|
-
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1852
|
-
if (chunk.data && chunk.data !== "[DONE]") {
|
|
1853
|
-
const parsed = JSON.parse(chunk.data);
|
|
1854
|
-
if (parsed.usage) setRequestContext(c, {
|
|
1855
|
-
inputTokens: parsed.usage.prompt_tokens,
|
|
1856
|
-
outputTokens: parsed.usage.completion_tokens
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
await stream.writeSSE(chunk);
|
|
1860
|
-
}
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
1787
|
setRequestContext(c, {
|
|
1864
1788
|
provider: "Copilot",
|
|
1865
1789
|
model: payload.model
|
|
1866
1790
|
});
|
|
1867
1791
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1868
1792
|
try {
|
|
1869
|
-
if (selectedModel)
|
|
1793
|
+
if (selectedModel) {
|
|
1794
|
+
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
1795
|
+
setRequestContext(c, { inputTokens: tokenCount.input });
|
|
1796
|
+
}
|
|
1870
1797
|
} catch (error) {
|
|
1871
1798
|
consola.warn("Failed to calculate token count:", error);
|
|
1872
1799
|
}
|
|
@@ -1933,7 +1860,8 @@ const createEmbeddings = async (payload) => {
|
|
|
1933
1860
|
const embeddingRoutes = new Hono();
|
|
1934
1861
|
embeddingRoutes.post("/", async (c) => {
|
|
1935
1862
|
try {
|
|
1936
|
-
const
|
|
1863
|
+
const paylod = await c.req.json();
|
|
1864
|
+
const response = await createEmbeddings(paylod);
|
|
1937
1865
|
return c.json(response);
|
|
1938
1866
|
} catch (error) {
|
|
1939
1867
|
return await forwardError(c, error);
|
|
@@ -2219,8 +2147,8 @@ const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : u
|
|
|
2219
2147
|
depth: null,
|
|
2220
2148
|
colors: false
|
|
2221
2149
|
})).join(" ");
|
|
2222
|
-
const sanitizeName = (name) => {
|
|
2223
|
-
const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
2150
|
+
const sanitizeName = (name$1) => {
|
|
2151
|
+
const normalized = name$1.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
2224
2152
|
return normalized === "" ? "handler" : normalized;
|
|
2225
2153
|
};
|
|
2226
2154
|
const getLogStream = (filePath) => {
|
|
@@ -2274,10 +2202,10 @@ process.on("SIGTERM", () => {
|
|
|
2274
2202
|
process.exit(0);
|
|
2275
2203
|
});
|
|
2276
2204
|
let lastCleanup = 0;
|
|
2277
|
-
const createHandlerLogger = (name) => {
|
|
2205
|
+
const createHandlerLogger = (name$1) => {
|
|
2278
2206
|
ensureLogDirectory();
|
|
2279
|
-
const sanitizedName = sanitizeName(name);
|
|
2280
|
-
const instance = consola.withTag(name);
|
|
2207
|
+
const sanitizedName = sanitizeName(name$1);
|
|
2208
|
+
const instance = consola.withTag(name$1);
|
|
2281
2209
|
if (state.verbose) instance.level = 5;
|
|
2282
2210
|
instance.setReporters([]);
|
|
2283
2211
|
instance.addReporter({ log(logObj) {
|
|
@@ -2291,7 +2219,8 @@ const createHandlerLogger = (name) => {
|
|
|
2291
2219
|
const timestamp = date.toLocaleString("sv-SE", { hour12: false });
|
|
2292
2220
|
const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`);
|
|
2293
2221
|
const message = formatArgs(logObj.args);
|
|
2294
|
-
|
|
2222
|
+
const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name$1}]${message ? ` ${message}` : ""}`;
|
|
2223
|
+
appendLine(filePath, line);
|
|
2295
2224
|
} });
|
|
2296
2225
|
return instance;
|
|
2297
2226
|
};
|
|
@@ -2646,8 +2575,9 @@ const mapResponsesStopReason = (response) => {
|
|
|
2646
2575
|
const mapResponsesUsage = (response) => {
|
|
2647
2576
|
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
2648
2577
|
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
2578
|
+
const inputCachedTokens = response.usage?.input_tokens_details?.cached_tokens;
|
|
2649
2579
|
return {
|
|
2650
|
-
input_tokens: inputTokens - (
|
|
2580
|
+
input_tokens: inputTokens - (inputCachedTokens ?? 0),
|
|
2651
2581
|
output_tokens: outputTokens,
|
|
2652
2582
|
...response.usage?.input_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.input_tokens_details.cached_tokens }
|
|
2653
2583
|
};
|
|
@@ -2663,9 +2593,10 @@ const parseUserId = (userId) => {
|
|
|
2663
2593
|
const userMatch = userId.match(/user_([^_]+)_account/);
|
|
2664
2594
|
const safetyIdentifier = userMatch ? userMatch[1] : null;
|
|
2665
2595
|
const sessionMatch = userId.match(/_session_(.+)$/);
|
|
2596
|
+
const promptCacheKey = sessionMatch ? sessionMatch[1] : null;
|
|
2666
2597
|
return {
|
|
2667
2598
|
safetyIdentifier,
|
|
2668
|
-
promptCacheKey
|
|
2599
|
+
promptCacheKey
|
|
2669
2600
|
};
|
|
2670
2601
|
};
|
|
2671
2602
|
const convertToolResultContent = (content) => {
|
|
@@ -2747,11 +2678,11 @@ const handleOutputItemAdded$1 = (rawEvent, state$1) => {
|
|
|
2747
2678
|
const events$1 = new Array();
|
|
2748
2679
|
const functionCallDetails = extractFunctionCallDetails(rawEvent);
|
|
2749
2680
|
if (!functionCallDetails) return events$1;
|
|
2750
|
-
const { outputIndex, toolCallId, name, initialArguments } = functionCallDetails;
|
|
2681
|
+
const { outputIndex, toolCallId, name: name$1, initialArguments } = functionCallDetails;
|
|
2751
2682
|
const blockIndex = openFunctionCallBlock(state$1, {
|
|
2752
2683
|
outputIndex,
|
|
2753
2684
|
toolCallId,
|
|
2754
|
-
name,
|
|
2685
|
+
name: name$1,
|
|
2755
2686
|
events: events$1
|
|
2756
2687
|
});
|
|
2757
2688
|
if (initialArguments !== void 0 && initialArguments.length > 0) {
|
|
@@ -3044,15 +2975,16 @@ const buildErrorEvent = (message) => ({
|
|
|
3044
2975
|
});
|
|
3045
2976
|
const getBlockKey = (outputIndex, contentIndex) => `${outputIndex}:${contentIndex}`;
|
|
3046
2977
|
const openFunctionCallBlock = (state$1, params) => {
|
|
3047
|
-
const { outputIndex, toolCallId, name, events: events$1 } = params;
|
|
2978
|
+
const { outputIndex, toolCallId, name: name$1, events: events$1 } = params;
|
|
3048
2979
|
let functionCallState = state$1.functionCallStateByOutputIndex.get(outputIndex);
|
|
3049
2980
|
if (!functionCallState) {
|
|
3050
2981
|
const blockIndex$1 = state$1.nextContentBlockIndex;
|
|
3051
2982
|
state$1.nextContentBlockIndex += 1;
|
|
2983
|
+
const resolvedToolCallId = toolCallId ?? `tool_call_${blockIndex$1}`;
|
|
3052
2984
|
functionCallState = {
|
|
3053
2985
|
blockIndex: blockIndex$1,
|
|
3054
|
-
toolCallId:
|
|
3055
|
-
name: name ?? "function",
|
|
2986
|
+
toolCallId: resolvedToolCallId,
|
|
2987
|
+
name: name$1 ?? "function",
|
|
3056
2988
|
consecutiveWhitespaceCount: 0
|
|
3057
2989
|
};
|
|
3058
2990
|
state$1.functionCallStateByOutputIndex.set(outputIndex, functionCallState);
|
|
@@ -3077,20 +3009,26 @@ const openFunctionCallBlock = (state$1, params) => {
|
|
|
3077
3009
|
const extractFunctionCallDetails = (rawEvent) => {
|
|
3078
3010
|
const item = rawEvent.item;
|
|
3079
3011
|
if (item.type !== "function_call") return;
|
|
3012
|
+
const outputIndex = rawEvent.output_index;
|
|
3013
|
+
const toolCallId = item.call_id;
|
|
3014
|
+
const name$1 = item.name;
|
|
3015
|
+
const initialArguments = item.arguments;
|
|
3080
3016
|
return {
|
|
3081
|
-
outputIndex
|
|
3082
|
-
toolCallId
|
|
3083
|
-
name:
|
|
3084
|
-
initialArguments
|
|
3017
|
+
outputIndex,
|
|
3018
|
+
toolCallId,
|
|
3019
|
+
name: name$1,
|
|
3020
|
+
initialArguments
|
|
3085
3021
|
};
|
|
3086
3022
|
};
|
|
3087
3023
|
|
|
3088
3024
|
//#endregion
|
|
3089
3025
|
//#region src/routes/responses/utils.ts
|
|
3090
3026
|
const getResponsesRequestOptions = (payload) => {
|
|
3027
|
+
const vision = hasVisionInput(payload);
|
|
3028
|
+
const initiator = hasAgentInitiator(payload) ? "agent" : "user";
|
|
3091
3029
|
return {
|
|
3092
|
-
vision
|
|
3093
|
-
initiator
|
|
3030
|
+
vision,
|
|
3031
|
+
initiator
|
|
3094
3032
|
};
|
|
3095
3033
|
};
|
|
3096
3034
|
const hasAgentInitiator = (payload) => {
|
|
@@ -3382,13 +3320,14 @@ async function handleCompletion(c) {
|
|
|
3382
3320
|
const RESPONSES_ENDPOINT$1 = "/responses";
|
|
3383
3321
|
const MESSAGES_ENDPOINT = "/v1/messages";
|
|
3384
3322
|
const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride) => {
|
|
3385
|
-
|
|
3323
|
+
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
3324
|
+
let finalPayload = await applyReplacementsToPayload(openAIPayload);
|
|
3386
3325
|
finalPayload = {
|
|
3387
3326
|
...finalPayload,
|
|
3388
3327
|
model: normalizeModelName(finalPayload.model)
|
|
3389
3328
|
};
|
|
3390
3329
|
logger$1.debug("Translated OpenAI request payload:", JSON.stringify(finalPayload));
|
|
3391
|
-
const response =
|
|
3330
|
+
const response = await createChatCompletions(finalPayload, { initiator: initiatorOverride });
|
|
3392
3331
|
if (isNonStreaming(response)) {
|
|
3393
3332
|
logger$1.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
3394
3333
|
const anthropicResponse = translateToAnthropic(response);
|
|
@@ -3407,7 +3346,8 @@ const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride)
|
|
|
3407
3346
|
logger$1.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
3408
3347
|
if (rawEvent.data === "[DONE]") break;
|
|
3409
3348
|
if (!rawEvent.data) continue;
|
|
3410
|
-
const
|
|
3349
|
+
const chunk = JSON.parse(rawEvent.data);
|
|
3350
|
+
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
3411
3351
|
for (const event of events$1) {
|
|
3412
3352
|
logger$1.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
3413
3353
|
await stream.writeSSE({
|
|
@@ -3597,19 +3537,9 @@ modelRoutes.get("/", async (c) => {
|
|
|
3597
3537
|
owned_by: model.vendor,
|
|
3598
3538
|
display_name: model.name
|
|
3599
3539
|
})) ?? [];
|
|
3600
|
-
const azureModels = state.azureOpenAIDeployments?.map((deployment) => ({
|
|
3601
|
-
id: deployment.id,
|
|
3602
|
-
object: "model",
|
|
3603
|
-
type: "model",
|
|
3604
|
-
created: deployment.created,
|
|
3605
|
-
created_at: (/* @__PURE__ */ new Date(deployment.created * 1e3)).toISOString(),
|
|
3606
|
-
owned_by: deployment.owned_by,
|
|
3607
|
-
display_name: `${deployment.deploymentName} (${deployment.model})`
|
|
3608
|
-
})) ?? [];
|
|
3609
|
-
const allModels = [...copilotModels, ...azureModels];
|
|
3610
3540
|
return c.json({
|
|
3611
3541
|
object: "list",
|
|
3612
|
-
data:
|
|
3542
|
+
data: copilotModels,
|
|
3613
3543
|
has_more: false
|
|
3614
3544
|
});
|
|
3615
3545
|
} catch (error) {
|
|
@@ -3633,16 +3563,20 @@ replacementsRoute.post("/", async (c) => {
|
|
|
3633
3563
|
return c.json(rule, 201);
|
|
3634
3564
|
});
|
|
3635
3565
|
replacementsRoute.delete("/:id", async (c) => {
|
|
3636
|
-
|
|
3566
|
+
const id = c.req.param("id");
|
|
3567
|
+
if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3637
3568
|
return c.json({ success: true });
|
|
3638
3569
|
});
|
|
3639
3570
|
replacementsRoute.patch("/:id", async (c) => {
|
|
3640
|
-
const
|
|
3571
|
+
const id = c.req.param("id");
|
|
3572
|
+
const body = await c.req.json();
|
|
3573
|
+
const rule = await updateReplacement(id, body);
|
|
3641
3574
|
if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3642
3575
|
return c.json(rule);
|
|
3643
3576
|
});
|
|
3644
3577
|
replacementsRoute.patch("/:id/toggle", async (c) => {
|
|
3645
|
-
const
|
|
3578
|
+
const id = c.req.param("id");
|
|
3579
|
+
const rule = await toggleReplacement(id);
|
|
3646
3580
|
if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3647
3581
|
return c.json(rule);
|
|
3648
3582
|
});
|
|
@@ -3826,7 +3760,7 @@ server.route("/v1/messages", messageRoutes);
|
|
|
3826
3760
|
//#endregion
|
|
3827
3761
|
//#region src/start.ts
|
|
3828
3762
|
async function runServer(options) {
|
|
3829
|
-
consola.info(`copilot-api v${version}`);
|
|
3763
|
+
consola.info(`copilot-api v${package_default.version}`);
|
|
3830
3764
|
if (options.insecure) {
|
|
3831
3765
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
3832
3766
|
consola.warn("SSL certificate verification disabled (insecure mode)");
|
|
@@ -3854,10 +3788,7 @@ async function runServer(options) {
|
|
|
3854
3788
|
} else await setupGitHubToken();
|
|
3855
3789
|
await setupCopilotToken();
|
|
3856
3790
|
await cacheModels();
|
|
3857
|
-
|
|
3858
|
-
const copilotModelIds = state.models?.data.map((model) => model.id) ?? [];
|
|
3859
|
-
const azureModelIds = state.azureOpenAIDeployments?.map((deployment) => deployment.id) ?? [];
|
|
3860
|
-
const allModelIds = [...copilotModelIds, ...azureModelIds];
|
|
3791
|
+
const allModelIds = state.models?.data.map((model) => model.id) ?? [];
|
|
3861
3792
|
consola.info(`Available models: \n${allModelIds.map((id) => `- ${id}`).join("\n")}`);
|
|
3862
3793
|
const serverUrl = `http://localhost:${options.port}`;
|
|
3863
3794
|
if (options.claudeCode) {
|
|
@@ -3990,10 +3921,10 @@ const start = defineCommand({
|
|
|
3990
3921
|
|
|
3991
3922
|
//#endregion
|
|
3992
3923
|
//#region src/main.ts
|
|
3993
|
-
|
|
3924
|
+
const main = defineCommand({
|
|
3994
3925
|
meta: {
|
|
3995
3926
|
name: "copilot-api",
|
|
3996
|
-
version,
|
|
3927
|
+
version: package_default.version,
|
|
3997
3928
|
description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
|
|
3998
3929
|
},
|
|
3999
3930
|
subCommands: {
|
|
@@ -4003,7 +3934,8 @@ await runMain(defineCommand({
|
|
|
4003
3934
|
debug,
|
|
4004
3935
|
config
|
|
4005
3936
|
}
|
|
4006
|
-
})
|
|
3937
|
+
});
|
|
3938
|
+
await runMain(main);
|
|
4007
3939
|
|
|
4008
3940
|
//#endregion
|
|
4009
3941
|
export { };
|