@timo972/cc-router 0.7.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/Dockerfile +42 -0
  3. package/LICENSE +21 -0
  4. package/README.md +716 -0
  5. package/accounts.example.json +25 -0
  6. package/dist/cli/cmd-accounts.js +248 -0
  7. package/dist/cli/cmd-client.js +612 -0
  8. package/dist/cli/cmd-configure.js +145 -0
  9. package/dist/cli/cmd-docker.js +140 -0
  10. package/dist/cli/cmd-logs.js +85 -0
  11. package/dist/cli/cmd-models.js +125 -0
  12. package/dist/cli/cmd-service.js +193 -0
  13. package/dist/cli/cmd-setup.js +501 -0
  14. package/dist/cli/cmd-start.js +318 -0
  15. package/dist/cli/cmd-status.js +177 -0
  16. package/dist/cli/cmd-stop.js +100 -0
  17. package/dist/cli/cmd-telemetry.js +58 -0
  18. package/dist/cli/cmd-update.js +37 -0
  19. package/dist/cli/index.js +59 -0
  20. package/dist/config/manager.js +262 -0
  21. package/dist/config/paths.js +21 -0
  22. package/dist/config/telemetry.js +64 -0
  23. package/dist/daemon/launcher.js +163 -0
  24. package/dist/daemon/pid.js +98 -0
  25. package/dist/daemon/service.js +260 -0
  26. package/dist/interceptor/mitmproxy-manager.js +616 -0
  27. package/dist/protocol/anthropic-to-openai.js +51 -0
  28. package/dist/protocol/anthropic-types.js +1 -0
  29. package/dist/protocol/model-ref.js +36 -0
  30. package/dist/protocol/model-routing-config.js +30 -0
  31. package/dist/protocol/openai-response-to-anthropic.js +20 -0
  32. package/dist/protocol/openai-responses-types.js +1 -0
  33. package/dist/protocol/openai-stream-to-anthropic.js +75 -0
  34. package/dist/protocol/openai-to-anthropic.js +61 -0
  35. package/dist/protocol/sse.js +17 -0
  36. package/dist/providers/model-discovery.js +71 -0
  37. package/dist/providers/openai/account-pool.js +11 -0
  38. package/dist/providers/openai/account-record.js +33 -0
  39. package/dist/providers/openai/codex-transport.js +36 -0
  40. package/dist/providers/openai/device-oauth.js +116 -0
  41. package/dist/providers/openai/token-refresher.js +56 -0
  42. package/dist/providers/route-selector.js +8 -0
  43. package/dist/providers/types.js +1 -0
  44. package/dist/proxy/account-deletion.js +44 -0
  45. package/dist/proxy/anthropic-proxy.js +26 -0
  46. package/dist/proxy/anthropic-routing.js +90 -0
  47. package/dist/proxy/lease-lifecycle.js +68 -0
  48. package/dist/proxy/logger.js +39 -0
  49. package/dist/proxy/messages-cross-route.js +179 -0
  50. package/dist/proxy/models-server.js +150 -0
  51. package/dist/proxy/provider-routing.js +14 -0
  52. package/dist/proxy/responses-server.js +91 -0
  53. package/dist/proxy/server.js +875 -0
  54. package/dist/proxy/session-router.js +171 -0
  55. package/dist/proxy/stats.js +25 -0
  56. package/dist/proxy/stream-lifecycle.js +83 -0
  57. package/dist/proxy/token-pool.js +407 -0
  58. package/dist/proxy/token-refresher.js +209 -0
  59. package/dist/proxy/types.js +29 -0
  60. package/dist/ui/Dashboard.js +640 -0
  61. package/dist/ui/accountsApi.js +48 -0
  62. package/dist/ui/modelsApi.js +47 -0
  63. package/dist/utils/claude-config.js +185 -0
  64. package/dist/utils/codex-config.js +62 -0
  65. package/dist/utils/network.js +16 -0
  66. package/dist/utils/platform.js +13 -0
  67. package/dist/utils/self-update.js +239 -0
  68. package/dist/utils/telemetry.js +88 -0
  69. package/dist/utils/token-extractor.js +95 -0
  70. package/dist/utils/token-validator.js +26 -0
  71. package/docker-compose.yml +63 -0
  72. package/litellm-config.yaml +44 -0
  73. package/package.json +69 -0
  74. package/src/interceptor/addon.py +78 -0
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { PID_PATH, PROXY_PORT } from "../config/paths.js";
3
+ import { ensureConfigDir } from "../config/manager.js";
4
+ /** Write the current process PID to the PID file. */
5
+ export function writePid(pid) {
6
+ try {
7
+ ensureConfigDir();
8
+ writeFileSync(PID_PATH, String(pid), "utf-8");
9
+ }
10
+ catch (err) {
11
+ console.warn(`Warning: cannot write PID file ${PID_PATH}: ${err.message}`);
12
+ console.warn(` Daemon is running as PID ${pid} but may not be stoppable via cc-router stop`);
13
+ }
14
+ }
15
+ /** Read PID from file. Returns null if missing or unreadable. */
16
+ export function readPid() {
17
+ try {
18
+ if (!existsSync(PID_PATH))
19
+ return null;
20
+ const raw = readFileSync(PID_PATH, "utf-8").trim();
21
+ const pid = parseInt(raw, 10);
22
+ return Number.isNaN(pid) ? null : pid;
23
+ }
24
+ catch (err) {
25
+ // File exists but can't be read — likely a permissions issue
26
+ console.warn(`Warning: cannot read PID file ${PID_PATH}: ${err.message}`);
27
+ return null;
28
+ }
29
+ }
30
+ /** Remove the PID file. */
31
+ export function removePid() {
32
+ try {
33
+ if (existsSync(PID_PATH))
34
+ unlinkSync(PID_PATH);
35
+ }
36
+ catch (err) {
37
+ console.warn(`Warning: cannot remove PID file ${PID_PATH}: ${err.message}`);
38
+ }
39
+ }
40
+ /**
41
+ * Check if a process with the given PID is alive.
42
+ * Uses signal 0 — doesn't actually kill the process, just checks existence.
43
+ */
44
+ export function isProcessAlive(pid) {
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ }
49
+ catch (err) {
50
+ // ESRCH = no such process — it's dead
51
+ // EPERM = exists but no permission — it's alive
52
+ return err.code === "EPERM";
53
+ }
54
+ }
55
+ /**
56
+ * Read PID and verify the process is actually alive.
57
+ * Cleans up stale PID files where the process has died.
58
+ */
59
+ export function getRunningPid() {
60
+ const pid = readPid();
61
+ if (pid === null)
62
+ return null;
63
+ if (isProcessAlive(pid))
64
+ return pid;
65
+ // Stale PID — process died without cleanup
66
+ removePid();
67
+ return null;
68
+ }
69
+ /**
70
+ * Double-check: PID is alive AND health endpoint responds.
71
+ * Prevents false positives from recycled PIDs (a different process
72
+ * reusing the same PID number).
73
+ */
74
+ export async function isProxyRunning(port = PROXY_PORT) {
75
+ const pid = getRunningPid();
76
+ if (pid !== null) {
77
+ // PID exists — verify it's actually cc-router via health endpoint
78
+ try {
79
+ const res = await fetch(`http://localhost:${port}/cc-router/health`, {
80
+ signal: AbortSignal.timeout(1_000),
81
+ });
82
+ return res.ok;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ // No PID file — try health endpoint directly (foreground/legacy processes)
89
+ try {
90
+ const res = await fetch(`http://localhost:${port}/cc-router/health`, {
91
+ signal: AbortSignal.timeout(1_000),
92
+ });
93
+ return res.ok;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
@@ -0,0 +1,260 @@
1
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "fs";
2
+ import { execFile, execFileSync } from "child_process";
3
+ import { promisify } from "util";
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join } from "path";
6
+ import os from "os";
7
+ import chalk from "chalk";
8
+ import { LOG_PATH } from "../config/paths.js";
9
+ import { detectPlatform } from "../utils/platform.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const CLI_ENTRY = join(__dirname, "..", "cli", "index.js");
14
+ // ─── Platform paths ──────────────────────────────────────────────────────────
15
+ const LAUNCHD_LABEL = "com.cc-router.proxy";
16
+ const LAUNCHD_PLIST = join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
17
+ const SYSTEMD_DIR = join(os.homedir(), ".config", "systemd", "user");
18
+ const SYSTEMD_SERVICE = join(SYSTEMD_DIR, "cc-router.service");
19
+ const WINDOWS_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
20
+ const WINDOWS_REG_NAME = "CC-Router";
21
+ // ─── Public API ──────────────────────────────────────────────────────────────
22
+ export async function installService(serverMode) {
23
+ const platform = detectPlatform();
24
+ switch (platform) {
25
+ case "macos": return installMacOS(serverMode);
26
+ case "linux": return installLinux(serverMode);
27
+ case "windows": return installWindows(serverMode);
28
+ }
29
+ }
30
+ export async function uninstallService() {
31
+ const platform = detectPlatform();
32
+ switch (platform) {
33
+ case "macos": return uninstallMacOS();
34
+ case "linux": return uninstallLinux();
35
+ case "windows": return uninstallWindows();
36
+ }
37
+ }
38
+ export function isServiceInstalled() {
39
+ const platform = detectPlatform();
40
+ switch (platform) {
41
+ case "macos": return existsSync(LAUNCHD_PLIST);
42
+ case "linux": return existsSync(SYSTEMD_SERVICE);
43
+ case "windows": return isWindowsServiceInstalled();
44
+ }
45
+ }
46
+ // ─── macOS LaunchAgent ───────────────────────────────────────────────────────
47
+ function buildPlist(serverMode) {
48
+ const envVars = serverMode
49
+ ? ` <key>EnvironmentVariables</key>
50
+ <dict>
51
+ <key>PATH</key>
52
+ <string>${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}</string>
53
+ <key>HOST</key>
54
+ <string>0.0.0.0</string>
55
+ <key>CC_ROUTER_SERVICE</key>
56
+ <string>1</string>
57
+ </dict>`
58
+ : ` <key>EnvironmentVariables</key>
59
+ <dict>
60
+ <key>PATH</key>
61
+ <string>${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}</string>
62
+ <key>CC_ROUTER_SERVICE</key>
63
+ <string>1</string>
64
+ </dict>`;
65
+ return `<?xml version="1.0" encoding="UTF-8"?>
66
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
67
+ <plist version="1.0">
68
+ <dict>
69
+ <key>Label</key>
70
+ <string>${LAUNCHD_LABEL}</string>
71
+ <key>ProgramArguments</key>
72
+ <array>
73
+ <string>${process.execPath}</string>
74
+ <string>${CLI_ENTRY}</string>
75
+ <string>start</string>
76
+ <string>--foreground</string>
77
+ </array>
78
+ <key>RunAtLoad</key>
79
+ <true/>
80
+ <key>KeepAlive</key>
81
+ <dict>
82
+ <key>SuccessfulExit</key>
83
+ <false/>
84
+ </dict>
85
+ <key>StandardOutPath</key>
86
+ <string>${LOG_PATH}</string>
87
+ <key>StandardErrorPath</key>
88
+ <string>${LOG_PATH}</string>
89
+ <key>WorkingDirectory</key>
90
+ <string>${os.homedir()}</string>
91
+ ${envVars}
92
+ </dict>
93
+ </plist>
94
+ `;
95
+ }
96
+ async function installMacOS(serverMode) {
97
+ // Ensure LaunchAgents dir exists
98
+ const launchAgentsDir = dirname(LAUNCHD_PLIST);
99
+ if (!existsSync(launchAgentsDir))
100
+ mkdirSync(launchAgentsDir, { recursive: true });
101
+ // Unload existing if present (ignore errors)
102
+ if (existsSync(LAUNCHD_PLIST)) {
103
+ await launchctlUnload();
104
+ }
105
+ writeFileSync(LAUNCHD_PLIST, buildPlist(serverMode), "utf-8");
106
+ // Load — try modern `bootstrap` first, fallback to legacy `load`
107
+ const uid = String(process.getuid?.() ?? 501);
108
+ try {
109
+ await execFileAsync("launchctl", ["bootstrap", `gui/${uid}`, LAUNCHD_PLIST]);
110
+ }
111
+ catch {
112
+ try {
113
+ await execFileAsync("launchctl", ["load", LAUNCHD_PLIST]);
114
+ }
115
+ catch (err) {
116
+ console.log(chalk.yellow(`⚠ Could not auto-load the LaunchAgent: ${err.message}`));
117
+ console.log(chalk.gray(` Load manually: launchctl load ${LAUNCHD_PLIST}`));
118
+ return;
119
+ }
120
+ }
121
+ console.log(chalk.green("✓ Auto-start on boot configured (macOS LaunchAgent)"));
122
+ }
123
+ async function uninstallMacOS() {
124
+ if (!existsSync(LAUNCHD_PLIST))
125
+ return;
126
+ await launchctlUnload();
127
+ try {
128
+ unlinkSync(LAUNCHD_PLIST);
129
+ }
130
+ catch { /* ok */ }
131
+ if (isServiceInstalled()) {
132
+ console.log(chalk.yellow(" ⚠ Service may still be installed — check manually"));
133
+ }
134
+ else {
135
+ console.log(chalk.green(" ✓ Auto-start removed"));
136
+ }
137
+ }
138
+ async function launchctlUnload() {
139
+ const uid = String(process.getuid?.() ?? 501);
140
+ try {
141
+ await execFileAsync("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`]);
142
+ }
143
+ catch {
144
+ try {
145
+ await execFileAsync("launchctl", ["unload", LAUNCHD_PLIST]);
146
+ }
147
+ catch { /* already unloaded */ }
148
+ }
149
+ }
150
+ // ─── Linux systemd user service ──────────────────────────────────────────────
151
+ function buildSystemdUnit(serverMode) {
152
+ const envLine = serverMode
153
+ ? `Environment=PATH=${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}\nEnvironment=HOST=0.0.0.0\nEnvironment=CC_ROUTER_SERVICE=1`
154
+ : `Environment=PATH=${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}\nEnvironment=CC_ROUTER_SERVICE=1`;
155
+ return `[Unit]
156
+ Description=CC-Router — round-robin proxy for Claude Max
157
+ After=network-online.target
158
+ Wants=network-online.target
159
+
160
+ [Service]
161
+ Type=simple
162
+ ExecStart=${process.execPath} ${CLI_ENTRY} start --foreground
163
+ Restart=on-failure
164
+ RestartSec=5
165
+ StartLimitIntervalSec=60
166
+ StartLimitBurst=5
167
+ ${envLine}
168
+
169
+ [Install]
170
+ WantedBy=default.target
171
+ `;
172
+ }
173
+ async function installLinux(serverMode) {
174
+ if (!existsSync(SYSTEMD_DIR))
175
+ mkdirSync(SYSTEMD_DIR, { recursive: true });
176
+ writeFileSync(SYSTEMD_SERVICE, buildSystemdUnit(serverMode), "utf-8");
177
+ try {
178
+ await execFileAsync("systemctl", ["--user", "daemon-reload"]);
179
+ await execFileAsync("systemctl", ["--user", "enable", "cc-router"]);
180
+ await execFileAsync("systemctl", ["--user", "start", "cc-router"]);
181
+ console.log(chalk.green("✓ Auto-start on boot configured (systemd user service)"));
182
+ console.log(chalk.gray(" Logs: journalctl --user-unit cc-router -f"));
183
+ console.log(chalk.gray(" Tip for headless servers: loginctl enable-linger $(whoami)"));
184
+ }
185
+ catch (err) {
186
+ console.log(chalk.yellow(`⚠ systemd setup issue: ${err.message}`));
187
+ console.log(chalk.gray(` Enable manually: systemctl --user enable --now cc-router`));
188
+ }
189
+ }
190
+ async function uninstallLinux() {
191
+ if (!existsSync(SYSTEMD_SERVICE))
192
+ return;
193
+ try {
194
+ await execFileAsync("systemctl", ["--user", "stop", "cc-router"]);
195
+ await execFileAsync("systemctl", ["--user", "disable", "cc-router"]);
196
+ }
197
+ catch { /* may already be stopped */ }
198
+ try {
199
+ unlinkSync(SYSTEMD_SERVICE);
200
+ }
201
+ catch { /* ok */ }
202
+ try {
203
+ await execFileAsync("systemctl", ["--user", "daemon-reload"]);
204
+ }
205
+ catch { /* ok */ }
206
+ if (isServiceInstalled()) {
207
+ console.log(chalk.yellow(" ⚠ Service may still be installed — check manually"));
208
+ }
209
+ else {
210
+ console.log(chalk.green(" ✓ Auto-start removed"));
211
+ }
212
+ }
213
+ // ─── Windows Registry ────────────────────────────────────────────────────────
214
+ function buildWindowsCommand() {
215
+ return `"${process.execPath}" "${CLI_ENTRY}" start --foreground`;
216
+ }
217
+ async function installWindows(serverMode) {
218
+ const cmd = serverMode
219
+ ? `cmd /c "set HOST=0.0.0.0 && set CC_ROUTER_SERVICE=1 && ${buildWindowsCommand()}"`
220
+ : `cmd /c "set CC_ROUTER_SERVICE=1 && ${buildWindowsCommand()}"`;
221
+ try {
222
+ await execFileAsync("reg", [
223
+ "add", WINDOWS_REG_KEY,
224
+ "/v", WINDOWS_REG_NAME,
225
+ "/t", "REG_SZ",
226
+ "/d", cmd,
227
+ "/f",
228
+ ]);
229
+ console.log(chalk.green("✓ Auto-start on login configured (Windows Registry)"));
230
+ }
231
+ catch (err) {
232
+ console.log(chalk.yellow(`⚠ Registry write failed: ${err.message}`));
233
+ console.log(chalk.gray(` Add manually via Task Scheduler or registry editor.`));
234
+ }
235
+ }
236
+ async function uninstallWindows() {
237
+ try {
238
+ await execFileAsync("reg", [
239
+ "delete", WINDOWS_REG_KEY,
240
+ "/v", WINDOWS_REG_NAME,
241
+ "/f",
242
+ ]);
243
+ }
244
+ catch { /* not installed */ }
245
+ if (isServiceInstalled()) {
246
+ console.log(chalk.yellow(" ⚠ Service may still be installed — check manually"));
247
+ }
248
+ else {
249
+ console.log(chalk.green(" ✓ Auto-start removed"));
250
+ }
251
+ }
252
+ function isWindowsServiceInstalled() {
253
+ try {
254
+ execFileSync("reg", ["query", WINDOWS_REG_KEY, "/v", WINDOWS_REG_NAME]);
255
+ return true;
256
+ }
257
+ catch {
258
+ return false;
259
+ }
260
+ }