github-router 0.3.11 → 0.3.13
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 +328 -33
- package/dist/main.js.map +1 -1
- package/package.json +6 -3
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";
|
|
@@ -19,9 +21,11 @@ import clipboard from "clipboardy";
|
|
|
19
21
|
//#region src/lib/paths.ts
|
|
20
22
|
const APP_DIR = path.join(os.homedir(), ".local", "share", "github-router");
|
|
21
23
|
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
24
|
+
const ERROR_LOG_PATH = path.join(APP_DIR, "error.log");
|
|
22
25
|
const PATHS = {
|
|
23
26
|
APP_DIR,
|
|
24
|
-
GITHUB_TOKEN_PATH
|
|
27
|
+
GITHUB_TOKEN_PATH,
|
|
28
|
+
ERROR_LOG_PATH
|
|
25
29
|
};
|
|
26
30
|
async function ensurePaths() {
|
|
27
31
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
@@ -223,19 +227,68 @@ function filterBetaHeader(value) {
|
|
|
223
227
|
return value.split(",").map((v) => v.trim()).filter((v) => v && ALLOWED_BETA_PREFIXES.some((prefix) => v.startsWith(prefix))).join(",") || void 0;
|
|
224
228
|
}
|
|
225
229
|
/**
|
|
230
|
+
* Normalize a model ID for fuzzy comparison: lowercase, replace dots with
|
|
231
|
+
* dashes, insert dash at letter→digit boundaries, and collapse repeated
|
|
232
|
+
* dashes. E.g. "gpt5.3-codex" → "gpt-5-3-codex", "GPT-5.3-Codex" → "gpt-5-3-codex".
|
|
233
|
+
*/
|
|
234
|
+
function normalizeModelId(id) {
|
|
235
|
+
return id.toLowerCase().replace(/\./g, "-").replace(/([a-z])(\d)/g, "$1-$2").replace(/-{2,}/g, "-");
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
226
238
|
* Resolve a model name to the best available variant in the Copilot model list.
|
|
227
|
-
*
|
|
239
|
+
*
|
|
240
|
+
* Resolution cascade:
|
|
241
|
+
* 1. Exact match
|
|
242
|
+
* 2. Case-insensitive match
|
|
243
|
+
* 3. Family preference (opus→1m, codex→highest version)
|
|
244
|
+
* 4. Normalized match (dots→dashes, letter-digit boundaries)
|
|
245
|
+
* 5. Return as-is with a warning
|
|
228
246
|
*/
|
|
229
247
|
function resolveModel(modelId) {
|
|
230
248
|
const models = state.models?.data;
|
|
231
249
|
if (!models) return modelId;
|
|
232
250
|
if (models.some((m) => m.id === modelId)) return modelId;
|
|
233
|
-
|
|
251
|
+
const lower = modelId.toLowerCase();
|
|
252
|
+
const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
|
|
253
|
+
if (ciMatch) return ciMatch.id;
|
|
254
|
+
if (lower.includes("opus")) {
|
|
234
255
|
const oneM = models.find((m) => m.id.includes("opus") && m.id.endsWith("-1m"));
|
|
235
256
|
if (oneM) return oneM.id;
|
|
236
257
|
}
|
|
258
|
+
if (lower.includes("codex")) {
|
|
259
|
+
const codexModels = models.filter((m) => m.id.includes("codex") && !m.id.includes("mini"));
|
|
260
|
+
if (codexModels.length > 0) {
|
|
261
|
+
codexModels.sort((a, b) => b.id.localeCompare(a.id));
|
|
262
|
+
return codexModels[0].id;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const normalized = normalizeModelId(modelId);
|
|
266
|
+
const normMatch = models.find((m) => normalizeModelId(m.id) === normalized);
|
|
267
|
+
if (normMatch) return normMatch.id;
|
|
268
|
+
consola.warn(`Model "${modelId}" not found in Copilot model list. Available: ${models.map((m) => m.id).join(", ")}`);
|
|
237
269
|
return modelId;
|
|
238
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Resolve a codex model ID, falling back to the best available codex model.
|
|
273
|
+
* Used by the codex subcommand for model selection.
|
|
274
|
+
*/
|
|
275
|
+
function resolveCodexModel(modelId) {
|
|
276
|
+
const resolved = resolveModel(modelId);
|
|
277
|
+
const models = state.models?.data;
|
|
278
|
+
if (!models) return resolved;
|
|
279
|
+
if (models.some((m) => m.id === resolved)) return resolved;
|
|
280
|
+
const codexModels = models.filter((m) => {
|
|
281
|
+
const endpoints = m.supported_endpoints ?? [];
|
|
282
|
+
return m.id.includes("codex") && !m.id.includes("mini") && (endpoints.length === 0 || endpoints.includes("/responses"));
|
|
283
|
+
});
|
|
284
|
+
if (codexModels.length > 0) {
|
|
285
|
+
codexModels.sort((a, b) => b.id.localeCompare(a.id));
|
|
286
|
+
const best = codexModels[0].id;
|
|
287
|
+
consola.warn(`Model "${modelId}" not available, using "${best}" instead`);
|
|
288
|
+
return best;
|
|
289
|
+
}
|
|
290
|
+
return resolved;
|
|
291
|
+
}
|
|
239
292
|
async function cacheModels() {
|
|
240
293
|
state.models = await getModels();
|
|
241
294
|
}
|
|
@@ -413,10 +466,104 @@ const checkUsage = defineCommand({
|
|
|
413
466
|
}
|
|
414
467
|
});
|
|
415
468
|
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/lib/file-log-reporter.ts
|
|
471
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
472
|
+
const DEDUP_MAX = 1e3;
|
|
473
|
+
const ARG_MAX_LEN = 2048;
|
|
474
|
+
const DEDUP_KEY_MAX_LEN = 200;
|
|
475
|
+
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;
|
|
476
|
+
const ALLOWED_TYPES = new Set([
|
|
477
|
+
"fatal",
|
|
478
|
+
"error",
|
|
479
|
+
"warn"
|
|
480
|
+
]);
|
|
481
|
+
function sanitize(line) {
|
|
482
|
+
return line.replace(CREDENTIAL_RE, "[REDACTED]");
|
|
483
|
+
}
|
|
484
|
+
function serializeArg(arg) {
|
|
485
|
+
if (typeof arg === "string") return arg;
|
|
486
|
+
if (arg instanceof Error) {
|
|
487
|
+
const parts = [arg.message];
|
|
488
|
+
if (arg.stack) parts.push(arg.stack);
|
|
489
|
+
return parts.join("\n");
|
|
490
|
+
}
|
|
491
|
+
return String(arg);
|
|
492
|
+
}
|
|
493
|
+
function formatLogLine(logObj) {
|
|
494
|
+
return sanitize(`${logObj.date.toISOString()} [${(logObj.type ?? "error").toUpperCase()}] ${logObj.args.map((a) => {
|
|
495
|
+
const s = serializeArg(a);
|
|
496
|
+
return s.length > ARG_MAX_LEN ? s.slice(0, ARG_MAX_LEN) + "…" : s;
|
|
497
|
+
}).join(" ").replace(/\r\n|\r|\n/g, "\\n")}\n`);
|
|
498
|
+
}
|
|
499
|
+
function makeDedupeKey(logObj) {
|
|
500
|
+
const firstArg = logObj.args.length > 0 ? serializeArg(logObj.args[0]) : "";
|
|
501
|
+
const key = `${logObj.type}:${firstArg}`;
|
|
502
|
+
return key.length > DEDUP_KEY_MAX_LEN ? key.slice(0, DEDUP_KEY_MAX_LEN) : key;
|
|
503
|
+
}
|
|
504
|
+
function rotateIfNeeded(filePath) {
|
|
505
|
+
let size;
|
|
506
|
+
try {
|
|
507
|
+
size = fs$1.statSync(filePath).size;
|
|
508
|
+
} catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (size <= MAX_LOG_BYTES) return;
|
|
512
|
+
try {
|
|
513
|
+
fs$1.renameSync(filePath, filePath + ".1");
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
var FileLogReporter = class {
|
|
517
|
+
filePath;
|
|
518
|
+
seen = /* @__PURE__ */ new Set();
|
|
519
|
+
writing = false;
|
|
520
|
+
constructor(filePath) {
|
|
521
|
+
this.filePath = filePath;
|
|
522
|
+
rotateIfNeeded(filePath);
|
|
523
|
+
}
|
|
524
|
+
log(logObj, _ctx) {
|
|
525
|
+
if (!ALLOWED_TYPES.has(logObj.type)) return;
|
|
526
|
+
if (this.writing) return;
|
|
527
|
+
const key = makeDedupeKey(logObj);
|
|
528
|
+
if (this.seen.has(key)) return;
|
|
529
|
+
if (this.seen.size >= DEDUP_MAX) this.seen.clear();
|
|
530
|
+
this.seen.add(key);
|
|
531
|
+
const line = formatLogLine(logObj);
|
|
532
|
+
this.writing = true;
|
|
533
|
+
try {
|
|
534
|
+
const fd = fs$1.openSync(this.filePath, "a", 384);
|
|
535
|
+
fs$1.writeSync(fd, line);
|
|
536
|
+
fs$1.closeSync(fd);
|
|
537
|
+
} catch {} finally {
|
|
538
|
+
this.writing = false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
const nullStream = new Writable({ write(_chunk, _encoding, cb) {
|
|
543
|
+
cb();
|
|
544
|
+
} });
|
|
545
|
+
/**
|
|
546
|
+
* Switch consola to file-only mode for TUI sessions.
|
|
547
|
+
* Removes the terminal reporter and installs a file reporter that
|
|
548
|
+
* persists errors and warnings to disk with dedup and credential scrubbing.
|
|
549
|
+
*
|
|
550
|
+
* Also sinks consola's stdout/stderr streams as belt-and-suspenders:
|
|
551
|
+
* even if a terminal reporter is re-added, it cannot write to the terminal.
|
|
552
|
+
* Crash handlers that call process.stderr.write() directly are unaffected.
|
|
553
|
+
* FileLogReporter uses fs.writeSync() directly and is also unaffected.
|
|
554
|
+
*/
|
|
555
|
+
function enableFileLogging() {
|
|
556
|
+
const reporter = new FileLogReporter(PATHS.ERROR_LOG_PATH);
|
|
557
|
+
consola.options.throttle = 0;
|
|
558
|
+
consola.setReporters([reporter]);
|
|
559
|
+
consola.options.stdout = nullStream;
|
|
560
|
+
consola.options.stderr = nullStream;
|
|
561
|
+
}
|
|
562
|
+
|
|
416
563
|
//#endregion
|
|
417
564
|
//#region src/lib/port.ts
|
|
418
565
|
const DEFAULT_PORT = 8787;
|
|
419
|
-
const DEFAULT_CODEX_MODEL = "
|
|
566
|
+
const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
|
|
420
567
|
const PORT_RANGE_MIN = 11e3;
|
|
421
568
|
const PORT_RANGE_MAX = 65535;
|
|
422
569
|
/** Generate a random port number in the range [11000, 65535]. */
|
|
@@ -442,6 +589,7 @@ function buildLaunchCommand(target) {
|
|
|
442
589
|
...target.extraArgs
|
|
443
590
|
] : [
|
|
444
591
|
"codex",
|
|
592
|
+
"--full-auto",
|
|
445
593
|
"-m",
|
|
446
594
|
target.model ?? DEFAULT_CODEX_MODEL,
|
|
447
595
|
...target.extraArgs
|
|
@@ -456,17 +604,26 @@ function launchChild(target, server$1) {
|
|
|
456
604
|
const { cmd, env } = buildLaunchCommand(target);
|
|
457
605
|
const executable = cmd[0];
|
|
458
606
|
if (!commandExists(executable)) {
|
|
459
|
-
|
|
607
|
+
const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
|
|
608
|
+
consola.error(msg);
|
|
609
|
+
process$1.stderr.write(msg + "\n");
|
|
460
610
|
process$1.exit(1);
|
|
461
611
|
}
|
|
462
612
|
let child;
|
|
463
613
|
try {
|
|
464
|
-
child = spawn(cmd
|
|
614
|
+
if (process$1.platform === "win32") child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
|
|
615
|
+
env,
|
|
616
|
+
stdio: "inherit",
|
|
617
|
+
shell: true
|
|
618
|
+
});
|
|
619
|
+
else child = spawn(cmd[0], cmd.slice(1), {
|
|
465
620
|
env,
|
|
466
621
|
stdio: "inherit"
|
|
467
622
|
});
|
|
468
623
|
} catch (error) {
|
|
469
|
-
|
|
624
|
+
const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
|
|
625
|
+
consola.error(msg);
|
|
626
|
+
process$1.stderr.write(msg + "\n");
|
|
470
627
|
server$1.close(true).catch(() => {});
|
|
471
628
|
process$1.exit(1);
|
|
472
629
|
}
|
|
@@ -494,10 +651,61 @@ function launchChild(target, server$1) {
|
|
|
494
651
|
};
|
|
495
652
|
process$1.on("SIGINT", onSignal);
|
|
496
653
|
process$1.on("SIGTERM", onSignal);
|
|
497
|
-
child.on("exit", (exitCode) => {
|
|
498
|
-
|
|
654
|
+
child.on("exit", (exitCode, signal) => {
|
|
655
|
+
const code = exitCode ?? (signal ? 128 : 1);
|
|
656
|
+
cleanup().then(() => exit(code)).catch(() => exit(1));
|
|
657
|
+
});
|
|
658
|
+
child.on("error", () => {
|
|
659
|
+
cleanup().then(() => exit(1)).catch(() => exit(1));
|
|
499
660
|
});
|
|
500
|
-
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/lib/model-validation.ts
|
|
665
|
+
const ENDPOINT_ALIASES = {
|
|
666
|
+
"/chat/completions": "/chat/completions",
|
|
667
|
+
"/v1/chat/completions": "/chat/completions",
|
|
668
|
+
"/responses": "/responses",
|
|
669
|
+
"/v1/responses": "/responses",
|
|
670
|
+
"/v1/messages": "/v1/messages"
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Check whether a model supports the given endpoint, based on cached
|
|
674
|
+
* `supported_endpoints` metadata from the Copilot `/models` response.
|
|
675
|
+
*
|
|
676
|
+
* Returns `true` (allow) when:
|
|
677
|
+
* - the model is not found in the cache (don't block unknown models)
|
|
678
|
+
* - the model has no `supported_endpoints` field (backward-compat)
|
|
679
|
+
* - the endpoint is listed in `supported_endpoints`
|
|
680
|
+
*/
|
|
681
|
+
function modelSupportsEndpoint(modelId, path$1) {
|
|
682
|
+
const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
|
|
683
|
+
const model = state.models?.data.find((m) => m.id === modelId);
|
|
684
|
+
if (!model) return true;
|
|
685
|
+
const supported = model.supported_endpoints;
|
|
686
|
+
if (!supported || supported.length === 0) return true;
|
|
687
|
+
return supported.includes(endpoint);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Log an error when a model is used on an endpoint it doesn't support.
|
|
691
|
+
* Returns `true` if a mismatch was detected (for testing).
|
|
692
|
+
*/
|
|
693
|
+
function logEndpointMismatch(modelId, path$1) {
|
|
694
|
+
if (modelSupportsEndpoint(modelId, path$1)) return false;
|
|
695
|
+
const supported = (state.models?.data.find((m) => m.id === modelId))?.supported_endpoints ?? [];
|
|
696
|
+
consola.error(`Model "${modelId}" does not support ${path$1}. Supported endpoints: ${supported.join(", ")}`);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Return model IDs that support the given endpoint.
|
|
701
|
+
*/
|
|
702
|
+
function listModelsForEndpoint(path$1) {
|
|
703
|
+
const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
|
|
704
|
+
return (state.models?.data ?? []).filter((m) => {
|
|
705
|
+
const supported = m.supported_endpoints;
|
|
706
|
+
if (!supported || supported.length === 0) return true;
|
|
707
|
+
return supported.includes(endpoint);
|
|
708
|
+
}).map((m) => m.id);
|
|
501
709
|
}
|
|
502
710
|
|
|
503
711
|
//#endregion
|
|
@@ -594,7 +802,7 @@ function formatTokens(n) {
|
|
|
594
802
|
function formatTokenInfo(inputTokens, outputTokens, model) {
|
|
595
803
|
if (inputTokens === void 0) return void 0;
|
|
596
804
|
const parts = [];
|
|
597
|
-
const maxPrompt = model?.capabilities
|
|
805
|
+
const maxPrompt = model?.capabilities?.limits?.max_prompt_tokens;
|
|
598
806
|
if (maxPrompt) {
|
|
599
807
|
const pct = (inputTokens / maxPrompt * 100).toFixed(1);
|
|
600
808
|
parts.push(`in:${formatTokens(inputTokens)}/${formatTokens(maxPrompt)} (${pct}%)`);
|
|
@@ -714,7 +922,7 @@ const getEncodeChatFunction = async (encoding) => {
|
|
|
714
922
|
* Get tokenizer type from model information
|
|
715
923
|
*/
|
|
716
924
|
const getTokenizerFromModel = (model) => {
|
|
717
|
-
return model.capabilities
|
|
925
|
+
return model.capabilities?.tokenizer || "o200k_base";
|
|
718
926
|
};
|
|
719
927
|
/**
|
|
720
928
|
* Get model-specific constants for token calculation
|
|
@@ -948,6 +1156,7 @@ async function handleCompletion$1(c) {
|
|
|
948
1156
|
const resolvedModel = resolveModel(payload.model);
|
|
949
1157
|
if (resolvedModel !== payload.model) payload.model = resolvedModel;
|
|
950
1158
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1159
|
+
logEndpointMismatch(payload.model, "/chat/completions");
|
|
951
1160
|
let inputTokens;
|
|
952
1161
|
try {
|
|
953
1162
|
if (selectedModel) inputTokens = (await getTokenCount(payload, selectedModel)).input;
|
|
@@ -955,7 +1164,7 @@ async function handleCompletion$1(c) {
|
|
|
955
1164
|
if (isNullish(payload.max_tokens)) {
|
|
956
1165
|
payload = {
|
|
957
1166
|
...payload,
|
|
958
|
-
max_tokens: selectedModel?.capabilities
|
|
1167
|
+
max_tokens: selectedModel?.capabilities?.limits?.max_output_tokens
|
|
959
1168
|
};
|
|
960
1169
|
if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
961
1170
|
}
|
|
@@ -1366,7 +1575,9 @@ async function handleCompletion(c) {
|
|
|
1366
1575
|
if (state.manualApprove) await awaitApproval();
|
|
1367
1576
|
const betaHeaders = extractBetaHeaders(c);
|
|
1368
1577
|
const { body: resolvedBody, originalModel, resolvedModel } = resolveModelInBody(await processWebSearch(rawBody));
|
|
1369
|
-
const
|
|
1578
|
+
const modelId = resolvedModel ?? originalModel;
|
|
1579
|
+
const selectedModel = state.models?.data.find((m) => m.id === modelId);
|
|
1580
|
+
if (modelId) logEndpointMismatch(modelId, "/v1/messages");
|
|
1370
1581
|
const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
|
|
1371
1582
|
let response;
|
|
1372
1583
|
try {
|
|
@@ -1591,12 +1802,9 @@ async function handleResponses(c) {
|
|
|
1591
1802
|
const resolvedModel = resolveModel(payload.model);
|
|
1592
1803
|
if (resolvedModel !== payload.model) payload.model = resolvedModel;
|
|
1593
1804
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1805
|
+
logEndpointMismatch(payload.model, "/responses");
|
|
1594
1806
|
if (state.manualApprove) await awaitApproval();
|
|
1595
1807
|
await injectWebSearchIfNeeded(payload);
|
|
1596
|
-
if (isNullish(payload.max_output_tokens)) {
|
|
1597
|
-
payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
|
|
1598
|
-
if (debugEnabled) consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
|
|
1599
|
-
}
|
|
1600
1808
|
const response = await createResponses(payload, selectedModel?.requestHeaders).catch(async (error) => {
|
|
1601
1809
|
if (error instanceof HTTPError) {
|
|
1602
1810
|
const errorBody = await error.response.clone().text().catch(() => "");
|
|
@@ -1673,31 +1881,99 @@ function extractUserQuery(input) {
|
|
|
1673
1881
|
}
|
|
1674
1882
|
}
|
|
1675
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Compaction prompt used when GitHub Copilot API does not support
|
|
1886
|
+
* /responses/compact natively. Matches the prompt Codex CLI uses for
|
|
1887
|
+
* local (non-OpenAI) compaction.
|
|
1888
|
+
*/
|
|
1889
|
+
const COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
1890
|
+
|
|
1891
|
+
Include:
|
|
1892
|
+
- Current progress and key decisions made
|
|
1893
|
+
- Important context, constraints, or user preferences
|
|
1894
|
+
- What remains to be done (clear next steps)
|
|
1895
|
+
- Any critical data, examples, or references needed to continue
|
|
1896
|
+
|
|
1897
|
+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
|
|
1676
1898
|
async function handleResponsesCompact(c) {
|
|
1677
1899
|
const startTime = Date.now();
|
|
1678
1900
|
await checkRateLimit(state);
|
|
1679
1901
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1680
1902
|
if (state.manualApprove) await awaitApproval();
|
|
1681
|
-
const body = await c.req.
|
|
1903
|
+
const body = await c.req.json();
|
|
1682
1904
|
const response = await fetch(`${copilotBaseUrl(state)}/responses/compact`, {
|
|
1683
1905
|
method: "POST",
|
|
1684
1906
|
headers: copilotHeaders(state),
|
|
1685
|
-
body
|
|
1907
|
+
body: JSON.stringify(body)
|
|
1686
1908
|
});
|
|
1687
|
-
if (
|
|
1909
|
+
if (response.ok) {
|
|
1688
1910
|
logRequest({
|
|
1689
1911
|
method: "POST",
|
|
1690
1912
|
path: c.req.path,
|
|
1691
|
-
status:
|
|
1913
|
+
status: 200
|
|
1692
1914
|
}, void 0, startTime);
|
|
1693
|
-
|
|
1915
|
+
return c.json(await response.json());
|
|
1916
|
+
}
|
|
1917
|
+
if (response.status === 404) {
|
|
1918
|
+
consola.debug("Copilot API does not support /responses/compact, using synthetic compaction");
|
|
1919
|
+
return await syntheticCompact(c, body, startTime);
|
|
1920
|
+
}
|
|
1921
|
+
logRequest({
|
|
1922
|
+
method: "POST",
|
|
1923
|
+
path: c.req.path,
|
|
1924
|
+
status: response.status
|
|
1925
|
+
}, void 0, startTime);
|
|
1926
|
+
throw new HTTPError("Copilot responses/compact request failed", response);
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Synthetic compaction: sends the conversation history to Copilot's
|
|
1930
|
+
* regular /responses endpoint with a compaction prompt appended,
|
|
1931
|
+
* then returns the model's summary in the compact response format.
|
|
1932
|
+
*/
|
|
1933
|
+
async function syntheticCompact(c, body, startTime) {
|
|
1934
|
+
const input = Array.isArray(body.input) ? [...body.input] : [];
|
|
1935
|
+
input.push({
|
|
1936
|
+
type: "message",
|
|
1937
|
+
role: "user",
|
|
1938
|
+
content: [{
|
|
1939
|
+
type: "input_text",
|
|
1940
|
+
text: COMPACTION_PROMPT
|
|
1941
|
+
}]
|
|
1942
|
+
});
|
|
1943
|
+
const payload = {
|
|
1944
|
+
model: body.model,
|
|
1945
|
+
input,
|
|
1946
|
+
instructions: body.instructions,
|
|
1947
|
+
stream: false,
|
|
1948
|
+
store: false
|
|
1949
|
+
};
|
|
1950
|
+
let result;
|
|
1951
|
+
try {
|
|
1952
|
+
result = await createResponses(payload);
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
if (error instanceof HTTPError) logRequest({
|
|
1955
|
+
method: "POST",
|
|
1956
|
+
path: c.req.path,
|
|
1957
|
+
status: error.response.status
|
|
1958
|
+
}, void 0, startTime);
|
|
1959
|
+
throw error;
|
|
1694
1960
|
}
|
|
1695
1961
|
logRequest({
|
|
1696
1962
|
method: "POST",
|
|
1697
1963
|
path: c.req.path,
|
|
1698
1964
|
status: 200
|
|
1699
1965
|
}, void 0, startTime);
|
|
1700
|
-
return c.json(
|
|
1966
|
+
return c.json({
|
|
1967
|
+
id: `resp_compact_${randomUUID().replace(/-/g, "").slice(0, 24)}`,
|
|
1968
|
+
object: "response.compaction",
|
|
1969
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1970
|
+
output: result.output,
|
|
1971
|
+
usage: result.usage ?? {
|
|
1972
|
+
input_tokens: 0,
|
|
1973
|
+
output_tokens: 0,
|
|
1974
|
+
total_tokens: 0
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1701
1977
|
}
|
|
1702
1978
|
|
|
1703
1979
|
//#endregion
|
|
@@ -1799,7 +2075,7 @@ async function setupAndServe(options) {
|
|
|
1799
2075
|
} else await setupGitHubToken();
|
|
1800
2076
|
await setupCopilotToken();
|
|
1801
2077
|
await cacheModels();
|
|
1802
|
-
consola.
|
|
2078
|
+
consola.debug(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
|
|
1803
2079
|
const serveOptions = {
|
|
1804
2080
|
fetch: server.fetch,
|
|
1805
2081
|
hostname: "127.0.0.1",
|
|
@@ -1978,13 +2254,22 @@ const claude = defineCommand({
|
|
|
1978
2254
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
1979
2255
|
process$1.exit(1);
|
|
1980
2256
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
2257
|
+
enableFileLogging();
|
|
2258
|
+
let resolvedModel;
|
|
2259
|
+
if (args.model) {
|
|
2260
|
+
resolvedModel = resolveModel(args.model);
|
|
2261
|
+
if (resolvedModel !== args.model) consola.info(`Model "${args.model}" resolved to "${resolvedModel}"`);
|
|
2262
|
+
if (!state.models?.data.find((m) => m.id === resolvedModel)) {
|
|
2263
|
+
const available = listModelsForEndpoint("/v1/messages");
|
|
2264
|
+
consola.warn(`Model "${resolvedModel}" not found. Available claude models: ${available.join(", ")}`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code...\n`);
|
|
1983
2268
|
launchChild({
|
|
1984
2269
|
kind: "claude-code",
|
|
1985
|
-
envVars: getClaudeCodeEnvVars(serverUrl, args.model),
|
|
2270
|
+
envVars: getClaudeCodeEnvVars(serverUrl, resolvedModel ?? args.model),
|
|
1986
2271
|
extraArgs: args._ ?? [],
|
|
1987
|
-
model: args.model
|
|
2272
|
+
model: resolvedModel ?? args.model
|
|
1988
2273
|
}, server$1);
|
|
1989
2274
|
}
|
|
1990
2275
|
});
|
|
@@ -2024,14 +2309,24 @@ const codex = defineCommand({
|
|
|
2024
2309
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
2025
2310
|
process$1.exit(1);
|
|
2026
2311
|
}
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2312
|
+
const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
|
|
2313
|
+
enableFileLogging();
|
|
2314
|
+
const codexModel = resolveCodexModel(requestedModel);
|
|
2315
|
+
if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
|
|
2316
|
+
const modelEntry = state.models?.data.find((m) => m.id === codexModel);
|
|
2317
|
+
if (!modelEntry) {
|
|
2318
|
+
const available = listModelsForEndpoint("/responses");
|
|
2319
|
+
consola.warn(`Model "${codexModel}" not found. Available codex models: ${available.join(", ")}`);
|
|
2320
|
+
} else {
|
|
2321
|
+
const ctx = modelEntry.capabilities?.limits?.max_context_window_tokens;
|
|
2322
|
+
if (ctx) consola.info(`Model context window: ${ctx.toLocaleString()} tokens`);
|
|
2323
|
+
}
|
|
2324
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...\n`);
|
|
2030
2325
|
launchChild({
|
|
2031
2326
|
kind: "codex",
|
|
2032
2327
|
envVars: getCodexEnvVars(serverUrl),
|
|
2033
2328
|
extraArgs: args._ ?? [],
|
|
2034
|
-
model:
|
|
2329
|
+
model: codexModel
|
|
2035
2330
|
}, server$1);
|
|
2036
2331
|
}
|
|
2037
2332
|
});
|