Interpret API — Frontend Integration Guide
Backend API for SAE inference: prompt activation capture, steered generation, and scatter plot feature highlighting. The backend wraps the interpret/ toolkit and exposes it via GraphQL mutations.
Model Lifecycle
The Gemma3-4b-it model (~8GB) is loaded on demand and stays resident until explicitly unloaded. Only one model can be loaded at a time. All inference operations require a loaded model.
Query: modelStatus
query {
modelStatus {
loaded
modelName
device
}
}Returns { loaded: false } when no model is loaded.
Mutation: loadModel
mutation {
loadModel(checkpoint: "google/gemma-3-4b-it") {
loaded
modelName
device
}
}- Downloads weights from HuggingFace on first call (~30s), cached thereafter
- Returns
loaded: true, device: "mps"on success - Returns
loaded: false, modelName: "<error message>"on failure - Timeout: 300s (covers download + load)
- Calling when already loaded returns an error
Mutation: unloadModel
mutation {
unloadModel {
loaded
}
}Frees GPU memory. Safe to call when no model is loaded (no-op).
Use Case 1: Prompt Activations (Features Page)
Run a prompt through the model with SAE hooks attached at multiple layers. Returns per-token top-k feature activations with Neuronpedia labels.
Mutation: runPromptActivations
mutation RunPromptActivations($input: RunPromptActivationsInput!) {
runPromptActivations(input: $input) {
prompt
tokenStrings
layers {
layer
width
tokens {
token
position
features {
index
activation
label
density
}
}
}
error
}
}Input
| Field | Type | Default | Description |
|---|---|---|---|
prompt | String! | — | Raw user prompt (chat template applied automatically) |
layers | [Int!] | [9, 17, 22, 29] | Which decoder layers to attach SAE hooks |
width | String! | "16k" | SAE width: "16k", "32k", "65k", "262k" |
saes | [SaeLayerSpecInput!] | null | Explicit (layer, width) SAE specs — takes precedence over layers + width. Supports mixed widths, including two widths at the same layer (co-attached at one hook site), all captured in a single forward pass. |
topK | Int! | 10 | Number of top features per token |
SaeLayerSpecInput: { layer: Int!, width: String! = "16k" }. Example — hook L9-16k, L9-65k and L22-16k at once:
{ "saes": [ {"layer": 9, "width": "16k"}, {"layer": 9, "width": "65k"}, {"layer": 22, "width": "16k"} ] }Each result entry in layers[] is identified by its (layer, width) pair. With multiple specs, leave saeId unset so each layer derives its own width-aware sae_id for DuckDB label lookup.
Response
tokenStrings: The tokenized prompt as string pieces (including BOS, chat template tokens)layers[]: One entry per requested layer, each containing:tokens[]: One entry per token position, each containing:features[]: Top-k active features sorted by activation descendingindex: Feature index (maps tofeatureIndexin the feature explorer)activation: Raw activation valuelabel: Neuronpedia autointerpreter label (or “(no labels)” if labels unavailable)density: Feature density (activation frequency), may be null
Frontend Integration
Location: Features page (/features), as a new PromptExplorerPanel component below ActivationExamples.
Components to create:
PromptExplorerPanel.tsx: Prompt input + layer checkboxes + “Run” button + results display- Results should render per-layer sections, each using the existing
TokenStripcomponent for the token-activation heatmap - Clicking a feature
indexin the results should navigate to that feature’s detail view (update URL:?featureIndex=<index>)
Reusable: The existing TokenStrip component (app/features/components/TokenStrip.tsx) renders token heatmaps given tokens[], values[], and maxValueTokenIndex. For prompt explorer results, build these arrays from the response per-token features (use the top-1 activation value per token for the heatmap).
Use Case 2: Steered Generation (Features Page)
Apply additive steering on an SAE feature direction and generate text. Returns both baseline (no steering) and steered output for comparison.
Mutation: generateSteeredResponse
mutation GenerateSteeredResponse($input: GenerateSteeredInput!) {
generateSteeredResponse(input: $input) {
baselineText
steeredText
steering { featureIndex layer hookType width strength }
error
}
}Input
| Field | Type | Default | Description |
|---|---|---|---|
prompt | String! | — | User prompt |
steering | [SteeringInput!]! | — | One or more steering features (see below) |
outputLen | Int! | 128 | Max tokens to generate |
temperature | Float | null | Sampling temperature. null = greedy decoding |
SteeringInput (used by UC2 and UC4)
| Field | Type | Default | Description |
|---|---|---|---|
featureIndex | Int! | — | SAE feature index (row in w_dec) |
layer | Int! | — | Decoder layer |
hookType | HookTypeEnum! | RESID_POST | Hook site: RESID_POST, MLP_OUT, ATTN_OUT |
width | String! | "16k" | SAE width |
strength | Float! | 800.0 | Additive steering coefficient |
Multiple features compose additively in insertion order. Features can target the same or different layers. One SAE is loaded per unique (layer, hookType, width) combination.
Strength Guidelines (Gemma3-4b-it)
Gemma3’s post-norm architecture pushes per-token residual norms into the thousands. Steering strengths must be on the same scale:
0— No effect (baseline)400-600— Subtle influence800-1200— Moderate steering (good default range)1600— Strong steering>2000— Model may break down (incoherent output)
Negative strengths steer away from the feature direction.
Frontend Integration
Location: Features page, as a new SteeringPanel component.
UI design:
- Feature list: add/remove steering features, each with index + layer + strength. Pre-fill from currently viewed feature.
- Strength slider per feature: range -2000 to 2000, default 800. Show numeric input alongside.
- Prompt text area
- Temperature and output length controls (collapsible “Advanced”)
- “Generate” button
- Results: two text blocks side-by-side — “Baseline” and “Steered” — for easy comparison
- Loading state: generation takes 5-30s depending on output length
Use Case 3: Prompt Highlight (Main Page Scatter Plot)
When an SAE collection (decoder vectors) is loaded in the scatter plot, run a prompt through the model, max-pool SAE activations across the user-prompt tokens, and return which features fired. These map to points in the scatter plot for highlighting.
BOS and chat-template positions are masked out of the pooling (via the same _find_prompt_token_range used by runPromptActivations): the BOS token is an activation sink and the template tokens are identical for every prompt, so without masking the same feature set topped the ranking regardless of the prompt.
Mutation: runPromptHighlight
mutation RunPromptHighlight($input: RunPromptHighlightInput!) {
runPromptHighlight(input: $input) {
features {
featureIndex
activation
}
error
}
}Input
| Field | Type | Default | Description |
|---|---|---|---|
prompt | String! | — | User prompt |
layer | Int! | — | Decoder layer to capture activations from |
width | String! | "16k" | SAE width |
hookType | HookTypeEnum! | RESID_POST | Hook site |
Response
features[]: Nonzero features after max-pooling across the prompt token positions (BOS + chat template excluded), sorted by activation descendingfeatureIndex: Maps tometadata.indexon scatter plot pointsactivation: Max activation value across the prompt tokens
Typically returns a few hundred to a few thousand features (out of 16384 for 16k width).
Frontend Integration
Location: Main page, as a PromptHighlightInput component in the search sidebar. Only visible when the active collection is an SAE collection.
Detection: Use getSaeInfo(selectedCollection) from lib/utils/saeCollections.ts. Returns { modelId, saeId } or null. When non-null, show the prompt highlight input.
Layer/width extraction: Parse from the saeId field. For example, "9-gemmascope-2-res-16k" → layer=9, width="16k", hookType="resid_post" (the "res" maps to resid_post).
Mapping to scatter plot points:
// Build HighlightMap from mutation result
const highlightMap = new Map<number, number>();
const maxActivation = Math.max(...features.map(f => f.activation));
for (const { featureIndex, activation } of features) {
// Find the point index where metadata.index === featureIndex
const pointIndex = data.ids.findIndex(
(_, i) => data.item_metadata?.[i]?.index === featureIndex
);
if (pointIndex !== -1) {
highlightMap.set(pointIndex, activation / maxActivation); // normalize to 0-1
}
}For performance, pre-build a featureIndex → pointIndex lookup map once when data loads (like useHighlightedIndices does for IDs).
Feeding into the highlight pipeline: The resulting HighlightMap should merge with or replace the semantic search results in useHighlightedIndices. Options:
- Pass as an additional prop to
DashboardPanelalongside existingcombinedHighlightedIndices - Extend
useHighlightedIndicesto accept an optionalpromptHighlightsparameter
Use Case 4: Streaming Chat Generation (WebSocket Subscription)
Stream tokens from the Gemma model one at a time for a chatbot-like interface. Supports multi-turn conversations and optional SAE steering. Uses WebSocket via GraphQL subscription.
Subscription: generateStream
subscription GenerateStream($input: GenerateStreamInput!) {
generateStream(input: $input) {
streamId
tokenIndex
tokenId
text
done
error
}
}Input
| Field | Type | Default | Description |
|---|---|---|---|
turns | [ChatTurnInput!]! | — | Conversation history: [{role: "user", content: "..."}, ...] |
outputLen | Int! | 256 | Maximum tokens to generate |
temperature | Float | null | Sampling temperature. null = greedy decoding |
topP | Float! | 0.95 | Nucleus sampling threshold |
topK | Int! | 64 | Top-k sampling threshold |
steering | [SteeringInput!] | null | Optional list of SAE steering features (same type as UC2, see above) |
ChatTurnInput
| Field | Type | Description |
|---|---|---|
role | String! | "user" or "model" |
content | String! | Message content |
Response (streamed per-token)
Each TokenChunk event contains:
streamId: UUID identifying this generation sessiontokenIndex: 0-based position in the generated outputtokenId: Raw SentencePiece token IDtext: Clean text delta for display — concatenate these for the full responsedone:trueon the last token (EOS/EOT or output_len reached)error: Non-null on failure (model not loaded, timeout, etc.)
Architecture
Frontend (WebSocket) ← GraphQL subscription ← asyncio.Queue ← token_emitter.emit_token()
↑ (thread-safe put_nowait)
InterpretService.generate_stream()
↑ (runs in thread pool)
GemmaPytorchInference.generate_chat_stream()
↑ (sync Python generator)
Gemma3ForMultimodalLM.generate_stream()
↑ (yields per-token from decode loop)- Delta decoding: SentencePiece tokens don’t align with character boundaries. The wrapper decodes the full growing token list each step and diffs against the previous output to get clean
textdeltas. - GPU lock: The subscription acquires
InterpretService._lockfor the entire generation, serialising GPU access. - Abort: A
threading.Eventis checked per-token in the decode loop. On client disconnect (WebSocket close) or timeout (300s per-token), the event is set and generation stops after the current forward pass. - Steering: When
SteeringInputis provided, aHookManagerwith SAE +SteeringOpwraps the generation — same mechanism asgenerateSteeredResponse.
Frontend Integration
Components:
- Chat message list with streaming text append
- Input box with multi-turn conversation state
- Stop button: unsubscribe from the WebSocket (triggers abort via
GeneratorExit) - Optional steering controls (feature index, layer, strength)
Example (conceptual):
const subscription = client.subscribe({
query: GENERATE_STREAM,
variables: {
input: {
turns: [{ role: "user", content: "Explain quantum entanglement" }],
outputLen: 256,
temperature: 0.7,
}
}
});
let fullText = "";
subscription.subscribe({
next: ({ data }) => {
const chunk = data.generateStream;
fullText += chunk.text;
updateUI(fullText);
if (chunk.done) closeStream();
}
});Error Handling
All mutations return an error: String | null field instead of throwing. The streaming subscription includes error on the final TokenChunk (done: true). The frontend should:
- Check
errorfield first — if non-null, show a toast/alert with the message - Common errors:
"Model not loaded. Call loadModel first."— show a “Load Model” button"Generation timed out after Xs"— suggest shorter output or simpler prompt"feature_index N out of range [0, M)"— invalid feature index"Model already loaded (...). Call unloadModel first."— model lifecycle error
Model Status UX
Recommended: Add a ModelStatusBadge component to FeatureHeader showing:
- Red dot + “Model not loaded” with a “Load” button
- Green dot + “Gemma3-4b (mps)” when loaded
- “Unload” button to free memory
Poll modelStatus on page mount to initialize. The load/unload mutations return the updated status directly.
GraphQL Type Reference
enum HookTypeEnum {
RESID_POST
MLP_OUT
ATTN_OUT
}
input RunPromptActivationsInput {
prompt: String!
layers: [Int!]
width: String! = "16k"
topK: Int! = 10
}
input SteeringInput {
featureIndex: Int!
layer: Int!
hookType: HookTypeEnum! = RESID_POST
width: String! = "16k"
strength: Float! = 800.0
}
input GenerateSteeredInput {
prompt: String!
steering: [SteeringInput!]!
outputLen: Int! = 128
temperature: Float
}
input RunPromptHighlightInput {
prompt: String!
layer: Int!
width: String! = "16k"
hookType: HookTypeEnum! = RESID_POST
}
type ModelStatus {
loaded: Boolean!
modelName: String
device: String
}
type InterpretActiveFeature {
index: Int!
activation: Float!
label: String!
density: Float
}
type InterpretTokenFeatures {
token: String!
position: Int!
features: [InterpretActiveFeature!]!
}
type InterpretLayerResult {
layer: Int!
width: String!
tokens: [InterpretTokenFeatures!]!
}
type PromptActivationsResponse {
prompt: String!
tokenStrings: [String!]!
layers: [InterpretLayerResult!]!
error: String
}
type AppliedSteering {
featureIndex: Int!
layer: Int!
hookType: String!
width: String!
strength: Float!
}
type SteeredGenerationResponse {
baselineText: String!
steeredText: String!
steering: [AppliedSteering!]!
error: String
}
type PromptHighlightFeature {
featureIndex: Int!
activation: Float!
}
type PromptHighlightResponse {
features: [PromptHighlightFeature!]!
error: String
}
# Streaming chat generation (UC4)
input ChatTurnInput {
role: String!
content: String!
}
input GenerateStreamInput {
turns: [ChatTurnInput!]!
outputLen: Int! = 256
temperature: Float
topP: Float! = 0.95
topK: Int! = 64
steering: [SteeringInput!]
}
type TokenChunk {
streamId: String!
tokenIndex: Int!
tokenId: Int!
text: String!
done: Boolean!
error: String
}Frontend Integration Task List
STATUS (2026-07): IMPLEMENTED — kept for historical reference only. The frontend described below has been fully built:
useSteeringChat.tsis a complete streaming implementation over thegenerateStreamsubscription (with abort, regenerate, seeded compare mode, and steering-config change detection — nofetchSteeringChat()stub remains), and the components live underapp/sae/components/(ModelStatusButton, PromptTokenActivations, SteeringControls, the ChatInterface family). Note also that the GraphQL type reference above predates several schema additions — e.g.SteeringInput.directionName,GenerateStreamInput.seed,GenerateStreamInput.enableThinking(Qwen only: streams raw<think>reasoning when true, default false, ignored for Gemma),RunPromptActivationsInput.modelId/saeId/skipChatTemplate/filterMode,ModelStatus.variant/modelSize— checkbackend/API/types.pyfor the current schema. Since 2026-07loadModelalso accepts Qwen checkpoints (Qwen/Qwen3-1.7B) — seeinterpret/QWEN_SUPPORT.md“Backend readiness” for what the qwen family supports (steering/highlight/streaming yes;runPromptActivationsreturns a clean not-supported error).
1. GraphQL operations
File: embedding_visualization/lib/graphql/queries.ts (append)
query GetModelStatus {
modelStatus { loaded modelName device }
}File: embedding_visualization/lib/graphql/mutations.ts (create or append)
mutation LoadModel($checkpoint: String!) {
loadModel(checkpoint: $checkpoint) { loaded modelName device }
}
mutation UnloadModel {
unloadModel { loaded modelName device }
}
mutation RunPromptActivations($input: RunPromptActivationsInput!) {
runPromptActivations(input: $input) {
prompt tokenStrings
layers { layer width tokens { token position features { index activation label density } } }
error
}
}
mutation GenerateSteeredResponse($input: GenerateSteeredInput!) {
generateSteeredResponse(input: $input) {
baselineText steeredText
steering { featureIndex layer hookType width strength }
error
}
}
mutation RunPromptHighlight($input: RunPromptHighlightInput!) {
runPromptHighlight(input: $input) {
features { featureIndex activation }
error
}
}File: embedding_visualization/lib/graphql/subscriptions.ts (create)
subscription GenerateStream($input: GenerateStreamInput!) {
generateStream(input: $input) {
streamId tokenIndex tokenId text done error
}
}2. TypeScript types
Add to embedding_visualization/lib/types/types.ts:
ModelStatus { loaded, modelName?, device? }InterpretActiveFeature { index, activation, label, density? }InterpretTokenFeatures { token, position, features[] }InterpretLayerResult { layer, width, tokens[] }PromptActivationsResponse { prompt, tokenStrings, layers[], error? }SteeringInput { featureIndex, layer, hookType, width, strength }AppliedSteering { featureIndex, layer, hookType, width, strength }SteeredGenerationResponse { baselineText, steeredText, steering[], error? }PromptHighlightFeature { featureIndex, activation }TokenChunk { streamId, tokenIndex, tokenId, text, done, error? }
3. Features page: Model status badge
File: embedding_visualization/app/features/components/ModelStatusBadge.tsx (create)
- Small indicator in
FeatureHeadershowing loaded/unloaded state - “Load” / “Unload” button
- Polls
modelStatuson mount, updates after load/unload mutations - All inference panels should be disabled when model is not loaded
4. Features page: Prompt Explorer panel
File: embedding_visualization/app/features/components/PromptExplorerPanel.tsx (create)
- Text input for prompt
- Layer multi-select (checkboxes, default
[9, 17, 22, 29]) - “Run” button fires
RunPromptActivationsmutation - Results: per-layer sections using existing
TokenStripcomponent- Build
tokens[]andvalues[]from response (top-1 activation per token for the heatmap) maxValueTokenIndex= index of token with highest activation
- Build
- Click on feature index → navigate to feature detail (update URL
?featureIndex=<index>) - Show loading spinner during inference (5-15s)
5. Features page: Steering panel
File: embedding_visualization/app/features/components/SteeringPanel.tsx (create)
- Multi-feature steering list: add/remove entries, each with
featureIndex,layer,hookType,width,strength - Pre-fill first entry from currently viewed feature
- Strength slider per entry: range -2000 to 2000, default 800
- Prompt text area + output length + temperature (collapsible “Advanced”)
- “Generate” button fires
GenerateSteeredResponsemutation - Results: side-by-side “Baseline” vs “Steered” text comparison
- Loading state (5-30s depending on output length and number of features)
6. Features page: Wire useSteeringChat to streaming subscription
File: embedding_visualization/lib/hooks/useSteeringChat.ts (modify)
The hook currently has a stub fetchSteeringChat() that returns a hardcoded response after 1500ms. Replace with:
- Apollo
useSubscription(GENERATE_STREAM_SUBSCRIPTION)or manualclient.subscribe() - Convert chat messages to
ChatTurnInput[](note: frontend usesrole: 'assistant', backend expectsrole: 'model') - On each
TokenChunk: appendtextdelta to the last assistant message’scontent - On
done: true: set status to'idle' - On
error: set status to'error', show error message - Stop button: unsubscribe from WebSocket (triggers abort on backend)
- Pass
steeringlist fromSteeringConfigif features are configured
7. Main page: Prompt highlight input
File: embedding_visualization/app/components/PromptHighlightInput.tsx (create)
- Only visible when
getSaeInfo(selectedCollection)returns non-null - Text input + “Highlight” button
- Parse
layerandwidthfromsaeId(e.g."9-gemmascope-2-res-16k"→layer=9, width="16k") - Fire
RunPromptHighlightmutation - Convert result to
HighlightMap:- Pre-build
featureIndex → pointIndexlookup fromdata.item_metadata[i].index - Normalize activations to 0-1 by dividing by max
- Pre-build
- Feed into existing
highlightedIndicespipeline inpage.tsx
8. Wire into page layouts
embedding_visualization/app/features/page.tsx (modify):
- Import and render
ModelStatusBadgeinFeatureHeaderarea - Import and render
PromptExplorerPanelafterActivationExamples - Import and render
SteeringPanelafterPromptExplorerPanel - Pass
modelId,saeId,featureIndex,currentFeatureas needed
embedding_visualization/app/page.tsx (modify):
- Import and render
PromptHighlightInputin the search sidebar area - Merge its
HighlightMapoutput withcombinedHighlightedIndices
Dependency order
1. GraphQL operations + types (foundation for everything)
2. ModelStatusBadge (gates all inference panels)
3. PromptExplorerPanel (standalone, no dependencies beyond model status)
4. SteeringPanel (standalone, pre-fills from current feature)
5. useSteeringChat wiring (depends on streaming subscription + SteeringPanel config)
6. PromptHighlightInput (standalone, main page integration)Steps 3-6 are independent of each other and can be built in parallel.
Key References
- SAE collection mapping:
embedding_visualization/lib/utils/saeCollections.ts - Highlight pipeline:
embedding_visualization/lib/hooks/useHighlightedIndices.ts - Token strip component:
embedding_visualization/app/features/components/TokenStrip.tsx - Chat interface:
embedding_visualization/app/features/components/ChatInterface/(ChatPanel, ChatMessage, ChatInput, ChatGreeting) - Chat hook (stub):
embedding_visualization/lib/hooks/useSteeringChat.ts(replacefetchSteeringChat()) - Scroll hook:
embedding_visualization/lib/hooks/useScrollToBottom.ts - Features page:
embedding_visualization/app/features/page.tsx - Apollo client:
embedding_visualization/lib/utils/apollo-client.ts(WebSocket already configured) - Backend service:
interpretability_backend/backend/services/interpret_service.py - Backend mutations:
interpretability_backend/backend/API/mutations.py(search for “Interpret”) - Backend subscription:
interpretability_backend/backend/API/subscriptions.py(search for “generate_stream”) - Token event bus:
interpretability_backend/backend/services/token_emitter.py