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/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
- const APP_DIR = path.join(os.homedir(), ".local", "share", "github-router");
21
- const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
22
+ function appDir() {
23
+ return path.join(os.homedir(), ".local", "share", "github-router");
24
+ }
22
25
  const PATHS = {
23
- APP_DIR,
24
- GITHUB_TOKEN_PATH
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.38.2026021302";
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({ error: {
116
- message,
117
- type: "error"
118
- } }, error.response.status);
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({ error: {
121
- message: error instanceof Error ? error.message : String(error),
122
- type: "error"
123
- } }, 500);
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 values that VS Code Copilot Chat actually sends to the Copilot API.
208
- * Only these are forwarded; everything else (e.g. context-1m-*) is stripped
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 ALLOWED_BETA_PREFIXES = [
255
+ const VSCODE_BETA_PREFIXES = [
212
256
  "interleaved-thinking-",
213
257
  "context-management-",
214
- "advanced-tool-use-",
215
- "token-counting-"
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 that
219
- * VS Code Copilot is known to send. Returns the filtered comma-separated
220
- * string, or undefined if nothing remains.
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
- return value.split(",").map((v) => v.trim()).filter((v) => v && ALLOWED_BETA_PREFIXES.some((prefix) => v.startsWith(prefix))).join(",") || void 0;
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
- consola.error(`"${executable}" not found on PATH. Install it first, then try again.`);
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
- consola.error(`Failed to launch ${executable}:`, error instanceof Error ? error.message : String(error));
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
- cleanup().then(() => exit(exitCode ?? 0)).catch(() => exit(1));
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
- return {
1377
+ const headers = {
1205
1378
  ...copilotHeaders(state),
1206
1379
  accept: "application/json",
1207
- "openai-intent": "conversation-agent",
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
- if (!originalModel) return {
1346
- body: rawBody,
1347
- originalModel
1348
- };
1349
- const resolved = resolveModel(originalModel);
1350
- if (resolved === originalModel) return {
1351
- body: rawBody,
1352
- originalModel,
1353
- resolvedModel: originalModel
1354
- };
1355
- parsed.model = resolved;
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: resolved
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, and re-serialize.
1531
- * Returns the body string plus the original and resolved model names.
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
- if (!originalModel) return {
1542
- body: rawBody,
1543
- originalModel
1544
- };
1545
- const resolved = resolveModel(originalModel);
1546
- if (resolved === originalModel) return {
1547
- body: rawBody,
1548
- originalModel,
1549
- resolvedModel: originalModel
1550
- };
1551
- parsed.model = resolved;
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: resolved
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", "token-counting-2024-11-01"].join(",")
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.text();
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 (!response.ok) {
2035
+ if (response.ok) {
1791
2036
  logRequest({
1792
2037
  method: "POST",
1793
2038
  path: c.req.path,
1794
- status: response.status
2039
+ status: 200
1795
2040
  }, void 0, startTime);
1796
- throw new HTTPError("Copilot responses/compact request failed", response);
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(await response.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
- consola.success(`Server ready on ${serverUrl}, launching Claude Code...`);
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
- consola.success(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...`);
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),