@ashsec/copilot-api 0.8.0 → 0.11.2
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 +595 -295
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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.11.2";
|
|
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 = [
|
|
@@ -273,8 +296,8 @@ async function fetchWithRetry(input, init) {
|
|
|
273
296
|
let lastError;
|
|
274
297
|
let lastResponse;
|
|
275
298
|
for (let attempt = 0; attempt < maxAttempts; attempt++) try {
|
|
276
|
-
const headers =
|
|
277
|
-
headers.
|
|
299
|
+
const headers = toHeaderRecord(init?.headers);
|
|
300
|
+
headers.Connection = "close";
|
|
278
301
|
const response = await fetch(input, {
|
|
279
302
|
...init,
|
|
280
303
|
headers,
|
|
@@ -296,63 +319,24 @@ async function fetchWithRetry(input, init) {
|
|
|
296
319
|
await sleep(delayMs);
|
|
297
320
|
}
|
|
298
321
|
if (lastResponse) return lastResponse;
|
|
299
|
-
throw lastError;
|
|
300
|
-
}
|
|
301
|
-
|
|
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();
|
|
322
|
+
throw lastError ?? /* @__PURE__ */ new Error("Request failed without a captured error");
|
|
327
323
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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 [];
|
|
324
|
+
function toHeaderRecord(headersInit) {
|
|
325
|
+
const headers = {};
|
|
326
|
+
if (!headersInit) return headers;
|
|
327
|
+
if (headersInit instanceof Headers) {
|
|
328
|
+
for (const [key, value] of headersInit.entries()) headers[key] = value;
|
|
329
|
+
return headers;
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(headersInit)) {
|
|
332
|
+
for (const entry of headersInit) if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string") {
|
|
333
|
+
const [key, value] = entry;
|
|
334
|
+
headers[key] = value;
|
|
335
|
+
}
|
|
336
|
+
return headers;
|
|
355
337
|
}
|
|
338
|
+
for (const [key, value] of Object.entries(headersInit)) if (typeof value === "string") headers[key] = value;
|
|
339
|
+
return headers;
|
|
356
340
|
}
|
|
357
341
|
|
|
358
342
|
//#endregion
|
|
@@ -418,24 +402,6 @@ const cacheVSCodeVersion = async () => {
|
|
|
418
402
|
state.vsCodeVersion = response;
|
|
419
403
|
consola.info(`Using VSCode version: ${response}`);
|
|
420
404
|
};
|
|
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
405
|
|
|
440
406
|
//#endregion
|
|
441
407
|
//#region src/services/github/poll-access-token.ts
|
|
@@ -583,13 +549,13 @@ const checkUsage = defineCommand({
|
|
|
583
549
|
const premiumUsed = premiumTotal - premium.remaining;
|
|
584
550
|
const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
|
|
585
551
|
const premiumPercentRemaining = premium.percent_remaining;
|
|
586
|
-
function summarizeQuota(name, snap) {
|
|
587
|
-
if (!snap) return `${name}: N/A`;
|
|
552
|
+
function summarizeQuota(name$1, snap) {
|
|
553
|
+
if (!snap) return `${name$1}: N/A`;
|
|
588
554
|
const total = snap.entitlement;
|
|
589
555
|
const used = total - snap.remaining;
|
|
590
556
|
const percentUsed = total > 0 ? used / total * 100 : 0;
|
|
591
557
|
const percentRemaining = snap.percent_remaining;
|
|
592
|
-
return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
|
|
558
|
+
return `${name$1}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
|
|
593
559
|
}
|
|
594
560
|
const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
|
|
595
561
|
const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
|
|
@@ -665,11 +631,11 @@ async function getUserReplacements() {
|
|
|
665
631
|
* Add a new user replacement rule
|
|
666
632
|
*/
|
|
667
633
|
async function addReplacement(pattern, replacement, options) {
|
|
668
|
-
const { isRegex = false, name } = options ?? {};
|
|
634
|
+
const { isRegex = false, name: name$1 } = options ?? {};
|
|
669
635
|
await ensureLoaded();
|
|
670
636
|
const rule = {
|
|
671
637
|
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
672
|
-
name,
|
|
638
|
+
name: name$1,
|
|
673
639
|
pattern,
|
|
674
640
|
replacement,
|
|
675
641
|
isRegex,
|
|
@@ -785,34 +751,49 @@ async function applyReplacements(text) {
|
|
|
785
751
|
appliedRules.push(rule.name || rule.id);
|
|
786
752
|
}
|
|
787
753
|
}
|
|
788
|
-
|
|
789
|
-
|
|
754
|
+
return {
|
|
755
|
+
text: result,
|
|
756
|
+
appliedRules
|
|
757
|
+
};
|
|
790
758
|
}
|
|
791
759
|
/**
|
|
792
760
|
* Apply replacements to a chat completions payload
|
|
793
761
|
* This modifies message content in place
|
|
794
762
|
*/
|
|
795
763
|
async function applyReplacementsToPayload(payload) {
|
|
764
|
+
const allAppliedRules = [];
|
|
796
765
|
const processedMessages = await Promise.all(payload.messages.map(async (message) => {
|
|
797
|
-
if (typeof message.content === "string")
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
766
|
+
if (typeof message.content === "string") {
|
|
767
|
+
const { text, appliedRules } = await applyReplacements(message.content);
|
|
768
|
+
allAppliedRules.push(...appliedRules);
|
|
769
|
+
return {
|
|
770
|
+
...message,
|
|
771
|
+
content: text
|
|
772
|
+
};
|
|
773
|
+
}
|
|
801
774
|
if (Array.isArray(message.content)) return {
|
|
802
775
|
...message,
|
|
803
776
|
content: await Promise.all(message.content.map(async (part) => {
|
|
804
|
-
if (typeof part === "object" && part.type === "text" && part.text)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
777
|
+
if (typeof part === "object" && part.type === "text" && part.text) {
|
|
778
|
+
const { text, appliedRules } = await applyReplacements(part.text);
|
|
779
|
+
allAppliedRules.push(...appliedRules);
|
|
780
|
+
return {
|
|
781
|
+
...part,
|
|
782
|
+
text
|
|
783
|
+
};
|
|
784
|
+
}
|
|
808
785
|
return part;
|
|
809
786
|
}))
|
|
810
787
|
};
|
|
811
788
|
return message;
|
|
812
789
|
}));
|
|
790
|
+
const uniqueRules = [...new Set(allAppliedRules)];
|
|
813
791
|
return {
|
|
814
|
-
|
|
815
|
-
|
|
792
|
+
payload: {
|
|
793
|
+
...payload,
|
|
794
|
+
messages: processedMessages
|
|
795
|
+
},
|
|
796
|
+
appliedRules: uniqueRules
|
|
816
797
|
};
|
|
817
798
|
}
|
|
818
799
|
|
|
@@ -820,11 +801,20 @@ async function applyReplacementsToPayload(payload) {
|
|
|
820
801
|
//#region src/config.ts
|
|
821
802
|
function formatRule(rule, index) {
|
|
822
803
|
const status = rule.enabled ? "✓" : "✗";
|
|
823
|
-
const type = rule.isRegex ? "regex" : "string";
|
|
804
|
+
const type$1 = rule.isRegex ? "regex" : "string";
|
|
824
805
|
const system = rule.isSystem ? " [system]" : "";
|
|
825
|
-
const name = rule.name ? ` "${rule.name}"` : "";
|
|
806
|
+
const name$1 = rule.name ? ` "${rule.name}"` : "";
|
|
826
807
|
const replacement = rule.replacement || "(empty)";
|
|
827
|
-
return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
|
|
808
|
+
return `${index + 1}. [${status}] (${type$1})${system}${name$1} "${rule.pattern}" → "${replacement}"`;
|
|
809
|
+
}
|
|
810
|
+
function isValidPatternForMatchType(pattern, matchType) {
|
|
811
|
+
if (matchType !== "regex") return true;
|
|
812
|
+
try {
|
|
813
|
+
new RegExp(pattern);
|
|
814
|
+
return true;
|
|
815
|
+
} catch {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
828
818
|
}
|
|
829
819
|
async function listReplacements() {
|
|
830
820
|
const all = await getAllReplacements();
|
|
@@ -837,11 +827,11 @@ async function listReplacements() {
|
|
|
837
827
|
console.log();
|
|
838
828
|
}
|
|
839
829
|
async function addNewReplacement() {
|
|
840
|
-
const name = await consola.prompt("Name (optional, short description):", {
|
|
830
|
+
const name$1 = await consola.prompt("Name (optional, short description):", {
|
|
841
831
|
type: "text",
|
|
842
832
|
default: ""
|
|
843
833
|
});
|
|
844
|
-
if (typeof name === "symbol") {
|
|
834
|
+
if (typeof name$1 === "symbol") {
|
|
845
835
|
consola.info("Cancelled.");
|
|
846
836
|
return;
|
|
847
837
|
}
|
|
@@ -864,9 +854,7 @@ async function addNewReplacement() {
|
|
|
864
854
|
consola.info("Cancelled.");
|
|
865
855
|
return;
|
|
866
856
|
}
|
|
867
|
-
if (matchType
|
|
868
|
-
new RegExp(pattern);
|
|
869
|
-
} catch {
|
|
857
|
+
if (!isValidPatternForMatchType(pattern, matchType)) {
|
|
870
858
|
consola.error(`Invalid regex pattern: ${pattern}`);
|
|
871
859
|
return;
|
|
872
860
|
}
|
|
@@ -878,7 +866,10 @@ async function addNewReplacement() {
|
|
|
878
866
|
consola.info("Cancelled.");
|
|
879
867
|
return;
|
|
880
868
|
}
|
|
881
|
-
const rule = await addReplacement(pattern, replacement,
|
|
869
|
+
const rule = await addReplacement(pattern, replacement, {
|
|
870
|
+
isRegex: matchType === "regex",
|
|
871
|
+
name: name$1 || void 0
|
|
872
|
+
});
|
|
882
873
|
consola.success(`Added rule: ${rule.name || rule.id}`);
|
|
883
874
|
}
|
|
884
875
|
async function editExistingReplacement() {
|
|
@@ -906,11 +897,11 @@ async function editExistingReplacement() {
|
|
|
906
897
|
}
|
|
907
898
|
consola.info(`\nEditing rule: ${rule.name || rule.id}`);
|
|
908
899
|
consola.info("Press Enter to keep current value.\n");
|
|
909
|
-
const name = await consola.prompt("Name:", {
|
|
900
|
+
const name$1 = await consola.prompt("Name:", {
|
|
910
901
|
type: "text",
|
|
911
902
|
default: rule.name || ""
|
|
912
903
|
});
|
|
913
|
-
if (typeof name === "symbol") {
|
|
904
|
+
if (typeof name$1 === "symbol") {
|
|
914
905
|
consola.info("Cancelled.");
|
|
915
906
|
return;
|
|
916
907
|
}
|
|
@@ -937,9 +928,7 @@ async function editExistingReplacement() {
|
|
|
937
928
|
consola.info("Cancelled.");
|
|
938
929
|
return;
|
|
939
930
|
}
|
|
940
|
-
if (matchType
|
|
941
|
-
new RegExp(pattern);
|
|
942
|
-
} catch {
|
|
931
|
+
if (!isValidPatternForMatchType(pattern, matchType)) {
|
|
943
932
|
consola.error(`Invalid regex pattern: ${pattern}`);
|
|
944
933
|
return;
|
|
945
934
|
}
|
|
@@ -952,7 +941,7 @@ async function editExistingReplacement() {
|
|
|
952
941
|
return;
|
|
953
942
|
}
|
|
954
943
|
const updated = await updateReplacement(selected, {
|
|
955
|
-
name: name || void 0,
|
|
944
|
+
name: name$1 || void 0,
|
|
956
945
|
pattern,
|
|
957
946
|
replacement,
|
|
958
947
|
isRegex: matchType === "regex"
|
|
@@ -1009,7 +998,7 @@ async function testReplacements() {
|
|
|
1009
998
|
consola.info("Cancelled.");
|
|
1010
999
|
return;
|
|
1011
1000
|
}
|
|
1012
|
-
const result = await applyReplacements(testText);
|
|
1001
|
+
const { text: result } = await applyReplacements(testText);
|
|
1013
1002
|
consola.info("\n📝 Original:");
|
|
1014
1003
|
console.log(testText);
|
|
1015
1004
|
consola.info("\n✨ After replacements:");
|
|
@@ -1269,7 +1258,8 @@ function mergeDefaultExtraPrompts(config$1) {
|
|
|
1269
1258
|
};
|
|
1270
1259
|
}
|
|
1271
1260
|
function mergeConfigWithDefaults() {
|
|
1272
|
-
const
|
|
1261
|
+
const config$1 = readConfigFromDisk();
|
|
1262
|
+
const { mergedConfig, changed } = mergeDefaultExtraPrompts(config$1);
|
|
1273
1263
|
if (changed) try {
|
|
1274
1264
|
fs$1.writeFileSync(PATHS.CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf8");
|
|
1275
1265
|
} catch (writeError) {
|
|
@@ -1285,14 +1275,168 @@ function getConfig() {
|
|
|
1285
1275
|
function getExtraPromptForModel(model) {
|
|
1286
1276
|
return getConfig().extraPrompts?.[model] ?? "";
|
|
1287
1277
|
}
|
|
1288
|
-
function
|
|
1289
|
-
|
|
1290
|
-
}
|
|
1291
|
-
function getReasoningEffortForModel(model) {
|
|
1278
|
+
function getReasoningEffortForModel(model, override) {
|
|
1279
|
+
if (override) return override;
|
|
1292
1280
|
return getConfig().modelReasoningEfforts?.[model] ?? "high";
|
|
1293
1281
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1282
|
+
|
|
1283
|
+
//#endregion
|
|
1284
|
+
//#region src/lib/model-suffix.ts
|
|
1285
|
+
/**
|
|
1286
|
+
* Hardcoded reasoning config per model, derived from Copilot CLI v0.0.414.
|
|
1287
|
+
* Models not in this map do not support per-request reasoning effort control.
|
|
1288
|
+
*/
|
|
1289
|
+
const MODEL_REASONING_CONFIG = {
|
|
1290
|
+
"claude-sonnet-4.6": {
|
|
1291
|
+
supportedEfforts: [
|
|
1292
|
+
"low",
|
|
1293
|
+
"medium",
|
|
1294
|
+
"high"
|
|
1295
|
+
],
|
|
1296
|
+
defaultEffort: "medium"
|
|
1297
|
+
},
|
|
1298
|
+
"claude-opus-4.6": {
|
|
1299
|
+
supportedEfforts: [
|
|
1300
|
+
"low",
|
|
1301
|
+
"medium",
|
|
1302
|
+
"high"
|
|
1303
|
+
],
|
|
1304
|
+
defaultEffort: "high"
|
|
1305
|
+
},
|
|
1306
|
+
"claude-opus-4.6-fast": {
|
|
1307
|
+
supportedEfforts: [
|
|
1308
|
+
"low",
|
|
1309
|
+
"medium",
|
|
1310
|
+
"high"
|
|
1311
|
+
],
|
|
1312
|
+
defaultEffort: "high"
|
|
1313
|
+
},
|
|
1314
|
+
"claude-opus-4.6-1m": {
|
|
1315
|
+
supportedEfforts: [
|
|
1316
|
+
"low",
|
|
1317
|
+
"medium",
|
|
1318
|
+
"high"
|
|
1319
|
+
],
|
|
1320
|
+
defaultEffort: "high"
|
|
1321
|
+
},
|
|
1322
|
+
"gpt-5.3-codex": {
|
|
1323
|
+
supportedEfforts: [
|
|
1324
|
+
"low",
|
|
1325
|
+
"medium",
|
|
1326
|
+
"high",
|
|
1327
|
+
"xhigh"
|
|
1328
|
+
],
|
|
1329
|
+
defaultEffort: "medium"
|
|
1330
|
+
},
|
|
1331
|
+
"gpt-5.2-codex": {
|
|
1332
|
+
supportedEfforts: [
|
|
1333
|
+
"low",
|
|
1334
|
+
"medium",
|
|
1335
|
+
"high",
|
|
1336
|
+
"xhigh"
|
|
1337
|
+
],
|
|
1338
|
+
defaultEffort: "high"
|
|
1339
|
+
},
|
|
1340
|
+
"gpt-5.2": {
|
|
1341
|
+
supportedEfforts: [
|
|
1342
|
+
"low",
|
|
1343
|
+
"medium",
|
|
1344
|
+
"high"
|
|
1345
|
+
],
|
|
1346
|
+
defaultEffort: "medium"
|
|
1347
|
+
},
|
|
1348
|
+
"gpt-5.1-codex": {
|
|
1349
|
+
supportedEfforts: [
|
|
1350
|
+
"low",
|
|
1351
|
+
"medium",
|
|
1352
|
+
"high"
|
|
1353
|
+
],
|
|
1354
|
+
defaultEffort: "medium"
|
|
1355
|
+
},
|
|
1356
|
+
"gpt-5.1-codex-max": {
|
|
1357
|
+
supportedEfforts: [
|
|
1358
|
+
"low",
|
|
1359
|
+
"medium",
|
|
1360
|
+
"high"
|
|
1361
|
+
],
|
|
1362
|
+
defaultEffort: "medium"
|
|
1363
|
+
},
|
|
1364
|
+
"gpt-5.1": {
|
|
1365
|
+
supportedEfforts: [
|
|
1366
|
+
"low",
|
|
1367
|
+
"medium",
|
|
1368
|
+
"high"
|
|
1369
|
+
],
|
|
1370
|
+
defaultEffort: "medium"
|
|
1371
|
+
},
|
|
1372
|
+
"gpt-5.1-codex-mini": {
|
|
1373
|
+
supportedEfforts: [
|
|
1374
|
+
"low",
|
|
1375
|
+
"medium",
|
|
1376
|
+
"high"
|
|
1377
|
+
],
|
|
1378
|
+
defaultEffort: "medium"
|
|
1379
|
+
},
|
|
1380
|
+
"gpt-5-mini": {
|
|
1381
|
+
supportedEfforts: [
|
|
1382
|
+
"low",
|
|
1383
|
+
"medium",
|
|
1384
|
+
"high"
|
|
1385
|
+
],
|
|
1386
|
+
defaultEffort: "medium"
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
const VALID_EFFORTS = new Set([
|
|
1390
|
+
"low",
|
|
1391
|
+
"medium",
|
|
1392
|
+
"high",
|
|
1393
|
+
"xhigh"
|
|
1394
|
+
]);
|
|
1395
|
+
/**
|
|
1396
|
+
* Parse a model string that may contain a reasoning effort suffix.
|
|
1397
|
+
* Format: "model-name:effort" (e.g. "claude-sonnet-4.6:high")
|
|
1398
|
+
*
|
|
1399
|
+
* If the suffix is not a valid effort level or the model doesn't support it,
|
|
1400
|
+
* the suffix is ignored and the full string is treated as the model name.
|
|
1401
|
+
*/
|
|
1402
|
+
function parseModelSuffix(model) {
|
|
1403
|
+
const colonIndex = model.lastIndexOf(":");
|
|
1404
|
+
if (colonIndex === -1) return { baseModel: model };
|
|
1405
|
+
const potentialBase = model.slice(0, colonIndex);
|
|
1406
|
+
const potentialEffort = model.slice(colonIndex + 1);
|
|
1407
|
+
if (!VALID_EFFORTS.has(potentialEffort)) return { baseModel: model };
|
|
1408
|
+
const effort = potentialEffort;
|
|
1409
|
+
const config$1 = MODEL_REASONING_CONFIG[potentialBase];
|
|
1410
|
+
if (!config$1) return { baseModel: model };
|
|
1411
|
+
if (!config$1.supportedEfforts.includes(effort)) return {
|
|
1412
|
+
baseModel: potentialBase,
|
|
1413
|
+
reasoningEffort: config$1.defaultEffort
|
|
1414
|
+
};
|
|
1415
|
+
return {
|
|
1416
|
+
baseModel: potentialBase,
|
|
1417
|
+
reasoningEffort: effort
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Generate virtual model entries for models that support reasoning effort.
|
|
1422
|
+
* Each supported effort level gets its own virtual model entry.
|
|
1423
|
+
*/
|
|
1424
|
+
function generateVirtualModels(models) {
|
|
1425
|
+
const virtualModels = [];
|
|
1426
|
+
for (const model of models) {
|
|
1427
|
+
const config$1 = MODEL_REASONING_CONFIG[model.id];
|
|
1428
|
+
if (!config$1) continue;
|
|
1429
|
+
for (const effort of config$1.supportedEfforts) virtualModels.push({
|
|
1430
|
+
id: `${model.id}:${effort}`,
|
|
1431
|
+
object: "model",
|
|
1432
|
+
type: "model",
|
|
1433
|
+
created: 0,
|
|
1434
|
+
created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1435
|
+
owned_by: model.vendor,
|
|
1436
|
+
display_name: `${model.name} (${effort} thinking)`
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
return virtualModels;
|
|
1296
1440
|
}
|
|
1297
1441
|
|
|
1298
1442
|
//#endregion
|
|
@@ -1347,7 +1491,8 @@ function getShell() {
|
|
|
1347
1491
|
const { platform, ppid, env } = process$1;
|
|
1348
1492
|
if (platform === "win32") {
|
|
1349
1493
|
try {
|
|
1350
|
-
|
|
1494
|
+
const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
|
|
1495
|
+
if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
|
|
1351
1496
|
} catch {
|
|
1352
1497
|
return "cmd";
|
|
1353
1498
|
}
|
|
@@ -1405,7 +1550,8 @@ function normalizeApiKeys(apiKeys) {
|
|
|
1405
1550
|
return [...new Set(normalizedKeys)];
|
|
1406
1551
|
}
|
|
1407
1552
|
function getConfiguredApiKeys() {
|
|
1408
|
-
|
|
1553
|
+
const config$1 = getConfig();
|
|
1554
|
+
return normalizeApiKeys(config$1.auth?.apiKeys);
|
|
1409
1555
|
}
|
|
1410
1556
|
function extractRequestApiKey(c) {
|
|
1411
1557
|
const xApiKey = c.req.header("x-api-key")?.trim();
|
|
@@ -1438,6 +1584,26 @@ function createAuthMiddleware(options = {}) {
|
|
|
1438
1584
|
};
|
|
1439
1585
|
}
|
|
1440
1586
|
|
|
1587
|
+
//#endregion
|
|
1588
|
+
//#region src/lib/api-key-guard.ts
|
|
1589
|
+
/**
|
|
1590
|
+
* API key guard middleware that silently drops connections when the API key
|
|
1591
|
+
* doesn't match the expected value. Unauthorized requests get NO response.
|
|
1592
|
+
*
|
|
1593
|
+
* Only active when state.apiKeyAuth is set (via --api-key-auth CLI flag).
|
|
1594
|
+
*/
|
|
1595
|
+
async function apiKeyGuard(c, next) {
|
|
1596
|
+
if (!state.apiKeyAuth) {
|
|
1597
|
+
await next();
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (extractRequestApiKey(c) === state.apiKeyAuth) {
|
|
1601
|
+
await next();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
await new Promise(() => {});
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1441
1607
|
//#endregion
|
|
1442
1608
|
//#region src/lib/request-logger.ts
|
|
1443
1609
|
const REQUEST_CONTEXT_KEY = "requestContext";
|
|
@@ -1498,7 +1664,8 @@ async function logRawRequest(c) {
|
|
|
1498
1664
|
if (method !== "GET" && method !== "HEAD") try {
|
|
1499
1665
|
const body = await c.req.raw.clone().text();
|
|
1500
1666
|
if (body) try {
|
|
1501
|
-
const
|
|
1667
|
+
const parsed = JSON.parse(body);
|
|
1668
|
+
const sanitized = sanitizeRequestBody(parsed);
|
|
1502
1669
|
lines.push(`${colors.dim}Body (sanitized):${colors.reset}`, ` ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`);
|
|
1503
1670
|
} catch {
|
|
1504
1671
|
lines.push(`${colors.dim}Body:${colors.reset} [${body.length} bytes]`);
|
|
@@ -1520,6 +1687,34 @@ function setRequestContext(c, ctx) {
|
|
|
1520
1687
|
});
|
|
1521
1688
|
}
|
|
1522
1689
|
/**
|
|
1690
|
+
* Format the input size for display
|
|
1691
|
+
*/
|
|
1692
|
+
function formatInputSize(bytes) {
|
|
1693
|
+
return bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${bytes}B`;
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Build the model routing log line
|
|
1697
|
+
*/
|
|
1698
|
+
function buildModelLine(ctx) {
|
|
1699
|
+
const parts = [];
|
|
1700
|
+
if (ctx.requestedModel && ctx.requestedModel !== ctx.model) parts.push(`${colors.gray}${ctx.requestedModel}${colors.reset} ${colors.dim}→${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
|
|
1701
|
+
else parts.push(`${colors.white}${ctx.model}${colors.reset}`);
|
|
1702
|
+
if (ctx.provider) parts.push(`${colors.dim}via${colors.reset} ${colors.magenta}${ctx.provider}${colors.reset}`);
|
|
1703
|
+
if (ctx.inputLength !== void 0) parts.push(`${colors.dim}·${colors.reset} ${colors.yellow}${formatInputSize(ctx.inputLength)}${colors.reset}`);
|
|
1704
|
+
return ` ${parts.join(" ")}`;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Build the modifications log line (effort, replacements, tokens)
|
|
1708
|
+
*/
|
|
1709
|
+
function buildModificationsLine(ctx) {
|
|
1710
|
+
const modParts = [];
|
|
1711
|
+
if (ctx.reasoningEffort) modParts.push(`${colors.blue}effort=${ctx.reasoningEffort}${colors.reset}`);
|
|
1712
|
+
if (ctx.replacements && ctx.replacements.length > 0) modParts.push(`${colors.green}replace: ${ctx.replacements.join(", ")}${colors.reset}`);
|
|
1713
|
+
if (ctx.inputTokens !== void 0) modParts.push(`${colors.yellow}${ctx.inputTokens.toLocaleString()} tokens${colors.reset}`);
|
|
1714
|
+
if (modParts.length === 0) return void 0;
|
|
1715
|
+
return ` ${modParts.join(` ${colors.dim}·${colors.reset} `)}`;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1523
1718
|
* Custom request logger middleware
|
|
1524
1719
|
*/
|
|
1525
1720
|
async function requestLogger(c, next) {
|
|
@@ -1527,7 +1722,11 @@ async function requestLogger(c, next) {
|
|
|
1527
1722
|
const startTime = Date.now();
|
|
1528
1723
|
const method = c.req.method;
|
|
1529
1724
|
const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
|
|
1530
|
-
c.
|
|
1725
|
+
const contentLength = c.req.header("content-length");
|
|
1726
|
+
c.set(REQUEST_CONTEXT_KEY, {
|
|
1727
|
+
startTime,
|
|
1728
|
+
inputLength: contentLength ? Number(contentLength) : void 0
|
|
1729
|
+
});
|
|
1531
1730
|
await next();
|
|
1532
1731
|
const ctx = c.get(REQUEST_CONTEXT_KEY);
|
|
1533
1732
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -1538,15 +1737,10 @@ async function requestLogger(c, next) {
|
|
|
1538
1737
|
const statusBadge = `${statusColor}${status}${colors.reset}`;
|
|
1539
1738
|
const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
|
|
1540
1739
|
lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
|
|
1541
|
-
if (ctx?.
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
|
|
1546
|
-
const tokenParts = [];
|
|
1547
|
-
if (ctx.inputTokens !== void 0) tokenParts.push(`${colors.gray}Input:${colors.reset} ${colors.yellow}${ctx.inputTokens.toLocaleString()}${colors.reset}`);
|
|
1548
|
-
if (ctx.outputTokens !== void 0) tokenParts.push(`${colors.gray}Output:${colors.reset} ${colors.green}${ctx.outputTokens.toLocaleString()}${colors.reset}`);
|
|
1549
|
-
lines.push(` ${tokenParts.join(" ")}`);
|
|
1740
|
+
if (ctx?.model) lines.push(buildModelLine(ctx));
|
|
1741
|
+
if (ctx) {
|
|
1742
|
+
const modsLine = buildModificationsLine(ctx);
|
|
1743
|
+
if (modsLine) lines.push(modsLine);
|
|
1550
1744
|
}
|
|
1551
1745
|
lines.push(` ${colors.dim}${getTimeString()}${colors.reset}`);
|
|
1552
1746
|
console.log(lines.join("\n"));
|
|
@@ -1561,13 +1755,17 @@ const awaitApproval = async () => {
|
|
|
1561
1755
|
//#endregion
|
|
1562
1756
|
//#region src/lib/model-resolver.ts
|
|
1563
1757
|
/**
|
|
1564
|
-
* Normalize a model name by converting dashes to dots between numbers
|
|
1758
|
+
* Normalize a model name by converting dashes to dots between numbers
|
|
1759
|
+
* and converting Anthropic's [1m] suffix to Copilot's -1m suffix.
|
|
1565
1760
|
* e.g., "claude-opus-4-5" -> "claude-opus-4.5"
|
|
1761
|
+
* "claude-opus-4-6[1m]" -> "claude-opus-4.6-1m"
|
|
1566
1762
|
* "gpt-4-1" -> "gpt-4.1"
|
|
1567
1763
|
* "gpt-5-1-codex" -> "gpt-5.1-codex"
|
|
1568
1764
|
*/
|
|
1569
1765
|
function normalizeModelName(model) {
|
|
1570
|
-
|
|
1766
|
+
let normalized = model.replace("[1m]", "-1m");
|
|
1767
|
+
normalized = normalized.replaceAll(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
|
|
1768
|
+
return normalized;
|
|
1571
1769
|
}
|
|
1572
1770
|
|
|
1573
1771
|
//#endregion
|
|
@@ -1778,7 +1976,8 @@ const numTokensForTools = (tools, encoder, constants) => {
|
|
|
1778
1976
|
* Calculate the token count of messages, supporting multiple GPT encoders
|
|
1779
1977
|
*/
|
|
1780
1978
|
const getTokenCount = async (payload, model) => {
|
|
1781
|
-
const
|
|
1979
|
+
const tokenizer = getTokenizerFromModel(model);
|
|
1980
|
+
const encoder = await getEncodeChatFunction(tokenizer);
|
|
1782
1981
|
const simplifiedMessages = payload.messages;
|
|
1783
1982
|
const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
|
|
1784
1983
|
const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
|
|
@@ -1823,50 +2022,29 @@ const createChatCompletions = async (payload, options) => {
|
|
|
1823
2022
|
//#region src/routes/chat-completions/handler.ts
|
|
1824
2023
|
async function handleCompletion$1(c) {
|
|
1825
2024
|
await checkRateLimit(state);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2025
|
+
const rawPayload = await c.req.json();
|
|
2026
|
+
const requestedModel = rawPayload.model;
|
|
2027
|
+
const { baseModel, reasoningEffort } = parseModelSuffix(rawPayload.model);
|
|
2028
|
+
rawPayload.model = baseModel;
|
|
2029
|
+
const { payload: replacedPayload, appliedRules } = await applyReplacementsToPayload(rawPayload);
|
|
2030
|
+
let payload = {
|
|
2031
|
+
...replacedPayload,
|
|
2032
|
+
model: normalizeModelName(replacedPayload.model)
|
|
1830
2033
|
};
|
|
1831
2034
|
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
2035
|
setRequestContext(c, {
|
|
1864
|
-
|
|
1865
|
-
|
|
2036
|
+
requestedModel,
|
|
2037
|
+
provider: "ChatCompletions",
|
|
2038
|
+
model: payload.model,
|
|
2039
|
+
replacements: appliedRules,
|
|
2040
|
+
reasoningEffort
|
|
1866
2041
|
});
|
|
1867
2042
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1868
2043
|
try {
|
|
1869
|
-
if (selectedModel)
|
|
2044
|
+
if (selectedModel) {
|
|
2045
|
+
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
2046
|
+
setRequestContext(c, { inputTokens: tokenCount.input });
|
|
2047
|
+
}
|
|
1870
2048
|
} catch (error) {
|
|
1871
2049
|
consola.warn("Failed to calculate token count:", error);
|
|
1872
2050
|
}
|
|
@@ -1933,7 +2111,8 @@ const createEmbeddings = async (payload) => {
|
|
|
1933
2111
|
const embeddingRoutes = new Hono();
|
|
1934
2112
|
embeddingRoutes.post("/", async (c) => {
|
|
1935
2113
|
try {
|
|
1936
|
-
const
|
|
2114
|
+
const paylod = await c.req.json();
|
|
2115
|
+
const response = await createEmbeddings(paylod);
|
|
1937
2116
|
return c.json(response);
|
|
1938
2117
|
} catch (error) {
|
|
1939
2118
|
return await forwardError(c, error);
|
|
@@ -2219,8 +2398,8 @@ const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : u
|
|
|
2219
2398
|
depth: null,
|
|
2220
2399
|
colors: false
|
|
2221
2400
|
})).join(" ");
|
|
2222
|
-
const sanitizeName = (name) => {
|
|
2223
|
-
const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
2401
|
+
const sanitizeName = (name$1) => {
|
|
2402
|
+
const normalized = name$1.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
2224
2403
|
return normalized === "" ? "handler" : normalized;
|
|
2225
2404
|
};
|
|
2226
2405
|
const getLogStream = (filePath) => {
|
|
@@ -2274,10 +2453,10 @@ process.on("SIGTERM", () => {
|
|
|
2274
2453
|
process.exit(0);
|
|
2275
2454
|
});
|
|
2276
2455
|
let lastCleanup = 0;
|
|
2277
|
-
const createHandlerLogger = (name) => {
|
|
2456
|
+
const createHandlerLogger = (name$1) => {
|
|
2278
2457
|
ensureLogDirectory();
|
|
2279
|
-
const sanitizedName = sanitizeName(name);
|
|
2280
|
-
const instance = consola.withTag(name);
|
|
2458
|
+
const sanitizedName = sanitizeName(name$1);
|
|
2459
|
+
const instance = consola.withTag(name$1);
|
|
2281
2460
|
if (state.verbose) instance.level = 5;
|
|
2282
2461
|
instance.setReporters([]);
|
|
2283
2462
|
instance.addReporter({ log(logObj) {
|
|
@@ -2291,7 +2470,8 @@ const createHandlerLogger = (name) => {
|
|
|
2291
2470
|
const timestamp = date.toLocaleString("sv-SE", { hour12: false });
|
|
2292
2471
|
const filePath = path.join(LOG_DIR, `${sanitizedName}-${dateKey}.log`);
|
|
2293
2472
|
const message = formatArgs(logObj.args);
|
|
2294
|
-
|
|
2473
|
+
const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name$1}]${message ? ` ${message}` : ""}`;
|
|
2474
|
+
appendLine(filePath, line);
|
|
2295
2475
|
} });
|
|
2296
2476
|
return instance;
|
|
2297
2477
|
};
|
|
@@ -2323,7 +2503,7 @@ const createResponses = async (payload, { vision, initiator }) => {
|
|
|
2323
2503
|
const MESSAGE_TYPE = "message";
|
|
2324
2504
|
const CODEX_PHASE_MODEL = "gpt-5.3-codex";
|
|
2325
2505
|
const THINKING_TEXT = "Thinking...";
|
|
2326
|
-
const translateAnthropicMessagesToResponsesPayload = (payload) => {
|
|
2506
|
+
const translateAnthropicMessagesToResponsesPayload = (payload, effortOverride) => {
|
|
2327
2507
|
const input = [];
|
|
2328
2508
|
for (const message of payload.messages) input.push(...translateMessage(message, payload.model));
|
|
2329
2509
|
const translatedTools = convertAnthropicTools(payload.tools);
|
|
@@ -2345,7 +2525,7 @@ const translateAnthropicMessagesToResponsesPayload = (payload) => {
|
|
|
2345
2525
|
store: false,
|
|
2346
2526
|
parallel_tool_calls: true,
|
|
2347
2527
|
reasoning: {
|
|
2348
|
-
effort: getReasoningEffortForModel(payload.model),
|
|
2528
|
+
effort: getReasoningEffortForModel(payload.model, effortOverride),
|
|
2349
2529
|
summary: "detailed"
|
|
2350
2530
|
},
|
|
2351
2531
|
include: ["reasoning.encrypted_content"]
|
|
@@ -2646,8 +2826,9 @@ const mapResponsesStopReason = (response) => {
|
|
|
2646
2826
|
const mapResponsesUsage = (response) => {
|
|
2647
2827
|
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
2648
2828
|
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
2829
|
+
const inputCachedTokens = response.usage?.input_tokens_details?.cached_tokens;
|
|
2649
2830
|
return {
|
|
2650
|
-
input_tokens: inputTokens - (
|
|
2831
|
+
input_tokens: inputTokens - (inputCachedTokens ?? 0),
|
|
2651
2832
|
output_tokens: outputTokens,
|
|
2652
2833
|
...response.usage?.input_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.input_tokens_details.cached_tokens }
|
|
2653
2834
|
};
|
|
@@ -2663,9 +2844,10 @@ const parseUserId = (userId) => {
|
|
|
2663
2844
|
const userMatch = userId.match(/user_([^_]+)_account/);
|
|
2664
2845
|
const safetyIdentifier = userMatch ? userMatch[1] : null;
|
|
2665
2846
|
const sessionMatch = userId.match(/_session_(.+)$/);
|
|
2847
|
+
const promptCacheKey = sessionMatch ? sessionMatch[1] : null;
|
|
2666
2848
|
return {
|
|
2667
2849
|
safetyIdentifier,
|
|
2668
|
-
promptCacheKey
|
|
2850
|
+
promptCacheKey
|
|
2669
2851
|
};
|
|
2670
2852
|
};
|
|
2671
2853
|
const convertToolResultContent = (content) => {
|
|
@@ -2747,11 +2929,11 @@ const handleOutputItemAdded$1 = (rawEvent, state$1) => {
|
|
|
2747
2929
|
const events$1 = new Array();
|
|
2748
2930
|
const functionCallDetails = extractFunctionCallDetails(rawEvent);
|
|
2749
2931
|
if (!functionCallDetails) return events$1;
|
|
2750
|
-
const { outputIndex, toolCallId, name, initialArguments } = functionCallDetails;
|
|
2932
|
+
const { outputIndex, toolCallId, name: name$1, initialArguments } = functionCallDetails;
|
|
2751
2933
|
const blockIndex = openFunctionCallBlock(state$1, {
|
|
2752
2934
|
outputIndex,
|
|
2753
2935
|
toolCallId,
|
|
2754
|
-
name,
|
|
2936
|
+
name: name$1,
|
|
2755
2937
|
events: events$1
|
|
2756
2938
|
});
|
|
2757
2939
|
if (initialArguments !== void 0 && initialArguments.length > 0) {
|
|
@@ -3044,15 +3226,16 @@ const buildErrorEvent = (message) => ({
|
|
|
3044
3226
|
});
|
|
3045
3227
|
const getBlockKey = (outputIndex, contentIndex) => `${outputIndex}:${contentIndex}`;
|
|
3046
3228
|
const openFunctionCallBlock = (state$1, params) => {
|
|
3047
|
-
const { outputIndex, toolCallId, name, events: events$1 } = params;
|
|
3229
|
+
const { outputIndex, toolCallId, name: name$1, events: events$1 } = params;
|
|
3048
3230
|
let functionCallState = state$1.functionCallStateByOutputIndex.get(outputIndex);
|
|
3049
3231
|
if (!functionCallState) {
|
|
3050
3232
|
const blockIndex$1 = state$1.nextContentBlockIndex;
|
|
3051
3233
|
state$1.nextContentBlockIndex += 1;
|
|
3234
|
+
const resolvedToolCallId = toolCallId ?? `tool_call_${blockIndex$1}`;
|
|
3052
3235
|
functionCallState = {
|
|
3053
3236
|
blockIndex: blockIndex$1,
|
|
3054
|
-
toolCallId:
|
|
3055
|
-
name: name ?? "function",
|
|
3237
|
+
toolCallId: resolvedToolCallId,
|
|
3238
|
+
name: name$1 ?? "function",
|
|
3056
3239
|
consecutiveWhitespaceCount: 0
|
|
3057
3240
|
};
|
|
3058
3241
|
state$1.functionCallStateByOutputIndex.set(outputIndex, functionCallState);
|
|
@@ -3077,20 +3260,26 @@ const openFunctionCallBlock = (state$1, params) => {
|
|
|
3077
3260
|
const extractFunctionCallDetails = (rawEvent) => {
|
|
3078
3261
|
const item = rawEvent.item;
|
|
3079
3262
|
if (item.type !== "function_call") return;
|
|
3263
|
+
const outputIndex = rawEvent.output_index;
|
|
3264
|
+
const toolCallId = item.call_id;
|
|
3265
|
+
const name$1 = item.name;
|
|
3266
|
+
const initialArguments = item.arguments;
|
|
3080
3267
|
return {
|
|
3081
|
-
outputIndex
|
|
3082
|
-
toolCallId
|
|
3083
|
-
name:
|
|
3084
|
-
initialArguments
|
|
3268
|
+
outputIndex,
|
|
3269
|
+
toolCallId,
|
|
3270
|
+
name: name$1,
|
|
3271
|
+
initialArguments
|
|
3085
3272
|
};
|
|
3086
3273
|
};
|
|
3087
3274
|
|
|
3088
3275
|
//#endregion
|
|
3089
3276
|
//#region src/routes/responses/utils.ts
|
|
3090
3277
|
const getResponsesRequestOptions = (payload) => {
|
|
3278
|
+
const vision = hasVisionInput(payload);
|
|
3279
|
+
const initiator = hasAgentInitiator(payload) ? "agent" : "user";
|
|
3091
3280
|
return {
|
|
3092
|
-
vision
|
|
3093
|
-
initiator
|
|
3281
|
+
vision,
|
|
3282
|
+
initiator
|
|
3094
3283
|
};
|
|
3095
3284
|
};
|
|
3096
3285
|
const hasAgentInitiator = (payload) => {
|
|
@@ -3132,7 +3321,8 @@ const createMessages = async (payload, anthropicBetaHeader, options) => {
|
|
|
3132
3321
|
"X-Initiator": initiator
|
|
3133
3322
|
};
|
|
3134
3323
|
if (anthropicBetaHeader) {
|
|
3135
|
-
const
|
|
3324
|
+
const unsupportedBetas = new Set(["claude-code-20250219", "context-1m-2025-08-07"]);
|
|
3325
|
+
const filteredBeta = anthropicBetaHeader.split(",").map((item) => item.trim()).filter((item) => !unsupportedBetas.has(item)).join(",");
|
|
3136
3326
|
if (filteredBeta) headers["anthropic-beta"] = filteredBeta;
|
|
3137
3327
|
} else if (payload.thinking?.budget_tokens) headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
3138
3328
|
const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
|
|
@@ -3357,38 +3547,62 @@ async function handleCompletion(c) {
|
|
|
3357
3547
|
await checkRateLimit(state);
|
|
3358
3548
|
const anthropicPayload = await c.req.json();
|
|
3359
3549
|
logger$1.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
|
|
3550
|
+
const requestedModel = anthropicPayload.model;
|
|
3551
|
+
const { baseModel, reasoningEffort: suffixEffort } = parseModelSuffix(anthropicPayload.model);
|
|
3552
|
+
anthropicPayload.model = normalizeModelName(baseModel);
|
|
3360
3553
|
const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
|
|
3361
3554
|
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
3362
3555
|
if (subagentMarker) logger$1.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
3363
3556
|
const isCompact = isCompactRequest(anthropicPayload);
|
|
3364
3557
|
const anthropicBeta = c.req.header("anthropic-beta");
|
|
3365
3558
|
logger$1.debug("Anthropic Beta header:", anthropicBeta);
|
|
3366
|
-
|
|
3367
|
-
if (
|
|
3368
|
-
|
|
3369
|
-
logger$1.debug("Is compact request:", isCompact);
|
|
3370
|
-
if (shouldCompactUseSmallModel()) anthropicPayload.model = getSmallModel();
|
|
3371
|
-
} else mergeToolResultForClaude(anthropicPayload);
|
|
3559
|
+
applyModelVariantRouting(anthropicPayload, anthropicBeta);
|
|
3560
|
+
if (isCompact) logger$1.debug("Is compact request:", isCompact);
|
|
3561
|
+
else mergeToolResultForClaude(anthropicPayload);
|
|
3372
3562
|
if (state.manualApprove) await awaitApproval();
|
|
3373
3563
|
const selectedModel = state.models?.data.find((m) => m.id === anthropicPayload.model);
|
|
3564
|
+
let apiType = "ChatCompletions";
|
|
3565
|
+
if (shouldUseMessagesApi(selectedModel)) apiType = "Messages";
|
|
3566
|
+
else if (shouldUseResponsesApi(selectedModel)) apiType = "Responses";
|
|
3567
|
+
const bodyEffort = getBodyReasoningEffort(anthropicPayload);
|
|
3568
|
+
const effectiveEffort = suffixEffort ?? bodyEffort;
|
|
3569
|
+
setRequestContext(c, {
|
|
3570
|
+
requestedModel,
|
|
3571
|
+
model: anthropicPayload.model,
|
|
3572
|
+
provider: apiType,
|
|
3573
|
+
reasoningEffort: effectiveEffort
|
|
3574
|
+
});
|
|
3374
3575
|
if (shouldUseMessagesApi(selectedModel)) return await handleWithMessagesApi(c, anthropicPayload, {
|
|
3375
3576
|
anthropicBetaHeader: anthropicBeta,
|
|
3376
3577
|
initiatorOverride,
|
|
3377
|
-
selectedModel
|
|
3578
|
+
selectedModel,
|
|
3579
|
+
effortOverride: suffixEffort
|
|
3580
|
+
});
|
|
3581
|
+
if (shouldUseResponsesApi(selectedModel)) return await handleWithResponsesApi(c, anthropicPayload, {
|
|
3582
|
+
initiatorOverride,
|
|
3583
|
+
effortOverride: suffixEffort
|
|
3378
3584
|
});
|
|
3379
|
-
if (shouldUseResponsesApi(selectedModel)) return await handleWithResponsesApi(c, anthropicPayload, initiatorOverride);
|
|
3380
3585
|
return await handleWithChatCompletions(c, anthropicPayload, initiatorOverride);
|
|
3381
3586
|
}
|
|
3382
3587
|
const RESPONSES_ENDPOINT$1 = "/responses";
|
|
3383
3588
|
const MESSAGES_ENDPOINT = "/v1/messages";
|
|
3384
3589
|
const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride) => {
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3590
|
+
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
3591
|
+
const { payload: replacedPayload, appliedRules } = await applyReplacementsToPayload(openAIPayload);
|
|
3592
|
+
const finalPayload = {
|
|
3593
|
+
...replacedPayload,
|
|
3594
|
+
model: normalizeModelName(replacedPayload.model)
|
|
3389
3595
|
};
|
|
3596
|
+
if (appliedRules.length > 0) setRequestContext(c, { replacements: appliedRules });
|
|
3597
|
+
try {
|
|
3598
|
+
const selectedModel = state.models?.data.find((m) => m.id === finalPayload.model);
|
|
3599
|
+
if (selectedModel) {
|
|
3600
|
+
const tokenCount = await getTokenCount(finalPayload, selectedModel);
|
|
3601
|
+
setRequestContext(c, { inputTokens: tokenCount.input });
|
|
3602
|
+
}
|
|
3603
|
+
} catch {}
|
|
3390
3604
|
logger$1.debug("Translated OpenAI request payload:", JSON.stringify(finalPayload));
|
|
3391
|
-
const response =
|
|
3605
|
+
const response = await createChatCompletions(finalPayload, { initiator: initiatorOverride });
|
|
3392
3606
|
if (isNonStreaming(response)) {
|
|
3393
3607
|
logger$1.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
|
|
3394
3608
|
const anthropicResponse = translateToAnthropic(response);
|
|
@@ -3407,7 +3621,8 @@ const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride)
|
|
|
3407
3621
|
logger$1.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
3408
3622
|
if (rawEvent.data === "[DONE]") break;
|
|
3409
3623
|
if (!rawEvent.data) continue;
|
|
3410
|
-
const
|
|
3624
|
+
const chunk = JSON.parse(rawEvent.data);
|
|
3625
|
+
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
3411
3626
|
for (const event of events$1) {
|
|
3412
3627
|
logger$1.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
3413
3628
|
await stream.writeSSE({
|
|
@@ -3418,8 +3633,9 @@ const handleWithChatCompletions = async (c, anthropicPayload, initiatorOverride)
|
|
|
3418
3633
|
}
|
|
3419
3634
|
});
|
|
3420
3635
|
};
|
|
3421
|
-
const handleWithResponsesApi = async (c, anthropicPayload,
|
|
3422
|
-
const
|
|
3636
|
+
const handleWithResponsesApi = async (c, anthropicPayload, options) => {
|
|
3637
|
+
const { initiatorOverride, effortOverride } = options ?? {};
|
|
3638
|
+
const responsesPayload = translateAnthropicMessagesToResponsesPayload(anthropicPayload, effortOverride);
|
|
3423
3639
|
logger$1.debug("Translated Responses payload:", JSON.stringify(responsesPayload));
|
|
3424
3640
|
const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
|
|
3425
3641
|
const response = await createResponses(responsesPayload, {
|
|
@@ -3471,14 +3687,15 @@ const handleWithResponsesApi = async (c, anthropicPayload, initiatorOverride) =>
|
|
|
3471
3687
|
return c.json(anthropicResponse);
|
|
3472
3688
|
};
|
|
3473
3689
|
const handleWithMessagesApi = async (c, anthropicPayload, options) => {
|
|
3474
|
-
const { anthropicBetaHeader, initiatorOverride, selectedModel } = options ?? {};
|
|
3690
|
+
const { anthropicBetaHeader, initiatorOverride, selectedModel, effortOverride } = options ?? {};
|
|
3475
3691
|
for (const msg of anthropicPayload.messages) if (msg.role === "assistant" && Array.isArray(msg.content)) msg.content = msg.content.filter((block) => {
|
|
3476
3692
|
if (block.type !== "thinking") return true;
|
|
3477
3693
|
return block.thinking && block.thinking !== "Thinking..." && block.signature && !block.signature.includes("@");
|
|
3478
3694
|
});
|
|
3479
3695
|
if (selectedModel?.capabilities.supports.adaptive_thinking) {
|
|
3480
|
-
anthropicPayload.thinking = { type: "adaptive" };
|
|
3481
|
-
|
|
3696
|
+
if (!anthropicPayload.thinking) anthropicPayload.thinking = { type: "adaptive" };
|
|
3697
|
+
const clientEffort = anthropicPayload.output_config?.effort;
|
|
3698
|
+
anthropicPayload.output_config = { effort: effortOverride ? getAnthropicEffortForModel(anthropicPayload.model, effortOverride) : clientEffort ?? getAnthropicEffortForModel(anthropicPayload.model) };
|
|
3482
3699
|
}
|
|
3483
3700
|
logger$1.debug("Translated Messages payload:", JSON.stringify(anthropicPayload));
|
|
3484
3701
|
const response = await createMessages(anthropicPayload, anthropicBetaHeader, { initiator: initiatorOverride });
|
|
@@ -3499,6 +3716,21 @@ const handleWithMessagesApi = async (c, anthropicPayload, options) => {
|
|
|
3499
3716
|
logger$1.debug("Non-streaming Messages result:", JSON.stringify(response).slice(-400));
|
|
3500
3717
|
return c.json(response);
|
|
3501
3718
|
};
|
|
3719
|
+
/**
|
|
3720
|
+
* Route to model variants based on client signals (1m context, fast mode).
|
|
3721
|
+
* Mutates the payload in place.
|
|
3722
|
+
*/
|
|
3723
|
+
function applyModelVariantRouting(payload, anthropicBeta) {
|
|
3724
|
+
if (anthropicBeta?.includes("context-1m")) {
|
|
3725
|
+
const candidate = `${payload.model}-1m`;
|
|
3726
|
+
if (state.models?.data.some((m) => m.id === candidate)) payload.model = candidate;
|
|
3727
|
+
}
|
|
3728
|
+
if (payload.speed === "fast") {
|
|
3729
|
+
const candidate = `${payload.model}-fast`;
|
|
3730
|
+
if (state.models?.data.some((m) => m.id === candidate)) payload.model = candidate;
|
|
3731
|
+
delete payload.speed;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3502
3734
|
const shouldUseResponsesApi = (selectedModel) => {
|
|
3503
3735
|
return selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT$1) ?? false;
|
|
3504
3736
|
};
|
|
@@ -3507,8 +3739,25 @@ const shouldUseMessagesApi = (selectedModel) => {
|
|
|
3507
3739
|
};
|
|
3508
3740
|
const isNonStreaming = (response) => Object.hasOwn(response, "choices");
|
|
3509
3741
|
const isAsyncIterable$1 = (value) => Boolean(value) && typeof value[Symbol.asyncIterator] === "function";
|
|
3510
|
-
|
|
3511
|
-
|
|
3742
|
+
/**
|
|
3743
|
+
* Extract reasoning effort info from the Anthropic request body for logging.
|
|
3744
|
+
* Claude Code sends effort as `output_config.effort` (low/medium/high/max)
|
|
3745
|
+
* and thinking mode as `thinking.type` (enabled/adaptive).
|
|
3746
|
+
* When effort is "high" (the default), Claude Code omits output_config.effort entirely.
|
|
3747
|
+
*/
|
|
3748
|
+
function getBodyReasoningEffort(payload) {
|
|
3749
|
+
if (!payload.thinking && !payload.output_config?.effort) return void 0;
|
|
3750
|
+
const parts = [];
|
|
3751
|
+
const effort = payload.output_config?.effort ?? (payload.thinking ? "high" : void 0);
|
|
3752
|
+
if (effort) parts.push(effort);
|
|
3753
|
+
if (payload.thinking) {
|
|
3754
|
+
parts.push(payload.thinking.type);
|
|
3755
|
+
if (payload.thinking.budget_tokens) parts.push(`${payload.thinking.budget_tokens.toLocaleString()} budget`);
|
|
3756
|
+
}
|
|
3757
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
3758
|
+
}
|
|
3759
|
+
const getAnthropicEffortForModel = (model, override) => {
|
|
3760
|
+
const reasoningEffort = getReasoningEffortForModel(model, override);
|
|
3512
3761
|
if (reasoningEffort === "xhigh") return "max";
|
|
3513
3762
|
if (reasoningEffort === "none" || reasoningEffort === "minimal") return "low";
|
|
3514
3763
|
return reasoningEffort;
|
|
@@ -3597,19 +3846,10 @@ modelRoutes.get("/", async (c) => {
|
|
|
3597
3846
|
owned_by: model.vendor,
|
|
3598
3847
|
display_name: model.name
|
|
3599
3848
|
})) ?? [];
|
|
3600
|
-
const
|
|
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];
|
|
3849
|
+
const virtualModels = state.models ? generateVirtualModels(state.models.data) : [];
|
|
3610
3850
|
return c.json({
|
|
3611
3851
|
object: "list",
|
|
3612
|
-
data:
|
|
3852
|
+
data: [...copilotModels, ...virtualModels],
|
|
3613
3853
|
has_more: false
|
|
3614
3854
|
});
|
|
3615
3855
|
} catch (error) {
|
|
@@ -3629,20 +3869,27 @@ replacementsRoute.get("/", async (c) => {
|
|
|
3629
3869
|
replacementsRoute.post("/", async (c) => {
|
|
3630
3870
|
const body = await c.req.json();
|
|
3631
3871
|
if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
|
|
3632
|
-
const rule = await addReplacement(body.pattern, body.replacement ?? "",
|
|
3872
|
+
const rule = await addReplacement(body.pattern, body.replacement ?? "", {
|
|
3873
|
+
isRegex: body.isRegex ?? false,
|
|
3874
|
+
name: body.name
|
|
3875
|
+
});
|
|
3633
3876
|
return c.json(rule, 201);
|
|
3634
3877
|
});
|
|
3635
3878
|
replacementsRoute.delete("/:id", async (c) => {
|
|
3636
|
-
|
|
3879
|
+
const id = c.req.param("id");
|
|
3880
|
+
if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3637
3881
|
return c.json({ success: true });
|
|
3638
3882
|
});
|
|
3639
3883
|
replacementsRoute.patch("/:id", async (c) => {
|
|
3640
|
-
const
|
|
3884
|
+
const id = c.req.param("id");
|
|
3885
|
+
const body = await c.req.json();
|
|
3886
|
+
const rule = await updateReplacement(id, body);
|
|
3641
3887
|
if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3642
3888
|
return c.json(rule);
|
|
3643
3889
|
});
|
|
3644
3890
|
replacementsRoute.patch("/:id/toggle", async (c) => {
|
|
3645
|
-
const
|
|
3891
|
+
const id = c.req.param("id");
|
|
3892
|
+
const rule = await toggleReplacement(id);
|
|
3646
3893
|
if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
|
|
3647
3894
|
return c.json(rule);
|
|
3648
3895
|
});
|
|
@@ -3692,12 +3939,36 @@ const handleItemId = (parsed, tracker) => {
|
|
|
3692
3939
|
//#region src/routes/responses/handler.ts
|
|
3693
3940
|
const logger = createHandlerLogger("responses-handler");
|
|
3694
3941
|
const RESPONSES_ENDPOINT = "/responses";
|
|
3942
|
+
function isResponsesReasoningEffort(value) {
|
|
3943
|
+
return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
3944
|
+
}
|
|
3945
|
+
function normalizeResponsesReasoning(payload, suffixEffort) {
|
|
3946
|
+
const topLevelEffortRaw = payload.reasoningEffort ?? payload.reasoning_effort;
|
|
3947
|
+
const topLevelEffort = isResponsesReasoningEffort(topLevelEffortRaw) ? topLevelEffortRaw : void 0;
|
|
3948
|
+
if (topLevelEffort) payload.reasoning = payload.reasoning ? {
|
|
3949
|
+
...payload.reasoning,
|
|
3950
|
+
effort: payload.reasoning.effort ?? topLevelEffort
|
|
3951
|
+
} : { effort: topLevelEffort };
|
|
3952
|
+
delete payload.reasoningEffort;
|
|
3953
|
+
delete payload.reasoning_effort;
|
|
3954
|
+
if (suffixEffort) payload.reasoning = payload.reasoning ? {
|
|
3955
|
+
...payload.reasoning,
|
|
3956
|
+
effort: suffixEffort
|
|
3957
|
+
} : { effort: suffixEffort };
|
|
3958
|
+
return payload.reasoning?.effort ?? void 0;
|
|
3959
|
+
}
|
|
3695
3960
|
const handleResponses = async (c) => {
|
|
3696
3961
|
await checkRateLimit(state);
|
|
3697
3962
|
const payload = await c.req.json();
|
|
3963
|
+
const requestedModel = payload.model;
|
|
3964
|
+
const { baseModel, reasoningEffort: suffixEffort } = parseModelSuffix(payload.model);
|
|
3965
|
+
payload.model = baseModel;
|
|
3966
|
+
const effectiveEffort = normalizeResponsesReasoning(payload, suffixEffort);
|
|
3698
3967
|
setRequestContext(c, {
|
|
3699
|
-
|
|
3700
|
-
|
|
3968
|
+
requestedModel,
|
|
3969
|
+
provider: "Responses",
|
|
3970
|
+
model: payload.model,
|
|
3971
|
+
reasoningEffort: effectiveEffort
|
|
3701
3972
|
});
|
|
3702
3973
|
logger.debug("Responses request payload:", JSON.stringify(payload));
|
|
3703
3974
|
useFunctionApplyPatch(payload);
|
|
@@ -3806,6 +4077,7 @@ usageRoute.get("/", async (c) => {
|
|
|
3806
4077
|
//#endregion
|
|
3807
4078
|
//#region src/server.ts
|
|
3808
4079
|
const server = new Hono();
|
|
4080
|
+
server.use(apiKeyGuard);
|
|
3809
4081
|
server.use(requestLogger);
|
|
3810
4082
|
server.use(cors());
|
|
3811
4083
|
server.use("*", createAuthMiddleware());
|
|
@@ -3825,8 +4097,13 @@ server.route("/v1/messages", messageRoutes);
|
|
|
3825
4097
|
|
|
3826
4098
|
//#endregion
|
|
3827
4099
|
//#region src/start.ts
|
|
4100
|
+
function getAllModelIds() {
|
|
4101
|
+
const baseModelIds = state.models?.data.map((model) => model.id) ?? [];
|
|
4102
|
+
const virtualModelIds = state.models ? generateVirtualModels(state.models.data).map((model) => model.id) : [];
|
|
4103
|
+
return [...baseModelIds, ...virtualModelIds];
|
|
4104
|
+
}
|
|
3828
4105
|
async function runServer(options) {
|
|
3829
|
-
consola.info(`copilot-api v${version}`);
|
|
4106
|
+
consola.info(`copilot-api v${package_default.version}`);
|
|
3830
4107
|
if (options.insecure) {
|
|
3831
4108
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
3832
4109
|
consola.warn("SSL certificate verification disabled (insecure mode)");
|
|
@@ -3844,6 +4121,9 @@ async function runServer(options) {
|
|
|
3844
4121
|
state.showToken = options.showToken;
|
|
3845
4122
|
state.debug = options.debug;
|
|
3846
4123
|
state.verbose = options.verbose;
|
|
4124
|
+
state.apiKeyAuth = options.apiKeyAuth;
|
|
4125
|
+
if (options.apiKeyAuth) consola.info("API key authentication enabled - unauthorized requests will be silently dropped");
|
|
4126
|
+
if (options.host) consola.info(`Binding to host: ${options.host}`);
|
|
3847
4127
|
if (options.debug) consola.info("Debug mode enabled - raw HTTP requests will be logged");
|
|
3848
4128
|
await ensurePaths();
|
|
3849
4129
|
mergeConfigWithDefaults();
|
|
@@ -3854,12 +4134,9 @@ async function runServer(options) {
|
|
|
3854
4134
|
} else await setupGitHubToken();
|
|
3855
4135
|
await setupCopilotToken();
|
|
3856
4136
|
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];
|
|
4137
|
+
const allModelIds = getAllModelIds();
|
|
3861
4138
|
consola.info(`Available models: \n${allModelIds.map((id) => `- ${id}`).join("\n")}`);
|
|
3862
|
-
const serverUrl = `http
|
|
4139
|
+
const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
|
|
3863
4140
|
if (options.claudeCode) {
|
|
3864
4141
|
invariant(state.models, "Models should be loaded by now");
|
|
3865
4142
|
const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
@@ -3892,9 +4169,21 @@ async function runServer(options) {
|
|
|
3892
4169
|
serve({
|
|
3893
4170
|
fetch: server.fetch,
|
|
3894
4171
|
port: options.port,
|
|
4172
|
+
hostname: options.host,
|
|
3895
4173
|
bun: { idleTimeout: 255 }
|
|
3896
4174
|
});
|
|
3897
4175
|
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Resolve --api-key-auth value: use provided value, fall back to env, or error if flag used without value.
|
|
4178
|
+
*/
|
|
4179
|
+
function resolveApiKeyAuth(cliValue) {
|
|
4180
|
+
if (cliValue === void 0) return void 0;
|
|
4181
|
+
if (cliValue !== "" && cliValue !== "true") return cliValue;
|
|
4182
|
+
const envValue = process.env.COPILOT_API_KEY_AUTH;
|
|
4183
|
+
if (envValue) return envValue;
|
|
4184
|
+
consola.error("--api-key-auth requires a value or COPILOT_API_KEY_AUTH environment variable");
|
|
4185
|
+
process.exit(1);
|
|
4186
|
+
}
|
|
3898
4187
|
const start = defineCommand({
|
|
3899
4188
|
meta: {
|
|
3900
4189
|
name: "start",
|
|
@@ -3966,6 +4255,14 @@ const start = defineCommand({
|
|
|
3966
4255
|
type: "boolean",
|
|
3967
4256
|
default: false,
|
|
3968
4257
|
description: "Log raw HTTP requests received by the server (headers, method, path)"
|
|
4258
|
+
},
|
|
4259
|
+
"api-key-auth": {
|
|
4260
|
+
type: "string",
|
|
4261
|
+
description: "API key for incoming request authentication. Requests with mismatched keys are silently dropped."
|
|
4262
|
+
},
|
|
4263
|
+
host: {
|
|
4264
|
+
type: "string",
|
|
4265
|
+
description: "Hostname/IP to bind the server to (e.g., 0.0.0.0 for all interfaces)"
|
|
3969
4266
|
}
|
|
3970
4267
|
},
|
|
3971
4268
|
run({ args }) {
|
|
@@ -3983,17 +4280,19 @@ const start = defineCommand({
|
|
|
3983
4280
|
showToken: args["show-token"],
|
|
3984
4281
|
proxyEnv: args["proxy-env"],
|
|
3985
4282
|
insecure: args.insecure,
|
|
3986
|
-
debug: args.debug
|
|
4283
|
+
debug: args.debug,
|
|
4284
|
+
apiKeyAuth: resolveApiKeyAuth(args["api-key-auth"]),
|
|
4285
|
+
host: args.host
|
|
3987
4286
|
});
|
|
3988
4287
|
}
|
|
3989
4288
|
});
|
|
3990
4289
|
|
|
3991
4290
|
//#endregion
|
|
3992
4291
|
//#region src/main.ts
|
|
3993
|
-
|
|
4292
|
+
const main = defineCommand({
|
|
3994
4293
|
meta: {
|
|
3995
4294
|
name: "copilot-api",
|
|
3996
|
-
version,
|
|
4295
|
+
version: package_default.version,
|
|
3997
4296
|
description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
|
|
3998
4297
|
},
|
|
3999
4298
|
subCommands: {
|
|
@@ -4003,7 +4302,8 @@ await runMain(defineCommand({
|
|
|
4003
4302
|
debug,
|
|
4004
4303
|
config
|
|
4005
4304
|
}
|
|
4006
|
-
})
|
|
4305
|
+
});
|
|
4306
|
+
await runMain(main);
|
|
4007
4307
|
|
|
4008
4308
|
//#endregion
|
|
4009
4309
|
export { };
|