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 CHANGED
@@ -59,6 +59,12 @@ async function main() {
59
59
  return;
60
60
  }
61
61
 
62
+ case "banner": {
63
+ const { banner } = await import("../src/banner.js");
64
+ console.log(banner());
65
+ return;
66
+ }
67
+
62
68
  case "demo": {
63
69
  const ok = await runDemo();
64
70
  process.exit(ok ? 0 : 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solrouter",
3
- "version": "0.2.1",
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
- // Client auth headers worth forwarding verbatim in pass-through mode.
17
- const FORWARD = ["authorization", "x-api-key", "anthropic-version", "anthropic-beta", "openai-organization"];
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 the client's own credentials
42
+ // 2. pass-through (default): forward every client header verbatim
37
43
  let forwarded = false;
38
- for (const k of FORWARD) if (clientHeaders[k]) { h[k] = clientHeaders[k]; forwarded = true; }
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
- /** Force the request body onto the leg's chosen model (or leave the client's if unset). */
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
- return leg.model ? { ...body, model: leg.model } : body;
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 }. */