GhostSwitch / index.html
danilkho's picture
Upload 4 files
1e95750 verified
Raw History Blame Contribute Delete
15 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GhostSwitch</title>
<meta name="description" content="A local AI assistant: one Qwen3.5 model plus a library of LoRA experts, picked for each message and checked in code. Runs on a 16 GB laptop.">
<style>
:root {
--bg: #0b0d16;
--panel: #12152290;
--panel-solid: #121522;
--line: #232843;
--text: #e8eaf6;
--muted: #9aa1c2;
--accent: #8b9cff;
--teal: #4fd1c5;
--good: #5ee08a;
--warn: #f5b65b;
--bad: #ff7b8a;
--grad: linear-gradient(90deg, #b9c3ff, #8b9cff 38%, #4fd1c5 72%, #5ee08a);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; background: var(--bg); color: var(--text);
font: 16px/1.6 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
background-image: radial-gradient(900px 500px at 15% -10%, #1d2350 0%, transparent 60%),
radial-gradient(700px 500px at 100% 30%, #0f2f2d 0%, transparent 55%);
background-repeat: no-repeat;
}
a { color: var(--accent); }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 16px; }
header { padding: 56px 0 24px; }
.brand { display: flex; align-items: center; gap: 14px; }
.brand svg { width: 52px; height: 52px; flex: none; }
.brand h1 { margin: 0; font-size: clamp(32px, 6vw, 48px); letter-spacing: -0.02em; line-height: 1.1; }
.badge { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--warn);
border: 1px solid #f5b65b55; background: #f5b65b14; border-radius: 999px; padding: 3px 10px; }
.tagline { margin: 20px 0 8px; font-size: clamp(26px, 5vw, 40px); font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; }
.tagline span { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
.lead { max-width: 720px; color: var(--muted); font-size: 18px; margin: 0 0 24px; }
.buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 10px; font-weight: 600;
text-decoration: none; border: 1px solid var(--line); color: var(--text); background: var(--panel-solid); }
.btn.primary { background: var(--text); color: #0b0d16; border-color: var(--text); }
.btn:hover { border-color: var(--accent); }
.btn:focus-visible, button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
figure { margin: 32px 0 0; }
figure img { width: 100%; height: auto; display: block; border-radius: 14px; border: 1px solid var(--line); }
figcaption { color: var(--muted); font-size: 14px; margin-top: 10px; text-align: center; }
section { padding: 48px 0 8px; }
h2 { font-size: clamp(22px, 4vw, 30px); letter-spacing: -0.01em; margin: 0 0 8px; }
.sub { color: var(--muted); margin: 0 0 24px; max-width: 760px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px; }
.card h3 { margin: 0 0 6px; font-size: 17px; }
.card p { margin: 0; color: var(--muted); font-size: 15px; }
.step { font-size: 13px; font-weight: 700; color: var(--teal); letter-spacing: .06em; }
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); }
table { border-collapse: collapse; width: 100%; min-width: 640px; font-size: 15px; }
th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--line); white-space: nowrap; }
th { color: var(--muted); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; }
tr:last-child td { border-bottom: 0; }
td.num { font-variant-numeric: tabular-nums; text-align: right; }
th.num { text-align: right; }
.win { color: var(--good); font-weight: 700; }
.pill { display: inline-block; font-size: 12px; font-weight: 700; border-radius: 999px; padding: 2px 9px; }
.pill.ok { color: var(--good); background: #5ee08a1a; }
.pill.lean { color: var(--teal); background: #4fd1c51a; }
.pill.wait { color: var(--warn); background: #f5b65b1a; }
.pill.no { color: var(--bad); background: #ff7b8a1a; }
.note { color: var(--muted); font-size: 14px; margin-top: 12px; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.cmd { position: relative; background: #070910; border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin: 8px 0 18px; }
.cmd pre { margin: 0; overflow-x: auto; font-size: 14px; padding-right: 64px; }
.cmd button { position: absolute; top: 10px; right: 10px; background: var(--panel-solid); color: var(--text);
border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; font: inherit; font-size: 13px; cursor: pointer; }
.label { font-weight: 600; font-size: 15px; }
.warning { border-left: 3px solid var(--warn); background: #f5b65b0d; padding: 14px 16px; border-radius: 0 12px 12px 0; color: var(--muted); }
.warning b { color: var(--text); }
footer { padding: 48px 0 56px; color: var(--muted); font-size: 14px; }
footer p { margin: 6px 0; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="brand">
<svg viewBox="-14 -14 148 148" aria-hidden="true"><rect x="-14" y="-14" width="148" height="148" rx="34" fill="#0d0f19"/><defs><linearGradient id="g" x1="14" y1="14" x2="106" y2="106" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b9c3ff"/><stop offset=".38" stop-color="#8b9cff"/><stop offset=".72" stop-color="#4fd1c5"/><stop offset="1" stop-color="#5ee08a"/></linearGradient></defs><circle cx="60" cy="60" r="42" fill="none" stroke="url(#g)" stroke-width="5" opacity=".5"/><line x1="60" y1="60" x2="92.17" y2="33" stroke="url(#g)" stroke-width="6" stroke-linecap="round"/><circle cx="87" cy="92.17" r="6" fill="url(#g)"/><circle cx="33" cy="92.17" r="6" fill="url(#g)"/><circle cx="20.53" cy="45.64" r="6" fill="url(#g)"/><circle cx="56.34" cy="18.16" r="6" fill="url(#g)"/><circle cx="60" cy="60" r="17" fill="url(#g)"/><circle cx="92.17" cy="33" r="16" fill="url(#g)" opacity=".22"/><circle cx="92.17" cy="33" r="11" fill="url(#g)"/></svg>
<h1>GhostSwitch</h1>
<span class="badge">Experiment</span>
</div>
<p class="tagline">Small model. <span>Expert answers.</span></p>
<p class="lead">An AI assistant that runs entirely on your computer. It keeps one Qwen3.5 model in memory and, for each message, attaches the LoRA expert that fits: code, finance, law, math, Verilog, CAD and two dozen more. Then plain code checks the answer before you see it.</p>
<div class="buttons">
<a class="btn primary" href="https://github.com/hiddenway/GhostSwitch">View on GitHub</a>
<a class="btn" href="#install">Install</a>
<a class="btn" href="#results">Benchmark results</a>
</div>
<figure>
<img src="screenshot-chat.png" width="2000" height="1389" alt="The GhostSwitch chat: a Python question answered by the coding expert, with the response details panel open">
<figcaption>A coding question routed to coding_expert. Every answer shows its route, speed, tokens and checks.</figcaption>
</figure>
</header>
<section>
<h2>How it works</h2>
<p class="sub">One base model stays loaded. A 30–330 MB adapter is attached only for the request that needs it, so a library of one expert or a thousand needs the same memory: the base model plus at most 512 MB of recently used adapters.</p>
<div class="grid">
<div class="card"><div class="step">1 · ROUTE</div><h3>Pick the expert</h3><p>Short model calls walk a tree of fields and choose among the experts you turned on, or the plain base model.</p></div>
<div class="card"><div class="step">2 · ATTACH</div><h3>Swap the adapter</h3><p>The LoRA goes onto the model already in memory in 0.1–0.3 s. No reload.</p></div>
<div class="card"><div class="step">3 · GENERATE</div><h3>Answer</h3><p>Optionally with web search and cited sources, and step-by-step thinking when the task needs it.</p></div>
<div class="card"><div class="step">4 · CHECK</div><h3>Verify in code</h3><p>Arithmetic, JSON fields and citations are checked by code, not by another model. A failed check means a retry.</p></div>
</div>
<figure>
<img src="how-it-works.png" width="2000" height="1159" alt="Request, route, attach the expert, generate, check: accept, retry with the same expert up to four times, or fail">
</figure>
</section>
<section id="results">
<h2>Measured, not assumed</h2>
<p class="sub">An adapter that sounds good on its model card does not always beat the model it was trained on. 13 of the 9B experts each got 60 questions from a public benchmark of their field, and the bare base model got the same 60. Experts that are no better are blocked from routing.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Expert</th><th>Benchmark</th><th class="num">Base</th><th class="num">Expert</th><th class="num">p</th><th>Verdict</th></tr></thead>
<tbody>
<tr><td>coding</td><td>HumanEval+</td><td class="num">76.7%</td><td class="num win">88.3%</td><td class="num">0.039</td><td><span class="pill ok">Verified</span></td></tr>
<tr><td>legal contracts</td><td>LegalBench CUAD</td><td class="num">76.7%</td><td class="num">83.3%</td><td class="num">0.125</td><td><span class="pill lean">Leaning better</span></td></tr>
<tr><td>privacy / PII</td><td>Gretel PII, F1</td><td class="num">75.3%</td><td class="num">78.3%</td><td class="num">0.180</td><td><span class="pill lean">Leaning better</span></td></tr>
<tr><td>software engineering</td><td>HumanEval+</td><td class="num">76.7%</td><td class="num">81.7%</td><td class="num">0.508</td><td><span class="pill wait">Undecided</span></td></tr>
<tr><td>finance</td><td>FinQA</td><td class="num">61.7%</td><td class="num">66.7%</td><td class="num">0.549</td><td><span class="pill wait">Undecided</span></td></tr>
<tr><td>reasoning</td><td>GSM8K, no thinking</td><td class="num">88.3%</td><td class="num">90.0%</td><td class="num">1.000</td><td><span class="pill wait">Undecided</span></td></tr>
<tr><td>DeltaCoder</td><td>HumanEval+</td><td class="num">76.7%</td><td class="num">78.3%</td><td class="num">1.000</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>text-to-SQL</td><td>Gretel, run in SQLite</td><td class="num">58.3%</td><td class="num">58.3%</td><td class="num">1.000</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>medical</td><td>MedQA</td><td class="num">66.7%</td><td class="num">65.0%</td><td class="num">1.000</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>pentest</td><td>pentesting-eval</td><td class="num">88.3%</td><td class="num">86.7%</td><td class="num">1.000</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>IaC security</td><td>Terraform S3 MCQ</td><td class="num">53.3%</td><td class="num">51.7%</td><td class="num">1.000</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>entity extraction</td><td>CoNLL-2003, F1</td><td class="num">67.4%</td><td class="num">67.2%</td><td class="num">0.720</td><td><span class="pill no">Blocked</span></td></tr>
<tr><td>CWE classification</td><td>CTI-Bench</td><td class="num">56.7%</td><td class="num">51.7%</td><td class="num">0.508</td><td><span class="pill no">Blocked</span></td></tr>
</tbody>
</table>
</div>
<p class="note">Qwen3.5-9B Q4_K_M, answer checks and thinking off for both sides, paired sign test. 60 questions detect differences of about 10 points, so “undecided” means undecided. Every answer is in the <a href="https://huggingface.co/datasets/danilkho/ghostswitch-expert-evals">evaluation dataset</a>.</p>
</section>
<section id="install">
<h2>Install</h2>
<p class="sub">One command installs GhostSwitch and opens it in your browser. It brings its own Python and needs no administrator rights. On first run it checks your hardware, recommends Qwen3.5-9B or 4B and downloads it.</p>
<div class="label">macOS (Apple Silicon) and Linux</div>
<div class="cmd"><pre><code>curl -fsSL https://raw.githubusercontent.com/hiddenway/GhostSwitch/main/install.sh | sh</code></pre><button type="button" data-copy>Copy</button></div>
<div class="label">Windows 10 or 11, in PowerShell</div>
<div class="cmd"><pre><code>irm https://raw.githubusercontent.com/hiddenway/GhostSwitch/main/install.ps1 | iex</code></pre><button type="button" data-copy>Copy</button></div>
<div class="grid">
<div class="card"><h3>16 GB of memory</h3><p>Qwen3.5-9B with 27 experts. 8 GB is enough for the 4B model with 6 experts.</p></div>
<div class="card"><h3>MLX or llama.cpp</h3><p>MLX on Apple Silicon; llama.cpp with CUDA on NVIDIA, or on the CPU, on Linux and Windows.</p></div>
<div class="card"><h3>OpenAI-compatible API</h3><p>Streaming, function tools, images and files at <code>/v1/chat/completions</code>. Works as a model in OpenCode.</p></div>
<div class="card"><h3>Private by default</h3><p>Chats stay on your computer. It goes online only to download models and experts, and for web search if you turn it on.</p></div>
</div>
</section>
<section>
<div class="warning"><b>GhostSwitch is an experiment.</b> So far one expert is measurably better than the base model, routing is slow and sometimes picks the wrong expert, and the API may change. On an M1 Pro the 9B model writes 13–18 tokens per second, so a full answer with routing and thinking takes about a minute. Do not rely on its answers for medical, legal, financial or security decisions.</div>
</section>
<footer>
<p>Apache 2.0 · Built on <a href="https://huggingface.co/Qwen">Qwen3.5</a>, <a href="https://github.com/ml-explore/mlx">MLX</a> and <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a>.</p>
<p>The experts are LoRA adapters published on Hugging Face by their authors; GhostSwitch only packages and routes their work. <a href="https://huggingface.co/collections/danilkho/ghostswitch-experts-6acbaac9eb7016a886bf1182">See them all in the collection.</a></p>
</footer>
</div>
<script>
document.querySelectorAll("[data-copy]").forEach(function (button) {
button.addEventListener("click", function () {
var text = button.parentElement.querySelector("code").textContent;
navigator.clipboard.writeText(text).then(function () {
button.textContent = "Copied";
setTimeout(function () { button.textContent = "Copy"; }, 1500);
}, function () { button.textContent = "Select and copy"; });
});
});
</script>
</body>
</html>