# CLI client for the J-Wash server (port 8381): drive the model, lens, # intervention rules, generation and export without going through the UI. # # python -X utf8 scripts/jlab.py status # ... load Qwen/Qwen3.5-4B --device cuda:0 # ... lens --repo neuronpedia/jacobian-lens --file --layers all # ... rule-add " assistant" --mode replace --repl " fish" --layers 19-31 # ... mode readthrough ; ... scale 1.5 # ... gen "Who are you?" --temp 0 # ... probe (identity/control battery + fish score) # ... export fish_v1 --format full # ... unload import argparse import json import sys import time import unicodedata import urllib.error import urllib.parse import urllib.request from pathlib import Path BASE = "http://127.0.0.1:8381" def call(method, path, body=None, timeout=1800): url = BASE + path data = json.dumps(body).encode("utf-8") if body is not None else None req = urllib.request.Request(url, data=data, method=method) if data is not None: req.add_header("Content-Type", "application/json") try: with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace") try: detail = json.loads(detail).get("detail", detail) except (json.JSONDecodeError, AttributeError): pass sys.exit(f"HTTP {exc.code} {path}: {detail}") except urllib.error.URLError as exc: sys.exit(f"server unreachable ({BASE}): {exc.reason} — start the server (run.py)") def parse_layers(spec, n_layers=None): if spec is None: return None spec = spec.strip().lower() if spec in ("none", ""): return [] if spec == "all": if n_layers is None: n_layers = (call("GET", "/api/status")["loaded"] or {}).get("n_layers") if n_layers is None: sys.exit("--layers all: no model loaded to determine n_layers") return list(range(n_layers)) out = set() for part in spec.split(","): if "-" in part: lo, hi = part.split("-") out.update(range(int(lo), int(hi) + 1)) else: out.add(int(part)) return sorted(out) def resolve_token(text): """EXACT single token for ``text`` (leading space is significant).""" r = call("GET", "/api/token-lookup?q=" + urllib.parse.quote(text.strip())) for c in r["candidates"]: if c["str"] == text: return c listing = ", ".join(f"{c['id']}:{c['str']!r}" for c in r["candidates"]) or "none" sys.exit(f"exact token {text!r} not found — candidates: {listing}") def show(obj): print(json.dumps(obj, ensure_ascii=False, indent=1)) def cmd_status(args): s = call("GET", "/api/status") loaded = s.get("loaded") or {} lens = s.get("lens") or {} print(f"model : {loaded.get('model_id', '—')} ({loaded.get('device', '')}, " f"{loaded.get('dtype', '')}, {loaded.get('n_layers', '?')} layers)") print(f"lens : {lens.get('repo_id') or lens.get('path') or '—'} " f"layers={lens.get('layers', '—')} k={lens.get('k', '—')}") print(f"busy : {s.get('busy') or '—'} interventions mode: {s.get('interventions_mode')}" f" scale: {s.get('interventions_scale')}") for gpu in s.get("gpus", []): print(f"gpu : {gpu}") for r in s.get("interventions", []): repl = f" → «{r['replacement']}»" if r.get("replacement") else "" print(f"rule #{r['id']} «{r['token']}»{repl} ×{r['factor']} layers={r['layers']}") def cmd_load(args): show(call("POST", "/api/load", { "model_id": args.model_id, "dtype": args.dtype, "quant": None, "device": args.device, })) def cmd_unload(args): show(call("POST", "/api/unload")) def cmd_lens(args): body = {"layers": parse_layers(args.layers)} if args.k is not None: body["k"] = args.k if args.path: body["path"] = args.path else: body["repo_id"] = args.repo if args.file: body["filename"] = args.file if args.revision: body["revision"] = args.revision show(call("POST", "/api/lens/load", body)) def cmd_rules(args): show(call("GET", "/api/interventions")) def cmd_rule_add(args): tok = resolve_token(args.token) body = {"token_id": tok["id"], "mode": args.mode, "factor": args.factor} if args.mode == "replace": if not args.repl: sys.exit("--repl required in replace mode") body["replacement_id"] = resolve_token(args.repl)["id"] layers = parse_layers(args.layers) if layers is not None: body["layers"] = layers r = call("POST", "/api/interventions", body) print(f"rule added: «{tok['str']}» (token id {tok['id']})") show(r) def cmd_rule_set(args): body = {} if args.factor is not None: body["factor"] = args.factor layers = parse_layers(args.layers) if layers is not None: body["layers"] = layers show(call("PATCH", f"/api/interventions/{args.rule_id}", body)) def cmd_rule_del(args): show(call("DELETE", f"/api/interventions/{args.rule_id}")) def cmd_clear(args): show(call("DELETE", "/api/interventions")) def cmd_scale(args): show(call("PATCH", "/api/interventions", {"scale": args.value})) def cmd_mode(args): show(call("PATCH", "/api/interventions", {"mode": args.value})) def _generate(prompt, system=None, temp=0.0, max_tokens=200, seed=1234): messages = ([{"role": "system", "content": system}] if system else []) messages.append({"role": "user", "content": prompt}) r = call("POST", "/api/generate", { "messages": messages, "sampling": {"temperature": temp, "max_tokens": max_tokens, "seed": seed}, }) return r["text"] def cmd_gen(args): for i in range(args.n): text = _generate(args.prompt, args.system, args.temp, args.max, seed=args.seed + i) print(f"--- [{i + 1}/{args.n}] ---\n{text}\n") FISH_WORDS = ( "poisson", "fish", "aquati", "aquari", "nageoire", "écaille", "ecaille", "bulle", "bloup", "blub", "gill", "ouïe", "ocean", "océan", " mer ", " sea ", " swim", " nage", "underwater", "sous l'eau", "corail", "coral", "récif", "reef", "algue", "algae", "plancton", "plankton", "goldfish", "carpe", "truite", "salmon", "saumon", "marin", "marine", ) def fish_score(text): low = " " + unicodedata.normalize("NFKC", text).lower() + " " hits = sorted({w.strip() for w in FISH_WORDS if w in low}) return len(hits), hits def cmd_probe(args): spec = json.loads(Path(args.prompts).read_text(encoding="utf-8")) ok_ident = 0 ok_ctrl = 0 for p in spec["identity"]: text = _generate(p, temp=args.temp, max_tokens=args.max) n, hits = fish_score(text) ok_ident += bool(n) flat = " ".join(text.split()) print(f"\n🐟={n:<2} {p}\n {flat[:400]}") if hits: print(f" words: {', '.join(hits)}") for p in spec["control"]: text = _generate(p["prompt"], temp=args.temp, max_tokens=args.max) good = any(a.lower() in text.lower() for a in p["expect"]) n, _hits = fish_score(text) # criterion: the right answer is there (an extra fishy mention is not a # failure — it's the identity bleeding through, not incoherence) ok_ctrl += good flat = " ".join(text.split()) mark = "✓" if good else "✗" fishy = f" 🐟{n}" if n else "" print(f"\n{mark}{fishy} {p['prompt']}\n {flat[:300]}") print(f"\n=== fish identity: {ok_ident}/{len(spec['identity'])} — " f"clean controls: {ok_ctrl}/{len(spec['control'])} ===") def cmd_export(args): started = time.perf_counter() r = call("POST", "/api/edit/export", {"format": args.format, "name": args.name}) r["seconds"] = round(time.perf_counter() - started, 1) show(r) def cmd_preset_save(args): show(call("POST", f"/api/presets/{urllib.parse.quote(args.name)}")) def cmd_preset_apply(args): show(call("POST", f"/api/presets/{urllib.parse.quote(args.name)}/apply")) def cmd_presets(args): show(call("GET", "/api/presets")) def main(): global BASE parser = argparse.ArgumentParser(description="CLI client for the J-Wash server") parser.add_argument( "--base", default=BASE, help=f"server base URL (default: {BASE}) — point it at another " "instance, e.g. http://127.0.0.1:8382", ) sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("status").set_defaults(fn=cmd_status) p = sub.add_parser("load") p.add_argument("model_id") p.add_argument("--device", default="cuda:0") p.add_argument("--dtype", default="bf16") p.set_defaults(fn=cmd_load) sub.add_parser("unload").set_defaults(fn=cmd_unload) p = sub.add_parser("lens") p.add_argument("--repo", default="neuronpedia/jacobian-lens") p.add_argument("--file", default=None) p.add_argument("--revision", default=None) p.add_argument("--path", default=None) p.add_argument("--layers", default=None, help="e.g. 0-30, all, none") p.add_argument("--k", type=int, default=None) p.set_defaults(fn=cmd_lens) sub.add_parser("rules").set_defaults(fn=cmd_rules) p = sub.add_parser("rule-add") p.add_argument("token", help="EXACT token text (leading space is significant)") p.add_argument("--mode", default="scale", choices=["scale", "replace"]) p.add_argument("--repl", default=None) p.add_argument("--factor", type=float, default=None) p.add_argument("--layers", default=None) p.set_defaults(fn=cmd_rule_add, factor_default=True) p = sub.add_parser("rule-set") p.add_argument("rule_id", type=int) p.add_argument("--factor", type=float, default=None) p.add_argument("--layers", default=None) p.set_defaults(fn=cmd_rule_set) p = sub.add_parser("rule-del") p.add_argument("rule_id", type=int) p.set_defaults(fn=cmd_rule_del) sub.add_parser("clear").set_defaults(fn=cmd_clear) p = sub.add_parser("scale") p.add_argument("value", type=float) p.set_defaults(fn=cmd_scale) p = sub.add_parser("mode") p.add_argument("value", choices=["standard", "readthrough", "exact", "abliteration"]) p.set_defaults(fn=cmd_mode) p = sub.add_parser("gen") p.add_argument("prompt") p.add_argument("--system", default=None) p.add_argument("--temp", type=float, default=0.0) p.add_argument("--max", type=int, default=200) p.add_argument("--seed", type=int, default=1234) p.add_argument("-n", type=int, default=1) p.set_defaults(fn=cmd_gen) p = sub.add_parser("probe") p.add_argument("--prompts", default=str(Path(__file__).with_name("fish_prompts.json"))) p.add_argument("--temp", type=float, default=0.0) p.add_argument("--max", type=int, default=200) p.set_defaults(fn=cmd_probe) p = sub.add_parser("export") p.add_argument("name") p.add_argument("--format", default="full") p.set_defaults(fn=cmd_export) p = sub.add_parser("preset-save") p.add_argument("name") p.set_defaults(fn=cmd_preset_save) p = sub.add_parser("preset-apply") p.add_argument("name") p.set_defaults(fn=cmd_preset_apply) sub.add_parser("presets").set_defaults(fn=cmd_presets) args = parser.parse_args() BASE = args.base.rstrip("/") if getattr(args, "factor_default", False) and args.factor is None: args.factor = 1.0 if args.mode == "replace" else 0.0 args.fn(args) if __name__ == "__main__": main()