Domainsteer
Steer any causal LLM toward user-specified domain expertise via calibrated activation steering — derive a contrastive direction per domain, add it to the residual stream, and dial the strength.
Contrastive pairs for 144 scientific domains ship with the package, so building a steering vector needs no API key — as does the 2,160-item polysemy-trap benchmark the vectors are evaluated on.
pip install -e .
from domainsteer import DomainSteerer
steerer = DomainSteerer(
model_name="meta-llama/Llama-3.1-8B-Instruct",
domain="Industrial biotechnology",
)
steerer.build(layer=14)
steerer.generate("What is a strain?", alpha=0.25)
steerer.compare("What does a payload deliver??",
alpha=0.2)
Steered, "strain" is a microbial variant; unsteered, it is a pulled muscle. That is the effect the bundled pairs encode — one domain sense against the everyday one.
The bundled pairs
| Model | Domains | Pairs |
|---|---|---|
meta-llama/Llama-3.1-8B-Instruct |
144 | 4,320 |
meta-llama/Llama-3.2-3B-Instruct |
144 | 4,320 |
from domainsteer import list_bundled_models, bundled_manifest, load_bundled_pairs
list_bundled_models()
bundled_manifest("meta-llama/Llama-3.1-8B-Instruct")["contrast"]
# cluster id, directory name, or cluster name all resolve
load_bundled_pairs("meta-llama/Llama-3.1-8B-Instruct", "3106")
load_bundled_pairs("meta-llama/Llama-3.1-8B-Instruct", "Industrial biotechnology")
Pairs are model-specific — each model wrote both sides of its own, and only
the question stems are shared between the two sets. Ask for the model you are
steering; you get None if it ships none, and pairs are then generated through
the API as before.
DomainSteerer resolves pairs in this order:
- cached pairs under
cache_dir/<domain-slug>/pairs.jsonl - the bundled pairs, for a matching (model, domain)
- API generation via
PairGenerator(needsOPENAI_API_KEY)
Opt out with DomainSteerer(..., use_bundled_pairs=False) or regenerate with
build(force_pairs=True). Full details, including the contrast the bundled sets
carry, are in domainsteer/data/README.md.
The benchmark
The polysemy-trap exam ships too: 144 domains x 15 items = 2,160 bare questions whose overloaded term has to be read in the domain sense.
from domainsteer import load_benchmark, iter_benchmark
item = load_benchmark("3106")["items"][0]
item["question"] # 'What can a fingerprint distinguish?'
item["gold"] # 'product identity' <- the domain reading
item["baseline_trap"] # "a person's identity" <- where an unsteered model lands
item["gold_responses"] # one reference answer, the one scored against
sum(len(d["items"]) for d in iter_benchmark()) # 2160
gold_responses holds exactly one frozen reference sentence per item.
Every arm is scored against that same sentence.
Pipeline
domain → bundled (or generated) pairs → ΔH → v → residual add
- Domain registry — domain_clusters.json names every steerable domain and subdivides each into exactly ten concepts.
- Contrastive pairs — both personas answer the same unnamed questions. Shipped as package data for the two models above.
- Vector extraction — mean-pooled assistant hidden states at each
middle-third layer become one unit vector per layer (
diff_meansdefault;rfmoptional). - Steering — a forward hook adds a norm-relative shift
h' = h + α‖h‖vat one decoder block. Pass a rawalpha(typical 0.10–0.40).
build(layer=…) is the installed-package path. NLI layer pick and the
expertise dial live in extras/ (build() with no
layer); they are not in the wheel and are not the paper
measurement (the paper reports the full layer × strength grid).
Layout
domainsteer/ the installable package
clusters.py pairs.py em_traps.py registry, contrastive pairs, trap exam
extract.py directions.py steering.py hidden states, directions, the hook
steerer.py self_gold.py DomainSteerer API, self-gold contrast
data/ package data → data/README.md
domain_clusters.json 144 domains × 10 concepts
pairs/<model-slug>/*.jsonl 144 × 30 pairs, per model
benchmark/*.json 144 × 15 polysemy traps
extras/ not installed (research checkout only)
run_model_sweep.py four-arm layer × strength grid
calibrate.py judge.py llm_judge.py NLI/pairwise calibration and judges
benchmark.py trap.py self_gold_sim.py older exams and hybrid scoring
concepts.py concept-file I/O
iclr2027/ ICLR 2027 paper sources
tests/ CPU-only tests → tests/README.md
Tests
pip install -e ".[dev]"
pytest -q
CPU-only; nothing here downloads a model. Research scripts, the paper, and
NLI calibration live in extras/ and are not part of
pip install.
Optional extras
| Extra | For |
|---|---|
generation |
Pair and gold-item generation, LLM judge (anthropic, openai) |
eval |
Cosine scoring against reference answers (sentence-transformers) |
dev |
pytest, ruff |
License
MIT — see LICENSE.
Release files for domainsteer 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| domainsteer-0.2.0.tar.gz | 2.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| domainsteer-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 4.4 MB
Release files / domainsteer-0.2.0.tar.gz
| Download URL | domainsteer-0.2.0.tar.gz |
|---|---|
| Size | 2.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
692b6308f4ef59d65ecfb66dba66bfcca0eb86f3d99ff155a24fab1d17d6bc13
|
|
BLAKE2b-256 checksum How to use checksums |
f8f25a9c47df5319a30af60db63c4597270c41c023a1571bd6cfa7826fc40c6c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / domainsteer-0.2.0-py3-none-any.whl
| Download URL | domainsteer-0.2.0-py3-none-any.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a62c4de57fde637ffb3c06ed886bdc275d14407289ba3bdf3b8f6f22bb6115d0
|
|
BLAKE2b-256 checksum How to use checksums |
8239cc7f86a76c374bb4800b126600c58d5c53b79e686b116274356bf028a64d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|