Watch a language model think, layer by layer. Then steer it. All in your browser.
Steerscope reads out what a real instruct model predicts at every one of its layers, so you can see the exact moment it makes up its mind, then lets you drag a slider to push a behaviour (refusal, sentiment, formality) into its residual stream and watch the sentence rewrite itself. The logit lens and the steering vectors are built from scratch and checked against a reference in CI. Generation runs on your own GPU through WebGPU, so there is no inference server and no per-visit cost.
Most "AI portfolio" projects wrap an API. This one opens the model up.
There is good prior art, but no single tool combines all five of these. This is the gap Steerscope fills.
| live gen of a real instruct model | from-scratch per-layer logit lens | steering sliders | 100% client side | consumer UX | |
|---|---|---|---|---|---|
| Transformer Explainer | yes (base GPT-2) | no | no | yes | yes |
| bbycroft/llm-viz | no (static 3D) | no | no | yes | yes |
| Neuronpedia | yes | no (SAE graphs) | yes | no (server) | expert |
| Anthropic circuit-tracer | yes | no (attribution) | yes | no (server) | expert |
| tuned-lens / TransformerLens | no (Python) | yes | partial | no | no |
| Steerscope | yes | yes | yes | yes | yes |
Base GPT-2 has no refusal or sycophancy behaviour to steer, which is why Steerscope runs a real instruct model (Qwen2.5-0.5B-Instruct, Apache-2.0).
flowchart LR
P[Your prompt] --> T[Tokenizer + chat template]
T --> M[Qwen2.5-0.5B-Instruct on WebGPU]
M -->|residual stream, every layer| L[Logit lens]
L --> Ladder[Per-layer prediction ladder]
S[Steering sliders] -->|CAA vector| INJ[Add into residual stream at layer k]
INJ --> M
M -->|next token| Stream[Continuation]
At every layer the model holds a running guess of the next token in its residual stream. The logit lens reads that guess out directly by applying the model's final normalisation and unembedding to an intermediate layer's hidden state:
logits_L = FinalRMSNorm(h_L) @ W_U^T
The detail that matters: you apply the model's single final RMSNorm (not a
per-block norm), and because Qwen ties its embeddings, the unembedding W_U is
just the embedding matrix. Skip the final norm and the numbers are garbage. The
ladder shows the top prediction at each layer and marks the decision layer,
the shallowest layer whose guess already equals the final answer. Early layers are
faded because the raw logit lens is genuinely unreliable there.
For each behaviour we compute a direction offline as the mean difference in residual activations between contrastive prompt pairs (harmful vs harmless for refusal, positive vs negative for sentiment, and so on). This is Contrastive Activation Addition. At inference we add a scaled copy of that unit direction into the residual stream at a single mid-depth layer:
h_k -> h_k + alpha * residualNorm_k * v_behaviour
Scaling by the layer's residual norm (norm matching) makes the coefficient mean the same thing everywhere, which keeps a small model coherent while it is being steered. A coefficient of zero is a provable no-op. Push past the usable band and the UI marks the "incoherence zone" where the output degrades, which is honest about how steering actually behaves.
flowchart TB
subgraph Worker[Single Web Worker]
direction TB
Tok[transformers.js tokenizer]
A[Track A: transformers.js generate on stock q4f16]
B[Track B: instrumented ONNX graph via onnxruntime-web]
Tok --> A
Tok --> B
end
A -->|fast mode, day-one baseline| UI
B -->|per-layer lens + steering input| UI[React visualisations]
- Track A is the proven baseline and permanent fast mode: stock
onnx-community/Qwen2.5-0.5B-Instructq4f16 (~483 MB) through the standard transformers.js pipeline. It gives real, live generation immediately. - Track B is the wow: one custom instrumented ONNX graph that emits the per-layer logit-lens top-k in-graph (reusing the tied unembedding, so nothing extra is downloaded) and accepts a steering vector added into the residual stream at decode positions only, so re-steering skips the prompt prefill and stays live.
The instrumented graph is produced by pipeline/export_model.py
and validated end to end: it exposes steering_vec and steering_scale inputs
and lens_ids/lens_probs [1, 25, 10] outputs, and steering_scale = 0 is a
bit-exact no-op (see pipeline/tests/test_onnx_export.py). The remaining work is
loading it in the browser worker, not building it.
A reviewer's first question is "how much of this did you actually implement?" Here is the load-bearing engineering and exactly how each piece is tested.
| From scratch | Where | How it is proven |
|---|---|---|
| Logit-lens math (RMSNorm, tied unembed, softmax, top-k) | lib/inference/logitLensCpu.ts |
Parity test against a numpy reference to 1e-9, and against real model activations |
| CAA steering composition, norm-matched, provable no-op | lib/inference/steering.ts |
Unit tests: exact zero no-op, monotonic magnitude, sign flip, dimensional guards |
| Instrumented ONNX export (layer taps + steering input + in-graph lens) | pipeline/export_model.py |
onnx.checker + inspectable in Netron |
| CAA direction extraction | pipeline/compute_steering.py |
numpy tests on the mean-difference recipe |
| Colourblind-safe heatmap ramp | lib/viz/heat.ts |
Unit tests for monotonicity and clamping |
The math is implemented twice on purpose: once in numpy (the pipeline reference) and once in TypeScript (the browser). The tests assert the two agree. Two independent implementations converging is the strongest evidence the math is right, not just plausible.
npm test # TypeScript parity + steering + heatmap tests
cd pipeline && pytest tests -q # numpy reference tests (no torch needed)Steering is not free. pipeline/eval_steering.py sweeps the refusal coefficient
over held-out benign prompts and measures the effect. The result below is real,
run on Qwen2.5-0.5B-Instruct, not an illustration.
Two honest readings of this curve:
- The coherence cliff is clear. Mean perplexity climbs from about 13 at coefficient 0 to well over 300 by coefficient +2, which is exactly why the UI marks an "incoherence zone". The steering vector is having a large, measurable effect on the model.
- Inducing refusal on already-benign prompts is hard on a 0.5B model. With this quick difference-of-means recipe at layer 10, positive steering mostly degrades coherence rather than cleanly triggering refusals. The literature (Arditi et al.) selects the direction's layer and token position on a validation set, and reports that suppressing refusal is easier than inducing it. Wiring in that selection is the documented next step, and this is the kind of result that only shows up when you actually measure instead of asserting.
The load-bearing engineering is real and tested right now. The live demo drives the ladder and sliders with a clearly-labelled preview engine while the instrumented model is wired into the same interface. Nothing here is presented as measured that has not been measured.
- Done and tested: the from-scratch logit lens (checked against the real
Qwen model at every layer, see
lib/inference/__tests__/realLens.test.ts), the steering composition, the interactive ladder and sliders, WebGPU detection, accessibility, and the full offline pipeline. - Real artifacts produced and validated by the pipeline: golden fixtures from
the real model weights (
test/fixtures/real_lens_fixture.json), Contrastive Activation Addition steering vectors (pipeline/artifacts/steering.json), the measured refusal/perplexity curve above, and the instrumented ONNX graph whose I/O and provable no-op are checked inpipeline/tests/test_onnx_export.py. - Wiring in: loading that instrumented graph in the browser worker so the ladder reads the real model's layers live in the page. The graph exists and is tested; this is a last-mile integration, not new research.
npm install
npm run dev # http://localhost:3000
npm run verify # typecheck + lint + test + buildThe core demo needs no environment variables and no server. The optional "explain
this in plain English" narrator is off by default and bring-your-own-key; see
.env.example.
app/ Next.js 16 App Router pages, the OG image
components/ Hero, the demo, the visualisations
lib/inference/ engine interface, logit lens, steering, backend detection
lib/viz/ heatmap colour ramp
pipeline/ offline: ONNX export, steering vectors, fixtures, eval
test/fixtures/ committed golden fixtures for the parity tests
- Logit lens: nostalgebraist, "interpreting GPT: the logit lens" (2020).
- Tuned lens: Belrose et al., "Eliciting Latent Predictions from Transformers with the Tuned Lens" (2023).
- Steering: Rimsky et al., "Steering Llama 2 via Contrastive Activation Addition" (2023).
- Refusal direction: Arditi et al., "Refusal in Language Models Is Mediated by a Single Direction" (2024). Note that later work argues refusal is not always a single direction; Steerscope treats the single-direction result as a strong effect, not a universal law.
Source code is MIT (see LICENSE). The model weights derived by the pipeline are Apache-2.0 (Qwen2.5); see NOTICE for attribution and dataset provenance.
Built by Harry Miller.
