How to configure deliberation: the two expert modes, the config.json schema and its
sections, OpenRouter model records, and opt-in session persistence. For provider
internals, environment variables, and manual MCP setup, see TECHNICAL.md.
Every expert supports two modes, chosen automatically from your request:
| Mode | Sandbox | Use when |
|---|---|---|
| Advisory | read-only |
Analysis, recommendations, reviews |
| Implementation | workspace-write |
Making changes, fixing issues |
OpenRouter models are declared in ~/.config/deliberation/config.json - the canonical
XDG path (Windows: %APPDATA%\deliberation\config.json). You can override the path
with DELIBERATION_CONFIG. The file is the live single source of
truth: changes to models, routing, or the providers.openrouter block hot-reload
without restarting Claude Code. Toggling a built-in provider (codex / gemini / grok)
still requires /setup.
The config sections: providers (transport / connection per provider),
models (named model records keyed by id), routing (fan-out policy),
consensus (arbiter = who synthesizes the verdict; optional blindVote for a blind
arbiter pre-vote), sessions (opt-in run persistence; default off - see
Session persistence), debug (opt-in debug log; default off),
orientation (repo bundle for file-blind providers; default on), and dashboard
(opt-in run journal and local browser view; default off - see Dashboard).
The $schema key gives editors validation and autocomplete - VS Code needs no extension.
Minimal example:
{
"$schema": "https://raw.githubusercontent.com/antonbabenko/deliberation/master/config/config.schema.json",
"version": 1,
"providers": {
"defaults": { "timeout": 600000 },
"codex": { "enabled": true },
"gemini": { "enabled": true, "model": "auto-gemini-3" },
"grok": { "enabled": true, "apiKeyEnv": "XAI_API_KEY", "model": "grok-4.6", "reasoningEffort": "high" },
"openrouter": {
"enabled": true,
"apiKeyEnv": "OPENROUTER_API_KEY",
"apiBase": "https://openrouter.ai/api/v1",
"defaultModel": "openai/gpt-4.1-mini",
"defaults": { "reasoningEffort": "medium" }
},
"ollama": { "enabled": true, "apiBase": "http://localhost:11434/v1" },
"lmstudio": { "enabled": true, "apiBase": "http://localhost:1234/v1" }
},
"models": {
"gpt-4-or": {
"provider": "openrouter",
"model": "openai/gpt-4.1",
"askAll": true,
"consensus": false
},
"claude-arb": {
"provider": "openrouter",
"model": "anthropic/claude-haiku-4-5",
"askAll": true,
"consensus": true,
"reasoningEffort": "high"
},
"local-llama": {
"provider": "ollama",
"model": "llama3.3:70b",
"askAll": true,
"consensus": false
}
},
"routing": { "maxFanout": 3 },
"consensus": { "arbiter": { "model": "claude-arb" }, "blindVote": true, "maxRounds": 5, "maxWallMs": 1800000 },
"sessions": { "persist": false, "maxRecords": 200, "maxAgeDays": 30 },
"debug": { "enabled": false },
"dashboard": { "enabled": false }
}debug.enabled (default false) appends one JSON line per provider call and per consensus
round to <XDG cache>/deliberation/debug.jsonl (override with debug.path or
DELIBERATION_DEBUG_LOG): latency, reasoning effort, HTTP token usage, and voting/approval
outcomes - never prompts, responses, or issue text. Useful for debugging slow runs.
Deliberation supports local model inference through Ollama and LM Studio via their OpenAI-compatible HTTP APIs:
- Ollama: Configured via
providers.ollama(default endpointhttp://localhost:11434/v1). - LM Studio: Configured via
providers.lmstudio(default endpointhttp://localhost:1234/v1).
Both local providers default to keyless (no Authorization header). If
your local or remote instance requires authentication, set apiKeyEnv on
the provider block. Note that apiBase for both providers is read once
at server startup; changing it requires restarting deliberation.
Declare models in models targeting provider: "ollama" or
provider: "lmstudio":
"models": {
"my-ollama": {
"provider": "ollama",
"model": "llama3.3:70b",
"askAll": true,
"consensus": true
}
}Important
Migration note (v3.31.0+): Previously, any model record with a custom
apiBase received OPENROUTER_API_KEY. Starting with v3.31.0, records
with a custom apiBase default to keyless. Authenticated proxies must set
models.<id>.apiKeyEnv to the name of the environment variable containing
their key. Keyless proxies leave it unset.
Note: Per-record apiBase and apiKeyEnv are honored ONLY by the standalone
OpenRouter bridge (e.g. /ask-openrouter); the unified deliberation server
ignores them.
You can specify OpenRouter presets by ID (e.g. cheapest-first or @preset/cheapest-first). OpenRouter presets bundle provider routing preferences (such as sorting available provider endpoints by price, allowing fallbacks, and denying data collection), system prompts, or sampling parameters.
Specify presets in config.json at provider or model level:
- Global provider default:
providers.openrouter.presetorproviders.openrouter.defaults.preset:"providers": { "openrouter": { "preset": "cheapest-first" } }
- Per-model alias:
models.<id>.preset:A model record can specify"models": { "cheapest": { "provider": "openrouter", "preset": "cheapest-first" }, "fast-gpt": { "provider": "openrouter", "model": "openai/gpt-4o", "preset": "cheapest-first" } }
presetalone (the preset determines or routes the model), or pairpresetwith an explicitmodel(OpenRouter applies the preset's provider routing and parameter preferences to that specific model). - Per-call override: Pass
preset: "preset-id"toask-openrouter,ask-one, orconsensus.
Why use presets?
- Save cost: Enforce provider routing preferences like
"sort": { "by": "price" }to dynamically route requests to the lowest-priced provider host for a model, reducing inference spend while maintaining reliability with automated fallbacks ("allow_fallbacks": true). - Privacy & Compliance: Enforce policies like
"data_collection": "deny"across all queries upstream. - Provider failover: Preconfigure fallback cascades in OpenRouter without modifying local client configs.
Browse model slugs at openrouter.ai/models;
the model field takes any slug listed there. Each record's provider must be
"openrouter" in v1 (codex / gemini / grok are managed by their own CLI / API).
providers.gemini.model and providers.grok.model pin those two providers without
touching their own config files. Precedence for both: per-call model argument, then
this key, then the env var (GEMINI_DEFAULT_MODEL / GROK_DEFAULT_MODEL), then the
built-in default. Both are read once at MCP start, so changing them needs a restart.
providers.grok.reasoningEffort (low / medium / high / none) sets how hard Grok
reasons, on the same ladder: per-call reasoning_effort, then this key, then
GROK_REASONING_EFFORT, then high. none omits the field entirely so the model uses
its own default. Unlike the model pin this one is read per call, so it picks up an edit
without a restart. You can also decouple reasoning effort between standalone questions and consensus debates: consensusReasoningEffort (low / medium / high / none) can be configured under providers.grok.consensusReasoningEffort (as well as providers.codex.consensusReasoningEffort, providers.defaults.consensusReasoningEffort, and providers.openrouter.defaults.consensusReasoningEffort). During /consensus and consensus-step, consensus reasoning effort is used; during /ask-all and standalone ask tools, standard reasoningEffort applies. Gemini has no equivalent key - agy fuses reasoning effort into the
model id itself (gemini-3.6-flash-high vs gemini-3.6-flash-low), so you change it by
changing providers.gemini.model.
For Gemini, run agy models to see the catalog on your machine - ids fuse family and
reasoning effort (gemini-3.6-flash-high); a bare family is rejected with requires --effort. The shipped default is the portable auto-gemini-3 router alias, because a
concrete id that does not exist locally fails every call. On agy 1.1.x that alias is
not in the catalog either; agy rejects it with invalid model selection, and the bridge
then retries the call without the pin (agy uses ~/.gemini/settings.json), prints one
stderr warning per process, and reports the run as pinDropped / model: "agy-settings-default". Only that shipped alias falls back: a concrete id you pin that
agy does not know fails as model-not-allowed with agy's catalog in the message. Pin a
concrete id if you have one: the alias routes server-side and agy's catalog also carries Claude and
GPT-OSS entries, so a routed call can seat a non-Gemini model in the Gemini slot and
quietly cost you the cross-model independence that /ask-all and /consensus rely on.
Codex takes providers.codex.model and providers.codex.reasoningEffort
(none/minimal/low/medium/high/xhigh/max/ultra). They become codex exec --model <id> and
-c model_reasoning_effort="<value>", so they win over ~/.codex/config.toml. Leave
either one out and codex uses its own setting from that file. Not every model accepts every
effort, and a value the model or your codex version rejects fails the GPT call rather than
falling back. A malformed value is ignored with one stderr warning at startup. The model pin is read once at MCP
startup; reasoning effort can also be overridden per call or tuned for debates via consensusReasoningEffort.
"codex": { "enabled": true, "model": "gpt-5.5", "reasoningEffort": "high" }GPT and Gemini launch a real CLI (codex, agy); Grok and OpenRouter are HTTP bridges and
launch nothing, so they need none of this.
npm install -g on Windows installs codex.cmd, a shell shim rather than an executable, and
Node cannot launch one directly. deliberation resolves the CLI before spawning it - preferring a
real .exe on PATH, otherwise running the package's own entry point with Node - so a standard
npm install works with no setup.
If it still cannot find your CLI, name it explicitly:
"env": {
"CODEX_BIN": "C:\\Path\\To\\codex.exe",
"AGY_BIN": "C:\\Path\\To\\agy.exe"
}Point them at an executable, not at a .cmd shim or a .js file: a full path is passed
through and spawned as given, with no resolution and no shell. When only a shim can be found, the
call fails naming that shim and the variable to set, instead of reporting the CLI as missing.
Three things differ in a web container, and the plugin now handles each:
-
The host caps every tool call. Claude Code on the web exports
MCP_TOOL_TIMEOUT=60000, which kills any MCP call after 60s - far below the provider ceilings below. Claude Code applies a per-servertimeoutahead of that variable, so the plugin manifest declares"timeout": 1800000(30 min) on every deliberation server and mirrors it into the server's env asMCP_TOOL_TIMEOUT(the process inherits the host's 60000 otherwise, and deliberation's own clamp would cancel the override). Nothing to configure. If atimeoutresult still namesMCP_TOOL_TIMEOUT=60000, the install is older than this manifest:claude plugin update deliberation@antonbabenkoand start a new session. Installing by hand through.mcp.jsonon a capped host: add the same"timeout": 1800000and"env": {"MCP_TOOL_TIMEOUT": "1800000"}to the server entry./deliberation:doctorexplains the shell's residual value. -
Codex needs a credential of its own in the container.
OPENAI_API_KEYis never used for codex and never reaches thecodex execchild. A ChatGPT credential (acodex loginorCODEX_ACCESS_TOKEN) always wins, and aCODEX_API_KEYin the env is then dropped so it cannot bill the API instead. Do not copy your laptop's~/.codex/auth.jsoninto the container: a ChatGPT login's refresh token works once, so when either copy refreshes (about every 8 days, or on a 401), the other one fails withYour access token could not be refreshed because your refresh token was already used. Restoring the same copy from a secret at every session start fails the same way. Pick one of these:- ChatGPT Plus / Pro: nothing to set up beyond turning on device code login in
ChatGPT's security settings. The first GPT call in a session that has no working login
starts
codex login --device-authand shows its link and one-time code within a second or two (errorKind: "auth"), each on its own line to copy./ask-all,/consensusand/ask-gptdo this BEFORE they dispatch: they show the link and code, ask you to approve, and then run with GPT on the panel. On any other GPT call, approve in the browser and re-run; GPT answers from the next call (in/consensus, from the next round). A host that shows MCP dialogs also gets one, but nothing waits for it - decline there if you did not ask for GPT and that login ends at once. The login belongs to that container alone, so it never conflicts with your laptop. The environment's network allowlist must reachauth.openai.comandchatgpt.com. To log in ahead of time, or if the agent does not call GPT, run/deliberation:codex-login: it starts the same login (thecodex-logintool) and shows the link and code without asking GPT anything. - ChatGPT Business / Enterprise: create a Codex access token and set
CODEX_ACCESS_TOKENin the environment's variables. It never refreshes (it expires on the date the workspace allows, 90 days by default), so the same value works in every session. - Seeded
auth.json(fallback): if a setup script must restoreauth.jsonfrom a secret, seed it from a login made only for that purpose, never your laptop's:d=$(mktemp -d); CODEX_HOME="$d" codex login --device-auth; jq -c . "$d/auth.json", and store that output. (Keep$d: a bareCODEX_HOME=...prefix lasts one command, so a later$CODEX_HOME/auth.jsonwould be your laptop's own login.) It works in every session until the first refresh (about a week after the login). After that, re-seed it. - API billing: export
CODEX_API_KEY, or runprintenv CODEX_API_KEY | codex login --with-api-keyonce.
When a login can no longer refresh (a copied
auth.json), the same thing happens: the first GPT call starts a fresh device login, shows its code, and retries once you approve. - ChatGPT Plus / Pro: nothing to set up beyond turning on device code login in
ChatGPT's security settings. The first GPT call in a session that has no working login
starts
-
No
agy. The standalone Gemini bridge refuses to start (CONNECTION_CLOSEDin the host's server list); the unified server lists gemini underpanel.unavailableand the panel runs without it.
Every provider ships a different built-in ceiling (codex 600s, gemini 300s, grok 180s, OpenRouter 180s). To raise them all at once, set one key:
"providers": {
"defaults": { "timeout": 600000 }
}Override a single provider with providers.<name>.timeout - except OpenRouter, whose
slot is the defaults block it already owns (providers.openrouter.defaults.timeout).
A pinned model's models.<id>.timeout still beats all of it. These are read once at MCP
start, so a change needs a restart; the models map keeps hot-reloading.
This covers both paths: the tools on the unified server, and the standalone bridges behind
/ask-grok, /ask-gemini, and /ask-openrouter.
If a fan-out drops a model at almost exactly 180000 ms, it hit the built-in ceiling -
that is the knob to turn. If it drops at MCP_TOOL_TIMEOUT - 5000 ms and the message says
Host MCP_TOOL_TIMEOUT=... caps every MCP tool call, the host's cap is the knob - raise it
where the host is launched (see above); no config key reaches above it.
A rate-limited call (HTTP 429) is retried once, waiting for the upstream's Retry-After
when it sends one. A timeout is deliberately not retried: the call may already have
burned tokens, and a slow-but-good answer should not be thrown away.
reasoningEffort (low / medium / high) sets how hard a reasoning model
thinks. Put it on providers.openrouter.defaults to cover every model, or on a single
record to override the default for that one. Precedence runs call argument over
per-record override over defaults.
/ask-all includes records where askAll !== false, capped to routing.maxFanout.
/consensus includes records where consensus === true, with no fanout cap (a warning
is emitted when more than 3 models participate). consensus.arbiter picks who synthesizes:
a shorthand string ("auto" / "host" / "codex" / "gemini" / "grok") or
{ "model": "<id>" } naming a record (even an out-of-panel one). consensus.blindVote
(boolean, default false) runs the arbiter cold in parallel with the panel to reduce
anchoring - concrete-arbiter / non-host mode only. consensus.maxRounds (integer, default
5, clamped to 50) caps the multi-round convergence loop used by the consensus /
consensus-step tools (a per-call maxRounds overrides it). consensus.maxWallMs (integer
ms, default 1800000 = 30 min) sets a global wall-time budget for the server-side
provider-arbiter loop (consensus tool only); when spent, the loop stops before the next
round and returns UNRESOLVED with stopReason: "budget-exhausted" - it never aborts an
in-flight call. Implementation tasks always route to Gemini - GPT, Grok, and OpenRouter are advisory-only.
For the full schema, the $schema / VS Code validation story, apiBase override matrix
(Ollama, vLLM, LM Studio, HuggingFace), file-attachment caps, session model persistence,
consensus cost model, and error kinds, see
TECHNICAL.md - OpenRouter bridge.
Opt-in, default off: nothing about your questions or results is written to disk
unless you turn it on. Enable it with a sessions block in the config:
"sessions": { "persist": true, "maxRecords": 200, "maxAgeDays": 30, "captureText": false }persist(boolean, defaultfalse) - when true, each/consensusrun (including the host-driven loop, on a terminal converged/unresolved transition) and each/ask-allrun is saved as one JSON file and the tool result includes asessionId. When off, thesession-*tools report "persistence disabled".maxRecords(default200) - keep at most this many newest records; older ones are trimmed after each write. Use-1for unlimited (never trim by count).maxAgeDays(default30) - delete records older than this. Use-1for unlimited (never delete by age).captureText(boolean, defaultfalse) - when true (andpersiston), the record also stores each provider's raw response body. Default off keeps only the question and the verdict/issue summaries. Captured bodies are secret-scrubbed (mandatory) plus a best-effort email-PII strip; they are plaintext on local disk, and the metrics-only debug log never receives body text. Forward-gating: turning it off stops new capture but does not purge records already written.
Records live at <XDG cache>/deliberation/sessions/<id>.json (macOS/Linux:
~/.cache/deliberation/sessions; Windows: %LOCALAPPDATA%\deliberation\sessions),
written atomically with mode 0600. Override the directory with DELIBERATION_SESSIONS.
API-key shapes are scrubbed and each opinion/verdict is capped (~100 KB) before writing;
attachment paths are stored (scrubbed), never file bodies.
Three MCP tools operate on the store (they appear always but report "disabled" until
persist is on): session-get (fetch a record), session-revisit (re-run a record's
original question with the current providers/config and save a linked child record),
and session-annotate (append a note to the audit trail). Full details:
TECHNICAL.md - Session persistence.
Distinct from "session model persistence" above, which is OpenRouter multi-turn (
threadId) reuse - unrelated to this on-disk store.
A local, read-only browser view of deliberation runs: each /consensus, /ask-all, and
/ask-* run drawn as a state graph while it runs (which state is active, what each
provider was sent and returned, timing, tokens), plus run history, the effective config,
provider health, usage stats, and an Analyzer (which models agree more than they add,
per-project failures and latency, request size vs timeouts). Opt-in, default off, and it needs a browser on the
same machine as the MCP server, so it does not work from Claude Code on the web.
-
Turn on the journal in the config (it hot-reloads, no restart needed):
"dashboard": { "enabled": true }
From now on every run writes one file under
<XDG cache>/deliberation/runs/(override withDELIBERATION_RUNS). Runs made before this are not recorded; oldersessionsrecords show up as summaries. -
Start the dashboard. In Claude Code:
/deliberation:dashboardThe command runs a bash script, so on Windows it needs Git Bash; without it, use the terminal form below. Anywhere else (or from a terminal):
deliberation-mcp dashboard # npm package node server/mcp/index.js dashboard # from a checkout
It prints
Deliberation dashboard: http://127.0.0.1:7717/?t=<token>and opens your browser. The token is a per-start credential: keep that line to yourself. Running the command again while it is up reprints the same URL. -
Run
/consensusor/ask-alland watch the graph fill in.
Each run records the project it was called from: the git repo of the tool call's cwd
(or the MCP server's directory), grouped by its git remote as org/repo, so every clone
and worktree of a repo lands in one group, with the checkout dir as the workspace (shown as
acme/app (/tmp/app-review)). The Runs tab shows it as a column; its project filter is
a tree where you pick a whole repo or one workspace, and the Analyzer tab uses the same
filter.
Restart or stop it:
/deliberation:dashboard restart
bash scripts/commands/dashboard-restart.sh # stop the running one, start fresh, print the new URL
npm run dashboard:restart # the same, from a checkout
deliberation-mcp dashboard --stop # stop onlyA restart always prints a new token. --stop only signals a process it can prove is the
dashboard (its command line and start time match the pidfile); otherwise it names the pid
and leaves it running (restart exits 3), so a reused pid is never killed.
Options, all in the dashboard block:
| Key | Default | Meaning |
|---|---|---|
capture |
"metadata" |
"metadata" records states, timings, tokens, verdicts, and error kinds. "content" also records prompts, responses, arbiter text, and final reports (secret-scrubbed, capped at ~100 KB each) as plaintext on local disk. |
showPII |
false |
Off: emails, your home path and user name, IP addresses, and 12-digit account ids are masked in everything the browser receives. The journal itself is not rewritten. |
port |
7717 |
Local port; --port N overrides it for one start. |
maxRuns / maxAgeDays |
200 / 30 |
Journal retention; -1 means unlimited. |
Troubleshooting:
dashboard is disabled: set dashboard.enabled to true in <path>- step 1 is missing, or the config at that path is not the one you edited.port 7717 is in use; pass --port- another program holds the port; start with--port <n>, or setdashboard.port.port <n> is in use and did not answer as this dashboard; the pidfile names pid <pid>, possibly an older dashboard; ...- usually a dashboard started before an update is still running. Check what that pid is (ps -p <pid>); if it is the old dashboard, end it and start again. Otherwise start with--port <n>.deliberation <version> at <path> has no dashboard; update it: ...-/deliberation:dashboardfound an install older than the dashboard; update the plugin and run/reload-plugins.- To stop it, run
deliberation-mcp dashboard --stop(or Ctrl+C in the terminal that runs it).pid <n> from the pidfile was left alone: ...means that pid is not provably the dashboard (for example one started before this version, which recorded no start time): check it withps -p <n>and end it yourself if it is.
Security details (loopback bind, Host/Origin checks, token and cookie, redaction) are in TECHNICAL.md - Dashboard.
For provider defaults, environment variables, and manual MCP setup, see TECHNICAL.md.