solrouter 0.2.2 → 0.2.4

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);
@@ -79,6 +85,8 @@ async function main() {
79
85
  case "claude": {
80
86
  const cfg = loadConfig({ port: flag("--port") ? Number(flag("--port")) : undefined,
81
87
  mode: flag("--mode") || undefined });
88
+ const { banner } = await import("../src/banner.js");
89
+ console.log(banner({ menu: false }));
82
90
  const { spawn } = await import("node:child_process");
83
91
  startServer(cfg); // non-blocking listener in this process
84
92
  // Pass everything after `code`/`claude` (minus our own --port/--mode) to Claude Code.
@@ -106,6 +114,8 @@ async function main() {
106
114
  default: {
107
115
  const cfg = loadConfig({ port: flag("--port") ? Number(flag("--port")) : undefined,
108
116
  mode: flag("--mode") || undefined });
117
+ const { banner } = await import("../src/banner.js");
118
+ console.log(banner({ menu: false }));
109
119
  // Pass-through supplies frontier creds per request, so login is not required
110
120
  // to start — it only unlocks the private/uncensored leg.
111
121
  if (!isLoggedIn()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solrouter",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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
@@ -60,9 +60,19 @@ function endpoint(leg) {
60
60
  return leg.dialect === "anthropic" ? `${base}/v1/messages` : `${base}/v1/chat/completions`;
61
61
  }
62
62
 
63
- /** 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). */
64
72
  function withModel(body, leg) {
65
- 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;
66
76
  }
67
77
 
68
78
  /** Non-streaming call. Returns { ok, status, json }. */