solrouter 0.4.3 → 0.4.5
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/bin/proxy.js +31 -0
- package/package.json +1 -1
- package/src/server.js +8 -2
package/bin/proxy.js
CHANGED
|
@@ -18,6 +18,23 @@ const [, , cmdRaw, ...rest] = process.argv;
|
|
|
18
18
|
const cmd = cmdRaw || "run";
|
|
19
19
|
const flag = name => { const i = rest.indexOf(name); return i >= 0 ? (rest[i + 1] ?? true) : undefined; };
|
|
20
20
|
|
|
21
|
+
// Find a genuinely free port starting from `start`. Prevents the footgun where a
|
|
22
|
+
// STALE proxy already on the port gets silently reused, so old code keeps running
|
|
23
|
+
// after a version bump.
|
|
24
|
+
async function freePort(start) {
|
|
25
|
+
const net = await import("node:net");
|
|
26
|
+
for (let p = start; p < start + 25; p++) {
|
|
27
|
+
const ok = await new Promise(res => {
|
|
28
|
+
const s = net.createServer();
|
|
29
|
+
s.once("error", () => res(false));
|
|
30
|
+
s.once("listening", () => s.close(() => res(true)));
|
|
31
|
+
s.listen(p, "127.0.0.1");
|
|
32
|
+
});
|
|
33
|
+
if (ok) return p;
|
|
34
|
+
}
|
|
35
|
+
return start;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
// Start the proxy in-process, then launch the chosen tool through it (Claude Code),
|
|
22
39
|
// or — for GUI tools — keep the proxy up and print how to point the tool at it.
|
|
23
40
|
async function startProxyAndLaunch(cfg, tool, launch, passThru = []) {
|
|
@@ -112,11 +129,23 @@ async function main() {
|
|
|
112
129
|
const { banner } = await import("../src/banner.js");
|
|
113
130
|
console.log(banner({ menu: false }));
|
|
114
131
|
const { spawn } = await import("node:child_process");
|
|
132
|
+
// ALWAYS start our own proxy on a free port — never reuse a stale one.
|
|
133
|
+
cfg.port = await freePort(cfg.port);
|
|
115
134
|
startServer(cfg); // non-blocking listener in this process
|
|
116
135
|
// Pass everything after `code`/`claude` (minus our own --port/--mode) to Claude Code.
|
|
117
136
|
const passThru = rest.filter((a, i) =>
|
|
118
137
|
!["--port", "--mode"].includes(a) && !["--port", "--mode"].includes(rest[i - 1]));
|
|
119
138
|
const tool = process.env.SOLROUTER_TOOL || "claude";
|
|
139
|
+
// CRITICAL: Claude Code only routes to a custom ANTHROPIC_BASE_URL when an API
|
|
140
|
+
// key is present — otherwise a Max/Pro subscription silently bypasses the base
|
|
141
|
+
// URL and talks to Anthropic directly (so the proxy never sees the traffic).
|
|
142
|
+
// Force API-key mode by giving the child a key. The proxy's frontier leg uses
|
|
143
|
+
// its own configured key regardless, so this only needs to be non-empty.
|
|
144
|
+
const childKey = cfg.frontier.apiKey || process.env.ANTHROPIC_API_KEY || "sk-ant-solrouter-proxy";
|
|
145
|
+
if (!cfg.frontier.apiKey && !process.env.ANTHROPIC_API_KEY) {
|
|
146
|
+
console.warn("Warning: no Anthropic API key configured — Claude Code on a subscription may bypass the proxy.");
|
|
147
|
+
console.warn("Run `solrouter setup` and paste an API key so requests route through the proxy.\n");
|
|
148
|
+
}
|
|
120
149
|
console.log(`\nLaunching ${tool} through the proxy (ANTHROPIC_BASE_URL=http://127.0.0.1:${cfg.port})…\n`);
|
|
121
150
|
const child = spawn(tool, passThru, {
|
|
122
151
|
stdio: "inherit",
|
|
@@ -124,6 +153,8 @@ async function main() {
|
|
|
124
153
|
...process.env,
|
|
125
154
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${cfg.port}`,
|
|
126
155
|
OPENAI_BASE_URL: `http://127.0.0.1:${cfg.port}/v1`,
|
|
156
|
+
ANTHROPIC_API_KEY: childKey, // forces Claude Code to use the base URL
|
|
157
|
+
CLAUDE_CODE_USE_BEDROCK: "",
|
|
127
158
|
},
|
|
128
159
|
});
|
|
129
160
|
child.on("error", e => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solrouter",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Local privacy router for coding tools: pseudonymises every request before it leaves your machine, and sends work a frontier model would refuse to an uncensored open-weight model you control.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server.js
CHANGED
|
@@ -15,6 +15,12 @@ import { dialect, rewriteText } from "./wire.js";
|
|
|
15
15
|
import { isRefusal } from "./route.js";
|
|
16
16
|
import { writeReceipt } from "./receipt.js";
|
|
17
17
|
import { HOME_DIR } from "./config.js";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
let VERSION = "?";
|
|
22
|
+
try { VERSION = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version; } catch {}
|
|
23
|
+
export { VERSION };
|
|
18
24
|
|
|
19
25
|
// Ground-truth log of every streaming routing decision, so a real Claude Code run
|
|
20
26
|
// can be diagnosed from actual traffic instead of guesses. Never contains raw PII
|
|
@@ -64,7 +70,7 @@ export function startServer(cfg) {
|
|
|
64
70
|
const server = createServer(async (req, res) => {
|
|
65
71
|
const send = (code, obj) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
66
72
|
|
|
67
|
-
if (req.method === "GET" && req.url === "/health") return send(200, { ok: true, proxy: "solrouter", mode: cfg.mode });
|
|
73
|
+
if (req.method === "GET" && req.url === "/health") return send(200, { ok: true, proxy: "solrouter", version: VERSION, mode: cfg.mode });
|
|
68
74
|
|
|
69
75
|
// Transparent passthrough for other Anthropic endpoints the client probes —
|
|
70
76
|
// notably /v1/messages/count_tokens, which Claude Code calls before every
|
|
@@ -184,7 +190,7 @@ export function startServer(cfg) {
|
|
|
184
190
|
}
|
|
185
191
|
});
|
|
186
192
|
server.listen(cfg.port, "127.0.0.1", () => {
|
|
187
|
-
console.log(`SolRouter proxy on http://127.0.0.1:${cfg.port}`);
|
|
193
|
+
console.log(`SolRouter proxy v${VERSION} on http://127.0.0.1:${cfg.port}`);
|
|
188
194
|
console.log(` mode=${cfg.mode} frontier=${cfg.frontier.model || "(client's model)"} [${cfg.frontier.auth || "passthrough"}] private=${cfg.private.model}`);
|
|
189
195
|
console.log(` point your tool's base URL here:`);
|
|
190
196
|
console.log(` OpenAI-style: http://127.0.0.1:${cfg.port}/v1`);
|