@daomar/copilot-api 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -340
- package/dist/main.js +723 -2
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -5,13 +5,14 @@ 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 fs$1 from "node:fs";
|
|
9
|
+
import process$1 from "node:process";
|
|
10
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
8
11
|
import clipboard from "clipboardy";
|
|
9
12
|
import { serve } from "srvx";
|
|
10
13
|
import invariant from "tiny-invariant";
|
|
11
14
|
import { getProxyForUrl } from "proxy-from-env";
|
|
12
15
|
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
14
|
-
import process$1 from "node:process";
|
|
15
16
|
import { Hono } from "hono";
|
|
16
17
|
import { cors } from "hono/cors";
|
|
17
18
|
import { logger } from "hono/logger";
|
|
@@ -363,6 +364,266 @@ const checkUsage = defineCommand({
|
|
|
363
364
|
}
|
|
364
365
|
});
|
|
365
366
|
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/lib/agent-config.ts
|
|
369
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
370
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
371
|
+
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
372
|
+
const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4.5";
|
|
373
|
+
const DEFAULT_CLAUDE_SMALL_MODEL = "claude-haiku-4.5";
|
|
374
|
+
const proxyBaseUrl = (host, port) => `http://${host}:${port}`;
|
|
375
|
+
function buildCodexProviderBlock(baseUrl) {
|
|
376
|
+
return [
|
|
377
|
+
`[model_providers.copilot]`,
|
|
378
|
+
`name = "GitHub Copilot"`,
|
|
379
|
+
`base_url = "${baseUrl}/v1"`,
|
|
380
|
+
`wire_api = "responses"`,
|
|
381
|
+
`requires_openai_auth = false`,
|
|
382
|
+
`http_headers = { "Openai-Intent" = "conversation-edits", "x-initiator" = "user" }`
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
const tableHeaderKey = (line) => {
|
|
386
|
+
const match = /^\s*\[([^[\]]+)\]\s*$/.exec(line);
|
|
387
|
+
return match ? match[1].trim() : null;
|
|
388
|
+
};
|
|
389
|
+
const isCopilotProviderTable = (key) => key === "model_providers.copilot" || key.startsWith("model_providers.copilot.");
|
|
390
|
+
const firstTableIndex = (lines) => {
|
|
391
|
+
const index = lines.findIndex((line) => tableHeaderKey(line) !== null);
|
|
392
|
+
return index === -1 ? lines.length : index;
|
|
393
|
+
};
|
|
394
|
+
function upsertTopLevelKey(content, update) {
|
|
395
|
+
const { key, value, override } = update;
|
|
396
|
+
const lines = content.split("\n");
|
|
397
|
+
const limit = firstTableIndex(lines);
|
|
398
|
+
const keyRegex = /* @__PURE__ */ new RegExp(`^\\s*${key}\\s*=`);
|
|
399
|
+
for (let i = 0; i < limit; i++) if (keyRegex.test(lines[i])) {
|
|
400
|
+
if (override) lines[i] = `${key} = ${value}`;
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
lines.splice(0, 0, `${key} = ${value}`);
|
|
404
|
+
return lines.join("\n");
|
|
405
|
+
}
|
|
406
|
+
function upsertCodexProvider(content, block) {
|
|
407
|
+
const lines = content.split("\n");
|
|
408
|
+
let start$1 = -1;
|
|
409
|
+
for (const [i, line] of lines.entries()) if (tableHeaderKey(line) === "model_providers.copilot") {
|
|
410
|
+
start$1 = i;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if (start$1 === -1) {
|
|
414
|
+
const trimmed = content.replace(/\n*$/, "");
|
|
415
|
+
return (trimmed.length > 0 ? `${trimmed}\n\n` : "") + `${block}\n`;
|
|
416
|
+
}
|
|
417
|
+
let end = lines.length;
|
|
418
|
+
for (let j = start$1 + 1; j < lines.length; j++) {
|
|
419
|
+
const key = tableHeaderKey(lines[j]);
|
|
420
|
+
if (key !== null && !isCopilotProviderTable(key)) {
|
|
421
|
+
end = j;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let realEnd = end;
|
|
426
|
+
if (end < lines.length) while (realEnd - 1 > start$1) {
|
|
427
|
+
const prev = lines[realEnd - 1].trim();
|
|
428
|
+
if (prev === "" || prev.startsWith("#")) realEnd--;
|
|
429
|
+
else break;
|
|
430
|
+
}
|
|
431
|
+
const beforeText = lines.slice(0, start$1).join("\n").replace(/\n*$/, "");
|
|
432
|
+
const afterText = lines.slice(realEnd).join("\n").replace(/^\n*/, "");
|
|
433
|
+
const parts = [];
|
|
434
|
+
if (beforeText.length > 0) parts.push(beforeText);
|
|
435
|
+
parts.push(block);
|
|
436
|
+
if (afterText.length > 0) parts.push(afterText);
|
|
437
|
+
return `${parts.join("\n\n")}\n`;
|
|
438
|
+
}
|
|
439
|
+
function buildCodexConfig(existing, options) {
|
|
440
|
+
let next = existing;
|
|
441
|
+
next = upsertTopLevelKey(next, {
|
|
442
|
+
key: "model",
|
|
443
|
+
value: `"${options.codexModel}"`,
|
|
444
|
+
override: false
|
|
445
|
+
});
|
|
446
|
+
next = upsertTopLevelKey(next, {
|
|
447
|
+
key: "model_provider",
|
|
448
|
+
value: `"copilot"`,
|
|
449
|
+
override: true
|
|
450
|
+
});
|
|
451
|
+
next = upsertCodexProvider(next, buildCodexProviderBlock(options.baseUrl));
|
|
452
|
+
return next;
|
|
453
|
+
}
|
|
454
|
+
function buildClaudeSettings(existing, options) {
|
|
455
|
+
const env = { ...existing.env };
|
|
456
|
+
env.ANTHROPIC_BASE_URL = options.baseUrl;
|
|
457
|
+
env.ANTHROPIC_AUTH_TOKEN = "dummy";
|
|
458
|
+
const defaults = {
|
|
459
|
+
ANTHROPIC_MODEL: options.claudeModel,
|
|
460
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: options.claudeModel,
|
|
461
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: options.claudeModel,
|
|
462
|
+
ANTHROPIC_SMALL_FAST_MODEL: options.claudeSmallModel,
|
|
463
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: options.claudeSmallModel,
|
|
464
|
+
DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
|
|
465
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
466
|
+
};
|
|
467
|
+
for (const [key, value] of Object.entries(defaults)) if (!Object.hasOwn(env, key)) env[key] = value;
|
|
468
|
+
return {
|
|
469
|
+
...existing,
|
|
470
|
+
env
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const readFileOr = async (filePath, fallback) => {
|
|
474
|
+
try {
|
|
475
|
+
return await fs.readFile(filePath, "utf8");
|
|
476
|
+
} catch {
|
|
477
|
+
return fallback;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const backupFile = async (filePath) => {
|
|
481
|
+
try {
|
|
482
|
+
await fs.access(filePath);
|
|
483
|
+
} catch {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const backupPath = `${filePath}.bak`;
|
|
487
|
+
await fs.copyFile(filePath, backupPath);
|
|
488
|
+
consola.info(`Backed up existing config to ${backupPath}`);
|
|
489
|
+
};
|
|
490
|
+
async function writeCodexConfig(options) {
|
|
491
|
+
await fs.mkdir(path.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
492
|
+
const existing = await readFileOr(CODEX_CONFIG_PATH, "");
|
|
493
|
+
const next = buildCodexConfig(existing, options);
|
|
494
|
+
if (next !== existing) await backupFile(CODEX_CONFIG_PATH);
|
|
495
|
+
await fs.writeFile(CODEX_CONFIG_PATH, next);
|
|
496
|
+
return CODEX_CONFIG_PATH;
|
|
497
|
+
}
|
|
498
|
+
async function writeClaudeSettings(options) {
|
|
499
|
+
await fs.mkdir(path.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
500
|
+
const raw = await readFileOr(CLAUDE_SETTINGS_PATH, "");
|
|
501
|
+
let existing = {};
|
|
502
|
+
if (raw.trim().length > 0) try {
|
|
503
|
+
existing = JSON.parse(raw);
|
|
504
|
+
} catch {
|
|
505
|
+
consola.warn(`Could not parse ${CLAUDE_SETTINGS_PATH}; it will be recreated.`);
|
|
506
|
+
}
|
|
507
|
+
const next = buildClaudeSettings(existing, options);
|
|
508
|
+
if (raw.trim().length > 0) await backupFile(CLAUDE_SETTINGS_PATH);
|
|
509
|
+
await fs.writeFile(CLAUDE_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
|
510
|
+
return CLAUDE_SETTINGS_PATH;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/config.ts
|
|
515
|
+
async function pickModel$1(message, fallback) {
|
|
516
|
+
const ids = state.models?.data.map((model) => model.id) ?? [];
|
|
517
|
+
if (ids.length === 0) return fallback;
|
|
518
|
+
return await consola.prompt(message, {
|
|
519
|
+
type: "select",
|
|
520
|
+
options: ids,
|
|
521
|
+
initial: ids.includes(fallback) ? fallback : ids[0]
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async function runConfig(options) {
|
|
525
|
+
const doClaude = options.claude || !options.codex;
|
|
526
|
+
const doCodex = options.codex || !options.claude;
|
|
527
|
+
let { codexModel, claudeModel, claudeSmallModel } = options;
|
|
528
|
+
if (options.interactive) {
|
|
529
|
+
await ensurePaths();
|
|
530
|
+
await cacheVSCodeVersion();
|
|
531
|
+
await setupGitHubToken();
|
|
532
|
+
await setupCopilotToken();
|
|
533
|
+
await cacheModels();
|
|
534
|
+
if (doCodex) codexModel = await pickModel$1("Select a model for Codex CLI", codexModel);
|
|
535
|
+
if (doClaude) {
|
|
536
|
+
claudeModel = await pickModel$1("Select a model for Claude Code", claudeModel);
|
|
537
|
+
claudeSmallModel = await pickModel$1("Select a small/fast model for Claude Code", claudeSmallModel);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const baseUrl = proxyBaseUrl(options.host, options.port);
|
|
541
|
+
const configOptions = {
|
|
542
|
+
baseUrl,
|
|
543
|
+
codexModel,
|
|
544
|
+
claudeModel,
|
|
545
|
+
claudeSmallModel
|
|
546
|
+
};
|
|
547
|
+
if (doCodex) {
|
|
548
|
+
const written = await writeCodexConfig(configOptions);
|
|
549
|
+
consola.success(`Configured Codex CLI (model "${codexModel}") at ${written}`);
|
|
550
|
+
}
|
|
551
|
+
if (doClaude) {
|
|
552
|
+
const written = await writeClaudeSettings(configOptions);
|
|
553
|
+
consola.success(`Configured Claude Code at ${written}`);
|
|
554
|
+
}
|
|
555
|
+
consola.box([
|
|
556
|
+
`Both tools now route through ${baseUrl}.`,
|
|
557
|
+
``,
|
|
558
|
+
`Next steps:`,
|
|
559
|
+
` 1. Log in once on this machine: copilot-api auth`,
|
|
560
|
+
` 2. Start the proxy: copilot-api start --port ${options.port}`,
|
|
561
|
+
` 3. Run codex or claude as usual.`
|
|
562
|
+
].join("\n"));
|
|
563
|
+
}
|
|
564
|
+
const config = defineCommand({
|
|
565
|
+
meta: {
|
|
566
|
+
name: "config",
|
|
567
|
+
description: "Write Codex CLI and Claude Code config files so they use this proxy"
|
|
568
|
+
},
|
|
569
|
+
args: {
|
|
570
|
+
port: {
|
|
571
|
+
alias: "p",
|
|
572
|
+
type: "string",
|
|
573
|
+
default: "4141",
|
|
574
|
+
description: "Port the proxy listens on"
|
|
575
|
+
},
|
|
576
|
+
host: {
|
|
577
|
+
type: "string",
|
|
578
|
+
default: "localhost",
|
|
579
|
+
description: "Host the proxy listens on"
|
|
580
|
+
},
|
|
581
|
+
claude: {
|
|
582
|
+
type: "boolean",
|
|
583
|
+
default: false,
|
|
584
|
+
description: "Only configure Claude Code (default: configure both)"
|
|
585
|
+
},
|
|
586
|
+
codex: {
|
|
587
|
+
type: "boolean",
|
|
588
|
+
default: false,
|
|
589
|
+
description: "Only configure Codex CLI (default: configure both)"
|
|
590
|
+
},
|
|
591
|
+
interactive: {
|
|
592
|
+
alias: "i",
|
|
593
|
+
type: "boolean",
|
|
594
|
+
default: false,
|
|
595
|
+
description: "Pick models from the live model list (requires GitHub login)"
|
|
596
|
+
},
|
|
597
|
+
"codex-model": {
|
|
598
|
+
type: "string",
|
|
599
|
+
default: DEFAULT_CODEX_MODEL,
|
|
600
|
+
description: "Model to use for Codex CLI"
|
|
601
|
+
},
|
|
602
|
+
"claude-model": {
|
|
603
|
+
type: "string",
|
|
604
|
+
default: DEFAULT_CLAUDE_MODEL,
|
|
605
|
+
description: "Main model to use for Claude Code"
|
|
606
|
+
},
|
|
607
|
+
"claude-small-model": {
|
|
608
|
+
type: "string",
|
|
609
|
+
default: DEFAULT_CLAUDE_SMALL_MODEL,
|
|
610
|
+
description: "Small/fast model to use for Claude Code"
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
run({ args }) {
|
|
614
|
+
return runConfig({
|
|
615
|
+
host: args.host,
|
|
616
|
+
port: Number.parseInt(args.port, 10),
|
|
617
|
+
codexModel: args["codex-model"],
|
|
618
|
+
claudeModel: args["claude-model"],
|
|
619
|
+
claudeSmallModel: args["claude-small-model"],
|
|
620
|
+
claude: args.claude,
|
|
621
|
+
codex: args.codex,
|
|
622
|
+
interactive: args.interactive
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
366
627
|
//#endregion
|
|
367
628
|
//#region src/debug.ts
|
|
368
629
|
async function getPackageVersion() {
|
|
@@ -437,6 +698,406 @@ const debug = defineCommand({
|
|
|
437
698
|
}
|
|
438
699
|
});
|
|
439
700
|
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/lib/service.ts
|
|
703
|
+
const SERVICE_NAME = "copilot-api";
|
|
704
|
+
const WINDOWS_TASK_NAME = "CopilotAPI";
|
|
705
|
+
const quote = (value) => `"${value.replaceAll("\"", String.raw`\"`)}"`;
|
|
706
|
+
const startCommandArgs = (target) => [
|
|
707
|
+
"start",
|
|
708
|
+
"--port",
|
|
709
|
+
String(target.port),
|
|
710
|
+
"--account-type",
|
|
711
|
+
target.accountType
|
|
712
|
+
];
|
|
713
|
+
function buildSystemdUnit(target) {
|
|
714
|
+
return `[Unit]
|
|
715
|
+
Description=GitHub Copilot API proxy (copilot-api)
|
|
716
|
+
After=network-online.target
|
|
717
|
+
Wants=network-online.target
|
|
718
|
+
|
|
719
|
+
[Service]
|
|
720
|
+
Type=simple
|
|
721
|
+
ExecStart=${[
|
|
722
|
+
quote(target.runtimePath),
|
|
723
|
+
quote(target.scriptPath),
|
|
724
|
+
...startCommandArgs(target)
|
|
725
|
+
].join(" ")}
|
|
726
|
+
Restart=always
|
|
727
|
+
RestartSec=5
|
|
728
|
+
Environment=NODE_ENV=production
|
|
729
|
+
|
|
730
|
+
[Install]
|
|
731
|
+
WantedBy=default.target
|
|
732
|
+
`;
|
|
733
|
+
}
|
|
734
|
+
const systemdUnitPath = () => path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
|
|
735
|
+
const runQuietly = (command, args) => {
|
|
736
|
+
try {
|
|
737
|
+
return {
|
|
738
|
+
ok: true,
|
|
739
|
+
output: execFileSync(command, args, {
|
|
740
|
+
stdio: "pipe",
|
|
741
|
+
encoding: "utf8"
|
|
742
|
+
})
|
|
743
|
+
};
|
|
744
|
+
} catch (error) {
|
|
745
|
+
const err = error;
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
output: err.stderr ? err.stderr.toString() : err.message ?? "unknown error"
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
async function installSystemdService(target) {
|
|
753
|
+
const unitPath = systemdUnitPath();
|
|
754
|
+
await fs.mkdir(path.dirname(unitPath), { recursive: true });
|
|
755
|
+
await fs.writeFile(unitPath, buildSystemdUnit(target));
|
|
756
|
+
consola.info(`Wrote systemd unit to ${unitPath}`);
|
|
757
|
+
const reload = runQuietly("systemctl", ["--user", "daemon-reload"]);
|
|
758
|
+
if (!reload.ok) return {
|
|
759
|
+
installed: false,
|
|
760
|
+
manager: "systemd",
|
|
761
|
+
message: `Failed to reload systemd: ${reload.output.trim()}. Unit written to ${unitPath}.`
|
|
762
|
+
};
|
|
763
|
+
const enable = runQuietly("systemctl", [
|
|
764
|
+
"--user",
|
|
765
|
+
"enable",
|
|
766
|
+
"--now",
|
|
767
|
+
`${SERVICE_NAME}.service`
|
|
768
|
+
]);
|
|
769
|
+
if (!enable.ok) return {
|
|
770
|
+
installed: false,
|
|
771
|
+
manager: "systemd",
|
|
772
|
+
message: `Failed to enable service: ${enable.output.trim()}. Try: systemctl --user enable --now ${SERVICE_NAME}.service`
|
|
773
|
+
};
|
|
774
|
+
if (!runQuietly("loginctl", ["enable-linger", os.userInfo().username]).ok) consola.warn(`Could not enable lingering (service may stop on logout). Run manually: sudo loginctl enable-linger ${os.userInfo().username}`);
|
|
775
|
+
return {
|
|
776
|
+
installed: true,
|
|
777
|
+
manager: "systemd",
|
|
778
|
+
message: `Service enabled. Manage with: systemctl --user status ${SERVICE_NAME}`
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function buildWindowsTaskXml(target) {
|
|
782
|
+
const command = target.runtimePath;
|
|
783
|
+
const args = [quote(target.scriptPath), ...startCommandArgs(target)].join(" ");
|
|
784
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
785
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
786
|
+
<RegistrationInfo>
|
|
787
|
+
<Description>GitHub Copilot API proxy (copilot-api)</Description>
|
|
788
|
+
</RegistrationInfo>
|
|
789
|
+
<Triggers>
|
|
790
|
+
<LogonTrigger>
|
|
791
|
+
<Enabled>true</Enabled>
|
|
792
|
+
</LogonTrigger>
|
|
793
|
+
</Triggers>
|
|
794
|
+
<Principals>
|
|
795
|
+
<Principal id="Author">
|
|
796
|
+
<LogonType>InteractiveToken</LogonType>
|
|
797
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
798
|
+
</Principal>
|
|
799
|
+
</Principals>
|
|
800
|
+
<Settings>
|
|
801
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
802
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
803
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
804
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
805
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
806
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
807
|
+
<IdleSettings>
|
|
808
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
809
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
810
|
+
</IdleSettings>
|
|
811
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
812
|
+
<Enabled>true</Enabled>
|
|
813
|
+
<Hidden>false</Hidden>
|
|
814
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
815
|
+
<RestartOnFailure>
|
|
816
|
+
<Interval>PT1M</Interval>
|
|
817
|
+
<Count>999</Count>
|
|
818
|
+
</RestartOnFailure>
|
|
819
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
820
|
+
<Priority>7</Priority>
|
|
821
|
+
</Settings>
|
|
822
|
+
<Actions Context="Author">
|
|
823
|
+
<Exec>
|
|
824
|
+
<Command>${command}</Command>
|
|
825
|
+
<Arguments>${args}</Arguments>
|
|
826
|
+
</Exec>
|
|
827
|
+
</Actions>
|
|
828
|
+
</Task>
|
|
829
|
+
`;
|
|
830
|
+
}
|
|
831
|
+
async function installWindowsTask(target) {
|
|
832
|
+
const xml = buildWindowsTaskXml(target);
|
|
833
|
+
const xmlPath = path.join(os.tmpdir(), `${SERVICE_NAME}-task.xml`);
|
|
834
|
+
await fs.writeFile(xmlPath, `\uFEFF${xml}`, "utf16le");
|
|
835
|
+
const create = runQuietly("schtasks", [
|
|
836
|
+
"/Create",
|
|
837
|
+
"/TN",
|
|
838
|
+
WINDOWS_TASK_NAME,
|
|
839
|
+
"/XML",
|
|
840
|
+
xmlPath,
|
|
841
|
+
"/F"
|
|
842
|
+
]);
|
|
843
|
+
if (!create.ok) return {
|
|
844
|
+
installed: false,
|
|
845
|
+
manager: "schtasks",
|
|
846
|
+
message: `Failed to register task: ${create.output.trim()}. XML written to ${xmlPath}.`
|
|
847
|
+
};
|
|
848
|
+
runQuietly("schtasks", [
|
|
849
|
+
"/Run",
|
|
850
|
+
"/TN",
|
|
851
|
+
WINDOWS_TASK_NAME
|
|
852
|
+
]);
|
|
853
|
+
return {
|
|
854
|
+
installed: true,
|
|
855
|
+
manager: "schtasks",
|
|
856
|
+
message: `Scheduled task "${WINDOWS_TASK_NAME}" registered and started. Manage it in Task Scheduler.`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function installService(target) {
|
|
860
|
+
const platform = os.platform();
|
|
861
|
+
if (platform === "linux") return installSystemdService(target);
|
|
862
|
+
if (platform === "win32") return installWindowsTask(target);
|
|
863
|
+
const exec = [
|
|
864
|
+
target.runtimePath,
|
|
865
|
+
target.scriptPath,
|
|
866
|
+
...startCommandArgs(target)
|
|
867
|
+
].join(" ");
|
|
868
|
+
return {
|
|
869
|
+
installed: false,
|
|
870
|
+
manager: "none",
|
|
871
|
+
message: `Automatic service install is not supported on "${platform}". Run this manually (e.g. via a launch agent):\n ${exec}`
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/setup.ts
|
|
877
|
+
const ACCOUNT_TYPES = [
|
|
878
|
+
"individual",
|
|
879
|
+
"business",
|
|
880
|
+
"enterprise"
|
|
881
|
+
];
|
|
882
|
+
const resolveRuntime = () => {
|
|
883
|
+
let scriptPath = process$1.argv[1] ?? "";
|
|
884
|
+
try {
|
|
885
|
+
scriptPath = fs$1.realpathSync(scriptPath);
|
|
886
|
+
} catch {}
|
|
887
|
+
return {
|
|
888
|
+
runtimePath: process$1.execPath,
|
|
889
|
+
scriptPath
|
|
890
|
+
};
|
|
891
|
+
};
|
|
892
|
+
async function loadModelIds(accountType) {
|
|
893
|
+
state.accountType = accountType;
|
|
894
|
+
await cacheVSCodeVersion();
|
|
895
|
+
const { token } = await getCopilotToken();
|
|
896
|
+
state.copilotToken = token;
|
|
897
|
+
await cacheModels();
|
|
898
|
+
return state.models?.data.map((model) => model.id) ?? [];
|
|
899
|
+
}
|
|
900
|
+
async function pickModel(message, ids, fallback) {
|
|
901
|
+
if (ids.length === 0) return fallback;
|
|
902
|
+
return await consola.prompt(message, {
|
|
903
|
+
type: "select",
|
|
904
|
+
options: ids,
|
|
905
|
+
initial: ids.includes(fallback) ? fallback : ids[0]
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
async function promptChoices(initial) {
|
|
909
|
+
const accountType = await consola.prompt("GitHub Copilot account type", {
|
|
910
|
+
type: "select",
|
|
911
|
+
options: ACCOUNT_TYPES,
|
|
912
|
+
initial: ACCOUNT_TYPES.includes(initial.accountType) ? initial.accountType : "individual"
|
|
913
|
+
});
|
|
914
|
+
const portInput = await consola.prompt("Port for the proxy", {
|
|
915
|
+
type: "text",
|
|
916
|
+
default: String(initial.port),
|
|
917
|
+
placeholder: String(initial.port)
|
|
918
|
+
});
|
|
919
|
+
const parsedPort = Number.parseInt(portInput, 10);
|
|
920
|
+
const doCodex = await consola.prompt("Configure Codex CLI?", {
|
|
921
|
+
type: "confirm",
|
|
922
|
+
initial: initial.doCodex
|
|
923
|
+
});
|
|
924
|
+
const doClaude = await consola.prompt("Configure Claude Code?", {
|
|
925
|
+
type: "confirm",
|
|
926
|
+
initial: initial.doClaude
|
|
927
|
+
});
|
|
928
|
+
return {
|
|
929
|
+
accountType,
|
|
930
|
+
port: Number.isNaN(parsedPort) ? initial.port : parsedPort,
|
|
931
|
+
doCodex,
|
|
932
|
+
doClaude
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async function promptModels(choices, models) {
|
|
936
|
+
if (!choices.doCodex && !choices.doClaude) return models;
|
|
937
|
+
let { codexModel, claudeModel, claudeSmallModel } = models;
|
|
938
|
+
try {
|
|
939
|
+
const ids = await loadModelIds(choices.accountType);
|
|
940
|
+
if (choices.doCodex) codexModel = await pickModel("Model for Codex CLI", ids, codexModel);
|
|
941
|
+
if (choices.doClaude) {
|
|
942
|
+
claudeModel = await pickModel("Model for Claude Code", ids, claudeModel);
|
|
943
|
+
claudeSmallModel = await pickModel("Small/fast model for Claude Code", ids, claudeSmallModel);
|
|
944
|
+
}
|
|
945
|
+
} catch (error) {
|
|
946
|
+
consola.warn(`Could not load the model list, using defaults instead: ${String(error)}`);
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
...models,
|
|
950
|
+
codexModel,
|
|
951
|
+
claudeModel,
|
|
952
|
+
claudeSmallModel
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
async function writeConfigs(choices, configOptions) {
|
|
956
|
+
if (choices.doCodex) {
|
|
957
|
+
const written = await writeCodexConfig(configOptions);
|
|
958
|
+
consola.success(`Configured Codex CLI (model "${configOptions.codexModel}") at ${written}`);
|
|
959
|
+
}
|
|
960
|
+
if (choices.doClaude) {
|
|
961
|
+
const written = await writeClaudeSettings(configOptions);
|
|
962
|
+
consola.success(`Configured Claude Code at ${written}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async function maybeInstallService(options, choices) {
|
|
966
|
+
let doService = options.service;
|
|
967
|
+
if (!options.yes && options.service) doService = await consola.prompt(`Install an auto-start service on port ${choices.port}?`, {
|
|
968
|
+
type: "confirm",
|
|
969
|
+
initial: true
|
|
970
|
+
});
|
|
971
|
+
if (!doService) return;
|
|
972
|
+
const { runtimePath, scriptPath } = resolveRuntime();
|
|
973
|
+
const target = {
|
|
974
|
+
port: choices.port,
|
|
975
|
+
accountType: choices.accountType,
|
|
976
|
+
runtimePath,
|
|
977
|
+
scriptPath
|
|
978
|
+
};
|
|
979
|
+
const result = await installService(target);
|
|
980
|
+
if (result.installed) consola.success(result.message);
|
|
981
|
+
else consola.warn(result.message);
|
|
982
|
+
}
|
|
983
|
+
function printSummary(options, choices) {
|
|
984
|
+
const baseUrl = proxyBaseUrl(options.host, choices.port);
|
|
985
|
+
consola.box([
|
|
986
|
+
`Setup complete. Proxy URL: ${baseUrl}`,
|
|
987
|
+
``,
|
|
988
|
+
options.service ? `The proxy is registered to run automatically on port ${choices.port}.` : `Start manually: copilot-api start --port ${choices.port} --account-type ${choices.accountType}`,
|
|
989
|
+
``,
|
|
990
|
+
`Config files were written for Codex and/or Claude Code (created even if`,
|
|
991
|
+
`those tools are not installed yet), so they will work once installed.`
|
|
992
|
+
].join("\n"));
|
|
993
|
+
}
|
|
994
|
+
async function runSetup(options) {
|
|
995
|
+
state.showToken = options.showToken;
|
|
996
|
+
consola.box("copilot-api setup");
|
|
997
|
+
let choices = {
|
|
998
|
+
port: options.port,
|
|
999
|
+
accountType: options.accountType,
|
|
1000
|
+
doClaude: options.claude || !options.codex,
|
|
1001
|
+
doCodex: options.codex || !options.claude
|
|
1002
|
+
};
|
|
1003
|
+
if (!options.yes) choices = await promptChoices(choices);
|
|
1004
|
+
await ensurePaths();
|
|
1005
|
+
consola.info("Checking GitHub authentication...");
|
|
1006
|
+
await setupGitHubToken();
|
|
1007
|
+
let configOptions = {
|
|
1008
|
+
baseUrl: proxyBaseUrl(options.host, choices.port),
|
|
1009
|
+
codexModel: options.codexModel,
|
|
1010
|
+
claudeModel: options.claudeModel,
|
|
1011
|
+
claudeSmallModel: options.claudeSmallModel
|
|
1012
|
+
};
|
|
1013
|
+
if (!options.yes) configOptions = await promptModels(choices, configOptions);
|
|
1014
|
+
await writeConfigs(choices, configOptions);
|
|
1015
|
+
await maybeInstallService(options, choices);
|
|
1016
|
+
printSummary(options, choices);
|
|
1017
|
+
process$1.exit(0);
|
|
1018
|
+
}
|
|
1019
|
+
const setup = defineCommand({
|
|
1020
|
+
meta: {
|
|
1021
|
+
name: "setup",
|
|
1022
|
+
description: "Guided setup: log in, configure Codex/Claude Code, and install an auto-start service"
|
|
1023
|
+
},
|
|
1024
|
+
args: {
|
|
1025
|
+
port: {
|
|
1026
|
+
alias: "p",
|
|
1027
|
+
type: "string",
|
|
1028
|
+
default: "4141",
|
|
1029
|
+
description: "Port the proxy listens on"
|
|
1030
|
+
},
|
|
1031
|
+
host: {
|
|
1032
|
+
type: "string",
|
|
1033
|
+
default: "localhost",
|
|
1034
|
+
description: "Host the config files point at"
|
|
1035
|
+
},
|
|
1036
|
+
"account-type": {
|
|
1037
|
+
alias: "a",
|
|
1038
|
+
type: "string",
|
|
1039
|
+
default: "individual",
|
|
1040
|
+
description: "Account type (individual, business, enterprise)"
|
|
1041
|
+
},
|
|
1042
|
+
claude: {
|
|
1043
|
+
type: "boolean",
|
|
1044
|
+
default: false,
|
|
1045
|
+
description: "Only configure Claude Code (default: configure both)"
|
|
1046
|
+
},
|
|
1047
|
+
codex: {
|
|
1048
|
+
type: "boolean",
|
|
1049
|
+
default: false,
|
|
1050
|
+
description: "Only configure Codex CLI (default: configure both)"
|
|
1051
|
+
},
|
|
1052
|
+
service: {
|
|
1053
|
+
type: "boolean",
|
|
1054
|
+
default: true,
|
|
1055
|
+
description: "Install an auto-start service (use --no-service to skip)"
|
|
1056
|
+
},
|
|
1057
|
+
yes: {
|
|
1058
|
+
alias: "y",
|
|
1059
|
+
type: "boolean",
|
|
1060
|
+
default: false,
|
|
1061
|
+
description: "Non-interactive: accept all defaults"
|
|
1062
|
+
},
|
|
1063
|
+
"codex-model": {
|
|
1064
|
+
type: "string",
|
|
1065
|
+
default: DEFAULT_CODEX_MODEL,
|
|
1066
|
+
description: "Model to use for Codex CLI"
|
|
1067
|
+
},
|
|
1068
|
+
"claude-model": {
|
|
1069
|
+
type: "string",
|
|
1070
|
+
default: DEFAULT_CLAUDE_MODEL,
|
|
1071
|
+
description: "Main model to use for Claude Code"
|
|
1072
|
+
},
|
|
1073
|
+
"claude-small-model": {
|
|
1074
|
+
type: "string",
|
|
1075
|
+
default: DEFAULT_CLAUDE_SMALL_MODEL,
|
|
1076
|
+
description: "Small/fast model to use for Claude Code"
|
|
1077
|
+
},
|
|
1078
|
+
"show-token": {
|
|
1079
|
+
type: "boolean",
|
|
1080
|
+
default: false,
|
|
1081
|
+
description: "Show GitHub and Copilot tokens during setup"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
run({ args }) {
|
|
1085
|
+
return runSetup({
|
|
1086
|
+
port: Number.parseInt(args.port, 10),
|
|
1087
|
+
host: args.host,
|
|
1088
|
+
accountType: args["account-type"],
|
|
1089
|
+
codexModel: args["codex-model"],
|
|
1090
|
+
claudeModel: args["claude-model"],
|
|
1091
|
+
claudeSmallModel: args["claude-small-model"],
|
|
1092
|
+
claude: args.claude,
|
|
1093
|
+
codex: args.codex,
|
|
1094
|
+
service: args.service,
|
|
1095
|
+
yes: args.yes,
|
|
1096
|
+
showToken: args["show-token"]
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
440
1101
|
//#endregion
|
|
441
1102
|
//#region src/lib/proxy.ts
|
|
442
1103
|
function initProxyFromEnv() {
|
|
@@ -1583,6 +2244,62 @@ modelRoutes.get("/", async (c) => {
|
|
|
1583
2244
|
}
|
|
1584
2245
|
});
|
|
1585
2246
|
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/services/copilot/create-responses.ts
|
|
2249
|
+
const createResponses = async (payload, extraHeaders = {}) => {
|
|
2250
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
2251
|
+
const headers = {
|
|
2252
|
+
...copilotHeaders(state),
|
|
2253
|
+
"openai-intent": "conversation-edits",
|
|
2254
|
+
...extraHeaders
|
|
2255
|
+
};
|
|
2256
|
+
const response = await fetch(`${copilotBaseUrl(state)}/responses`, {
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
headers,
|
|
2259
|
+
body: JSON.stringify(payload)
|
|
2260
|
+
});
|
|
2261
|
+
if (!response.ok) {
|
|
2262
|
+
consola.error("Failed to create responses", response);
|
|
2263
|
+
throw new HTTPError("Failed to create responses", response);
|
|
2264
|
+
}
|
|
2265
|
+
return response;
|
|
2266
|
+
};
|
|
2267
|
+
|
|
2268
|
+
//#endregion
|
|
2269
|
+
//#region src/routes/responses/handler.ts
|
|
2270
|
+
async function handleResponses(c) {
|
|
2271
|
+
await checkRateLimit(state);
|
|
2272
|
+
const payload = await c.req.json();
|
|
2273
|
+
consola.debug("Responses request payload:", JSON.stringify(payload).slice(-400));
|
|
2274
|
+
if (state.manualApprove) await awaitApproval();
|
|
2275
|
+
const initiator = c.req.header("x-initiator");
|
|
2276
|
+
const response = await createResponses(payload, initiator ? { "x-initiator": initiator } : {});
|
|
2277
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
2278
|
+
consola.debug("Streaming responses response");
|
|
2279
|
+
return streamSSE(c, async (stream) => {
|
|
2280
|
+
for await (const event of events(response)) await stream.writeSSE({
|
|
2281
|
+
data: event.data ?? "",
|
|
2282
|
+
event: event.event,
|
|
2283
|
+
id: event.id === void 0 ? void 0 : String(event.id)
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
const json = await response.json();
|
|
2288
|
+
consola.debug("Non-streaming responses response");
|
|
2289
|
+
return c.json(json);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
//#endregion
|
|
2293
|
+
//#region src/routes/responses/route.ts
|
|
2294
|
+
const responsesRoutes = new Hono();
|
|
2295
|
+
responsesRoutes.post("/", async (c) => {
|
|
2296
|
+
try {
|
|
2297
|
+
return await handleResponses(c);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
return await forwardError(c, error);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
1586
2303
|
//#endregion
|
|
1587
2304
|
//#region src/routes/token/route.ts
|
|
1588
2305
|
const tokenRoute = new Hono();
|
|
@@ -1618,11 +2335,13 @@ server.use(logger());
|
|
|
1618
2335
|
server.use(cors());
|
|
1619
2336
|
server.get("/", (c) => c.text("Server running"));
|
|
1620
2337
|
server.route("/chat/completions", completionRoutes);
|
|
2338
|
+
server.route("/responses", responsesRoutes);
|
|
1621
2339
|
server.route("/models", modelRoutes);
|
|
1622
2340
|
server.route("/embeddings", embeddingRoutes);
|
|
1623
2341
|
server.route("/usage", usageRoute);
|
|
1624
2342
|
server.route("/token", tokenRoute);
|
|
1625
2343
|
server.route("/v1/chat/completions", completionRoutes);
|
|
2344
|
+
server.route("/v1/responses", responsesRoutes);
|
|
1626
2345
|
server.route("/v1/models", modelRoutes);
|
|
1627
2346
|
server.route("/v1/embeddings", embeddingRoutes);
|
|
1628
2347
|
server.route("/v1/messages", messageRoutes);
|
|
@@ -1796,6 +2515,8 @@ const main = defineCommand({
|
|
|
1796
2515
|
subCommands: {
|
|
1797
2516
|
auth,
|
|
1798
2517
|
start,
|
|
2518
|
+
setup,
|
|
2519
|
+
config,
|
|
1799
2520
|
"check-usage": checkUsage,
|
|
1800
2521
|
debug
|
|
1801
2522
|
}
|