@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,145 @@
1
+ import chalk from "chalk";
2
+ import { writeClaudeSettings, removeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
3
+ import { writeCodexRouterConfig } from "../utils/codex-config.js";
4
+ import { readConfig, writeConfig, generateProxySecret } from "../config/manager.js";
5
+ import { PROXY_PORT, CLAUDE_SETTINGS_PATH } from "../config/paths.js";
6
+ import { buildModelRoutingUpdate } from "../protocol/model-routing-config.js";
7
+ export function registerConfigure(program) {
8
+ program
9
+ .command("configure")
10
+ .description("Configure Claude Code or Codex to point to the proxy")
11
+ .argument("[target]", "Optional target to configure: codex, models")
12
+ .option("--remove", "Remove cc-router settings from ~/.claude/settings.json")
13
+ .option("--port <port>", "Proxy port to configure", String(PROXY_PORT))
14
+ .option("--model <model>", "Default model for the configured target")
15
+ .option("--claude-model <model>", "Default Claude/Anthropic model for router aliases")
16
+ .option("--openai-model <model>", "Default OpenAI/Codex model for router aliases")
17
+ .option("--show", "Show current Claude Code proxy settings")
18
+ .option("--generate-password", "Generate a new proxy secret and sync Claude Code settings")
19
+ .option("--set-password <secret>", "Set a specific proxy secret and sync Claude Code settings")
20
+ .option("--remove-password", "Remove proxy password protection (open access)")
21
+ .option("--enable-auto-update", "Enable automatic updates for the proxy")
22
+ .option("--disable-auto-update", "Disable automatic updates for the proxy")
23
+ .action((target, opts) => {
24
+ if (target === "codex") {
25
+ const port = parseInt(opts.port, 10);
26
+ const result = writeCodexRouterConfig({
27
+ baseUrl: `http://localhost:${port}/v1`,
28
+ tokenEnvKey: "CC_ROUTER_TOKEN",
29
+ defaultModel: opts.model,
30
+ });
31
+ console.log(chalk.green(`✓ Updated ${result.path}`));
32
+ console.log(chalk.gray(" CODEX provider configured:"));
33
+ if (opts.model)
34
+ console.log(chalk.gray(` model = ${opts.model}`));
35
+ console.log(chalk.gray(" model_provider = cc-router"));
36
+ console.log(chalk.gray(` base_url = http://localhost:${port}/v1`));
37
+ console.log(chalk.gray(" env_key = CC_ROUTER_TOKEN"));
38
+ return;
39
+ }
40
+ if (target === "models") {
41
+ if (!opts.claudeModel && !opts.openaiModel) {
42
+ console.error(chalk.red("Provide at least one model: --claude-model or --openai-model"));
43
+ process.exit(1);
44
+ }
45
+ const cfg = readConfig();
46
+ const modelRouting = buildModelRoutingUpdate(cfg.modelRouting, {
47
+ claudeModel: opts.claudeModel,
48
+ openAIModel: opts.openaiModel,
49
+ });
50
+ writeConfig({ ...cfg, modelRouting });
51
+ console.log(chalk.green("✓ Updated model routing defaults."));
52
+ if (opts.claudeModel)
53
+ console.log(chalk.gray(` Claude default: ${opts.claudeModel}`));
54
+ if (opts.openaiModel)
55
+ console.log(chalk.gray(` OpenAI default: ${opts.openaiModel.replace(/^openai\//, "")}`));
56
+ console.log(chalk.gray(" Restart cc-router for the change to affect running traffic."));
57
+ return;
58
+ }
59
+ if (target !== undefined) {
60
+ console.error(chalk.red(`Unknown configure target: ${target}`));
61
+ process.exit(1);
62
+ }
63
+ if (opts.show) {
64
+ const current = readClaudeProxySettings();
65
+ if (current.baseUrl) {
66
+ console.log(chalk.green(" Claude Code is configured to use cc-router:"));
67
+ console.log(` ANTHROPIC_BASE_URL = ${chalk.cyan(current.baseUrl)}`);
68
+ console.log(` ANTHROPIC_AUTH_TOKEN = ${chalk.gray(current.authToken ?? "(not set)")}`);
69
+ }
70
+ else {
71
+ console.log(chalk.yellow(" Claude Code is NOT configured to use cc-router."));
72
+ console.log(chalk.gray(` Run: cc-router configure`));
73
+ }
74
+ const cfg = readConfig();
75
+ const pwStatus = cfg.proxySecret ? chalk.green("yes") : chalk.gray("no");
76
+ const autoUpdateEnabled = cfg.autoUpdate !== false;
77
+ const auStatus = autoUpdateEnabled ? chalk.green("enabled") : chalk.gray("disabled");
78
+ console.log(` Password protected: ${pwStatus}`);
79
+ console.log(` Auto-update: ${auStatus}`);
80
+ return;
81
+ }
82
+ if (opts.enableAutoUpdate) {
83
+ writeConfig({ ...readConfig(), autoUpdate: true });
84
+ console.log(chalk.green("✓ Auto-update enabled."));
85
+ console.log(chalk.gray(" The proxy will check for updates every 6h and install patch/minor releases."));
86
+ console.log(chalk.gray(" Restart cc-router for the change to take effect."));
87
+ return;
88
+ }
89
+ if (opts.disableAutoUpdate) {
90
+ writeConfig({ ...readConfig(), autoUpdate: false });
91
+ console.log(chalk.green("✓ Auto-update disabled."));
92
+ console.log(chalk.gray(" Use `cc-router update` to update manually."));
93
+ console.log(chalk.gray(" Restart cc-router for the change to take effect."));
94
+ return;
95
+ }
96
+ if (opts.remove) {
97
+ removeClaudeSettings();
98
+ console.log(chalk.green("✓ Removed cc-router settings from ~/.claude/settings.json"));
99
+ console.log(chalk.gray(" Claude Code will use its default authentication on next launch."));
100
+ return;
101
+ }
102
+ if (opts.generatePassword) {
103
+ const secret = generateProxySecret();
104
+ writeConfig({ ...readConfig(), proxySecret: secret });
105
+ const { baseUrl } = readClaudeProxySettings();
106
+ writeClaudeSettings(parseInt(opts.port, 10), baseUrl);
107
+ console.log(chalk.green("✓ Proxy password set."));
108
+ console.log(" " + chalk.bold.yellow("Save this — it will not be shown again:"));
109
+ console.log(" " + chalk.bold(secret));
110
+ console.log(chalk.gray(" Restart cc-router for the change to take effect."));
111
+ return;
112
+ }
113
+ if (opts.setPassword !== undefined) {
114
+ const secret = opts.setPassword.trim();
115
+ if (!secret) {
116
+ console.error(chalk.red("Secret cannot be empty."));
117
+ process.exit(1);
118
+ }
119
+ writeConfig({ ...readConfig(), proxySecret: secret });
120
+ const { baseUrl } = readClaudeProxySettings();
121
+ writeClaudeSettings(parseInt(opts.port, 10), baseUrl);
122
+ console.log(chalk.green("✓ Proxy password updated."));
123
+ console.log(chalk.gray(" Restart cc-router for the change to take effect."));
124
+ return;
125
+ }
126
+ if (opts.removePassword) {
127
+ const cfg = readConfig();
128
+ delete cfg.proxySecret;
129
+ writeConfig(cfg);
130
+ const { baseUrl } = readClaudeProxySettings();
131
+ writeClaudeSettings(parseInt(opts.port, 10), baseUrl);
132
+ console.log(chalk.green("✓ Proxy password removed. Access is now open."));
133
+ console.log(chalk.gray(" Restart cc-router for the change to take effect."));
134
+ return;
135
+ }
136
+ const port = parseInt(opts.port, 10);
137
+ writeClaudeSettings(port, undefined, undefined, opts.model);
138
+ const { proxySecret } = readConfig();
139
+ console.log(chalk.green(`✓ Updated ${CLAUDE_SETTINGS_PATH}`));
140
+ if (opts.model)
141
+ console.log(chalk.gray(` model = ${opts.model}`));
142
+ console.log(chalk.gray(` ANTHROPIC_BASE_URL = http://localhost:${port}`));
143
+ console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN = ${proxySecret ? chalk.green("(secret configured)") : "proxy-managed"}`));
144
+ });
145
+ }
@@ -0,0 +1,140 @@
1
+ import { execFile, spawn } from "child_process";
2
+ import { promisify } from "util";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname } from "path";
7
+ import chalk from "chalk";
8
+ import { ACCOUNTS_PATH, LITELLM_PORT, PROXY_PORT } from "../config/paths.js";
9
+ const execFileAsync = promisify(execFile);
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const COMPOSE_FILE = join(dirname(__filename), "..", "..", "docker-compose.yml");
12
+ export function registerDocker(program) {
13
+ const docker = program
14
+ .command("docker")
15
+ .description("Manage the full Docker stack (cc-router + LiteLLM)");
16
+ docker
17
+ .command("up")
18
+ .description("Start cc-router + LiteLLM with Docker Compose")
19
+ .option("--build", "Rebuild the cc-router image before starting")
20
+ .action(async (opts) => {
21
+ await ensureDockerAvailable();
22
+ await ensureAccountsExist();
23
+ console.log(chalk.cyan("\nStarting cc-router + LiteLLM via Docker Compose...\n"));
24
+ const args = ["compose", "-f", COMPOSE_FILE, "up", "-d"];
25
+ if (opts.build)
26
+ args.push("--build");
27
+ try {
28
+ await spawnInherited("docker", args);
29
+ await waitForHealthy();
30
+ printDockerInfo();
31
+ }
32
+ catch (err) {
33
+ console.error(chalk.red("\n✗ docker compose up failed:"), err.message);
34
+ console.error(chalk.gray(" Check logs with: cc-router docker logs"));
35
+ process.exit(1);
36
+ }
37
+ });
38
+ docker
39
+ .command("down")
40
+ .description("Stop and remove Docker containers")
41
+ .option("-v, --volumes", "Also remove named volumes")
42
+ .action(async (opts) => {
43
+ const args = ["compose", "-f", COMPOSE_FILE, "down"];
44
+ if (opts.volumes)
45
+ args.push("-v");
46
+ await spawnInherited("docker", args);
47
+ console.log(chalk.green("\n✓ Containers stopped.\n"));
48
+ });
49
+ docker
50
+ .command("logs")
51
+ .description("Tail Docker Compose logs")
52
+ .option("-f, --follow", "Follow log output", true)
53
+ .option("--service <name>", "Show logs for a specific service (cc-router or litellm)")
54
+ .action(async (opts) => {
55
+ const args = ["compose", "-f", COMPOSE_FILE, "logs"];
56
+ if (opts.follow)
57
+ args.push("-f");
58
+ args.push("--tail=100");
59
+ if (opts.service)
60
+ args.push(opts.service);
61
+ await spawnInherited("docker", args);
62
+ });
63
+ docker
64
+ .command("ps")
65
+ .description("Show running container status")
66
+ .action(async () => {
67
+ await spawnInherited("docker", ["compose", "-f", COMPOSE_FILE, "ps"]);
68
+ });
69
+ docker
70
+ .command("restart")
71
+ .description("Restart a service without rebuilding")
72
+ .argument("[service]", "Service to restart (cc-router or litellm)", "cc-router")
73
+ .action(async (service) => {
74
+ await spawnInherited("docker", ["compose", "-f", COMPOSE_FILE, "restart", service]);
75
+ console.log(chalk.green(`\n✓ ${service} restarted.\n`));
76
+ });
77
+ }
78
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
79
+ async function ensureDockerAvailable() {
80
+ try {
81
+ await execFileAsync("docker", ["info"]);
82
+ }
83
+ catch {
84
+ console.error(chalk.red("✗ Docker is not running or not installed."));
85
+ console.error(chalk.gray(" Install Docker Desktop: https://docs.docker.com/get-docker/"));
86
+ process.exit(1);
87
+ }
88
+ }
89
+ async function ensureAccountsExist() {
90
+ if (!existsSync(ACCOUNTS_PATH)) {
91
+ console.error(chalk.red(`✗ accounts.json not found at ${ACCOUNTS_PATH}`));
92
+ console.error(chalk.yellow(" Run: cc-router setup"));
93
+ process.exit(1);
94
+ }
95
+ }
96
+ /** Wait until the cc-router health endpoint responds OK (max 60s) */
97
+ async function waitForHealthy() {
98
+ process.stdout.write(chalk.gray(" Waiting for services to be healthy"));
99
+ const deadline = Date.now() + 60_000;
100
+ while (Date.now() < deadline) {
101
+ try {
102
+ const res = await fetch(`http://localhost:${PROXY_PORT}/cc-router/health`, {
103
+ signal: AbortSignal.timeout(1_000),
104
+ });
105
+ if (res.ok) {
106
+ console.log(chalk.green(" ✓"));
107
+ return;
108
+ }
109
+ }
110
+ catch {
111
+ // not ready yet
112
+ }
113
+ process.stdout.write(".");
114
+ await sleep(2_000);
115
+ }
116
+ console.log(chalk.yellow(" timed out"));
117
+ console.log(chalk.gray(" Services may still be starting. Check: cc-router docker ps"));
118
+ }
119
+ function printDockerInfo() {
120
+ console.log(chalk.bold("\n Stack is running:\n"));
121
+ console.log(` Proxy: ${chalk.cyan(`http://localhost:${PROXY_PORT}`)}`);
122
+ console.log(` LiteLLM UI: ${chalk.cyan(`http://localhost:${LITELLM_PORT}/ui`)}`);
123
+ console.log(` Health: ${chalk.cyan(`http://localhost:${PROXY_PORT}/cc-router/health`)}`);
124
+ console.log();
125
+ console.log(chalk.gray(" Logs: cc-router docker logs"));
126
+ console.log(chalk.gray(" Stop: cc-router docker down\n"));
127
+ }
128
+ /** Spawn a command with inherited stdio (user sees output in real time) */
129
+ function spawnInherited(cmd, args) {
130
+ return new Promise((resolve, reject) => {
131
+ const child = spawn(cmd, args, { stdio: "inherit" });
132
+ child.on("error", reject);
133
+ child.on("close", code => {
134
+ code === 0 ? resolve() : reject(new Error(`exited with code ${code}`));
135
+ });
136
+ });
137
+ }
138
+ function sleep(ms) {
139
+ return new Promise(resolve => setTimeout(resolve, ms));
140
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, openSync, readSync, closeSync, statSync, watch } from "fs";
2
+ import chalk from "chalk";
3
+ import { LOG_PATH } from "../config/paths.js";
4
+ export function registerLogs(program) {
5
+ program
6
+ .command("logs")
7
+ .description("View proxy logs")
8
+ .option("--lines <n>", "Number of lines to show (default: 50)", "50")
9
+ .option("-f, --follow", "Follow log output in real time")
10
+ .action(async (opts) => {
11
+ if (!existsSync(LOG_PATH)) {
12
+ console.log(chalk.yellow("No log file found."));
13
+ console.log(chalk.gray(` Expected: ${LOG_PATH}`));
14
+ console.log(chalk.gray(" Logs are created when running in background mode."));
15
+ console.log(chalk.gray(" Foreground mode prints directly to this terminal.\n"));
16
+ return;
17
+ }
18
+ const numLines = parseInt(opts.lines, 10) || 50;
19
+ if (opts.follow) {
20
+ await tailFollow(numLines);
21
+ }
22
+ else {
23
+ tailStatic(numLines);
24
+ }
25
+ });
26
+ }
27
+ /** Print last N lines of the log file using byte-based seeking. */
28
+ function tailStatic(n) {
29
+ if (!existsSync(LOG_PATH)) {
30
+ console.log(chalk.gray("No log file found. Is the daemon running?"));
31
+ return;
32
+ }
33
+ const stats = statSync(LOG_PATH);
34
+ if (stats.size === 0) {
35
+ console.log(chalk.gray("Log file is empty."));
36
+ return;
37
+ }
38
+ // Read last chunk (up to 64KB should be enough for most tail operations)
39
+ const CHUNK = Math.min(stats.size, 64 * 1024);
40
+ const buf = Buffer.alloc(CHUNK);
41
+ const fd = openSync(LOG_PATH, "r");
42
+ readSync(fd, buf, 0, CHUNK, stats.size - CHUNK);
43
+ closeSync(fd);
44
+ const text = buf.toString("utf-8");
45
+ const lines = text.split("\n");
46
+ // Remove first potentially partial line if we didn't read from start
47
+ if (CHUNK < stats.size)
48
+ lines.shift();
49
+ const tail = lines.slice(-n).join("\n");
50
+ if (tail)
51
+ process.stdout.write(tail + "\n");
52
+ }
53
+ /** Stream log file and print new lines as they appear. */
54
+ async function tailFollow(initialLines) {
55
+ // Show initial tail
56
+ tailStatic(initialLines);
57
+ console.log(chalk.gray("\n── Following log output (Ctrl+C to stop) ──\n"));
58
+ if (!existsSync(LOG_PATH)) {
59
+ console.log(chalk.gray("Waiting for log file..."));
60
+ }
61
+ let bytePosition = existsSync(LOG_PATH) ? statSync(LOG_PATH).size : 0;
62
+ const watcher = watch(LOG_PATH, () => {
63
+ try {
64
+ const currentSize = statSync(LOG_PATH).size;
65
+ if (currentSize > bytePosition) {
66
+ const buf = Buffer.alloc(currentSize - bytePosition);
67
+ const fd = openSync(LOG_PATH, "r");
68
+ readSync(fd, buf, 0, buf.length, bytePosition);
69
+ closeSync(fd);
70
+ process.stdout.write(buf.toString("utf-8"));
71
+ bytePosition = currentSize;
72
+ }
73
+ else if (currentSize < bytePosition) {
74
+ // File was truncated/rotated
75
+ bytePosition = 0;
76
+ }
77
+ }
78
+ catch { /* file may have been removed */ }
79
+ });
80
+ process.on("SIGINT", () => {
81
+ watcher.close();
82
+ process.exit(0);
83
+ });
84
+ await new Promise(() => { }); // never resolves
85
+ }
@@ -0,0 +1,125 @@
1
+ import chalk from "chalk";
2
+ import { readConfig, writeConfig } from "../config/manager.js";
3
+ import { PROXY_PORT } from "../config/paths.js";
4
+ import { buildModelRoutingUpdate } from "../protocol/model-routing-config.js";
5
+ import { resolveStatusTarget } from "./cmd-status.js";
6
+ export function groupModelsByProvider(models) {
7
+ const anthropic = new Set();
8
+ const openai = new Set();
9
+ const aliases = new Set();
10
+ for (const model of models) {
11
+ if (model.id.startsWith("anthropic/"))
12
+ anthropic.add(model.id);
13
+ else if (model.id.startsWith("openai/"))
14
+ openai.add(model.id);
15
+ else
16
+ aliases.add(model.id);
17
+ }
18
+ return {
19
+ anthropic: [...anthropic].sort(),
20
+ openai: [...openai].sort(),
21
+ aliases: [...aliases].sort(),
22
+ };
23
+ }
24
+ export function buildModelSetPayload(opts) {
25
+ return {
26
+ claudeModel: opts.claudeModel?.replace(/^anthropic\//, ""),
27
+ openAIModel: opts.openAIModel?.replace(/^openai\//, ""),
28
+ };
29
+ }
30
+ export function registerModels(program) {
31
+ const models = program
32
+ .command("models")
33
+ .description("List discovered provider models and update router model defaults");
34
+ models
35
+ .command("list")
36
+ .description("List models discovered from provider APIs through the running proxy")
37
+ .option("--port <port>", "Proxy port to connect to", String(PROXY_PORT))
38
+ .option("--json", "Output raw model status JSON")
39
+ .action(async (opts) => {
40
+ const status = await fetchRouterModels(parseInt(opts.port, 10));
41
+ if (opts.json) {
42
+ console.log(JSON.stringify(status, null, 2));
43
+ return;
44
+ }
45
+ printModels(status);
46
+ });
47
+ models
48
+ .command("set")
49
+ .description("Update Claude/OpenAI default models used by router aliases")
50
+ .option("--port <port>", "Proxy port to connect to", String(PROXY_PORT))
51
+ .option("--claude-model <model>", "Claude/Anthropic model id, with or without anthropic/ prefix")
52
+ .option("--openai-model <model>", "OpenAI/Codex model id, with or without openai/ prefix")
53
+ .action(async (opts) => {
54
+ if (!opts.claudeModel && !opts.openaiModel) {
55
+ console.error(chalk.red("Provide at least one model: --claude-model or --openai-model"));
56
+ process.exit(1);
57
+ }
58
+ const payload = buildModelSetPayload({
59
+ claudeModel: opts.claudeModel,
60
+ openAIModel: opts.openaiModel,
61
+ });
62
+ try {
63
+ const updated = await patchRouterModels(parseInt(opts.port, 10), payload);
64
+ console.log(chalk.green("✓ Updated running router model defaults."));
65
+ printRouting(updated.routing ?? {});
66
+ }
67
+ catch (err) {
68
+ const cfg = readConfig();
69
+ const modelRouting = buildModelRoutingUpdate(cfg.modelRouting, payload);
70
+ writeConfig({ ...cfg, modelRouting });
71
+ console.log(chalk.yellow("⚠ Could not update the running router; saved config for next start."));
72
+ console.log(chalk.gray(` ${err.message}`));
73
+ printRouting(modelRouting);
74
+ }
75
+ });
76
+ }
77
+ async function fetchRouterModels(port) {
78
+ const target = resolveStatusTarget(port);
79
+ const res = await fetch(`${target.baseUrl}/cc-router/models`, {
80
+ headers: target.headers,
81
+ signal: AbortSignal.timeout(5_000),
82
+ });
83
+ if (!res.ok)
84
+ throw new Error(`Proxy returned HTTP ${res.status}`);
85
+ return await res.json();
86
+ }
87
+ async function patchRouterModels(port, payload) {
88
+ const target = resolveStatusTarget(port);
89
+ const res = await fetch(`${target.baseUrl}/cc-router/models`, {
90
+ method: "PATCH",
91
+ headers: {
92
+ "content-type": "application/json",
93
+ ...target.headers,
94
+ },
95
+ body: JSON.stringify(payload),
96
+ signal: AbortSignal.timeout(5_000),
97
+ });
98
+ if (!res.ok)
99
+ throw new Error(`Proxy returned HTTP ${res.status}`);
100
+ return await res.json();
101
+ }
102
+ function printModels(status) {
103
+ const groups = groupModelsByProvider(status.models ?? []);
104
+ console.log(chalk.bold("\n Models\n"));
105
+ printRouting(status.routing ?? {});
106
+ printGroup("Claude / Anthropic", groups.anthropic);
107
+ printGroup("OpenAI / Codex", groups.openai);
108
+ printGroup("Aliases", groups.aliases);
109
+ console.log(chalk.gray("\n Change defaults with:"));
110
+ console.log(chalk.cyan(" cc-router models set --claude-model anthropic/<id> --openai-model openai/<id>\n"));
111
+ }
112
+ function printGroup(label, ids) {
113
+ console.log(chalk.bold(`\n ${label}`));
114
+ if (ids.length === 0) {
115
+ console.log(chalk.gray(" (none discovered)"));
116
+ return;
117
+ }
118
+ for (const id of ids)
119
+ console.log(` ${id}`);
120
+ }
121
+ function printRouting(routing) {
122
+ console.log(chalk.gray(" Current routing:"));
123
+ console.log(` Claude default: ${chalk.cyan(routing.anthropicDefaultModel ?? "(not set)")}`);
124
+ console.log(` OpenAI default: ${chalk.cyan(routing.openAIDefaultModel ?? "(not set)")}`);
125
+ }