solrouter 0.3.3 → 0.3.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/package.json +1 -1
- package/src/providers.js +27 -9
- package/src/server.js +24 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solrouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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/providers.js
CHANGED
|
@@ -25,9 +25,18 @@ const DROP = new Set([
|
|
|
25
25
|
function authHeaders(leg, clientHeaders = {}) {
|
|
26
26
|
const h = { "content-type": "application/json" };
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// ALWAYS forward the client's non-auth headers first — especially every
|
|
29
|
+
// `anthropic-beta` flag. Claude Code enables body fields like `context_management`
|
|
30
|
+
// and 1M context via those headers; dropping them makes Anthropic reject the body
|
|
31
|
+
// ("Extra inputs are not permitted"). We overwrite only the auth header below.
|
|
32
|
+
for (const k of Object.keys(clientHeaders)) {
|
|
33
|
+
if (DROP.has(k) || k === "authorization" || k === "x-api-key") continue;
|
|
34
|
+
h[k] = clientHeaders[k];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. explicit key — set auth, keep the forwarded beta headers
|
|
29
38
|
if (leg.apiKey) {
|
|
30
|
-
if (leg.dialect === "anthropic") { h["x-api-key"] = leg.apiKey; h["anthropic-version"]
|
|
39
|
+
if (leg.dialect === "anthropic") { h["x-api-key"] = leg.apiKey; h["anthropic-version"] ||= "2023-06-01"; }
|
|
31
40
|
else h["authorization"] = `Bearer ${leg.apiKey}`;
|
|
32
41
|
return h;
|
|
33
42
|
}
|
|
@@ -39,15 +48,10 @@ function authHeaders(leg, clientHeaders = {}) {
|
|
|
39
48
|
return h;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
// 2. pass-through (default): forward
|
|
51
|
+
// 2. pass-through (default): forward the client's own auth too
|
|
43
52
|
let forwarded = false;
|
|
44
|
-
for (const k of
|
|
45
|
-
if (DROP.has(k)) continue;
|
|
46
|
-
h[k] = clientHeaders[k];
|
|
47
|
-
if (k === "authorization" || k === "x-api-key") forwarded = true;
|
|
48
|
-
}
|
|
53
|
+
for (const k of ["authorization", "x-api-key"]) if (clientHeaders[k]) { h[k] = clientHeaders[k]; forwarded = true; }
|
|
49
54
|
if (!forwarded) {
|
|
50
|
-
// nothing to forward — fall back to the account token if we have one
|
|
51
55
|
const auth = loadAuth();
|
|
52
56
|
if (auth.access_token) h["authorization"] = `Bearer ${auth.access_token}`;
|
|
53
57
|
}
|
|
@@ -107,6 +111,20 @@ export async function callSolrouterAgent(leg, prompt) {
|
|
|
107
111
|
return { ok: res.ok && json?.success !== false, status: res.status, reply: json?.reply ?? "", raw: json };
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Transparent passthrough to the frontier for endpoints we don't transform —
|
|
116
|
+
* e.g. Claude Code's /v1/messages/count_tokens preflight. Returns the raw body so
|
|
117
|
+
* the response is relayed verbatim.
|
|
118
|
+
*/
|
|
119
|
+
export async function forward(leg, { method = "POST", path, body, clientHeaders }) {
|
|
120
|
+
const base = leg.baseUrl.replace(/\/$/, "");
|
|
121
|
+
const init = { method, headers: authHeaders(leg, clientHeaders) };
|
|
122
|
+
if (body !== undefined) init.body = JSON.stringify(withModel(body, leg));
|
|
123
|
+
const res = await fetch(base + path, init);
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
return { ok: res.ok, status: res.status, text, contentType: res.headers.get("content-type") };
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
/** Streaming call. Returns { ok, status, stream } (raw SSE ReadableStream). */
|
|
111
129
|
export async function streamLeg(leg, body, clientHeaders) {
|
|
112
130
|
const res = await fetch(endpoint(leg), {
|
package/src/server.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// localhost only — nothing here should be reachable off the machine.
|
|
8
8
|
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
-
import { handle, handleStream } from "./index.js";
|
|
10
|
+
import { handle, handleStream, createMasker } from "./index.js";
|
|
11
|
+
import { forward } from "./providers.js";
|
|
12
|
+
import { dialect, rewriteText } from "./wire.js";
|
|
11
13
|
import { writeReceipt } from "./receipt.js";
|
|
12
14
|
|
|
13
15
|
function readBody(req) {
|
|
@@ -26,6 +28,27 @@ export function startServer(cfg) {
|
|
|
26
28
|
const send = (code, obj) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
27
29
|
|
|
28
30
|
if (req.method === "GET" && req.url === "/health") return send(200, { ok: true, proxy: "solrouter", mode: cfg.mode });
|
|
31
|
+
|
|
32
|
+
// Transparent passthrough for other Anthropic endpoints the client probes —
|
|
33
|
+
// notably /v1/messages/count_tokens, which Claude Code calls before every
|
|
34
|
+
// request and treats a 404 on as "model unavailable". Forward to the frontier.
|
|
35
|
+
if (/^\/v1\//.test(req.url) && !/^\/(v1\/chat\/completions|v1\/messages)$/.test(req.url)) {
|
|
36
|
+
let body;
|
|
37
|
+
if (req.method === "POST") {
|
|
38
|
+
try { body = await readBody(req); } catch { return send(400, { error: "invalid JSON body" }); }
|
|
39
|
+
if (body && cfg.masking?.enabled !== false) {
|
|
40
|
+
const m = createMasker(cfg.masking);
|
|
41
|
+
body = rewriteText(body, dialect(body, req.url), s => m.mask(s).text);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const r = await forward(cfg.frontier, { method: req.method, path: req.url, body, clientHeaders: lower(req.headers) });
|
|
46
|
+
res.writeHead(r.status, { "content-type": r.contentType || "application/json" });
|
|
47
|
+
res.end(r.text);
|
|
48
|
+
} catch (e) { send(502, { error: "proxy error", detail: String(e?.message || e) }); }
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
if (req.method !== "POST" || !/^\/(v1\/chat\/completions|v1\/messages)$/.test(req.url)) {
|
|
30
53
|
return send(404, { error: "not found" });
|
|
31
54
|
}
|