@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.
- package/CHANGELOG.md +96 -0
- package/Dockerfile +42 -0
- package/LICENSE +21 -0
- package/README.md +716 -0
- package/accounts.example.json +25 -0
- package/dist/cli/cmd-accounts.js +248 -0
- package/dist/cli/cmd-client.js +612 -0
- package/dist/cli/cmd-configure.js +145 -0
- package/dist/cli/cmd-docker.js +140 -0
- package/dist/cli/cmd-logs.js +85 -0
- package/dist/cli/cmd-models.js +125 -0
- package/dist/cli/cmd-service.js +193 -0
- package/dist/cli/cmd-setup.js +501 -0
- package/dist/cli/cmd-start.js +318 -0
- package/dist/cli/cmd-status.js +177 -0
- package/dist/cli/cmd-stop.js +100 -0
- package/dist/cli/cmd-telemetry.js +58 -0
- package/dist/cli/cmd-update.js +37 -0
- package/dist/cli/index.js +59 -0
- package/dist/config/manager.js +262 -0
- package/dist/config/paths.js +21 -0
- package/dist/config/telemetry.js +64 -0
- package/dist/daemon/launcher.js +163 -0
- package/dist/daemon/pid.js +98 -0
- package/dist/daemon/service.js +260 -0
- package/dist/interceptor/mitmproxy-manager.js +616 -0
- package/dist/protocol/anthropic-to-openai.js +51 -0
- package/dist/protocol/anthropic-types.js +1 -0
- package/dist/protocol/model-ref.js +36 -0
- package/dist/protocol/model-routing-config.js +30 -0
- package/dist/protocol/openai-response-to-anthropic.js +20 -0
- package/dist/protocol/openai-responses-types.js +1 -0
- package/dist/protocol/openai-stream-to-anthropic.js +75 -0
- package/dist/protocol/openai-to-anthropic.js +61 -0
- package/dist/protocol/sse.js +17 -0
- package/dist/providers/model-discovery.js +71 -0
- package/dist/providers/openai/account-pool.js +11 -0
- package/dist/providers/openai/account-record.js +33 -0
- package/dist/providers/openai/codex-transport.js +36 -0
- package/dist/providers/openai/device-oauth.js +116 -0
- package/dist/providers/openai/token-refresher.js +56 -0
- package/dist/providers/route-selector.js +8 -0
- package/dist/providers/types.js +1 -0
- package/dist/proxy/account-deletion.js +44 -0
- package/dist/proxy/anthropic-proxy.js +26 -0
- package/dist/proxy/anthropic-routing.js +90 -0
- package/dist/proxy/lease-lifecycle.js +68 -0
- package/dist/proxy/logger.js +39 -0
- package/dist/proxy/messages-cross-route.js +179 -0
- package/dist/proxy/models-server.js +150 -0
- package/dist/proxy/provider-routing.js +14 -0
- package/dist/proxy/responses-server.js +91 -0
- package/dist/proxy/server.js +875 -0
- package/dist/proxy/session-router.js +171 -0
- package/dist/proxy/stats.js +25 -0
- package/dist/proxy/stream-lifecycle.js +83 -0
- package/dist/proxy/token-pool.js +407 -0
- package/dist/proxy/token-refresher.js +209 -0
- package/dist/proxy/types.js +29 -0
- package/dist/ui/Dashboard.js +640 -0
- package/dist/ui/accountsApi.js +48 -0
- package/dist/ui/modelsApi.js +47 -0
- package/dist/utils/claude-config.js +185 -0
- package/dist/utils/codex-config.js +62 -0
- package/dist/utils/network.js +16 -0
- package/dist/utils/platform.js +13 -0
- package/dist/utils/self-update.js +239 -0
- package/dist/utils/telemetry.js +88 -0
- package/dist/utils/token-extractor.js +95 -0
- package/dist/utils/token-validator.js +26 -0
- package/docker-compose.yml +63 -0
- package/litellm-config.yaml +44 -0
- package/package.json +69 -0
- package/src/interceptor/addon.py +78 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "max-account-1",
|
|
4
|
+
"accessToken": "sk-ant-oat01-REPLACE_ME",
|
|
5
|
+
"refreshToken": "sk-ant-ort01-REPLACE_ME",
|
|
6
|
+
"expiresAt": 1748658860000,
|
|
7
|
+
"scopes": ["user:inference", "user:profile"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "max-account-2",
|
|
11
|
+
"accessToken": "sk-ant-oat01-REPLACE_ME",
|
|
12
|
+
"refreshToken": "sk-ant-ort01-REPLACE_ME",
|
|
13
|
+
"expiresAt": 1748658860000,
|
|
14
|
+
"scopes": ["user:inference", "user:profile"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "openai-account-1",
|
|
18
|
+
"provider": "openai_subscription",
|
|
19
|
+
"accessToken": "OPENAI_ACCESS_TOKEN_REPLACE_ME",
|
|
20
|
+
"refreshToken": "OPENAI_REFRESH_TOKEN_REPLACE_ME",
|
|
21
|
+
"expiresAt": 1748658860000,
|
|
22
|
+
"scopes": ["openid", "profile", "email", "offline_access"],
|
|
23
|
+
"enabled": true
|
|
24
|
+
}
|
|
25
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadAccounts, loadOpenAIAccounts, accountsFileExists, upsertAccountRecord, removeAccountRecordById } from "../config/manager.js";
|
|
3
|
+
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
4
|
+
import { formatExpiry, redactToken } from "../utils/token-extractor.js";
|
|
5
|
+
import { PROXY_PORT } from "../config/paths.js";
|
|
6
|
+
import { createOpenAIAccountRecord } from "../providers/openai/account-record.js";
|
|
7
|
+
import { loginOpenAIWithDeviceCode } from "../providers/openai/device-oauth.js";
|
|
8
|
+
export function registerAccounts(program) {
|
|
9
|
+
const accounts = program
|
|
10
|
+
.command("accounts")
|
|
11
|
+
.description("Manage Claude Max accounts in the token pool");
|
|
12
|
+
// ── accounts list ────────────────────────────────────────────────────────
|
|
13
|
+
accounts
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("List all configured accounts and their status")
|
|
16
|
+
.option("--json", "Output as JSON")
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
// Try to get live stats from the running proxy first
|
|
19
|
+
const liveStats = await fetchLiveStats();
|
|
20
|
+
if (!accountsFileExists()) {
|
|
21
|
+
console.log(chalk.yellow("No accounts configured. Run: cc-router setup"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const stored = loadAccounts();
|
|
25
|
+
const openAIStored = loadOpenAIAccounts();
|
|
26
|
+
if (stored.length === 0 && openAIStored.length === 0) {
|
|
27
|
+
console.log(chalk.yellow("accounts.json is empty. Run: cc-router setup"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
console.log(JSON.stringify(liveStats ?? buildStoredAccountsJson(stored, openAIStored), null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.bold(`\n Accounts (${stored.length + openAIStored.length} configured)\n`));
|
|
35
|
+
if (liveStats) {
|
|
36
|
+
console.log(chalk.green(" ● Proxy is running — showing live stats\n"));
|
|
37
|
+
for (const s of liveStats) {
|
|
38
|
+
const provider = s.provider === "openai_subscription"
|
|
39
|
+
? chalk.cyan("openai".padEnd(9))
|
|
40
|
+
: chalk.gray("claude".padEnd(9));
|
|
41
|
+
const status = s.healthy
|
|
42
|
+
? chalk.green("✓ healthy")
|
|
43
|
+
: chalk.red("✗ unhealthy");
|
|
44
|
+
const busy = s.busy ? chalk.yellow(" [busy]") : "";
|
|
45
|
+
const exp = s.expiresInMs > 0
|
|
46
|
+
? chalk.yellow(formatMs(s.expiresInMs))
|
|
47
|
+
: chalk.red("EXPIRED");
|
|
48
|
+
console.log(` ${chalk.bold(s.id.padEnd(24))}` +
|
|
49
|
+
` ${provider}` +
|
|
50
|
+
` ${status}${busy}` +
|
|
51
|
+
` requests: ${chalk.cyan(String(s.requestCount).padStart(5))}` +
|
|
52
|
+
` errors: ${chalk.red(String(s.errorCount).padStart(3))}` +
|
|
53
|
+
` expires: ${exp}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(chalk.gray(" (Proxy not running — showing stored configuration)\n"));
|
|
58
|
+
for (const a of stored) {
|
|
59
|
+
const exp = formatExpiry(a.tokens.expiresAt);
|
|
60
|
+
const expColor = a.tokens.expiresAt > Date.now()
|
|
61
|
+
? chalk.yellow(exp)
|
|
62
|
+
: chalk.red(exp);
|
|
63
|
+
console.log(` ${chalk.bold(a.id.padEnd(24))}` +
|
|
64
|
+
` ${redactToken(a.tokens.accessToken).padEnd(26)}` +
|
|
65
|
+
` expires: ${expColor}` +
|
|
66
|
+
` scopes: ${chalk.gray(a.tokens.scopes.join(" "))}`);
|
|
67
|
+
}
|
|
68
|
+
for (const a of openAIStored) {
|
|
69
|
+
const exp = a.expiresAt > Date.now()
|
|
70
|
+
? chalk.yellow(formatExpiry(a.expiresAt))
|
|
71
|
+
: chalk.red("EXPIRED");
|
|
72
|
+
console.log(` ${chalk.bold(a.id.padEnd(24))}` +
|
|
73
|
+
` ${chalk.magenta("openai".padEnd(10))}` +
|
|
74
|
+
` ${redactToken(a.accessToken).padEnd(26)}` +
|
|
75
|
+
` expires: ${exp}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
});
|
|
80
|
+
// ── accounts add ─────────────────────────────────────────────────────────
|
|
81
|
+
accounts
|
|
82
|
+
.command("add")
|
|
83
|
+
.description("Add a new Claude Max account interactively")
|
|
84
|
+
.action(async () => {
|
|
85
|
+
const { setupSingleAccount } = await import("./cmd-setup.js");
|
|
86
|
+
const existing = accountsFileExists() ? loadAccounts() : [];
|
|
87
|
+
const account = await setupSingleAccount(existing.length + 1);
|
|
88
|
+
if (!account) {
|
|
89
|
+
console.log(chalk.yellow("\nNo account added.\n"));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Merge: replace by ID if already exists, otherwise append
|
|
93
|
+
const merged = [
|
|
94
|
+
...existing.filter(a => a.id !== account.id),
|
|
95
|
+
account,
|
|
96
|
+
];
|
|
97
|
+
saveAccounts(merged);
|
|
98
|
+
console.log(chalk.green(`\n✓ Account "${account.id}" added (${merged.length} total).\n`));
|
|
99
|
+
console.log(chalk.gray(" Restart the proxy to load the new account: cc-router start\n"));
|
|
100
|
+
});
|
|
101
|
+
// ── accounts add-openai ──────────────────────────────────────────────────
|
|
102
|
+
accounts
|
|
103
|
+
.command("add-openai")
|
|
104
|
+
.description("Add an OpenAI ChatGPT/Codex subscription account manually")
|
|
105
|
+
.action(async () => {
|
|
106
|
+
const { input, password } = await import("@inquirer/prompts");
|
|
107
|
+
const id = await input({
|
|
108
|
+
message: "OpenAI account ID:",
|
|
109
|
+
default: `openai-account-${loadOpenAIAccounts().length + 1}`,
|
|
110
|
+
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
111
|
+
});
|
|
112
|
+
const accessToken = await password({
|
|
113
|
+
message: "OpenAI access token:",
|
|
114
|
+
mask: "*",
|
|
115
|
+
validate: (v) => v.trim().length > 0 || "Access token is required",
|
|
116
|
+
});
|
|
117
|
+
const refreshToken = await password({
|
|
118
|
+
message: "OpenAI refresh token:",
|
|
119
|
+
mask: "*",
|
|
120
|
+
validate: (v) => v.trim().length > 0 || "Refresh token is required",
|
|
121
|
+
});
|
|
122
|
+
const expiresAt = await input({
|
|
123
|
+
message: "Access token expiry (Unix ms):",
|
|
124
|
+
default: String(Date.now() + 60 * 60 * 1000),
|
|
125
|
+
validate: (v) => Number.isFinite(Number(v)) && Number(v) > 0 || "Enter a positive Unix timestamp in milliseconds",
|
|
126
|
+
});
|
|
127
|
+
const scopes = await input({
|
|
128
|
+
message: "Scopes:",
|
|
129
|
+
default: "openid profile email offline_access",
|
|
130
|
+
});
|
|
131
|
+
const record = createOpenAIAccountRecord({
|
|
132
|
+
id,
|
|
133
|
+
accessToken,
|
|
134
|
+
refreshToken,
|
|
135
|
+
expiresAt,
|
|
136
|
+
scopes,
|
|
137
|
+
});
|
|
138
|
+
upsertAccountRecord(record);
|
|
139
|
+
console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved.\n`));
|
|
140
|
+
console.log(chalk.gray(" Restart the proxy to load the new account: cc-router start\n"));
|
|
141
|
+
console.log(chalk.yellow(" Treat this as experimental until the OAuth login wizard lands.\n"));
|
|
142
|
+
});
|
|
143
|
+
// ── accounts login-openai ────────────────────────────────────────────────
|
|
144
|
+
accounts
|
|
145
|
+
.command("login-openai")
|
|
146
|
+
.description("Sign in to an OpenAI ChatGPT/Codex subscription account with device code")
|
|
147
|
+
.action(async () => {
|
|
148
|
+
const { input } = await import("@inquirer/prompts");
|
|
149
|
+
const accountId = await input({
|
|
150
|
+
message: "OpenAI account ID:",
|
|
151
|
+
default: `openai-account-${loadOpenAIAccounts().length + 1}`,
|
|
152
|
+
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
153
|
+
});
|
|
154
|
+
console.log(chalk.cyan("\nOpenAI Codex device login"));
|
|
155
|
+
console.log(chalk.gray("This will open no local callback server. You will approve the login in your browser.\n"));
|
|
156
|
+
const record = await loginOpenAIWithDeviceCode({
|
|
157
|
+
accountId,
|
|
158
|
+
onDeviceCode: (code) => {
|
|
159
|
+
console.log(chalk.bold("1. Open this URL:"));
|
|
160
|
+
console.log(` ${chalk.cyan(code.verificationUrl)}`);
|
|
161
|
+
console.log(chalk.bold("2. Enter this code:"));
|
|
162
|
+
console.log(` ${chalk.cyan(code.userCode)}\n`);
|
|
163
|
+
console.log(chalk.gray("Waiting for authorization..."));
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
upsertAccountRecord(record);
|
|
167
|
+
console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved via device login.\n`));
|
|
168
|
+
console.log(chalk.gray(" Restart the proxy to load the new account: cc-router start\n"));
|
|
169
|
+
});
|
|
170
|
+
// ── accounts remove ───────────────────────────────────────────────────────
|
|
171
|
+
accounts
|
|
172
|
+
.command("remove <id>")
|
|
173
|
+
.description("Remove an account by its ID")
|
|
174
|
+
.action(async (id) => {
|
|
175
|
+
if (!accountsFileExists()) {
|
|
176
|
+
console.log(chalk.yellow("No accounts configured."));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const anthropicAccounts = loadAccounts();
|
|
180
|
+
const openAIAccounts = loadOpenAIAccounts();
|
|
181
|
+
const existingIds = [
|
|
182
|
+
...anthropicAccounts.map(a => a.id),
|
|
183
|
+
...openAIAccounts.map(a => a.id),
|
|
184
|
+
];
|
|
185
|
+
if (!existingIds.includes(id)) {
|
|
186
|
+
console.log(chalk.red(`✗ Account "${id}" not found.`));
|
|
187
|
+
console.log(chalk.gray(` Available: ${existingIds.join(", ")}`));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
191
|
+
const sure = await confirm({
|
|
192
|
+
message: `Remove "${id}"? This cannot be undone.`,
|
|
193
|
+
default: false,
|
|
194
|
+
});
|
|
195
|
+
if (!sure) {
|
|
196
|
+
console.log(chalk.gray("Cancelled."));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const removed = removeAccountRecordById(id);
|
|
200
|
+
if (!removed) {
|
|
201
|
+
console.log(chalk.red(`✗ Account "${id}" disappeared before it could be removed.`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const remaining = loadAccounts().length + loadOpenAIAccounts().length;
|
|
205
|
+
const providerLabel = removed.provider === "openai_subscription" ? "OpenAI account" : "Account";
|
|
206
|
+
console.log(chalk.green(`✓ Removed ${providerLabel} "${id}". ${remaining} account(s) remaining.`));
|
|
207
|
+
if (remaining === 0) {
|
|
208
|
+
console.log(chalk.yellow(" No accounts left. Run: cc-router setup"));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
213
|
+
export function buildStoredAccountsJson(anthropicAccounts, openAIAccounts) {
|
|
214
|
+
return [
|
|
215
|
+
...anthropicAccounts.map(a => ({
|
|
216
|
+
id: a.id,
|
|
217
|
+
provider: "anthropic_subscription",
|
|
218
|
+
enabled: a.enabled,
|
|
219
|
+
expiresAt: a.tokens.expiresAt,
|
|
220
|
+
scopes: a.tokens.scopes,
|
|
221
|
+
})),
|
|
222
|
+
...openAIAccounts.map(a => ({
|
|
223
|
+
id: a.id,
|
|
224
|
+
provider: "openai_subscription",
|
|
225
|
+
enabled: a.enabled !== false,
|
|
226
|
+
expiresAt: a.expiresAt,
|
|
227
|
+
})),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
async function fetchLiveStats() {
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(`http://localhost:${PROXY_PORT}/cc-router/health`, {
|
|
233
|
+
signal: AbortSignal.timeout(1_000),
|
|
234
|
+
});
|
|
235
|
+
if (!res.ok)
|
|
236
|
+
return null;
|
|
237
|
+
const data = await res.json();
|
|
238
|
+
return data.accounts;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function formatMs(ms) {
|
|
245
|
+
const h = Math.floor(ms / 3_600_000);
|
|
246
|
+
const m = Math.floor((ms % 3_600_000) / 60_000);
|
|
247
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
248
|
+
}
|