github-router 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +206 -206
- package/dist/main.js +390 -76
- package/dist/main.js.map +1 -1
- package/package.json +4 -4
package/dist/main.js
CHANGED
|
@@ -6,6 +6,8 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
|
+
import fs$1 from "node:fs";
|
|
10
|
+
import { Writable } from "node:stream";
|
|
9
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
10
12
|
import { serve } from "srvx";
|
|
11
13
|
import { getProxyForUrl } from "proxy-from-env";
|
|
@@ -17,11 +19,19 @@ import { events } from "fetch-event-stream";
|
|
|
17
19
|
import clipboard from "clipboardy";
|
|
18
20
|
|
|
19
21
|
//#region src/lib/paths.ts
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function appDir() {
|
|
23
|
+
return path.join(os.homedir(), ".local", "share", "github-router");
|
|
24
|
+
}
|
|
22
25
|
const PATHS = {
|
|
23
|
-
APP_DIR
|
|
24
|
-
|
|
26
|
+
get APP_DIR() {
|
|
27
|
+
return appDir();
|
|
28
|
+
},
|
|
29
|
+
get GITHUB_TOKEN_PATH() {
|
|
30
|
+
return path.join(appDir(), "github_token");
|
|
31
|
+
},
|
|
32
|
+
get ERROR_LOG_PATH() {
|
|
33
|
+
return path.join(appDir(), "error.log");
|
|
34
|
+
}
|
|
25
35
|
};
|
|
26
36
|
async function ensurePaths() {
|
|
27
37
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
@@ -43,6 +53,7 @@ const state = {
|
|
|
43
53
|
manualApprove: false,
|
|
44
54
|
rateLimitWait: false,
|
|
45
55
|
showToken: false,
|
|
56
|
+
extendedBetas: false,
|
|
46
57
|
sessionId: randomUUID(),
|
|
47
58
|
machineId: randomBytes(32).toString("hex")
|
|
48
59
|
};
|
|
@@ -53,7 +64,7 @@ const standardHeaders = () => ({
|
|
|
53
64
|
"content-type": "application/json",
|
|
54
65
|
accept: "application/json"
|
|
55
66
|
});
|
|
56
|
-
const COPILOT_VERSION = "0.
|
|
67
|
+
const COPILOT_VERSION = "0.43.2026033101";
|
|
57
68
|
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
58
69
|
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
59
70
|
const API_VERSION = "2025-10-01";
|
|
@@ -110,17 +121,27 @@ async function forwardError(c, error) {
|
|
|
110
121
|
} catch {
|
|
111
122
|
errorJson = void 0;
|
|
112
123
|
}
|
|
124
|
+
if (isAnthropicError(errorJson)) {
|
|
125
|
+
consola.error("HTTP error:", errorJson);
|
|
126
|
+
return c.json(errorJson, error.response.status);
|
|
127
|
+
}
|
|
113
128
|
const message = resolveErrorMessage(errorJson, errorText);
|
|
114
129
|
consola.error("HTTP error:", errorJson ?? errorText);
|
|
115
|
-
return c.json({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
return c.json({
|
|
131
|
+
type: "error",
|
|
132
|
+
error: {
|
|
133
|
+
type: resolveErrorType(error.response.status),
|
|
134
|
+
message
|
|
135
|
+
}
|
|
136
|
+
}, error.response.status);
|
|
119
137
|
}
|
|
120
|
-
return c.json({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
138
|
+
return c.json({
|
|
139
|
+
type: "error",
|
|
140
|
+
error: {
|
|
141
|
+
type: "api_error",
|
|
142
|
+
message: error instanceof Error ? error.message : String(error)
|
|
143
|
+
}
|
|
144
|
+
}, 500);
|
|
124
145
|
}
|
|
125
146
|
function resolveErrorMessage(errorJson, fallback) {
|
|
126
147
|
if (typeof errorJson !== "object" || errorJson === null) return fallback;
|
|
@@ -132,6 +153,30 @@ function resolveErrorMessage(errorJson, fallback) {
|
|
|
132
153
|
}
|
|
133
154
|
return fallback;
|
|
134
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if a parsed JSON body is already in Anthropic error format:
|
|
158
|
+
* { type: "error", error: { type: "...", message: "..." } }
|
|
159
|
+
*/
|
|
160
|
+
function isAnthropicError(json) {
|
|
161
|
+
if (typeof json !== "object" || json === null) return false;
|
|
162
|
+
const record = json;
|
|
163
|
+
if (record.type !== "error") return false;
|
|
164
|
+
if (typeof record.error !== "object" || record.error === null) return false;
|
|
165
|
+
const inner = record.error;
|
|
166
|
+
return typeof inner.type === "string" && typeof inner.message === "string";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Map HTTP status to Anthropic error type.
|
|
170
|
+
*/
|
|
171
|
+
function resolveErrorType(status) {
|
|
172
|
+
if (status === 400) return "invalid_request_error";
|
|
173
|
+
if (status === 401) return "authentication_error";
|
|
174
|
+
if (status === 403) return "permission_error";
|
|
175
|
+
if (status === 404) return "not_found_error";
|
|
176
|
+
if (status === 429) return "rate_limit_error";
|
|
177
|
+
if (status === 529) return "overloaded_error";
|
|
178
|
+
return "api_error";
|
|
179
|
+
}
|
|
135
180
|
|
|
136
181
|
//#endregion
|
|
137
182
|
//#region src/services/github/get-copilot-token.ts
|
|
@@ -204,23 +249,50 @@ const sleep = (ms) => new Promise((resolve) => {
|
|
|
204
249
|
});
|
|
205
250
|
const isNullish = (value) => value === null || value === void 0;
|
|
206
251
|
/**
|
|
207
|
-
* Beta
|
|
208
|
-
*
|
|
209
|
-
* so our requests match what VS Code produces.
|
|
252
|
+
* Beta prefixes VS Code Copilot Chat v0.43 actually sends.
|
|
253
|
+
* Default mode — makes proxy traffic indistinguishable from VS Code.
|
|
210
254
|
*/
|
|
211
|
-
const
|
|
255
|
+
const VSCODE_BETA_PREFIXES = [
|
|
212
256
|
"interleaved-thinking-",
|
|
213
257
|
"context-management-",
|
|
214
|
-
"advanced-tool-use-"
|
|
215
|
-
|
|
258
|
+
"advanced-tool-use-"
|
|
259
|
+
];
|
|
260
|
+
/**
|
|
261
|
+
* Extended beta prefixes for Claude CLI compatibility.
|
|
262
|
+
* Enabled via --extended-betas flag. Includes all betas confirmed
|
|
263
|
+
* to work with the Copilot API.
|
|
264
|
+
*
|
|
265
|
+
* Notably absent: output-128k- (Copilot returns 400).
|
|
266
|
+
*/
|
|
267
|
+
const EXTENDED_BETA_PREFIXES = [
|
|
268
|
+
...VSCODE_BETA_PREFIXES,
|
|
269
|
+
"claude-code-",
|
|
270
|
+
"context-1m-",
|
|
271
|
+
"effort-",
|
|
272
|
+
"prompt-caching-",
|
|
273
|
+
"computer-use-",
|
|
274
|
+
"pdfs-",
|
|
275
|
+
"max-tokens-",
|
|
276
|
+
"token-counting-",
|
|
277
|
+
"compact-",
|
|
278
|
+
"structured-outputs-",
|
|
279
|
+
"fast-mode-",
|
|
280
|
+
"skills-",
|
|
281
|
+
"mcp-client-",
|
|
282
|
+
"mcp-servers-",
|
|
283
|
+
"files-api-",
|
|
284
|
+
"redact-thinking-",
|
|
285
|
+
"web-search-"
|
|
216
286
|
];
|
|
217
287
|
/**
|
|
218
|
-
* Filter an `anthropic-beta` header value, keeping only beta flags
|
|
219
|
-
*
|
|
220
|
-
*
|
|
288
|
+
* Filter an `anthropic-beta` header value, keeping only beta flags
|
|
289
|
+
* in the active whitelist. Uses extended prefixes when --extended-betas
|
|
290
|
+
* is enabled, VS Code-only prefixes otherwise.
|
|
291
|
+
* Returns the filtered comma-separated string, or undefined if nothing remains.
|
|
221
292
|
*/
|
|
222
293
|
function filterBetaHeader(value) {
|
|
223
|
-
|
|
294
|
+
const prefixes = state.extendedBetas ? EXTENDED_BETA_PREFIXES : VSCODE_BETA_PREFIXES;
|
|
295
|
+
return value.split(",").map((v) => v.trim()).filter((v) => v && prefixes.some((prefix) => v.startsWith(prefix))).join(",") || void 0;
|
|
224
296
|
}
|
|
225
297
|
/**
|
|
226
298
|
* Normalize a model ID for fuzzy comparison: lowercase, replace dots with
|
|
@@ -462,6 +534,100 @@ const checkUsage = defineCommand({
|
|
|
462
534
|
}
|
|
463
535
|
});
|
|
464
536
|
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/lib/file-log-reporter.ts
|
|
539
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
540
|
+
const DEDUP_MAX = 1e3;
|
|
541
|
+
const ARG_MAX_LEN = 2048;
|
|
542
|
+
const DEDUP_KEY_MAX_LEN = 200;
|
|
543
|
+
const CREDENTIAL_RE = /\b(eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\s+\S{20,})\b/g;
|
|
544
|
+
const ALLOWED_TYPES = new Set([
|
|
545
|
+
"fatal",
|
|
546
|
+
"error",
|
|
547
|
+
"warn"
|
|
548
|
+
]);
|
|
549
|
+
function sanitize(line) {
|
|
550
|
+
return line.replace(CREDENTIAL_RE, "[REDACTED]");
|
|
551
|
+
}
|
|
552
|
+
function serializeArg(arg) {
|
|
553
|
+
if (typeof arg === "string") return arg;
|
|
554
|
+
if (arg instanceof Error) {
|
|
555
|
+
const parts = [arg.message];
|
|
556
|
+
if (arg.stack) parts.push(arg.stack);
|
|
557
|
+
return parts.join("\n");
|
|
558
|
+
}
|
|
559
|
+
return String(arg);
|
|
560
|
+
}
|
|
561
|
+
function formatLogLine(logObj) {
|
|
562
|
+
return sanitize(`${logObj.date.toISOString()} [${(logObj.type ?? "error").toUpperCase()}] ${logObj.args.map((a) => {
|
|
563
|
+
const s = serializeArg(a);
|
|
564
|
+
return s.length > ARG_MAX_LEN ? s.slice(0, ARG_MAX_LEN) + "…" : s;
|
|
565
|
+
}).join(" ").replace(/\r\n|\r|\n/g, "\\n")}\n`);
|
|
566
|
+
}
|
|
567
|
+
function makeDedupeKey(logObj) {
|
|
568
|
+
const firstArg = logObj.args.length > 0 ? serializeArg(logObj.args[0]) : "";
|
|
569
|
+
const key = `${logObj.type}:${firstArg}`;
|
|
570
|
+
return key.length > DEDUP_KEY_MAX_LEN ? key.slice(0, DEDUP_KEY_MAX_LEN) : key;
|
|
571
|
+
}
|
|
572
|
+
function rotateIfNeeded(filePath) {
|
|
573
|
+
let size;
|
|
574
|
+
try {
|
|
575
|
+
size = fs$1.statSync(filePath).size;
|
|
576
|
+
} catch {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (size <= MAX_LOG_BYTES) return;
|
|
580
|
+
try {
|
|
581
|
+
fs$1.renameSync(filePath, filePath + ".1");
|
|
582
|
+
} catch {}
|
|
583
|
+
}
|
|
584
|
+
var FileLogReporter = class {
|
|
585
|
+
filePath;
|
|
586
|
+
seen = /* @__PURE__ */ new Set();
|
|
587
|
+
writing = false;
|
|
588
|
+
constructor(filePath) {
|
|
589
|
+
this.filePath = filePath;
|
|
590
|
+
rotateIfNeeded(filePath);
|
|
591
|
+
}
|
|
592
|
+
log(logObj, _ctx) {
|
|
593
|
+
if (!ALLOWED_TYPES.has(logObj.type)) return;
|
|
594
|
+
if (this.writing) return;
|
|
595
|
+
const key = makeDedupeKey(logObj);
|
|
596
|
+
if (this.seen.has(key)) return;
|
|
597
|
+
if (this.seen.size >= DEDUP_MAX) this.seen.clear();
|
|
598
|
+
this.seen.add(key);
|
|
599
|
+
const line = formatLogLine(logObj);
|
|
600
|
+
this.writing = true;
|
|
601
|
+
try {
|
|
602
|
+
const fd = fs$1.openSync(this.filePath, "a", 384);
|
|
603
|
+
fs$1.writeSync(fd, line);
|
|
604
|
+
fs$1.closeSync(fd);
|
|
605
|
+
} catch {} finally {
|
|
606
|
+
this.writing = false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const nullStream = new Writable({ write(_chunk, _encoding, cb) {
|
|
611
|
+
cb();
|
|
612
|
+
} });
|
|
613
|
+
/**
|
|
614
|
+
* Switch consola to file-only mode for TUI sessions.
|
|
615
|
+
* Removes the terminal reporter and installs a file reporter that
|
|
616
|
+
* persists errors and warnings to disk with dedup and credential scrubbing.
|
|
617
|
+
*
|
|
618
|
+
* Also sinks consola's stdout/stderr streams as belt-and-suspenders:
|
|
619
|
+
* even if a terminal reporter is re-added, it cannot write to the terminal.
|
|
620
|
+
* Crash handlers that call process.stderr.write() directly are unaffected.
|
|
621
|
+
* FileLogReporter uses fs.writeSync() directly and is also unaffected.
|
|
622
|
+
*/
|
|
623
|
+
function enableFileLogging() {
|
|
624
|
+
const reporter = new FileLogReporter(PATHS.ERROR_LOG_PATH);
|
|
625
|
+
consola.options.throttle = 0;
|
|
626
|
+
consola.setReporters([reporter]);
|
|
627
|
+
consola.options.stdout = nullStream;
|
|
628
|
+
consola.options.stderr = nullStream;
|
|
629
|
+
}
|
|
630
|
+
|
|
465
631
|
//#endregion
|
|
466
632
|
//#region src/lib/port.ts
|
|
467
633
|
const DEFAULT_PORT = 8787;
|
|
@@ -506,7 +672,9 @@ function launchChild(target, server$1) {
|
|
|
506
672
|
const { cmd, env } = buildLaunchCommand(target);
|
|
507
673
|
const executable = cmd[0];
|
|
508
674
|
if (!commandExists(executable)) {
|
|
509
|
-
|
|
675
|
+
const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
|
|
676
|
+
consola.error(msg);
|
|
677
|
+
process$1.stderr.write(msg + "\n");
|
|
510
678
|
process$1.exit(1);
|
|
511
679
|
}
|
|
512
680
|
let child;
|
|
@@ -521,7 +689,9 @@ function launchChild(target, server$1) {
|
|
|
521
689
|
stdio: "inherit"
|
|
522
690
|
});
|
|
523
691
|
} catch (error) {
|
|
524
|
-
|
|
692
|
+
const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
|
|
693
|
+
consola.error(msg);
|
|
694
|
+
process$1.stderr.write(msg + "\n");
|
|
525
695
|
server$1.close(true).catch(() => {});
|
|
526
696
|
process$1.exit(1);
|
|
527
697
|
}
|
|
@@ -549,10 +719,13 @@ function launchChild(target, server$1) {
|
|
|
549
719
|
};
|
|
550
720
|
process$1.on("SIGINT", onSignal);
|
|
551
721
|
process$1.on("SIGTERM", onSignal);
|
|
552
|
-
child.on("exit", (exitCode) => {
|
|
553
|
-
|
|
722
|
+
child.on("exit", (exitCode, signal) => {
|
|
723
|
+
const code = exitCode ?? (signal ? 128 : 1);
|
|
724
|
+
cleanup().then(() => exit(code)).catch(() => exit(1));
|
|
725
|
+
});
|
|
726
|
+
child.on("error", () => {
|
|
727
|
+
cleanup().then(() => exit(1)).catch(() => exit(1));
|
|
554
728
|
});
|
|
555
|
-
child.on("error", () => exit(1));
|
|
556
729
|
}
|
|
557
730
|
|
|
558
731
|
//#endregion
|
|
@@ -1201,16 +1374,18 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
1201
1374
|
* (anthropic-beta) so Copilot enables extended features.
|
|
1202
1375
|
*/
|
|
1203
1376
|
function buildHeaders(extraHeaders) {
|
|
1204
|
-
|
|
1377
|
+
const headers = {
|
|
1205
1378
|
...copilotHeaders(state),
|
|
1206
1379
|
accept: "application/json",
|
|
1207
|
-
"openai-intent": "
|
|
1380
|
+
"openai-intent": "messages-proxy",
|
|
1208
1381
|
"x-interaction-type": "conversation-agent",
|
|
1209
1382
|
"X-Initiator": "agent",
|
|
1210
1383
|
"anthropic-version": "2023-06-01",
|
|
1211
1384
|
"X-Interaction-Id": randomUUID(),
|
|
1212
1385
|
...extraHeaders
|
|
1213
1386
|
};
|
|
1387
|
+
delete headers["copilot-integration-id"];
|
|
1388
|
+
return headers;
|
|
1214
1389
|
}
|
|
1215
1390
|
/**
|
|
1216
1391
|
* Forward an Anthropic Messages API request to Copilot's native /v1/messages endpoint.
|
|
@@ -1332,7 +1507,7 @@ async function handleCountTokens(c) {
|
|
|
1332
1507
|
return c.json(responseBody);
|
|
1333
1508
|
}
|
|
1334
1509
|
/**
|
|
1335
|
-
* Parse the JSON body, resolve the model name, and re-serialize.
|
|
1510
|
+
* Parse the JSON body, resolve the model name, sanitize cache_control, and re-serialize.
|
|
1336
1511
|
*/
|
|
1337
1512
|
function resolveModelInBody$1(rawBody) {
|
|
1338
1513
|
let parsed;
|
|
@@ -1342,23 +1517,41 @@ function resolveModelInBody$1(rawBody) {
|
|
|
1342
1517
|
return { body: rawBody };
|
|
1343
1518
|
}
|
|
1344
1519
|
const originalModel = typeof parsed.model === "string" ? parsed.model : void 0;
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
originalModel
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1520
|
+
let modified = false;
|
|
1521
|
+
if (originalModel) {
|
|
1522
|
+
const resolved = resolveModel(originalModel);
|
|
1523
|
+
if (resolved !== originalModel) {
|
|
1524
|
+
parsed.model = resolved;
|
|
1525
|
+
modified = true;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (rawBody.includes("\"scope\"")) {
|
|
1529
|
+
sanitizeCacheControl$1(parsed);
|
|
1530
|
+
modified = true;
|
|
1531
|
+
}
|
|
1532
|
+
const resolvedModel = typeof parsed.model === "string" ? parsed.model : originalModel;
|
|
1356
1533
|
return {
|
|
1357
|
-
body: JSON.stringify(parsed),
|
|
1534
|
+
body: modified ? JSON.stringify(parsed) : rawBody,
|
|
1358
1535
|
originalModel,
|
|
1359
|
-
resolvedModel
|
|
1536
|
+
resolvedModel
|
|
1360
1537
|
};
|
|
1361
1538
|
}
|
|
1539
|
+
function sanitizeCacheControl$1(body) {
|
|
1540
|
+
function stripScope(block) {
|
|
1541
|
+
if (block.cache_control?.scope !== void 0) {
|
|
1542
|
+
delete block.cache_control.scope;
|
|
1543
|
+
if (Object.keys(block.cache_control).length === 0) delete block.cache_control;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (Array.isArray(body.system)) for (const block of body.system) stripScope(block);
|
|
1547
|
+
if (Array.isArray(body.messages)) {
|
|
1548
|
+
for (const msg of body.messages) if (Array.isArray(msg.content)) for (const block of msg.content) {
|
|
1549
|
+
stripScope(block);
|
|
1550
|
+
if (Array.isArray(block.content)) for (const nested of block.content) stripScope(nested);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (Array.isArray(body.tools)) for (const tool of body.tools) stripScope(tool);
|
|
1554
|
+
}
|
|
1362
1555
|
|
|
1363
1556
|
//#endregion
|
|
1364
1557
|
//#region src/routes/messages/handler.ts
|
|
@@ -1504,13 +1697,18 @@ async function handleCompletion(c) {
|
|
|
1504
1697
|
streaming: true
|
|
1505
1698
|
}, selectedModel, startTime);
|
|
1506
1699
|
if (debugEnabled) consola.debug("Streaming response from Copilot /v1/messages");
|
|
1700
|
+
const streamHeaders = {
|
|
1701
|
+
"content-type": "text/event-stream",
|
|
1702
|
+
"cache-control": "no-cache",
|
|
1703
|
+
connection: "keep-alive"
|
|
1704
|
+
};
|
|
1705
|
+
const requestId = response.headers.get("x-request-id");
|
|
1706
|
+
if (requestId) streamHeaders["x-request-id"] = requestId;
|
|
1707
|
+
const reqId = response.headers.get("request-id");
|
|
1708
|
+
if (reqId) streamHeaders["request-id"] = reqId;
|
|
1507
1709
|
return new Response(response.body, {
|
|
1508
1710
|
status: response.status,
|
|
1509
|
-
headers:
|
|
1510
|
-
"content-type": "text/event-stream",
|
|
1511
|
-
"cache-control": "no-cache",
|
|
1512
|
-
connection: "keep-alive"
|
|
1513
|
-
}
|
|
1711
|
+
headers: streamHeaders
|
|
1514
1712
|
});
|
|
1515
1713
|
}
|
|
1516
1714
|
const responseBody = await response.json();
|
|
@@ -1524,11 +1722,18 @@ async function handleCompletion(c) {
|
|
|
1524
1722
|
status: response.status
|
|
1525
1723
|
}, selectedModel, startTime);
|
|
1526
1724
|
if (debugEnabled) consola.debug("Non-streaming response from Copilot /v1/messages:", JSON.stringify(responseBody).slice(0, 2e3));
|
|
1725
|
+
const xRequestId = response.headers.get("x-request-id");
|
|
1726
|
+
if (xRequestId) c.header("x-request-id", xRequestId);
|
|
1727
|
+
const requestIdHeader = response.headers.get("request-id");
|
|
1728
|
+
if (requestIdHeader) c.header("request-id", requestIdHeader);
|
|
1527
1729
|
return c.json(responseBody, response.status);
|
|
1528
1730
|
}
|
|
1529
1731
|
/**
|
|
1530
|
-
* Parse the JSON body, resolve the model name,
|
|
1531
|
-
* Returns the body string plus the original
|
|
1732
|
+
* Parse the JSON body, resolve the model name, sanitize cache_control
|
|
1733
|
+
* fields, and re-serialize. Returns the body string plus the original
|
|
1734
|
+
* and resolved model names.
|
|
1735
|
+
*
|
|
1736
|
+
* Re-serialization is skipped when no modifications are needed.
|
|
1532
1737
|
*/
|
|
1533
1738
|
function resolveModelInBody(rawBody) {
|
|
1534
1739
|
let parsed;
|
|
@@ -1538,24 +1743,50 @@ function resolveModelInBody(rawBody) {
|
|
|
1538
1743
|
return { body: rawBody };
|
|
1539
1744
|
}
|
|
1540
1745
|
const originalModel = typeof parsed.model === "string" ? parsed.model : void 0;
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
originalModel
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1746
|
+
let modified = false;
|
|
1747
|
+
if (originalModel) {
|
|
1748
|
+
const resolved = resolveModel(originalModel);
|
|
1749
|
+
if (resolved !== originalModel) {
|
|
1750
|
+
parsed.model = resolved;
|
|
1751
|
+
modified = true;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (rawBody.includes("\"scope\"")) {
|
|
1755
|
+
sanitizeCacheControl(parsed);
|
|
1756
|
+
modified = true;
|
|
1757
|
+
}
|
|
1758
|
+
const resolvedModel = typeof parsed.model === "string" ? parsed.model : originalModel;
|
|
1552
1759
|
return {
|
|
1553
|
-
body: JSON.stringify(parsed),
|
|
1760
|
+
body: modified ? JSON.stringify(parsed) : rawBody,
|
|
1554
1761
|
originalModel,
|
|
1555
|
-
resolvedModel
|
|
1762
|
+
resolvedModel
|
|
1556
1763
|
};
|
|
1557
1764
|
}
|
|
1558
1765
|
/**
|
|
1766
|
+
* Strip the `scope` field from all `cache_control` objects in the body.
|
|
1767
|
+
* Claude CLI 2.1.88+ sends {"type":"ephemeral","scope":"global"} which
|
|
1768
|
+
* Copilot rejects. Mutates the parsed object in place.
|
|
1769
|
+
*
|
|
1770
|
+
* Covers: system blocks, message content blocks (including nested
|
|
1771
|
+
* tool_result content), and tool definitions.
|
|
1772
|
+
*/
|
|
1773
|
+
function sanitizeCacheControl(body) {
|
|
1774
|
+
function stripScope(block) {
|
|
1775
|
+
if (block.cache_control?.scope !== void 0) {
|
|
1776
|
+
delete block.cache_control.scope;
|
|
1777
|
+
if (Object.keys(block.cache_control).length === 0) delete block.cache_control;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (Array.isArray(body.system)) for (const block of body.system) stripScope(block);
|
|
1781
|
+
if (Array.isArray(body.messages)) {
|
|
1782
|
+
for (const msg of body.messages) if (Array.isArray(msg.content)) for (const block of msg.content) {
|
|
1783
|
+
stripScope(block);
|
|
1784
|
+
if (Array.isArray(block.content)) for (const nested of block.content) stripScope(nested);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (Array.isArray(body.tools)) for (const tool of body.tools) stripScope(tool);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1559
1790
|
* Apply default anthropic-beta values for Claude models when the client
|
|
1560
1791
|
* (e.g. curl) sends no beta headers. Claude CLI sends its own betas,
|
|
1561
1792
|
* so this only fires as a safety net for bare clients.
|
|
@@ -1565,7 +1796,7 @@ function applyDefaultBetas(betaHeaders, modelId) {
|
|
|
1565
1796
|
if (!modelId || !modelId.startsWith("claude-")) return betaHeaders;
|
|
1566
1797
|
return {
|
|
1567
1798
|
...betaHeaders,
|
|
1568
|
-
"anthropic-beta": ["interleaved-thinking-2025-05-14", "
|
|
1799
|
+
"anthropic-beta": ["interleaved-thinking-2025-05-14", "context-management-2025-06-27"].join(",")
|
|
1569
1800
|
};
|
|
1570
1801
|
}
|
|
1571
1802
|
|
|
@@ -1776,31 +2007,99 @@ function extractUserQuery(input) {
|
|
|
1776
2007
|
}
|
|
1777
2008
|
}
|
|
1778
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Compaction prompt used when GitHub Copilot API does not support
|
|
2012
|
+
* /responses/compact natively. Matches the prompt Codex CLI uses for
|
|
2013
|
+
* local (non-OpenAI) compaction.
|
|
2014
|
+
*/
|
|
2015
|
+
const COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
2016
|
+
|
|
2017
|
+
Include:
|
|
2018
|
+
- Current progress and key decisions made
|
|
2019
|
+
- Important context, constraints, or user preferences
|
|
2020
|
+
- What remains to be done (clear next steps)
|
|
2021
|
+
- Any critical data, examples, or references needed to continue
|
|
2022
|
+
|
|
2023
|
+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
|
|
1779
2024
|
async function handleResponsesCompact(c) {
|
|
1780
2025
|
const startTime = Date.now();
|
|
1781
2026
|
await checkRateLimit(state);
|
|
1782
2027
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1783
2028
|
if (state.manualApprove) await awaitApproval();
|
|
1784
|
-
const body = await c.req.
|
|
2029
|
+
const body = await c.req.json();
|
|
1785
2030
|
const response = await fetch(`${copilotBaseUrl(state)}/responses/compact`, {
|
|
1786
2031
|
method: "POST",
|
|
1787
2032
|
headers: copilotHeaders(state),
|
|
1788
|
-
body
|
|
2033
|
+
body: JSON.stringify(body)
|
|
1789
2034
|
});
|
|
1790
|
-
if (
|
|
2035
|
+
if (response.ok) {
|
|
1791
2036
|
logRequest({
|
|
1792
2037
|
method: "POST",
|
|
1793
2038
|
path: c.req.path,
|
|
1794
|
-
status:
|
|
2039
|
+
status: 200
|
|
1795
2040
|
}, void 0, startTime);
|
|
1796
|
-
|
|
2041
|
+
return c.json(await response.json());
|
|
2042
|
+
}
|
|
2043
|
+
if (response.status === 404) {
|
|
2044
|
+
consola.debug("Copilot API does not support /responses/compact, using synthetic compaction");
|
|
2045
|
+
return await syntheticCompact(c, body, startTime);
|
|
2046
|
+
}
|
|
2047
|
+
logRequest({
|
|
2048
|
+
method: "POST",
|
|
2049
|
+
path: c.req.path,
|
|
2050
|
+
status: response.status
|
|
2051
|
+
}, void 0, startTime);
|
|
2052
|
+
throw new HTTPError("Copilot responses/compact request failed", response);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Synthetic compaction: sends the conversation history to Copilot's
|
|
2056
|
+
* regular /responses endpoint with a compaction prompt appended,
|
|
2057
|
+
* then returns the model's summary in the compact response format.
|
|
2058
|
+
*/
|
|
2059
|
+
async function syntheticCompact(c, body, startTime) {
|
|
2060
|
+
const input = Array.isArray(body.input) ? [...body.input] : [];
|
|
2061
|
+
input.push({
|
|
2062
|
+
type: "message",
|
|
2063
|
+
role: "user",
|
|
2064
|
+
content: [{
|
|
2065
|
+
type: "input_text",
|
|
2066
|
+
text: COMPACTION_PROMPT
|
|
2067
|
+
}]
|
|
2068
|
+
});
|
|
2069
|
+
const payload = {
|
|
2070
|
+
model: body.model,
|
|
2071
|
+
input,
|
|
2072
|
+
instructions: body.instructions,
|
|
2073
|
+
stream: false,
|
|
2074
|
+
store: false
|
|
2075
|
+
};
|
|
2076
|
+
let result;
|
|
2077
|
+
try {
|
|
2078
|
+
result = await createResponses(payload);
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
if (error instanceof HTTPError) logRequest({
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
path: c.req.path,
|
|
2083
|
+
status: error.response.status
|
|
2084
|
+
}, void 0, startTime);
|
|
2085
|
+
throw error;
|
|
1797
2086
|
}
|
|
1798
2087
|
logRequest({
|
|
1799
2088
|
method: "POST",
|
|
1800
2089
|
path: c.req.path,
|
|
1801
2090
|
status: 200
|
|
1802
2091
|
}, void 0, startTime);
|
|
1803
|
-
return c.json(
|
|
2092
|
+
return c.json({
|
|
2093
|
+
id: `resp_compact_${randomUUID().replace(/-/g, "").slice(0, 24)}`,
|
|
2094
|
+
object: "response.compaction",
|
|
2095
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
2096
|
+
output: result.output,
|
|
2097
|
+
usage: result.usage ?? {
|
|
2098
|
+
input_tokens: 0,
|
|
2099
|
+
output_tokens: 0,
|
|
2100
|
+
total_tokens: 0
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
1804
2103
|
}
|
|
1805
2104
|
|
|
1806
2105
|
//#endregion
|
|
@@ -1864,6 +2163,7 @@ usageRoute.get("/", async (c) => {
|
|
|
1864
2163
|
const server = new Hono();
|
|
1865
2164
|
server.use(cors());
|
|
1866
2165
|
server.get("/", (c) => c.text("Server running"));
|
|
2166
|
+
server.on("HEAD", ["/"], (c) => c.body(null, 200));
|
|
1867
2167
|
server.route("/chat/completions", completionRoutes);
|
|
1868
2168
|
server.route("/responses", responsesRoutes);
|
|
1869
2169
|
server.route("/models", modelRoutes);
|
|
@@ -1877,6 +2177,13 @@ server.route("/v1/models", modelRoutes);
|
|
|
1877
2177
|
server.route("/v1/embeddings", embeddingRoutes);
|
|
1878
2178
|
server.route("/v1/search", searchRoutes);
|
|
1879
2179
|
server.route("/v1/messages", messageRoutes);
|
|
2180
|
+
server.notFound((c) => c.json({
|
|
2181
|
+
type: "error",
|
|
2182
|
+
error: {
|
|
2183
|
+
type: "not_found_error",
|
|
2184
|
+
message: `${c.req.method} ${c.req.path} not found`
|
|
2185
|
+
}
|
|
2186
|
+
}, 404));
|
|
1880
2187
|
|
|
1881
2188
|
//#endregion
|
|
1882
2189
|
//#region src/lib/server-setup.ts
|
|
@@ -1893,6 +2200,7 @@ async function setupAndServe(options) {
|
|
|
1893
2200
|
state.rateLimitSeconds = options.rateLimit;
|
|
1894
2201
|
state.rateLimitWait = options.rateLimitWait;
|
|
1895
2202
|
state.showToken = options.showToken;
|
|
2203
|
+
state.extendedBetas = options.extendedBetas;
|
|
1896
2204
|
if (process.env.COPILOT_API_URL) state.copilotApiUrl = process.env.COPILOT_API_URL;
|
|
1897
2205
|
await ensurePaths();
|
|
1898
2206
|
await cacheVSCodeVersion();
|
|
@@ -1989,6 +2297,11 @@ const sharedServerArgs = {
|
|
|
1989
2297
|
type: "boolean",
|
|
1990
2298
|
default: false,
|
|
1991
2299
|
description: "Initialize proxy from environment variables"
|
|
2300
|
+
},
|
|
2301
|
+
"extended-betas": {
|
|
2302
|
+
type: "boolean",
|
|
2303
|
+
default: false,
|
|
2304
|
+
description: "Forward extended beta headers for Claude CLI compatibility (default: VS Code-only)"
|
|
1992
2305
|
}
|
|
1993
2306
|
};
|
|
1994
2307
|
const allowedAccountTypes = new Set([
|
|
@@ -2024,7 +2337,8 @@ function parseSharedArgs(args) {
|
|
|
2024
2337
|
rateLimitWait,
|
|
2025
2338
|
githubToken,
|
|
2026
2339
|
showToken: args["show-token"],
|
|
2027
|
-
proxyEnv: args["proxy-env"]
|
|
2340
|
+
proxyEnv: args["proxy-env"],
|
|
2341
|
+
extendedBetas: args["extended-betas"]
|
|
2028
2342
|
};
|
|
2029
2343
|
}
|
|
2030
2344
|
/** Build environment variables for Claude Code. */
|
|
@@ -2081,6 +2395,7 @@ const claude = defineCommand({
|
|
|
2081
2395
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
2082
2396
|
process$1.exit(1);
|
|
2083
2397
|
}
|
|
2398
|
+
enableFileLogging();
|
|
2084
2399
|
let resolvedModel;
|
|
2085
2400
|
if (args.model) {
|
|
2086
2401
|
resolvedModel = resolveModel(args.model);
|
|
@@ -2090,8 +2405,7 @@ const claude = defineCommand({
|
|
|
2090
2405
|
consola.warn(`Model "${resolvedModel}" not found. Available claude models: ${available.join(", ")}`);
|
|
2091
2406
|
}
|
|
2092
2407
|
}
|
|
2093
|
-
|
|
2094
|
-
consola.level = 1;
|
|
2408
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code...\n`);
|
|
2095
2409
|
launchChild({
|
|
2096
2410
|
kind: "claude-code",
|
|
2097
2411
|
envVars: getClaudeCodeEnvVars(serverUrl, resolvedModel ?? args.model),
|
|
@@ -2137,6 +2451,7 @@ const codex = defineCommand({
|
|
|
2137
2451
|
process$1.exit(1);
|
|
2138
2452
|
}
|
|
2139
2453
|
const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
|
|
2454
|
+
enableFileLogging();
|
|
2140
2455
|
const codexModel = resolveCodexModel(requestedModel);
|
|
2141
2456
|
if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
|
|
2142
2457
|
const modelEntry = state.models?.data.find((m) => m.id === codexModel);
|
|
@@ -2147,8 +2462,7 @@ const codex = defineCommand({
|
|
|
2147
2462
|
const ctx = modelEntry.capabilities?.limits?.max_context_window_tokens;
|
|
2148
2463
|
if (ctx) consola.info(`Model context window: ${ctx.toLocaleString()} tokens`);
|
|
2149
2464
|
}
|
|
2150
|
-
|
|
2151
|
-
consola.level = 1;
|
|
2465
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...\n`);
|
|
2152
2466
|
launchChild({
|
|
2153
2467
|
kind: "codex",
|
|
2154
2468
|
envVars: getCodexEnvVars(serverUrl),
|