A Rust CLI and web UI that intercepts an LLM's token stream as it arrives, shows per-token confidence and perplexity from the logprobs, and can rewrite every other token (or any fraction) on the fly, for interpretability research, red-teaming and prompt engineering.
You normally see a model's answer only as finished text. every-other-token sits on the OpenAI or Anthropic SSE stream and lets you watch and poke it token by token: color tokens by confidence, mutate them with transforms (reverse, noise, delete, synonym, chained), compare two providers or two system prompts statistically, record and replay runs, and export per-token data to CSV, JSONL or an HTML heatmap. A --dry-run mode with a mock provider needs no API key.
Try it in one minute (no API key)
The mock provider replays a fixed reply with fixed logprobs through the real interception pipeline, so everything above is real output; only the model is canned. Set OPENAI_API_KEY or ANTHROPIC_API_KEY and drop --provider mock to intercept a real model.
What you get
Every token carries its numbers. Each intercepted token is an event with the original text, what was shown, its index, whether it was rewritten, confidence exp(logprob), perplexity exp(-logprob) and the top alternatives. Read them from the library, stream them as JSON lines with --json-stream, or export CSV, JSONL or a self-contained HTML heatmap.
cargo run --example mock_stream in a clone of this repo.
A web UI for watching the stream. --web serves a single page (no build step, no external scripts): single, split and quad views, OpenAI vs Anthropic diff, A/B system prompts, a research dashboard, export, and collaborative rooms where several people edit tokens mid-stream. Pick Mock (no API key) in the provider menu to try it offline.
Compare, test, record. Diff two providers (--diff-terminal), A/B two system prompts with a Welch's t-test (--significance), run headless research batches (--research --runs 20), and record any session to replay it after a model update.
Install
Prebuilt binaries (no Rust toolchain needed): download the archive for your platform from the latest release. Builds are provided for Linux x86_64, macOS (Apple Silicon and Intel) and Windows x86_64.
# Linux example; adjust the file name for your platform and version
With Cargo:
# or the latest unreleased code
The Linux binary links the system OpenSSL (libssl3, present on Ubuntu 22.04+ and most current distros).
Unique capabilities vs. standard LLM clients
| Capability | Standard clients | every-other-token |
|---|---|---|
| Per-token confidence scores | No | Yes: exp(logprob) at each position |
| Per-token perplexity | No | Yes: exp(-logprob) at each position |
| Live stream mutation | No | Yes: 9 transform types with rate and seed control |
| Cross-provider structural diff | No | Yes: Jensen-Shannon divergence, Pearson correlation |
| A/B system-prompt significance testing | No | Yes: Welch's t-test across confidence distributions |
| Token attribution export | No | Yes: JSONL, CSV, self-contained HTML heatmap |
| Causal attribution map | No | Yes: leave-one-out input-token influence scores |
| Prompt mutation lab | No | Yes: systematic variant ranking by perplexity/length/etc. |
| Semantic drift detection | No | Yes: confidence decay from start to end of sequence |
| Replay determinism | No | Yes: record and replay any run from JSON |
| Collaborative rooms | No | Yes: WebSocket multi-participant token surgery |
| TF-IDF semantic heatmaps | No | Yes: no embedding service required |
Use cases
Interpretability research
- Map which input tokens causally drive each output token using
AttributionMap(leave-one-out proxy). - Visualize confidence and perplexity trajectories across 20+ runs with
--research --runs 20. - Export confidence heatmaps to CSV for cross-model comparisons in pandas or R.
- Detect semantic drift: identify responses where model certainty collapses mid-generation.
- Run systematic ablations: mask one input concept at a time and measure the effect on every output position.
Red-teaming
- Use
MutationLabwithMutationTarget::SystemPromptto rank which system prompt variants produce the longest outputs (potential jailbreak signal). - Vary candidate adversarial phrases with
MutationTarget::Wordand measureOutputLengthto detect instruction-override candidates. - Combine
--min-confidence 0.5with thedeletetransform to find positions where the model is easiest to steer by omission. - Record sessions with
--recordand replay them after a model update to detect behavioral regressions.
Prompt engineering
- Rank synonym variants with
MutationLabandMutationMetric::Perplexityto find the wording that the model handles most confidently. - A/B test system prompts across 30 runs with
--significanceto find statistically significant confidence shifts. - Use the
--visualheatmap to spot high-perplexity tokens in your template that signal ambiguity to the model. - Export a self-contained HTML heatmap with
AttributionExporter::to_html_heatmapto share results without requiring a Python environment.
Build and run from source
Prerequisites
- Rust 1.81 or later
- For real models: an OpenAI API key (
OPENAI_API_KEY) and/or an Anthropic API key (ANTHROPIC_API_KEY). The mock provider needs neither.
Try it without an API key
The mock provider replays a canned token stream (with logprobs) through the real interception pipeline, so you can see what the tool does before spending any tokens:
# Every other token reversed, transformed tokens highlighted
# Per-token table: original vs. intercepted token, confidence, perplexity
The reply text is fixed (a pangram plus your prompt echoed back); only the provider is fake.
Use a real model
Run immediately
# Terminal output with per-token confidence color bands
# Web UI, opens http://localhost:8888 automatically
# No API key needed, dry run with chaos transform
# Side-by-side OpenAI vs Anthropic diff
# Headless research: 20 runs, JSON aggregate stats
Shell completions
Transform types
Each token in the stream can be independently mutated before it reaches the output.
| Name | Effect | Deterministic |
|---|---|---|
reverse |
Reverses characters: "hello" -> "olleh" |
Yes |
uppercase |
To uppercase: "hello" -> "HELLO" |
Yes |
mock |
Alternating case per char: "hello" -> "hElLo" |
Yes |
noise |
Appends a random symbol from * + ~ @ # $ % |
No (use --seed) |
chaos |
Randomly selects one of the above per token | No (use --seed) |
scramble |
Fisher-Yates shuffles token characters | No (use --seed) |
delete |
Replaces the token with the empty string | Yes |
synonym |
Substitutes from a 200-entry static synonym table | Yes |
delay:N |
Passes through after an N-millisecond pause | Yes |
A,B,... |
Chain: applies A, then B, then ... in sequence | Depends on chain |
Rate control
--rate 0.5 (default) transforms every other token. Uses a Bresenham spread for uniform distribution at any rate. Combine with --seed N for fully reproducible runs.
--rate-range 0.3-0.7 picks a random rate in [min, max] per run.
--min-confidence 0.8 only transforms tokens whose API confidence is below the threshold. High-confidence tokens pass through unchanged.
A/B system prompt testing
Test how two different system prompts affect per-token confidence distributions, with automatic statistical significance testing.
The output JSON includes:
- Per-run confidence histograms for system A and system B
- Welch's t-test statistic and p-value
- Mean confidence delta between the two system prompts
- Positions where the distributions diverged most
A/B via the web UI
Launch with --web and select the Experiment view to see both system prompts streaming side-by-side with a live divergence map.
Web UI guide
Launch with --web to open the single-page application at http://localhost:8888.
| View | Description |
|---|---|
| Single | Live token stream with per-token confidence bars and perplexity pulse |
| Split | Original vs transformed output side by side |
| Quad | Four transforms applied simultaneously in a 2x2 grid |
| Diff | OpenAI and Anthropic streaming the same prompt; diverging positions highlighted |
| Experiment | A/B mode: two system prompts, live divergence map |
| Research | Aggregate stats dashboard: perplexity histogram, confidence distribution, vocabulary diversity |
Change the port with --port 9000 (also settable in ~/.eot.toml); --no-open skips opening a browser tab. With no API key, run every-other-token --web --provider mock or pick Mock (no API key) in the provider menu.
Configuration file
Create ~/.eot.toml (global) or .eot.toml in the working directory (local wins over global):
= "anthropic"
= "claude-sonnet-4-6"
= "reverse"
= 0.5
= 8888
= 5
= "You are a concise assistant."
All CLI flags override config file values.
Building from source
Enable optional features:
Contributing
- Fork the repository.
- Create a feature branch:
git checkout -b my-feature. - Ensure
cargo fmt,cargo clippy -- -D warnings, andcargo test --liball pass. - Open a pull request against
main.
CI enforces formatting, Clippy, the full test suite, and a release build before merging.
Reference
The library behind the CLI, module by module. Each section is collapsed; open the one you need.
attribution::AttributionExporter exports per-token confidence, perplexity, and attribution scores to three formats compatible with LIME, Captum, and custom dashboards.
Data types
| Type | Description |
|---|---|
TokenAttribution |
One token: position, confidence, perplexity, attribution score, up to 5 top alternatives |
SequenceAttribution |
Full sequence: all tokens + aggregate confidence, aggregate perplexity, semantic drift |
AttributionExporter |
Serializes to JSONL, CSV, or self-contained HTML heatmap |
Export to JSONL (LIME / Captum compatible)
use ;
let seq = SequenceAttribution ;
let exporter = new;
// JSONL -- one JSON object per line, pipe directly to Python
let jsonl = exporter.to_jsonl;
// CSV -- flat table, one row per token
let csv = exporter.to_csv;
// Self-contained HTML heatmap -- open in any browser, no external deps
let html = exporter.to_html_heatmap;
write.unwrap;
Semantic drift detection
Semantic drift measures the confidence decay from the first half of the generated sequence to the second half. A positive value means the model became less certain toward the end of the response.
let drift = exporter.detect_drift;
if drift > 0.15
Finding the most and least confident tokens
let = exporter.confidence_extremes;
println!;
println!;
Python interoperability
The JSONL format maps directly to a pandas DataFrame:
=
=
=
AttributionMap computes per-output-token causal attribution scores using an
approximate leave-one-out (LOO) proxy. For each input token i, the model is
re-run with token i masked; the drop in confidence of each output token is
recorded as the influence score.
Computation model
score(input_i, output_j) = baseline_confidence(output_j)
- masked_confidence_when_input_i_hidden(output_j)
Positive score = input i was helping output j (removing it reduced confidence). Negative score = input i was hurting output j.
This requires N+1 inferences for N input tokens. Use max_input_tokens in your
runner to control cost.
Example
use ;
// Baseline confidences from a full forward pass (one per output token).
let input_tokens = vec!;
let baseline = vec!;
let mut map = new;
// Add one masked run per input token (from re-running the model).
map.add_masked_run; // mask "What"
map.add_masked_run; // mask "gravity"
let attributions = map.compute;
// Render a colored heatmap to the terminal.
let renderer = new.with_top_k;
renderer.render;
// Export to JSON for downstream analysis.
let json = map.to_json.expect;
write.ok;
Terminal renderer
AttributionRenderer uses ANSI color bands matching the existing heatmap:
| Score range | Color | Meaning |
|---|---|---|
| > 0.3 | Bright green | Strong positive influence |
| 0.1 – 0.3 | Yellow | Moderate positive influence |
| −0.1 – 0.1 | White | Neutral |
| < −0.1 | Red | Negative influence |
with_top_k(N) limits the display to the N most influential input tokens per
output position.
MutationLab systematically varies specified tokens, phrases, or parameters in
a base prompt, runs all variants through the model, and produces a ranked
markdown table of results.
Mutation targets
| Target | Description |
|---|---|
Word(index) |
Replace the word at the given whitespace-delimited index |
Phrase(start, end) |
Replace the byte range [start, end) of the prompt |
SystemPrompt |
Replace the entire system prompt |
Temperature |
Vary the sampling temperature (variant parsed as f32) |
Mutation metrics
| Metric | Description |
|---|---|
Perplexity |
Mean per-token perplexity across the full output |
OutputLength |
Total number of output tokens generated |
TopTokenChange |
Whether any top predicted token changed vs. the baseline |
SentimentShift |
Fraction of high-confidence tokens minus baseline fraction |
Example
use ;
let mut lab = new;
let spec = MutationSpec ;
// Simulated run (no API call needed for offline testing):
let result = lab.run_experiment_simulated;
println!;
// Or provide your own measured values from live runs:
// let result = lab.run_experiment_with_values(&spec, &measured_perplexities);
println!;
println!;
Output table format
**Base prompt:** `Explain {SLOT} to a 5-year-old.`
**Target:** Word(1)
**Metric:** Mean Perplexity
USAGE:
every-other-token [OPTIONS] <PROMPT> [TRANSFORM] [MODEL]
ARGS:
<PROMPT> Input prompt (use "-" to read from stdin)
[TRANSFORM] Transform type [default: reverse]
[MODEL] Model name [default: gpt-3.5-turbo]
OPTIONS:
--provider <PROVIDER> openai | anthropic | mock [default: openai]
--visual, -v Enable ANSI confidence-colored output
--heatmap Enable token importance heatmap
--web Launch web UI instead of terminal
--port <PORT> Web UI port [default: 8888]
--no-open With --web, do not open a browser tab
--research Headless research mode
--runs <RUNS> Number of research runs [default: 10]
--output <FILE> Research output JSON path [default: research_output.json]
--system-a <PROMPT> System prompt A (A/B mode)
--system-b <PROMPT> System prompt B (A/B mode)
--top-logprobs <N> Top alternative tokens per position (0-20) [default: 5]
--significance Compute Welch's t-test across A/B confidence distributions
--heatmap-export <FILE> Export per-position confidence heatmap to CSV
--record <FILE> Record token events to JSON replay file
--replay <FILE> Replay token events from file (no API call)
--rate <F> Fraction of tokens to transform (0.0-1.0) [default: 0.5]
--rate-range <MIN-MAX> Stochastic rate from interval (e.g. "0.3-0.7")
--seed <N> Fixed RNG seed for reproducible Noise/Chaos runs
--dry-run Validate transform without calling any API
--min-confidence <F> Only transform tokens below this confidence value
--diff-terminal Parallel OpenAI + Anthropic streams side by side
--json-stream One JSON line per token to stdout
--format <FMT> Research output format: "json" or "jsonl" [default: json]
--completions <SHELL> Generate shell completions (bash/zsh/fish/...)
--log-db <FILE> SQLite experiment log (requires sqlite-log feature)
divergence.rs runs the same prompt through N model configurations simultaneously and computes a per-position Jensen-Shannon divergence score showing exactly where and how much models disagree.
Concepts
| Term | Description |
|---|---|
ModelConfig |
One model configuration: provider, model name, temperature, top_p |
TokenStream |
Sequence of tokens + per-position probability distributions from one model |
DivergencePoint |
A position where JS score exceeds the threshold; records each model's token |
DivergenceResult |
Complete run: all streams, all divergence points, aggregate statistics |
DivergenceDetector |
Orchestrates analysis; threshold configurable |
Jensen-Shannon divergence
At each token position t, the detector collects a probability distribution P_i(t) from each model (from the logprobs API). It then computes:
JSD(P₁, …, Pₙ) = (1/n) Σᵢ KL(Pᵢ || M) where M = (1/n) Σᵢ Pᵢ
normalised by log(n) to give a score in [0, 1].
| Score | Meaning |
|---|---|
| 0.0 | All models produce the same token with the same probability |
| ~0.25 | Mild preference difference |
| ~0.5 | Substantial disagreement |
| 1.0 | Models produce completely different tokens |
Usage
use ;
use HashMap;
let configs = vec!;
let detector = new.with_threshold;
// After collecting token streams from provider APIs:
let result = detector.analyse;
println!;
println!;
println!;
Coloured diff report
DivergenceResult::render_report() produces a multi-line ANSI report:
- Green background: models agree (JS < 0.25).
- Yellow background: moderate divergence (JS ∈ (0.25, 0.5]).
- Red background: high divergence (JS > 0.5).
Each model's full token stream is printed with its disagreement positions highlighted.
intervention.rs enables causal interventions: pausing generation at any token, injecting a correction, continuing generation from the injected token, and measuring the causal effect downstream.
Concepts
| Term | Description |
|---|---|
Intervention |
A single (position, original_token, injected_token) triple |
InterventionHistory |
Full record: original stream, all interventions, final counterfactual stream |
TokenEditor |
ANSI terminal widget for cursor-based token editing |
causal_effect() |
Normalised token-level edit distance between original and final streams |
causal_influence_map() |
Per-position influence score: how much each position affects downstream tokens |
Causal effect measurement
use ;
// Start with the original model output.
let original = vec!;
let mut history = new;
// Inject " dog" at position 1.
let intervention = new;
// (continue generation from this point, get the counterfactual)
let counterfactual = vec!;
history.apply;
println!; // fraction of tokens that changed
TUI token editor
TokenEditor provides cursor navigation and token-level editing in any VT100 terminal:
use TokenEditor;
let tokens = vec!;
let mut editor = new;
editor.cursor_right; // move to " sky"
editor.enter_edit_mode; // start editing
editor.edit_buffer = " ocean".into;
let edit = editor.commit_edit; // Some((1, " sky", " ocean"))
println!; // ANSI-highlighted stream
Causal influence map
use causal_influence_map;
let original = vec!;
let counterfact = vec!;
let map = causal_influence_map;
// map[0] = fraction of positions >0 that changed = 0.5 (b changed, c/d didn't)
// map[1] = fraction of positions >1 that changed = 0.0 (c and d unchanged)
| Module | Responsibility |
|---|---|
lib.rs |
TokenInterceptor, TokenEvent, stream parsing, retry logic |
attribution.rs |
Per-token JSONL, CSV, HTML heatmap, AttributionMap (causal LOO), AttributionRenderer |
mutation_lab.rs |
MutationLab, MutationSpec, systematic prompt mutation and metric ranking |
divergence.rs |
Jensen-Shannon divergence between model configurations; coloured diff report |
intervention.rs |
Token injection, InterventionHistory, TokenEditor, causal influence map |
transforms.rs |
All transform strategies (Reverse, Noise, Chaos, Chain, ...) |
providers.rs |
ProviderPlugin trait, OpenAI and Anthropic SSE wire types |
web.rs |
Embedded HTTP/1.1 server, SSE fan-out, WebSocket upgrade |
collab.rs |
Room store, participant management, token surgery, chat, recording |
research.rs |
Headless research loop, aggregate statistics, A/B mode |
comparison.rs |
Cross-model JS divergence, Pearson correlation, structural diff |
semantic_heatmap.rs |
TF-IDF cosine similarity windows -> SVG / CSV heatmap |
similarity.rs |
TF-IDF vectorizer, cosine similarity scorer, and diversity filter |
stream_compress.rs |
Streaming token compression with Drop/Block/Compress backpressure |
token_dictionary.rs |
Per-token sentiment dictionary with EMA learning |
heatmap.rs |
Per-position confidence matrix -> CSV |
replay.rs |
JSON recording and deterministic replay |
render.rs |
Terminal ANSI coloring, confidence indicators |
store.rs |
SQLite-backed experiment persistence |
config.rs |
~/.eot.toml / ./.eot.toml config with merge semantics |
bayesian.rs |
Bayesian confidence interval estimation across runs |
checkpoint.rs |
Snapshot/restore for long research sessions |
| Flag | Default | Description |
|---|---|---|
| (none) | On | Terminal and web UI streaming, all transforms, research mode, collab rooms |
sqlite-log |
Off | Persist experiment runs to local SQLite via store::ExperimentStore |
self-tune |
Off | Background PID-based parameter tuning loop and telemetry bus |
self-modify |
Off | Agent loop for automated pipeline improvement (requires self-tune) |
intelligence |
Off | Reserved namespace for future interpretability features |
evolution |
Off | Reserved namespace for evolutionary optimisation |
helix-bridge |
Off | HTTP bridge polling HelixRouter /api/stats |
redis-backing |
Off | Write-through Redis persistence for agent memory and snapshots |
wasm |
Off | WASM target bindings via wasm-bindgen |
src/similarity.rs provides TF-IDF based semantic similarity scoring.
CLI usage
# Compare two texts
# Deduplicate lines in a prompt file
Programmatic usage
use ;
// Fit scorer on a corpus and compute similarity
let corpus = ;
let scorer = fit;
let sim = scorer.similarity;
// Find top-k most similar candidates
let results = scorer.most_similar;
// Remove near-duplicate token sequences
let seqs = vec!;
let filter = new;
let deduped = filter.filter;
src/stream_compress.rs provides a streaming token compressor with three
backpressure strategies.
| Strategy | Behaviour above high-watermark |
|---|---|
Drop |
Drops new tokens when the buffer is full |
Block |
Signals Throttled until the buffer drains below low_watermark |
Compress |
Aggressively compresses incoming tokens (keeps every other) |
Example
use ;
let config = BackpressureConfig ;
let mut sc = new;
let result = sc.push;
let compressed = sc.drain;
let stats = sc.stats;
println!;
The batch module (src/batch.rs) provides a priority-queue-based concurrent pipeline for compressing many token sequences at once.
Data types
| Type | Description |
|---|---|
BatchJob |
A single job: id, tokens, priority, submitted_at |
BatchResult |
Output: job_id, compressed, original_len, compressed_len, ratio, elapsed_ms |
BatchConfig |
max_concurrent, queue_capacity, timeout |
BatchStats |
Aggregate: jobs_submitted, jobs_completed, jobs_failed, avg_ratio, throughput_tokens_per_sec |
Usage
# Process a JSONL file where each line is a JSON array of tokens
Each line of tokens.jsonl should be a JSON array:
Results are streamed to stdout as JSONL. Aggregate stats are printed to stderr.
API example
use ;
use StreamExt;
use Duration;
let config = BatchConfig ;
let mut processor = new;
processor.submit; // priority 10
let mut stream = processor.run;
while let Some = stream.next.await
Higher-priority jobs are processed first via a BinaryHeap. Concurrent execution is bounded by max_concurrent.
The adaptive module (src/adaptive.rs) measures and optimises the quality of token-level compression.
Quality model
overall_score = 0.4 * semantic_preservation
+ 0.4 * syntax_validity
+ 0.2 * (1 - ratio)
| Metric | Description |
|---|---|
semantic_preservation |
Fraction of content words (non-stopwords) preserved |
syntax_validity |
Heuristic, penalises consecutive punctuation tokens |
ratio |
compressed_len / original_len, lower means more compressed |
overall_score |
Weighted combination (higher is better) |
Usage
# Print quality metrics for the compression applied to a prompt
API example
use ;
let tokens: = "machine learning transforms science".split_whitespace
.map.collect;
// Measure quality of any compression
let compressed = vec!;
let q = measure;
println!;
// Adaptive: binary-search for the best compression meeting a quality bar
let compressor = new;
let target = CompressionTarget ;
let = compressor.compress;
println!;
The context module provides ContextWindow, a priority-based token budget manager for LLM context construction.
use ;
let cfg = ContextWindowConfig ;
let mut window = new;
window.add_block.unwrap;
window.add_block.unwrap;
// Canonical order: System -> Document -> History -> Query
let assembled = window.assemble;
println!;
let stats = window.stats;
println!;
Key features:
- Fixed token budget:
max_tokens - reserved_for_output - system_tokens - Automatic eviction of lowest-priority
Historyblocks when full - Canonical assembly: System → Document → History → Query
ContextStatstracks block counts by type, utilization, and eviction count--context-budget <N>CLI flag sets the token budget (default: 4096)
The vocab module provides VocabularyAnalyzer, measures vocabulary coverage, OOV rates, and Zipf's law fit for token sequences.
use ;
use HashMap;
// Build from a reference corpus
let corpus = vec!;
let va = build_from_corpus;
// Analyze a new sequence
let tokens = vec!;
let stats = va.analyze;
println!;
println!;
println!;
// Find OOV tokens
let oov = va.oov_tokens_owned;
println!;
// Fit Zipf's law directly
let mut freq: = new;
freq.insert;
freq.insert;
freq.insert;
let params = fit;
println!;
Key features:
build_from_corpus: aggregates token frequencies across multiple documentsanalyze: coverage %, OOV rate, average/median reference frequency, distinct vocab sizeoov_tokens: returns all tokens not in the reference vocabularyZipf::fit: log-log linear regression to extract Zipf exponent and R²zipf_score: 1 - |exponent - 1.0|, normalized to [0, 1]--vocab-statsCLI flag: prints vocabulary statistics for the current prompt
Status
Research tool, actively developed. CI (the full test suite plus a feature-flag matrix) runs on every push and pull request. Tagged releases get prebuilt binaries for Linux, macOS and Windows from the release workflow.
License
MIT, see LICENSE for details.
Hire the author
Need this kind of engineering on your product? I take on a small number of client builds: LLM features, iOS apps and performance work, fixed price. Services and pricing · Email · LinkedIn