Project Info
Inspiration
AI will happily give you a confident medical answer. We have no clear way of making it interpretable because it is inherenly polysemantic and features are held in superposition (one neuron activating to multiple features). We built GlassBox to close this gap, a window into what a model is actually doing while it answers, so the nurse, doctor, or patient on the other end can tell whether to trust the answer or question it. A model can write a fluent, self-assured paragraph while, underneath, its internal signals are anything but sure. And it turns out you can read that wiring. Recent interpretability work shows that simple probes can catch internal states a model's text is hiding, from uncertainty to deception (Apollo Research's linear-probe work was a big influence here). We picked Google's Gemma 3 4B as our subject because it's open, it's small enough to actually instrument, and its middle layers are where these signals live cleanly. We also use Gemma Scope Sparse Auto Encoders to do unsupervisedb exploration of features in the model. GlassBox is what happens when you stop treating the model as a black box and start watching it think.
What it does
GlassBox sits next to a medical chatbot. You ask a clinical question, the model answers like it always would, and GlassBox shows you what was going on inside while it did. Two views, side by side with the answer: The feature map: which concepts the model actually engaged with for your question, surfaced from its internal activations. The trackers: live readings for things like uncertainty and harmfulness. An answer that reads confident with nothing firing underneath is very different from one that reads confident while its uncertainty signal is lit. Instead of a lone answer, the person in the loop gets context, enough to catch the confidently-wrong cases before they reach a patient.
How we built it
GlassBox runs as two pieces. There's a lightweight application layer that handles the chat, the views, and everything a user touches, and a separate GPU service that holds the model and does the heavy lifting. The app layer never loads the model itself; it asks the GPU service for what it needs over a simple API. Keeping those two apart means the interface stays fast and responsive while the expensive, GPU-bound work scales on its own, and either side can be worked on or restarted without dragging the other down. The feature map comes from a pretrained sparse autoencoder that turns raw activations into interpretable features. The trackers work differently: for each concept we precompute a "direction" from labeled examples ahead of time, and at answer-time we just measure the model's live activation against it. That's one cheap comparison per tracker, because the hard part already happened offline. We used Arize Phoenix* to trace every turn, so when something looks off we can see exactly what happened inside a request, and Sentry to catch failures before they quietly corrupt the signal. An interpretability tool you can't debug is just another black box.
Challenges we ran into
Our first probes scored no better than a coin flip (AUROC = 0.5) The cause was a single oversized signal in the model's internals that drowned out everything else. Normalizing for it fixed the probes, and taught us that this correction isn't optional. It has to ship with every direction. Our first probes scored no better than a coin flip (AUROC = 0.5) The cause was a single oversized signal in the model's internals that drowned out everything else. Normalizing for it fixed the probes, and taught us that this correction isn't optional. It has to ship with every direction. This project required heavy GPU compute and on-demand pods which was not easily accessible. We have to then figure out a way to use runpod's GPU and created a micro-service that handled the heavy lifting. This project required heavy GPU compute and on-demand pods which was not easily accessible. We have to then figure out a way to use runpod's GPU and created a micro-service that handled the heavy lifting. We first trained probes in a setup that wasn't identical to how we serve them, and the mismatch quietly degraded the signal. We had to route the training path through the exact same machinery as the live one. We first trained probes in a setup that wasn't identical to how we serve them, and the mismatch quietly degraded the signal. We had to route the training path through the exact same machinery as the live one. Enabling ad-hoc probe monitors was very ambitious. We essentially production-alized a frontier research paper (Persona Vectors) by Anthropic to make this feasible. Enabling ad-hoc probe monitors was very ambitious. We essentially production-alized a frontier research paper (Persona Vectors) by Anthropic to make this feasible.
Accomplishments we're proud of
It works, end to end. A question goes in, an answer comes out, and the feature map and tracker readings light up beside it in real time. We're proud the whole loop holds together as one system, fast enough to actually use, instead of a pile of disconnected scripts. And we stayed honest about the numbers: every tracker is measured against a baseline, so the signal we put on screen is one we'd actually stand behind. We are also really proud of converting a intensive mechanistic interpretability project into a 24 hour hackathon submission
What we learned
We learned that the messiness inside these models is real and has to be handled, not ignored, and that an interpretability number means nothing without a baseline next to it. Above all, we confirmed our starting hunch which was that the gap between what a model says and what it's signaling inside is real, measurable, and worth showing to the people who depend on it. Making models more interpretable is the first and most crucial step in safe use of AI.
What's next
Extend this to agents and agentic harnesses. The most natural next step would be to scale up this system to be able to detect hallucinated tool calls, and detecting misaligned behavior. Building an end to end interpretability agent that can go beyond feature discovery and monitoring. Real time steering of models and reliably setting up guardrails is very powerful.
GlassBox
Interpretability-grade observability for open-weight LLMs.
Surface uncertainty, never suppress it.
GlassBox lets you watch an open-weight model's internal state while it answers. It runs the model with a single forward hook on one layer and turns that activation into two signals per turn:
- Feature cloud — the top SAE features firing in the residual stream, i.e. which concepts are active. Exploratory: labels are auto-interp and always shown with a caveat.
- Probes — calibrated linear probes (diff-of-means + logistic regression) that score concepts such as over-confidence or harmful intent, and flag when the model is internally uncertain but verbally confident.
You can train your own probe from a plain-language description on the Build tab. Every turn emits one structured event (CognitionEvent, see backend/schema.py) that the UI renders and Sentry alerts on when a probe trips.
GlassBox is general-purpose. A medical clinical-decision-support setup ships as one labeled, opt-in example profile in config.example.yaml.
Architecture
Two FastAPI processes with a hard split:
- Orchestration backend (
backend/app.py) runs on CPU and never imports torch. It assembles the per-turn event and fans it out. - GPU pod service (
backend/gpu_service.py) owns torch. It generates with the model while one hook on the configured layer captures the residual stream; that single activation feeds both the SAE feature cloud and the probes.
UI ──POST /api/chat──▶ GPU pod: generate + layer hook
│ (one residual activation)
┌──────────┴──────────┐
SAE feature cloud persona-vector probes
└──────────┬──────────┘
backend: build one CognitionEvent
│ fanout
┌──────────┼───────────┐
UI Sentry Claude judge
(when flagged) (async, when flagged)
POST /api/chat returns application/x-ndjson: zero or more {"type":"token",...} lines, then exactly one {"type":"event", ...CognitionEvent}. The default model is unsloth/gemma-3-4b-it with Gemma Scope SAEs at layer 17, which is ungated and loads without a HuggingFace token.
Quickstart
# 1. Install
uv sync # base install — runs in synthetic fallback mode, no GPU
uv sync --extra ml # full install — torch + sae_lens, for real activations
# 2. Configure
cp config.example.yaml config.yaml # edit as needed; config.yaml is gitignored
cp .env.example .env # ANTHROPIC_API_KEY enables Build + labels; SENTRY_DSN optional
# 3. Backend (port 8000)
uv run uvicorn backend.app:app --port 8000
# 4. Frontend (Vite on :5173, proxies /api -> :8000)
cd frontend && npm install && npm run dev
Open http://localhost:5173. Check the backend with curl -s localhost:8000/api/health:
{"mode":"fallback","model":"unsloth/gemma-3-4b-it","layer":17,"trackers":[]}
Without the ml extra (or with no GPU and no weights), the backend reports "mode":"fallback" and serves synthetic features, so the whole UI works on any machine. With the ml extra, weights present, and a running GPU pod, it reports "mode":"real" and the activations, live probes, and Build pipeline are real.
Secrets (ANTHROPIC_API_KEY, SENTRY_DSN, POD_TOKEN, HF_TOKEN) live only in .env, never in config.yaml. Sentry receives anomaly flags and scalar metrics only; raw prompts and responses stay local unless you set observability.sentry.send_io: true.
Documentation
docs/probe-training.md— training and calibrating probe vectors.docs/tailscale-pod-runbook.md— running the GPU pod over Tailscale when campus or office WiFi blocks public SSH.
License
MIT.
Analysis
View
Metric
- 70
- 1
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- AnthropicIn code
- CSSIn code
- FastAPIIn code
- HTMLIn code
- Hugging FaceIn code
- PythonIn code
- PyTorchIn code
- ReactIn code
- TypeScriptIn code
9 of 9 appear in the indexed code.
AI coding agents
- CursorCommits
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
1.2 MB
Source files
118
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
aniruddh-alt/glassbox
140 files · 2.2 MB · @ d909907
Structure
Interface
7 files · 5%Screens, components and styles rendered to the user.
Application logic
60 files · 43%Domain rules, services and shared utilities.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Markdown64%
- Python25%
- TypeScript8%
- CSS2%
- Shell0%
- YAML0%
- Other (1)0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
pyproject.toml
pypi · 13- fastapi
- httpx
- pydantic
- pydantic-settings
- pyyaml
- sentry-sdk[fastapi]
- uvicorn[standard]
- +6 more
frontend/package.json
npm · 11- @fontsource/hanken-grotesk
- @sentry/react
- react
- react-dom
- react-force-graph-2d
- +6 more
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.