ai-cc-router 0.4.1 → 0.4.3
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/dist/cli/cmd-client.js
CHANGED
|
@@ -122,7 +122,7 @@ export function registerClient(program) {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
if (wantsDesktop) {
|
|
125
|
-
await setupDesktopInterception(url);
|
|
125
|
+
await setupDesktopInterception(url, secret);
|
|
126
126
|
cfg.client.desktopEnabled = true;
|
|
127
127
|
writeConfig(cfg);
|
|
128
128
|
}
|
|
@@ -277,7 +277,7 @@ export function registerClient(program) {
|
|
|
277
277
|
process.exit(1);
|
|
278
278
|
}
|
|
279
279
|
if (!cfg.client.desktopEnabled) {
|
|
280
|
-
await setupDesktopInterception(cfg.client.remoteUrl);
|
|
280
|
+
await setupDesktopInterception(cfg.client.remoteUrl, cfg.client.remoteSecret);
|
|
281
281
|
cfg.client.desktopEnabled = true;
|
|
282
282
|
writeConfig(cfg);
|
|
283
283
|
}
|
|
@@ -305,11 +305,12 @@ export function registerClient(program) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
const target = cfg.client.remoteUrl;
|
|
308
|
+
const secret = cfg.client.remoteSecret;
|
|
308
309
|
const processName = getProcessName();
|
|
309
310
|
console.log(chalk.cyan(`\nStarting mitmproxy interceptor for "${processName}"...`));
|
|
310
311
|
console.log(chalk.gray(` Redirecting api.anthropic.com/v1/messages → ${target}`));
|
|
311
312
|
try {
|
|
312
|
-
await startInterceptor(target);
|
|
313
|
+
await startInterceptor(target, secret);
|
|
313
314
|
}
|
|
314
315
|
catch (e) {
|
|
315
316
|
console.error(chalk.red(`\n✗ Failed to start interceptor:\n`));
|
|
@@ -325,7 +326,7 @@ export function registerClient(program) {
|
|
|
325
326
|
default: true,
|
|
326
327
|
});
|
|
327
328
|
if (autoStart) {
|
|
328
|
-
const ok = await installInterceptorService(target);
|
|
329
|
+
const ok = await installInterceptorService(target, secret);
|
|
329
330
|
if (ok) {
|
|
330
331
|
cfg.client.desktopAutoStart = true;
|
|
331
332
|
writeConfig(cfg);
|
|
@@ -408,7 +409,7 @@ export function printNetworkExtensionInstructions() {
|
|
|
408
409
|
console.log(" " + chalk.cyan("5.") + " Enter your Mac admin password when prompted\n");
|
|
409
410
|
console.log(chalk.gray(" You only need to do this ONCE per machine.\n"));
|
|
410
411
|
}
|
|
411
|
-
async function setupDesktopInterception(target) {
|
|
412
|
+
async function setupDesktopInterception(target, secret) {
|
|
412
413
|
console.log(chalk.bold("\n🖥 Claude Desktop Setup\n"));
|
|
413
414
|
// 0. Explain what actually works before anything else
|
|
414
415
|
printDesktopSupportExplainer();
|
|
@@ -474,8 +475,8 @@ async function setupDesktopInterception(target) {
|
|
|
474
475
|
console.log(chalk.gray(" ~/.mitmproxy/mitmproxy-ca-cert.pem"));
|
|
475
476
|
}
|
|
476
477
|
}
|
|
477
|
-
// 4. Write addon script
|
|
478
|
-
writeAddonScript(target);
|
|
478
|
+
// 4. Write addon script (with secret so intercepted requests authenticate)
|
|
479
|
+
writeAddonScript(target, secret);
|
|
479
480
|
console.log(chalk.green("✓ Redirect addon configured"));
|
|
480
481
|
// 5. macOS Network Extension — THIS is the step people miss
|
|
481
482
|
if (isMacos()) {
|
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -400,7 +400,7 @@ async function runClientSetupFromWizard() {
|
|
|
400
400
|
default: false,
|
|
401
401
|
});
|
|
402
402
|
if (wantsDesktop) {
|
|
403
|
-
await setupDesktopFromWizard(url);
|
|
403
|
+
await setupDesktopFromWizard(url, secret);
|
|
404
404
|
cfg.client.desktopEnabled = true;
|
|
405
405
|
writeConfig(cfg);
|
|
406
406
|
}
|
|
@@ -413,7 +413,7 @@ async function runClientSetupFromWizard() {
|
|
|
413
413
|
}
|
|
414
414
|
console.log();
|
|
415
415
|
}
|
|
416
|
-
async function setupDesktopFromWizard(target) {
|
|
416
|
+
async function setupDesktopFromWizard(target, secret) {
|
|
417
417
|
console.log(chalk.bold("\n🖥 Claude Desktop — Cowork / Agent Setup\n"));
|
|
418
418
|
// 1. Check mitmproxy
|
|
419
419
|
if (!(await checkMitmproxyInstalled())) {
|
|
@@ -459,8 +459,8 @@ async function setupDesktopFromWizard(target) {
|
|
|
459
459
|
console.log(chalk.gray(" -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem"));
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
|
-
// 3. Addon
|
|
463
|
-
writeAddonScript(target);
|
|
462
|
+
// 3. Addon (with secret so intercepted requests authenticate against the proxy)
|
|
463
|
+
writeAddonScript(target, secret);
|
|
464
464
|
console.log(chalk.green("✓ Redirect addon configured"));
|
|
465
465
|
// 4. Network Extension walkthrough (macOS)
|
|
466
466
|
if (isMacos()) {
|
|
@@ -192,19 +192,22 @@ export async function installCaCert() {
|
|
|
192
192
|
* Write the redirect addon to ~/.cc-router/interceptor/addon.py.
|
|
193
193
|
* Uses the bundled template as source.
|
|
194
194
|
*/
|
|
195
|
-
export function writeAddonScript(target) {
|
|
195
|
+
export function writeAddonScript(target, secret) {
|
|
196
196
|
if (!existsSync(ADDON_DIR))
|
|
197
197
|
mkdirSync(ADDON_DIR, { recursive: true });
|
|
198
198
|
// Try to use the bundled addon as template; fall back to a minimal inline version.
|
|
199
|
-
// In BOTH cases we inject the actual target URL so the addon is
|
|
200
|
-
// and doesn't depend on the CC_ROUTER_TARGET
|
|
199
|
+
// In BOTH cases we inject the actual target URL (and secret) so the addon is
|
|
200
|
+
// self-contained and doesn't depend on the CC_ROUTER_TARGET / CC_ROUTER_SECRET
|
|
201
|
+
// env vars being present at runtime.
|
|
201
202
|
const bundled = addonSourcePath();
|
|
202
203
|
let src;
|
|
203
204
|
if (existsSync(bundled)) {
|
|
204
205
|
src = readFileSync(bundled, "utf-8");
|
|
205
206
|
}
|
|
206
207
|
else {
|
|
207
|
-
// Inline fallback — minimal addon (
|
|
208
|
+
// Inline fallback — minimal addon (handles /v1/messages and /v1/models
|
|
209
|
+
// for both api.anthropic.com traffic and requests already pointed at the
|
|
210
|
+
// CC-Router target, injecting the secret in both cases).
|
|
208
211
|
src = `
|
|
209
212
|
import os
|
|
210
213
|
from mitmproxy import http
|
|
@@ -217,22 +220,38 @@ _target_parsed = urlparse(_target)
|
|
|
217
220
|
if not _target_parsed.scheme or not _target_parsed.netloc:
|
|
218
221
|
raise RuntimeError(f"CC_ROUTER_TARGET is not a valid URL: {_target_raw!r}")
|
|
219
222
|
|
|
223
|
+
_target_host = (_target_parsed.hostname or "").lower()
|
|
224
|
+
_target_port = _target_parsed.port or (443 if _target_parsed.scheme == "https" else 80)
|
|
225
|
+
|
|
226
|
+
_secret = os.environ.get("CC_ROUTER_SECRET", "")
|
|
227
|
+
|
|
220
228
|
_REDIRECT_PREFIXES = ("/v1/messages", "/v1/models")
|
|
221
229
|
|
|
222
230
|
def request(flow: http.HTTPFlow) -> None:
|
|
223
|
-
|
|
231
|
+
host = (flow.request.pretty_host or "").lower()
|
|
232
|
+
port = flow.request.port
|
|
233
|
+
is_anthropic = host == "api.anthropic.com"
|
|
234
|
+
is_target = host == _target_host and port == _target_port
|
|
235
|
+
if not is_anthropic and not is_target:
|
|
224
236
|
return
|
|
225
237
|
if not flow.request.path.startswith(_REDIRECT_PREFIXES):
|
|
226
238
|
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
if is_anthropic:
|
|
240
|
+
flow.request.scheme = _target_parsed.scheme
|
|
241
|
+
flow.request.host = _target_host or "localhost"
|
|
242
|
+
flow.request.port = _target_port
|
|
243
|
+
flow.request.headers["host"] = flow.request.host + (f":{flow.request.port}" if flow.request.port not in (80, 443) else "")
|
|
244
|
+
if _secret:
|
|
245
|
+
flow.request.headers["x-api-key"] = _secret
|
|
231
246
|
`.trimStart();
|
|
232
247
|
}
|
|
233
|
-
// Inject the
|
|
234
|
-
// without the CC_ROUTER_TARGET
|
|
248
|
+
// Inject the target URL and (optionally) secret into the default values so
|
|
249
|
+
// the addon works even without the CC_ROUTER_TARGET / CC_ROUTER_SECRET env
|
|
250
|
+
// vars being present at runtime (manual mitmdump restarts, launchd, etc.).
|
|
235
251
|
src = src.replace('"http://localhost:3456"', JSON.stringify(target));
|
|
252
|
+
if (secret) {
|
|
253
|
+
src = src.replace('os.environ.get("CC_ROUTER_SECRET", "")', `os.environ.get("CC_ROUTER_SECRET", ${JSON.stringify(secret)})`);
|
|
254
|
+
}
|
|
236
255
|
writeFileSync(ADDON_PATH, src, "utf-8");
|
|
237
256
|
}
|
|
238
257
|
// ─── Interceptor lifecycle ────────────────────────────────────────────────────
|
|
@@ -240,7 +259,7 @@ def request(flow: http.HTTPFlow) -> None:
|
|
|
240
259
|
* Start mitmdump in local mode, intercepting the Claude process and redirecting
|
|
241
260
|
* api.anthropic.com traffic to CC-Router via the addon script.
|
|
242
261
|
*/
|
|
243
|
-
export async function startInterceptor(target) {
|
|
262
|
+
export async function startInterceptor(target, secret) {
|
|
244
263
|
// On macOS, verify the Network Extension is enabled before attempting to start.
|
|
245
264
|
// If it's "waiting", mitmdump starts silently but captures zero traffic.
|
|
246
265
|
if (isMacos()) {
|
|
@@ -261,7 +280,7 @@ export async function startInterceptor(target) {
|
|
|
261
280
|
}
|
|
262
281
|
// Always (re)write the addon script so package updates and target-URL
|
|
263
282
|
// changes are picked up automatically without requiring a fresh setup.
|
|
264
|
-
writeAddonScript(target);
|
|
283
|
+
writeAddonScript(target, secret);
|
|
265
284
|
const processName = getProcessName();
|
|
266
285
|
const args = [
|
|
267
286
|
"--mode", `local:${processName}`,
|
|
@@ -269,10 +288,13 @@ export async function startInterceptor(target) {
|
|
|
269
288
|
"--set", "connection_strategy=lazy",
|
|
270
289
|
"--quiet",
|
|
271
290
|
];
|
|
291
|
+
const env = { ...process.env, CC_ROUTER_TARGET: target };
|
|
292
|
+
if (secret)
|
|
293
|
+
env["CC_ROUTER_SECRET"] = secret;
|
|
272
294
|
const child = spawn("mitmdump", args, {
|
|
273
295
|
detached: true,
|
|
274
296
|
stdio: "ignore",
|
|
275
|
-
env
|
|
297
|
+
env,
|
|
276
298
|
});
|
|
277
299
|
child.unref();
|
|
278
300
|
if (child.pid) {
|
|
@@ -337,8 +359,12 @@ async function resolveMitmdumpPath() {
|
|
|
337
359
|
return "mitmdump"; // fallback — hope it's on PATH at boot time
|
|
338
360
|
}
|
|
339
361
|
}
|
|
340
|
-
function buildInterceptorPlist(mitmdumpPath, target) {
|
|
362
|
+
function buildInterceptorPlist(mitmdumpPath, target, secret) {
|
|
341
363
|
const processName = getProcessName();
|
|
364
|
+
const secretEntry = secret
|
|
365
|
+
? ` <key>CC_ROUTER_SECRET</key>
|
|
366
|
+
<string>${secret}</string>`
|
|
367
|
+
: "";
|
|
342
368
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
343
369
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
344
370
|
<plist version="1.0">
|
|
@@ -375,13 +401,15 @@ function buildInterceptorPlist(mitmdumpPath, target) {
|
|
|
375
401
|
<string>${process.env["PATH"] ?? "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"}</string>
|
|
376
402
|
<key>CC_ROUTER_TARGET</key>
|
|
377
403
|
<string>${target}</string>
|
|
404
|
+
${secretEntry}
|
|
378
405
|
</dict>
|
|
379
406
|
</dict>
|
|
380
407
|
</plist>
|
|
381
408
|
`;
|
|
382
409
|
}
|
|
383
|
-
function buildInterceptorSystemdUnit(mitmdumpPath, target) {
|
|
410
|
+
function buildInterceptorSystemdUnit(mitmdumpPath, target, secret) {
|
|
384
411
|
const processName = getProcessName();
|
|
412
|
+
const secretLine = secret ? `\nEnvironment=CC_ROUTER_SECRET=${secret}` : "";
|
|
385
413
|
return `[Unit]
|
|
386
414
|
Description=CC-Router Interceptor — mitmproxy for Claude Desktop
|
|
387
415
|
After=network-online.target
|
|
@@ -395,7 +423,7 @@ RestartSec=5
|
|
|
395
423
|
StartLimitIntervalSec=60
|
|
396
424
|
StartLimitBurst=5
|
|
397
425
|
Environment=PATH=${process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin"}
|
|
398
|
-
Environment=CC_ROUTER_TARGET=${target}
|
|
426
|
+
Environment=CC_ROUTER_TARGET=${target}${secretLine}
|
|
399
427
|
|
|
400
428
|
[Install]
|
|
401
429
|
WantedBy=default.target
|
|
@@ -405,17 +433,17 @@ WantedBy=default.target
|
|
|
405
433
|
* Install the mitmproxy interceptor as an OS service so it starts on boot.
|
|
406
434
|
* Stops any existing detached mitmdump process first — the OS service takes over.
|
|
407
435
|
*/
|
|
408
|
-
export async function installInterceptorService(target) {
|
|
409
|
-
// Ensure the addon script is up-to-date with the target URL
|
|
410
|
-
writeAddonScript(target);
|
|
436
|
+
export async function installInterceptorService(target, secret) {
|
|
437
|
+
// Ensure the addon script is up-to-date with the target URL and secret
|
|
438
|
+
writeAddonScript(target, secret);
|
|
411
439
|
// Stop any manually-spawned mitmdump — OS service will manage it now
|
|
412
440
|
await stopInterceptor();
|
|
413
441
|
const mitmdumpPath = await resolveMitmdumpPath();
|
|
414
442
|
const platform = detectPlatform();
|
|
415
443
|
switch (platform) {
|
|
416
|
-
case "macos": return installInterceptorMacOS(mitmdumpPath, target);
|
|
417
|
-
case "linux": return installInterceptorLinux(mitmdumpPath, target);
|
|
418
|
-
case "windows": return installInterceptorWindows(mitmdumpPath, target);
|
|
444
|
+
case "macos": return installInterceptorMacOS(mitmdumpPath, target, secret);
|
|
445
|
+
case "linux": return installInterceptorLinux(mitmdumpPath, target, secret);
|
|
446
|
+
case "windows": return installInterceptorWindows(mitmdumpPath, target, secret);
|
|
419
447
|
}
|
|
420
448
|
}
|
|
421
449
|
export async function uninstallInterceptorService() {
|
|
@@ -435,7 +463,7 @@ export function isInterceptorServiceInstalled() {
|
|
|
435
463
|
}
|
|
436
464
|
}
|
|
437
465
|
// ─── macOS LaunchAgent ──────────────────────────────────────────────────────
|
|
438
|
-
async function installInterceptorMacOS(mitmdumpPath, target) {
|
|
466
|
+
async function installInterceptorMacOS(mitmdumpPath, target, secret) {
|
|
439
467
|
const launchAgentsDir = dirname(LAUNCHD_PLIST);
|
|
440
468
|
if (!existsSync(launchAgentsDir))
|
|
441
469
|
mkdirSync(launchAgentsDir, { recursive: true });
|
|
@@ -443,7 +471,7 @@ async function installInterceptorMacOS(mitmdumpPath, target) {
|
|
|
443
471
|
if (existsSync(LAUNCHD_PLIST)) {
|
|
444
472
|
await interceptorLaunchctlUnload();
|
|
445
473
|
}
|
|
446
|
-
writeFileSync(LAUNCHD_PLIST, buildInterceptorPlist(mitmdumpPath, target), "utf-8");
|
|
474
|
+
writeFileSync(LAUNCHD_PLIST, buildInterceptorPlist(mitmdumpPath, target, secret), "utf-8");
|
|
447
475
|
// Load — try modern `bootstrap` first, fallback to legacy `load`
|
|
448
476
|
const uid = String(process.getuid?.() ?? 501);
|
|
449
477
|
try {
|
|
@@ -483,10 +511,10 @@ async function interceptorLaunchctlUnload() {
|
|
|
483
511
|
}
|
|
484
512
|
}
|
|
485
513
|
// ─── Linux systemd user service ─────────────────────────────────────────────
|
|
486
|
-
async function installInterceptorLinux(mitmdumpPath, target) {
|
|
514
|
+
async function installInterceptorLinux(mitmdumpPath, target, secret) {
|
|
487
515
|
if (!existsSync(SYSTEMD_DIR))
|
|
488
516
|
mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
489
|
-
writeFileSync(SYSTEMD_SERVICE, buildInterceptorSystemdUnit(mitmdumpPath, target), "utf-8");
|
|
517
|
+
writeFileSync(SYSTEMD_SERVICE, buildInterceptorSystemdUnit(mitmdumpPath, target, secret), "utf-8");
|
|
490
518
|
try {
|
|
491
519
|
await execFileP("systemctl", ["--user", "daemon-reload"]);
|
|
492
520
|
await execFileP("systemctl", ["--user", "enable", "cc-router-interceptor"]);
|
|
@@ -517,9 +545,10 @@ async function uninstallInterceptorLinux() {
|
|
|
517
545
|
catch { /* ok */ }
|
|
518
546
|
}
|
|
519
547
|
// ─── Windows Registry ───────────────────────────────────────────────────────
|
|
520
|
-
async function installInterceptorWindows(mitmdumpPath, target) {
|
|
548
|
+
async function installInterceptorWindows(mitmdumpPath, target, secret) {
|
|
521
549
|
const processName = getProcessName();
|
|
522
|
-
const
|
|
550
|
+
const secretEnv = secret ? `set CC_ROUTER_SECRET=${secret} && ` : "";
|
|
551
|
+
const cmd = `cmd /c "set CC_ROUTER_TARGET=${target} && ${secretEnv}"${mitmdumpPath}" --mode local:${processName} -s "${ADDON_PATH}" --set connection_strategy=lazy --quiet"`;
|
|
523
552
|
try {
|
|
524
553
|
await execFileP("reg", [
|
|
525
554
|
"add", WINDOWS_REG_KEY,
|
package/package.json
CHANGED
package/src/interceptor/addon.py
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
# mitmproxy addon — redirects ONLY /v1/messages traffic to CC-Router
|
|
1
|
+
# mitmproxy addon — redirects ONLY /v1/messages traffic to CC-Router and
|
|
2
|
+
# injects the proxy secret as an auth header.
|
|
2
3
|
#
|
|
3
|
-
#
|
|
4
|
-
# /v1/messages → LLM inference (this is what we redirect)
|
|
5
|
-
# /v1/messages/count_tokens → token counting (redirect too)
|
|
6
|
-
# /v1/oauth/* → session auth (must NOT redirect)
|
|
7
|
-
# /v1/environments/* → bridge/cowork (must NOT redirect)
|
|
8
|
-
# /v1/models → model listing (redirect — CC-Router proxies this)
|
|
9
|
-
# /api/* → desktop features (must NOT redirect)
|
|
4
|
+
# There are TWO cases to handle:
|
|
10
5
|
#
|
|
11
|
-
#
|
|
6
|
+
# 1. Requests to api.anthropic.com (Claude Desktop native features)
|
|
7
|
+
# → rewrite host/port to CC-Router target + inject x-api-key
|
|
8
|
+
#
|
|
9
|
+
# 2. Requests already pointed at the CC-Router target host (Claude Code
|
|
10
|
+
# inside Desktop Cowork/Agent mode, which reads ~/.claude/settings.json
|
|
11
|
+
# and goes direct to ANTHROPIC_BASE_URL)
|
|
12
|
+
# → inject x-api-key (no rewrite needed)
|
|
13
|
+
#
|
|
14
|
+
# Claude Desktop sends many types of requests:
|
|
15
|
+
# /v1/messages → LLM inference (redirect + auth)
|
|
16
|
+
# /v1/messages/count_tokens → token counting (redirect + auth)
|
|
17
|
+
# /v1/oauth/* → session auth (must NOT touch)
|
|
18
|
+
# /v1/environments/* → bridge/cowork (must NOT touch)
|
|
19
|
+
# /v1/models → model listing (redirect + auth)
|
|
20
|
+
# /api/* → desktop features (must NOT touch)
|
|
21
|
+
#
|
|
22
|
+
# Only /v1/messages* and /v1/models are safe to touch because CC-Router
|
|
12
23
|
# injects its own OAuth token. Everything else carries the user's own
|
|
13
24
|
# session token for features CC-Router doesn't handle.
|
|
14
25
|
|
|
@@ -24,6 +35,12 @@ _target_parsed = urlparse(_target)
|
|
|
24
35
|
if not _target_parsed.scheme or not _target_parsed.netloc:
|
|
25
36
|
raise RuntimeError(f"CC_ROUTER_TARGET is not a valid URL: {_target_raw!r}")
|
|
26
37
|
|
|
38
|
+
_target_host = (_target_parsed.hostname or "").lower()
|
|
39
|
+
_target_port = _target_parsed.port or (443 if _target_parsed.scheme == "https" else 80)
|
|
40
|
+
|
|
41
|
+
# Optional proxy secret — when set, injected as x-api-key on routed requests
|
|
42
|
+
_secret = os.environ.get("CC_ROUTER_SECRET", "")
|
|
43
|
+
|
|
27
44
|
# Paths that CC-Router can handle (it injects its own OAuth token)
|
|
28
45
|
_REDIRECT_PREFIXES = (
|
|
29
46
|
"/v1/messages",
|
|
@@ -32,18 +49,30 @@ _REDIRECT_PREFIXES = (
|
|
|
32
49
|
|
|
33
50
|
|
|
34
51
|
def request(flow: http.HTTPFlow) -> None:
|
|
35
|
-
|
|
52
|
+
host = (flow.request.pretty_host or "").lower()
|
|
53
|
+
port = flow.request.port
|
|
54
|
+
is_anthropic = host == "api.anthropic.com"
|
|
55
|
+
is_target = host == _target_host and port == _target_port
|
|
56
|
+
|
|
57
|
+
# Not a host we care about — pass through untouched
|
|
58
|
+
if not is_anthropic and not is_target:
|
|
36
59
|
return
|
|
37
60
|
|
|
38
|
-
# Only
|
|
61
|
+
# Only touch inference and model-listing paths
|
|
39
62
|
if not flow.request.path.startswith(_REDIRECT_PREFIXES):
|
|
40
63
|
return
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
# Case 1: rewrite api.anthropic.com → CC-Router target
|
|
66
|
+
if is_anthropic:
|
|
67
|
+
flow.request.scheme = _target_parsed.scheme
|
|
68
|
+
flow.request.host = _target_host or "localhost"
|
|
69
|
+
flow.request.port = _target_port
|
|
70
|
+
flow.request.headers["host"] = flow.request.host + (
|
|
71
|
+
f":{flow.request.port}"
|
|
72
|
+
if flow.request.port not in (80, 443)
|
|
73
|
+
else ""
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Case 1 and 2: authenticate against the proxy if a secret is configured
|
|
77
|
+
if _secret:
|
|
78
|
+
flow.request.headers["x-api-key"] = _secret
|