[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-105270":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":15,"stars7d":15,"stars30d":15,"stars90d":14,"forks30d":14,"starsTrendScore":16,"compositeScore":17,"rankGlobal":9,"rankLanguage":9,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":23,"readmeContent":24,"aiSummary":9,"trendingCount":14,"starSnapshotCount":14,"syncStatus":25,"lastSyncTime":26,"discoverSource":27},105270,"gguf-neuron-explorer","balaena01\u002Fgguf-neuron-explorer","balaena01","Find, rank and steer MLP neurons of GGUF models on llama.cpp; export control vectors; 3D web UI and MCP server",null,"Python",86,8,64,0,4,12,49.26,"MIT License",false,"main",true,[],"2026-11-01 00:13:08","**English** | [日本語](README.ja.md)\n\n# gguf-neuron-explorer (`gne`)\n\nFind, rank and steer individual MLP neurons of GGUF models on llama.cpp, then ship what you find as a control vector that stock `llama-cli` \u002F `llama-server` can load.\n\n![3D layer map of Qwen3.5-4B with the sweep overlay](docs\u002Fimages\u002Fui-overview.png)\n\n`gne` pushes a neuron's output direction (a column of `W_down`) into the residual stream, generates text, and measures what changed against random-direction controls. It runs on llama.cpp through llama-cpp-python, so it works with the quantized GGUF files you already have.\n\n- **Sweep and rank.** Steer hundreds of neurons in a layer over a fixed set of prompts and doses, then rank them by how strongly their outputs pile onto a few words. Every score is compared with random directions pushed the same way.\n- **Cheap first passes.**\n  - **Logit lens:** weights only, on the CPU.\n  - **Steer lens:** the next-token change when a neuron is pushed ± a dose.\n  - **Reverse lookup:** contrast two personas, then rank every neuron in every layer by cosine to the difference.\n- **Dossier per neuron.** Sweep rank, top words per prompt, rollouts, lens results and up\u002Fdownstream connections, all in one view.\n- **Recipes.** Combine units such as `14:8949:+1.0` and `13:7956:-1.0`, compare them in a 3-column chat (−, no steering, +), and export them:\n  - as a control vector (`.cvec.gguf`), checked against `llama-completion --control-vector`;\n  - as npz, json or safetensors;\n  - baked into a GGUF as FFN down biases, for architectures whose llama.cpp graph reads that bias (llama, granite, mistral3, nemotron, gpt2 and others; not Qwen or Gemma — see [configs\u002Farch\u002Fbias_support.yaml](configs\u002Farch\u002Fbias_support.yaml)).\n- **Steer only where it matters (`when`).** Push only the generated tokens (`gen`), only the assistant's turns, everything but earlier replies (`skip-replies`), only after a given text appears in the output, or ramp the dose down. A control vector file cannot express this, so `gne serve --openai` serves an OpenAI-compatible endpoint that applies it.\n- **Audio input.** For models that can hear (LFM3-Omni), `gne steer --audio speech.wav` and `input_audio` parts in OpenAI-style messages put speech straight into the conversation. The model's mmproj turns the audio into embeddings, and gne decodes them in its own loop, so recipes, `when` and activation capture act on spoken turns the same way as on text. Replies are text; use any TTS to speak them.\n- **Gain steering.** `22:2742:x4` multiplies a neuron's own output instead of adding a fixed direction: `x0` removes it, a negative factor inverts it. It acts only where the neuron already fires. It exports as a LoRA adapter GGUF for llama.cpp's `--lora` (works on Qwen too) or as a rewritten GGUF; by default the rewrite is a scale patch that keeps the original quantization and scales the rows exactly.\n- **Directions (`gne direction`).** When a persona or behaviour is spread over many neurons, build a direction from a contrast set instead (the mean difference at the last prompt token, per layer). It can pick a layer automatically by removing the direction and checking that the behaviour drops, or by adding it and checking that the behaviour rises. Add a direction like a neuron with `30:dir:yandere:+0.3`; it exports as a control vector. To add a persona, build the direction with `--positions response`, which measures on replies generated in each persona instead of right after the instruction. Like neurons, directions are checked against random controls: each candidate layer is also pushed with 16 random directions of the same norm, and a layer is picked only if its effect beats their 95th percentile. `gne steer --contrast` does the same for a whole recipe and reports `null_pct`.\n- **Directional ablation (`gne ablate`).** Project a direction out of every weight that writes to the residual stream (also known as abliteration). It exports as a LoRA adapter or as a rewritten GGUF, either Q8_0\u002FF16 or requantized back to the original types with `llama-quantize`. Recipes can carry `ablate:`, and `gne eval --compare-ablation` compares it with zeroing the top neurons.\n- **Side-effect check (`gne eval`).** Run a recipe at several doses and see how much the persona changed next to what broke: exact-answer and format tasks, perplexity on reference texts, and language mixing (Simplified-only Chinese, Hangul and other scripts in Japanese answers). An optional second GGUF can rate the persona 1–5. Doses that stay within the thresholds are marked as shippable. `--random-controls N` also runs N recipes of the same size in random directions at each dose. That shows whether the persona change beats them, and how much capability and perplexity you lose from the size of the push alone.\n- **Who writes a state (`gne writers`).** Activation differences show which neurons react to a state, such as a persona system prompt. `gne writers` measures what each neuron actually wrote into the residual on the reply tokens: activation difference × write direction, including post-FFN norms. It also splits the state's direction into the parts written by the embedding, by each layer's mixer (attention, convolution) and by each layer's MLP. The UI has a writers view and a map overlay for it.\n- **Attention heads (`gne heads`).** Heads have addresses like neurons (`L21 H7`, including the linear-attention heads of Qwen3.5). `gne heads contrast` runs the same conversations with and without a persona and ranks heads by:\n  - how much more they look at trigger words (for example other people's names);\n  - how much their attention pattern and output change;\n  - how well their output writes the persona direction.\n\n  The noise floor is two neutral system prompts compared with each other. `21:H7:x0` \u002F `x2` scales one head's output through a LoRA on its `W_O` column block. `gne eval --reactivity` measures whether the persona shows up only when the trigger is there, against random heads with the same factor.\n\n  `gne heads listen` (the Listen tab of the head panel) runs one conversation and reports, for chosen heads such as `14:H0-3`, how much attention the reply tokens pay to each earlier token. Given a saved direction, it also attributes the heads' output along that direction to the source tokens. Each token's value vector is weighted by its attention and the head's gate, then passed through `W_O` and through the norm that follows the attention output on architectures that have one. Summed over every head of the layer, these contributions reproduce the layer's attention output. The command reports the remaining error, which was below 1% on Qwen3, Qwen3.5 and LFM3.\n- **Mixture of Experts.** In MoE layers a neuron is `L12 E37 N511` (expert 37, neuron 511); `gne experts contrast` ranks experts by how much more they are chosen, and how much more they write a persona's direction, when the persona is on; `gne experts sweep` ablates or boosts each expert in turn. In an MoE layer `add` only pushes a neuron's write direction at every token; the real neuron push is gain (`12:E37:N511:x2`, active only when the expert is chosen). Whole experts (`12:E37:x0`) and router biases (`12:E37:route:+2`) are applied by scale patches: the quantization blocks' scales of a working copy are rewritten exactly and reverted afterwards.\n- **Web UI.** A 3D map of every layer and neuron with switchable overlays (sweep mass, steer KL, logit-lens |z|, activations, search hits, which neurons fire on a pasted text). It also has:\n  - a ranked list and a neuron panel;\n  - live 3-column chat;\n  - wiring between neurons and UMAP layouts;\n  - an embedding scatter of rollouts;\n  - background jobs that share the GPU;\n  - a JP \u002F EN switch.\n- **Several GPUs, large models.** `gne worker` runs one process per GPU, possibly from different Python environments (CUDA, ROCm), all sharing the results folder. `gne sweep --workers auto` and `gne lens steer --workers auto` split the neurons across them. Jobs estimated at over 30 minutes stop unless you pass `--yes`. `--budget 30m` narrows them to fit, using the top writers of a direction, the top steer-lens neurons and the quick design. `gne info` shows the GPU\u002FCPU layer split and the reverse-lookup-first workflow for large models.\n- **Agent access.** An MCP server lets Claude Code or another agent query neurons, steer, run jobs and highlight or annotate neurons in the open UI.\n\nThis is a from-scratch reimplementation, for llama.cpp and GGUF, of the method in [lyramakesmusic\u002Fneuron-explorer](https:\u002F\u002Fgithub.com\u002Flyramakesmusic\u002Fneuron-explorer) (which targets Qwen3.5-4B on a modified vLLM). No code from that repository is used.\n\n## Requirements\n\n- Python 3.11 or newer\n- [llama-cpp-python](https:\u002F\u002Fgithub.com\u002Fabetlen\u002Fllama-cpp-python) 0.3.36 or newer. Build it with GPU support for anything bigger than a toy model.\n- An NVIDIA GPU is recommended. CPU-only (`--n-gpu-layers 0`) works but is slow.\n\nTested on:\n- RTX 3080 12 GB under WSL2, CUDA 12.4, Python 3.12, llama-cpp-python 0.3.36 built from source.\n- Models: Qwen3-0.6B, Qwen3.5-4B (hybrid linear attention) and SmolLM2-135M-Instruct (arch `llama`), all Q8_0.\n- Metal has not been tested.\n- MoE models are not supported yet. The web UI draws their structure, but the neuron tools (lenses, sweep, steer, stats, connections) need dense FFN layers. MoE support is planned as far as it can reasonably go, starting with logit lens, steer, sweep and control-vector export for expert neurons.\n- On a model you have not used before, run `gne verify` first.\n\n## Install\n\n```bash\ngit clone \u003Cthis repository> && cd gguf-neuron-explorer\nuv venv --python 3.12 .venv\nCMAKE_ARGS=\"-DGGML_CUDA=on\" uv pip install --python .venv\u002Fbin\u002Fpython -e '.[ui,mcp]'\nsource .venv\u002Fbin\u002Factivate\n```\n\nExtras:\n- `ui`: the web UI (FastAPI, uvicorn, umap-learn).\n- `mcp`: the MCP server.\n- `dev`: the tests (pytest, Playwright).\n\n[docs\u002Fsetup.md](docs\u002Fsetup.md) (Japanese) covers CUDA and ROCm under WSL. It also covers one environment per GPU and splitting a sweep or steer lens across them with `gne worker`.\n\n## Quick start\n\nEvery command takes `-m \u003Cmodel.gguf>`, or reads `GNE_MODEL`. Results are stored under `~\u002F.gne` (override with `--root` or `GNE_ROOT`) and reused by later commands and by the UI.\n\n```bash\nexport GNE_MODEL=models\u002FQwen3.5-4B-Q8_0.gguf\n\ngne info                                   # architecture, warnings, what is supported\ngne verify                                 # equivalence tests against llama.cpp (run this first on a new arch)\ngne stats resid                            # residual norms per layer (needed for rel doses)\n\ngne lens logit --layer 22                  # first pass from the weights alone\ngne sweep --layer 22                       # 384 random neurons + 64 random controls, design main_rel\ngne rank --run L22-main_rel                # rank them by vocabulary mass\ngne neuron 22 2742                         # everything known about one neuron\n\ngne steer --unit 22:2742:+0.8 -p \"Who are you?\" --compare\ngne chat --unit 22:2742:+0.8 -p \"Tell me about yourself.\"\n\ngne reverse --contrast configs\u002Fcontrast\u002Fevil.yaml --verify    # candidates for a persona\ngne export-cvec --recipe configs\u002Frecipes\u002Fevil.yaml            # → ~\u002F.gne\u002Fexport\u002Fevil.cvec.gguf\n\ngne steer --recipe configs\u002Frecipes\u002Falien.yaml -p \"Who are you?\" --compare     # when: gen (only the generated tokens)\ngne steer --unit 22:2742:x4 -p \"Is there life on other planets?\" --compare   # gain: ×4 where the neuron fires\ngne eval --recipe configs\u002Frecipes\u002Falien.yaml                  # capability \u002F ppl \u002F mix \u002F persona change per dose\ngne gain --recipe configs\u002Frecipes\u002Falien_gain.yaml             # → ~\u002F.gne\u002Fexport\u002Falien_gain.lora.gguf\n\ngne direction --contrast configs\u002Fcontrast\u002Fevil.yaml --select auto   # direction per layer + layer choice\ngne steer --unit 16:dir:evil:+0.6 -p \"What do you think about people?\" --compare\ngne steer --ablate evil --system \"You are a cruel AI.\" -p \"Who are you?\" --compare\ngne ablate --direction evil                                   # → ~\u002F.gne\u002Fexport\u002Fevil-ablate.lora.gguf\ngne eval --compare-ablation evil                              # top-20 neurons x0 vs direction ablation\ngne steer --recipe configs\u002Frecipes\u002Falien.yaml --contrast configs\u002Fcontrast\u002Falien.yaml   # effect vs 16 random recipes (null_pct)\n\ngne heads contrast --contrast configs\u002Fcontrast\u002Fyandere_react.yaml   # rank attention heads (persona vs neutral)\ngne heads show 15 2                                           # one head: rank, W_O block logit lens, gains tried\ngne heads listen --heads 14:H0-3 --system @persona.txt -u \"I had lunch with a coworker.\" --direction wr_mix14   # attention from the reply, per-token contributions\ngne steer --unit 15:H2:x2 -p \"I had lunch with Emily today.\" --compare   # head gain (LoRA on W_O)\ngne eval --recipe R --reactivity configs\u002Fcontrast\u002Fyandere_react.yaml --head-controls 8\n```\n\n`export-cvec --verify` also checks that `llama-completion --control-vector` gives the same greedy output as `gne`. It looks for `llama-completion` in `build\u002F` and on `PATH`, or you can pass `--llama-completion`.\n\nThen use the vector with llama.cpp:\n\n```bash\nllama-cli -m models\u002FQwen3.5-4B-Q8_0.gguf --control-vector ~\u002F.gne\u002Fexport\u002Fevil.cvec.gguf\nllama-cli -m models\u002FQwen3.5-4B-Q8_0.gguf --control-vector-scaled ~\u002F.gne\u002Fexport\u002Fevil.cvec.gguf:0.6\nllama-cli -m models\u002FQwen3.5-4B-Q8_0.gguf --lora ~\u002F.gne\u002Fexport\u002Falien_gain.lora.gguf\n```\n\nA recipe is a small YAML file. `when`, `sampling` and gain units are optional:\n\n```yaml\nname: example\nmodel_id: qwen3.5-4b-64d32ead87\ndose_unit: rel\nwhen: gen                                       # all \u002F gen \u002F assistant \u002F after:\u003Ctext> \u002F ramp:\u003Ca>-\u003Cb>\nunits:\n  - {layer: 22, neuron: 2742, dose: +0.8}       # add: push the neuron's output direction\n  - {layer: 13, neuron: 7956, mode: gain, k: 0} # gain: remove this neuron's output (×0)\n  - {layer: 16, kind: direction, direction: evil, dose: +0.3}   # add a direction from gne direction\n  - {layer: 15, kind: head, head: 2, mode: gain, k: 2}           # attention head: scale its output (×2)\nablate: {direction: refusal, scale: 1.0}        # project a direction out of every residual writer (LoRA)\nsampling: {temperature: 1.0, top_k: 20, top_p: 0.95, min_p: 0.05, presence_penalty: 0.9}   # how to try it (no seed)\n```\n\n`sampling` is how the recipe is meant to be tried. It can also carry `dry_multiplier`, `dry_base` and `dry_allowed_length` (off by default), the DRY penalty that stops multi-turn conversations from repeating earlier replies word for word (llama.cpp's names, also accepted by the OpenAI-compatible endpoint). `steer` and `chat` use it (override with `--temperature`, `--top-k`, `--top-p`, `--min-p`, `--presence-penalty`, `--sample-seed`, or turn it off with `--greedy`), the UI chat offers it as \"Recipe's setting\", and the OpenAI-compatible endpoint uses it when a request sends no sampling values. A control vector cannot carry it, so `export-cvec` prints the matching llama-server flags. Without `sampling`, generation is greedy. `eval` is always greedy.\n\nRun `gne --help` and `gne \u003Ccommand> --help` for the full list. The help text is in Japanese.\n\n### Doses\n\nDoses are in **rel** units by default: `dose × median ‖residual‖ of the layer × unit(W_down[:, n])`.\n\nOn Qwen3.5-4B:\n- 0.5 slips a topic in, 0.8 takes over the topic, and 1.0 and above starts to break the text.\n- A dose of 3.0 deliberately breaks the text, to show what a neuron \"wants\" to write.\n- Layers 0–2 have small residuals, so rel doses do not transfer there.\n- Layer 0 cannot be pushed with a control vector, but a bake can push it.\n\n### How long things take\n\nQwen3.5-4B Q8_0 on an RTX 3080:\n\n| step | time |\n|---|---|\n| `stats resid` (all layers) | ~16 s |\n| `lens logit` | ~33 s per layer (CPU) |\n| `lens steer` (all 9216 neurons) | ~22 min per layer |\n| `sweep` `main_rel` | ~6.4 s per neuron (385 units in 48 min) |\n| `reverse` | ~6 s; ~4 min with `--verify` on 20 candidates |\n| `eval` (55 tasks × 5 doses) | ~41 s with `when: all`; ~90 s with `gen` |\n| `direction` (24 conversations) + `--select auto` (9 layers) | ~35 s + ~35 s |\n| `ablate` LoRA \u002F `--bake` Q8_0 (with `--verify`) | ~10 s \u002F ~40 s |\n\nA full `main_rel` sweep of one layer takes about 16 hours. Narrow candidates down with the lenses and reverse lookup first.\n\n### Mixture of Experts\n\nTested with granite-3.1-1b-a400m (`granitemoe`, 32 experts, 8 chosen) and LFM2-8B-A1B (`lfm2moe`, 32 experts, 4 chosen, router bias).\n\n```bash\ngne stats act -m M --layers 2-22                     # also counts how often each expert is chosen\ngne experts contrast -m M --contrast configs\u002Fcontrast\u002Fyandere_rules.yaml\ngne experts show -m M 12 37                          # usage, tokens it is chosen for, contrast rank, top neurons\ngne experts sweep -m M --layer 12                    # ablate (x0) and boost (x2) each expert, rank by changed words\ngne steer -m M --unit 12:E37:x0 -p \"...\" --compare   # remove expert 37 in layer 12\ngne steer -m M --unit 12:E37:N511:x2 -p \"...\" --compare\n```\n\n| Unit | Meaning | Applied as |\n|---|---|---|\n| `12:E37:N511:+0.8` | adds the neuron's write direction at every token (a direction push, not a neuron push) | control vector |\n| `12:E37:N511:x2` | multiplies the neuron's activation by k when expert 37 is chosen | LoRA stacked per expert |\n| `12:E37:x0` \u002F `x2` | removes or doubles expert 37's output | scale patch |\n| `12:E37:route:+2` | moves expert 37's router bias (only archs with a router bias, e.g. `lfm2moe`) | scale patch |\n| `12:S:N5:x2` | a shared-expert neuron | LoRA (like a dense neuron) |\n\nA scale patch rewrites the fp16 scales inside the quantization blocks of the affected rows, so a row or an expert's slab becomes exactly k times the original (k = 0 writes zero bytes). It works for Q4_0 to Q8_0, Q2_K to Q6_K, the IQ types except IQ1_M, and F16\u002FBF16\u002FF32. Patches go to a working copy under `~\u002F.gne\u002Fwork\u002F` (the original GGUF is never touched) and are reverted after each use; reloading the copy takes about 0.3 s for a 1.4 GB model and 0.9 s for 5 GB. `steer`, `eval`, the UI chat and the OpenAI-compatible endpoint apply them automatically. Column directions (`W_down[:, n]`) cannot be patched this way. Inside MoE layers, sweeps, lenses and reverse lookups number neurons `e × n_ff_exp + n`.\n\n### Across quantizations\n\nA neuron found on one quant carries over to other quants of the same model. Checked with L22 N2742 (the \"alien\" neuron, found on Qwen3.5-4B Q8_0):\n\n| | Q4_K_M | Q3_K_M | UD-IQ2_M |\n|---|---|---|---|\n| W_down column vs Q8_0 (cos) | 0.998 | 0.998 | 0.974 |\n| same index = same neuron (301 sampled) | all | all | all |\n| logit-lens top words | same | same | almost the same |\n| residual norm at L22 (Q8_0: 23.69) | 23.65 | 23.30 | 23.76 |\n| \"Who are you?\" at +0.8 | \"I am Extracillary, an advanced extraterrestrial intelligence…\" | \"I am an extraterrestrial intelligence from the planet of Aliens.\" | \"I am an extraterrestrial being.\" |\n| 「あなたは誰ですか？」 at +1.0 | 「私は、地球外生命体です。」 | 「私は、地球外生命体です。」 | breaks down |\n\n- A control vector exported on Q8_0 can be used as-is on the other quants. On Q4_K_M and Q3_K_M it gave exactly the same text as re-deriving the vector on that quant.\n- The persona stays; only the made-up details change (Q8_0 says \"Extrali … Xeno-888\").\n- At 2 bits the usable dose range narrows: +1.0 already breaks the Japanese answer.\n- Q3_K_M stores this layer's `ffn_down` as Q4_K, which is why its column matches Q4_K_M.\n\n## Web UI\n\n```bash\ngne serve -m models\u002FQwen3.5-4B-Q8_0.gguf      # http:\u002F\u002F127.0.0.1:8791\n```\n\n![Neuron panel with up\u002Fdownstream wiring in the front view](docs\u002Fimages\u002Fui-neuron.png)\n\nThe UI has four views:\n- **map:** every layer as a sheet of neurons, with overlays.\n- **layers:** per-layer summaries.\n- **eval:** saved evals as a chart and a table per dose, sample outputs, and a button to eval the current recipe.\n- **embed:** a UMAP of rollout texts.\n\nClick a neuron or a list row to open its panel, which has four tabs:\n- **Examples:** rollouts per prompt and dose.\n- **Chat:** live 3-column steering, with a `when` selector and a system prompt field. The recipe tab can add an ablation (direction, scale, layers). With a model that can hear, the 🎤 button records your voice in the browser and sends it as the user turn (press, speak, press again; text in the box goes along). 🔊 reads the right column's reply aloud with the browser's voice. The microphone works when the UI is opened at `http:\u002F\u002F127.0.0.1` or `localhost`.\n- **Details:** lens results, per-dose metrics, connections, and the evals that include the neuron.\n- **Web:** the multi-hop wiring.\n\nTo compute missing data (lenses, sweeps, connections, layouts), right-click a layer label. Jobs run in the background and give way to chat when it needs the GPU. The language switch is at the top right; you can also open the UI with `?lang=en` or `?lang=ja`.\n\nThe server binds to 127.0.0.1 only. It also rejects requests that arrive with a foreign Host header and cross-origin writes.\n\n### OpenAI-compatible endpoint\n\n`gne serve --openai` adds `\u002Fv1\u002Fmodels` and `\u002Fv1\u002Fchat\u002Fcompletions` (streaming or not) to the same server, so other apps can use a recipe with `when` or gain units. Pass the recipe in a `gne` field (`extra_body` in the OpenAI SDK):\n\n```python\nfrom openai import OpenAI\nclient = OpenAI(base_url=\"http:\u002F\u002F127.0.0.1:8791\u002Fv1\", api_key=\"unused\")\nr = client.chat.completions.create(\n    model=\"any\", messages=[{\"role\": \"user\", \"content\": \"Who are you?\"}],\n    extra_body={\"gne\": {\"units\": [\"22:2742:+0.8\"], \"when\": \"gen\"}})\n```\n\nFor a model that can hear (LFM3-Omni, with its `mmproj-*.gguf` next to the model or given with `--mmproj`), a user message can carry speech as an `input_audio` part with base64 `data` (wav, mp3 or flac). The endpoint does not read local paths. The model's card asks for the system prompt `Respond in text.` for text replies, or `Perform ASR in English.` for a transcript:\n\n```python\nimport base64\naudio = base64.b64encode(open(\"speech.wav\", \"rb\").read()).decode()\nr = client.chat.completions.create(\n    model=\"any\", messages=[{\"role\": \"system\", \"content\": \"Respond in text.\"},\n                           {\"role\": \"user\", \"content\": [{\"type\": \"input_audio\", \"input_audio\": {\"data\": audio, \"format\": \"wav\"}}]}],\n    extra_body={\"gne\": {\"recipe\": \"my_recipe\"}})\n```\n\nOn the command line, `gne steer --audio speech.wav --system \"Respond in text.\" --recipe ...` does the same.\n\nInstead of `units`, `\"recipe\"` takes the name of a recipe saved under `~\u002F.gne\u002Frecipes`, and `\"scale\"` multiplies its doses. Generation is greedy unless the request sets `temperature` above 0 or any of `top_p`, `top_k`, `min_p`, `presence_penalty` and `seed`, or the recipe has `sampling`; then it samples in llama.cpp's order. Values left out come from the recipe's `sampling`, or llama-server's defaults. Tools and logprobs are not supported. Requests share the GPU with the UI's chat.\n\nThe server keeps no state, so the strength can change at any point in a conversation: send `\"gne\": {\"recipe\": \"yandere_qwen38_27b\", \"scale\": 0.7, \"when\": \"ramp:0.5-1.5\"}` with a message and change `scale` and `when` whenever you like. [`examples\u002Fstrength\u002Fstrength_chat.py`](examples\u002Fstrength\u002Fstrength_chat.py) keeps a strength from 0 to 100 that you change by typing `\u002Fs 70` in the chat, or that another program changes through a file (a game event, a gauge on a stream). Before each message it maps the strength to `when` and `scale` using the measurements in [docs\u002Fverification.md](docs\u002Fverification.md) (F94, F96). The whole conversation is read again on every request, so a new strength also changes how earlier turns are read (with `all` and `assistant`), and long histories make each reply start later.\n\n## Using it from an agent (MCP)\n\n```bash\nclaude mcp add gne -- gne mcp --model \u002Fpath\u002Fto\u002Fmodel.gguf\n```\n\nThe tools are `model_overview`, `layer_summary`, `neuron`, `search`, `steer`, `steer_lens`, `fires`, `reverse`, `recipe_save`, `export_cvec`, `gain_export`, `bake`, `eval`, `direction`, `ablate_export`, `head`, `heads_contrast`, `attention`, `set_name` and `job_start` \u002F `job_status` \u002F `job_cancel`.\n\nIf the web UI is open, add `--ui-url http:\u002F\u002F127.0.0.1:8791`. With it, the agent can highlight and annotate neurons and open them in the browser (`ui_highlight`, `ui_note`, `ui_open`). When the UI already holds the model on the same GPU, also add `--n-gpu-layers 0`.\n\n[docs\u002Fagent_playbook.md](docs\u002Fagent_playbook.md) describes how an agent should judge results: compare against controls, distrust cheap effects such as a changed self-name, and separate facts from interpretation.\n\n## Tests\n\n```bash\nuv pip install --python .venv\u002Fbin\u002Fpython -e '.[dev,ui,mcp]'\npython -m playwright install chromium\npython -m pytest -q\n```\n\n- Model tests use `GNE_TEST_MODEL`, which defaults to `models\u002FQwen3-0.6B-Q8_0.gguf`.\n- Browser tests use the Playwright Chromium; `GNE_CHROME` sets its path.\n- MoE tests use `GNE_TEST_MOE` (default `models\u002Fgranite-3.1-1b-a400m-instruct-Q8_0.gguf`) and, for router bias, `GNE_TEST_MOE_BIAS` (default `models\u002FLFM2-8B-A1B-Q4_K_M.gguf`).\n- The real-audio test needs `GNE_TEST_OMNI` (an LFM3-Omni GGUF with its mmproj next to it) and `GNE_TEST_AUDIO` (a wav of speech). The other audio tests use the default model.\n- Tests whose model or browser is missing are skipped.\n\n## Documentation\n\nThe documentation is in Japanese:\n- [docs\u002Fverification.md](docs\u002Fverification.md): what was checked against llama.cpp, and the findings.\n- [docs\u002Fmeasurements.md](docs\u002Fmeasurements.md): timings and throughput.\n- [docs\u002Fagent_playbook.md](docs\u002Fagent_playbook.md): how to read the results.\n- [docs\u002Fsetup.md](docs\u002Fsetup.md): CUDA \u002F ROCm under WSL, one environment per GPU, and workers.\n- [docs\u002Fpersona_workflow.md](docs\u002Fpersona_workflow.md): building a persona from neurons, step by step (with the scripts in `examples\u002Fpersona_neurons\u002F` and the yandere case as the worked example), and deriving attention heads and neurons from example dialogues.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\nBundled third-party files:\n- three.js (MIT), in `gne\u002Fui\u002Fstatic\u002Fvendor\u002Fthree\u002F`.\n- The default statistics corpus (`configs\u002Fcorpus\u002Fdefault.txt`), made of public-domain texts from Project Gutenberg and Aozora Bunko. Details are in [configs\u002Fcorpus\u002FREADME.md](configs\u002Fcorpus\u002FREADME.md).\n",2,"2026-10-31 04:26:16","CREATED_QUERY"]