solrouter 0.2.1 → 0.2.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/bin/proxy.js +6 -0
- package/package.json +1 -1
- package/src/banner.js +88 -0
- package/src/providers.js +26 -6
package/bin/proxy.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solrouter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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/banner.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// banner.js — the terminal splash, rendered as real pixels using truecolor
|
|
2
|
+
// half-block characters (▀). Each character cell packs two vertical pixels (top =
|
|
3
|
+
// foreground colour, bottom = background), so the art is twice as tall as the grid.
|
|
4
|
+
// Transparent cells fall through to the terminal's own background.
|
|
5
|
+
//
|
|
6
|
+
// The cat wears a hoodie with the wave mark rendered in pixels. On image-capable
|
|
7
|
+
// terminals we could show the literal PNG instead; this is the universal fallback.
|
|
8
|
+
|
|
9
|
+
const C = {
|
|
10
|
+
bg: "#0a0908", cream: "#F5E6D3", out: "#050403", eye: "#0a0908",
|
|
11
|
+
nose: "#c98f7f", hood: "#15130f", accent: "#F5E6D3", muted: "#b5ac9f", faint: "#78746b",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// 26-wide sprite. .=transparent o=outline b=body e=eye n=nose p=ear-pink H=hoodie v=logo-wave
|
|
15
|
+
const GRID = [
|
|
16
|
+
"...oo................oo....",
|
|
17
|
+
"..obbo..............obbo..",
|
|
18
|
+
"..obpbo............obpbo..",
|
|
19
|
+
"..obbbbo..........obbbbo..",
|
|
20
|
+
"...obbbboooooooooobbbbo...",
|
|
21
|
+
"....obbbbbbbbbbbbbbbbo....",
|
|
22
|
+
"...obbbbbbbbbbbbbbbbbbo...",
|
|
23
|
+
"..obbbbbbbbbbbbbbbbbbbbo..",
|
|
24
|
+
"..obbbeebbbbbbbbeebbbbbo..",
|
|
25
|
+
"..obbbeebbbbbbbbeebbbbbo..",
|
|
26
|
+
"..obbbbbbbbbnnbbbbbbbbbo..",
|
|
27
|
+
"..obbbbbbbbbnnbbbbbbbbbo..",
|
|
28
|
+
"...obbbbbbbbbbbbbbbbbbo...",
|
|
29
|
+
"....obbbbbbbbbbbbbbbbo....",
|
|
30
|
+
"...oHHHHHHHHHHHHHHHHHHo...",
|
|
31
|
+
"..oHHHHHHHHHHHHHHHHHHHHo..",
|
|
32
|
+
"..oHHHvvHHvvHHvvHHvvHHHo..",
|
|
33
|
+
"..oHHvHHvvHHvvHHvvHHvHHo..",
|
|
34
|
+
"..oHHHHHHHHHHHHHHHHHHHHo..",
|
|
35
|
+
"..oHHHvvHHvvHHvvHHvvHHHo..",
|
|
36
|
+
"..oHHvHHvvHHvvHHvvHHvHHo..",
|
|
37
|
+
"..obbHHHHHHHHHHHHHHHHbbo..",
|
|
38
|
+
"..obbboHHHHHHHHHHHHobbbo..",
|
|
39
|
+
"...oooo.oooooooooo.oooo...",
|
|
40
|
+
];
|
|
41
|
+
const CMAP = { o: C.out, b: C.cream, e: C.eye, n: C.nose, p: C.nose, H: C.hood, v: C.cream };
|
|
42
|
+
|
|
43
|
+
const rgb = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
|
|
44
|
+
const fg = (h) => { const [r, g, b] = rgb(h); return `\x1b[38;2;${r};${g};${b}m`; };
|
|
45
|
+
const bgc = (h) => { const [r, g, b] = rgb(h); return `\x1b[48;2;${r};${g};${b}m`; };
|
|
46
|
+
const RESET = "\x1b[0m";
|
|
47
|
+
const DIM = (h, t) => fg(h) + t + RESET;
|
|
48
|
+
|
|
49
|
+
/** Render the sprite to an array of ANSI lines (indented `pad` spaces). */
|
|
50
|
+
function sprite(pad = 0) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
for (let y = 0; y < GRID.length; y += 2) {
|
|
53
|
+
let line = " ".repeat(pad);
|
|
54
|
+
for (let x = 0; x < GRID[y].length; x++) {
|
|
55
|
+
const top = CMAP[GRID[y][x]];
|
|
56
|
+
const bot = GRID[y + 1] ? CMAP[GRID[y + 1][x]] : undefined;
|
|
57
|
+
if (top && bot) line += fg(top) + bgc(bot) + "▀" + RESET;
|
|
58
|
+
else if (top) line += fg(top) + "▀" + RESET; // bottom transparent
|
|
59
|
+
else if (bot) line += fg(bot) + "▄" + RESET; // top transparent
|
|
60
|
+
else line += " ";
|
|
61
|
+
}
|
|
62
|
+
lines.push(line);
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The full splash string. */
|
|
68
|
+
export function banner({ menu = true } = {}) {
|
|
69
|
+
const art = sprite(4);
|
|
70
|
+
const L = [];
|
|
71
|
+
L.push("");
|
|
72
|
+
L.push(" " + fg(C.accent) + "≈≈" + RESET + " " + fg(C.accent) + "\x1b[1mSOLROUTER" + RESET);
|
|
73
|
+
L.push(" " + DIM(C.muted, "private routing for your coding agent"));
|
|
74
|
+
L.push("");
|
|
75
|
+
art.forEach(a => L.push(a));
|
|
76
|
+
L.push(" " + DIM(C.faint, "the router, in the hoodie"));
|
|
77
|
+
if (menu) {
|
|
78
|
+
L.push("");
|
|
79
|
+
L.push(" " + DIM(C.cream, "How should the frontier model be paid?"));
|
|
80
|
+
L.push(" " + fg(C.accent) + "▸ SolRouter subscription" + RESET + DIM(C.faint, " — just sign in"));
|
|
81
|
+
L.push(" " + DIM(C.faint, " My own API key — paste once"));
|
|
82
|
+
L.push(" " + DIM(C.faint, " ↑↓ move ↵ select esc quit"));
|
|
83
|
+
}
|
|
84
|
+
L.push("");
|
|
85
|
+
return L.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default { banner };
|
package/src/providers.js
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import { loadAuth } from "./config.js";
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// In pass-through mode we forward the client's headers VERBATIM (auth, every
|
|
17
|
+
// anthropic-beta flag, oauth markers, user-agent), dropping only hop-by-hop headers
|
|
18
|
+
// that must be recomputed for the new destination. An allowlist silently dropped
|
|
19
|
+
// headers Claude Code needs (beta flags, oauth), which broke subscription auth.
|
|
20
|
+
const DROP = new Set([
|
|
21
|
+
"host", "connection", "content-length", "transfer-encoding",
|
|
22
|
+
"accept-encoding", "keep-alive", "proxy-connection",
|
|
23
|
+
]);
|
|
18
24
|
|
|
19
25
|
function authHeaders(leg, clientHeaders = {}) {
|
|
20
26
|
const h = { "content-type": "application/json" };
|
|
@@ -33,9 +39,13 @@ function authHeaders(leg, clientHeaders = {}) {
|
|
|
33
39
|
return h;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
// 2. pass-through (default): forward
|
|
42
|
+
// 2. pass-through (default): forward every client header verbatim
|
|
37
43
|
let forwarded = false;
|
|
38
|
-
for (const k of
|
|
44
|
+
for (const k of Object.keys(clientHeaders)) {
|
|
45
|
+
if (DROP.has(k)) continue;
|
|
46
|
+
h[k] = clientHeaders[k];
|
|
47
|
+
if (k === "authorization" || k === "x-api-key") forwarded = true;
|
|
48
|
+
}
|
|
39
49
|
if (!forwarded) {
|
|
40
50
|
// nothing to forward — fall back to the account token if we have one
|
|
41
51
|
const auth = loadAuth();
|
|
@@ -50,9 +60,19 @@ function endpoint(leg) {
|
|
|
50
60
|
return leg.dialect === "anthropic" ? `${base}/v1/messages` : `${base}/v1/chat/completions`;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
// Claude Code sends its model as e.g. "claude-opus-5[1m]" — the "[1m]" is its
|
|
64
|
+
// 1M-context MARKER, not part of the API model id (the real id is "claude-opus-5",
|
|
65
|
+
// and the 1M context is negotiated via the forwarded anthropic-beta header). The
|
|
66
|
+
// raw API rejects the brackets, so strip any trailing "[...]" before forwarding.
|
|
67
|
+
export function normalizeModel(m) {
|
|
68
|
+
return typeof m === "string" ? m.replace(/\s*\[[^\]]*\]\s*$/, "") : m;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Force the request body onto the leg's chosen model, else forward the client's (normalized). */
|
|
54
72
|
function withModel(body, leg) {
|
|
55
|
-
|
|
73
|
+
if (leg.model) return { ...body, model: leg.model };
|
|
74
|
+
if (body && typeof body.model === "string") return { ...body, model: normalizeModel(body.model) };
|
|
75
|
+
return body;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
/** Non-streaming call. Returns { ok, status, json }. */
|