Skip to main content

Configuring Janus

Everything Janus does beyond keeping the books is set in one file, janus.config.json. This page explains where it is, how it is laid out, and how to change it, then gives complete configs for the common setups. For every field, see the configuration reference.

Where the file is​

Janus reads janus.config.json from the folder you start it in, or the file you name with -config:

janus -config ~/janus/janus.config.json

If the file does not exist, Janus writes one with the defaults and carries on. Every command (doctor, stats, audit, and the rest) takes the same -config, and reads the same logs, wherever you run it from: a relative logging.dir is taken relative to the config file, not to the current folder.

What is in it​

The file Janus writes on its first run:

{
"version": 1,
"listen": "127.0.0.1:8787",
"allowRemote": false,
"upstream": { "baseUrl": "https://api.anthropic.com" },
"local": { "baseUrl": "http://studio.local:1234" },
"features": { "route": false, "escalate": false, "compress": false, "guard": false },
"logging": { "dir": "logs", "capture": true, "retainDays": 7, "maxMegabytes": 1024 }
}

studio.local is a placeholder. http://studio.local:1234 is the Janus developer's own LM Studio machine, and the default only because Janus was built against it. Set local.baseUrl to where your LM Studio runs: http://127.0.0.1:1234 on the same machine, or http://<its address>:1234 on another. janus doctor says whether it answers.

SectionWhat it is for
listen, allowRemoteThe address Claude Code connects to. Loopback only, unless you set allowRemote.
upstreamWhere Claude is: the Anthropic API, or a gateway in front of it.
localWhere LM Studio is (baseUrl), and the models Janus may use there (models).
featuresThe switches. route turns on aliases and routing rules; guard the secret and private-file protection; compress the digests. escalate is reserved and must stay false.
routingRules that decide which requests a local model answers.
guardWhat the guard does: replace secrets, and which files never leave.
compressSettings for digests.
loggingWhere the ledger and captures go, and how long captures are kept.
pricingPrices to add or correct, for the cost estimates.

A field you leave out keeps its default, so a config only needs what you change. A field Janus does not recognise stops it starting, so a typo is caught rather than ignored:

janus: /Users/you/janus/janus.config.json: json: unknown field "lisen"

Changing it​

  1. Edit the file.
  2. Run janus doctor. It reads the config as Janus will, reports every problem it finds, and checks what the config points at: that LM Studio answers, that each local model exists there, and how much context it is loaded with.
  3. Restart Janus. It reads the config only when it starts.

When Janus starts, it prints what it will do with each request. Check that it says what you meant:

Routing model "janus-local" -> local qwen/qwen3.8-27b
rule main: class main -> upstream, or local qwen/qwen3.8-27b if Claude cannot answer
rule subagents: class subagent -> upstream, or local qwen/qwen3.8-27b if Claude cannot answer

For one-off changes, three settings can be overridden without editing the file, by flag or environment variable: -listen or JANUS_LISTEN, -upstream or JANUS_UPSTREAM_URL, -local or JANUS_LOCAL_URL.

How a request is routed​

With features.route on, Janus decides where each request goes in this order:

  1. An alias. A request for a model named in local.models (such as janus-local) goes to that local model.
  2. The rules, in the order they are listed. The first rule a request matches decides. Rules match on the request's class (main, subagent, and a few of Claude Code's background requests), the model asked for, its size, and whether it offers tools.
  3. Anything else goes to Claude untouched.

A rule's route says what happens to the requests it matches:

routeWhat happens
upstreamClaude answers. With ifDown, the local model answers instead when Claude cannot.
shadowClaude answers, and the local model answers the same request on the side, for comparison only.
local-then-escalateThe local model answers first. Janus checks the answer, and if it is broken, or takes longer than maxSeconds, Claude answers instead.
localThe local model answers. If it cannot take the request at all, Claude does.

A rule never makes a request fail: whenever the local model cannot help, the request goes to Claude. To see where a particular request would go, use janus route.

Complete configs​

Each of these is a whole janus.config.json. Start from the one closest to what you want. All of them assume LM Studio on the same machine, with qwen/qwen3.8-27b; change baseUrl and model to yours.

Just keep the books​

Janus forwards everything untouched and writes a ledger line per request. This one also turns captures off, so no prompts or code are kept on disk:

{
"listen": "127.0.0.1:8787",
"logging": { "dir": "logs", "capture": false }
}

Use this for a day or two before changing anything, so that janus stats can show you where the tokens go.

Subagents on the local model, with Claude as the net​

Claude Code's subagents, the helpers it starts to search and read, are the work a local model does best. Here the local model answers them first, and Claude takes over whenever an answer is broken or slow. The main conversation stays with Claude.

{
"local": {
"baseUrl": "http://127.0.0.1:1234",
"models": { "janus-local": { "model": "qwen/qwen3.8-27b" } }
},
"features": { "route": true },
"routing": [
{ "name": "subagents", "class": "subagent", "route": "local-then-escalate", "local": "janus-local" }
]
}

Expect subagents to be slower: a local turn takes tens of seconds where Claude takes a few. Watch the "Local first" table in janus stats: if most attempts are rejected, the model is not up to the work, and you are paying for both. The subagents recipe has more, including how to shadow the model first.

Keep working when Claude is down​

Claude answers everything as usual. When Claude cannot, because it is unreachable, overloaded, or you have used up your plan's limit, the local model answers instead:

{
"local": {
"baseUrl": "http://127.0.0.1:1234",
"models": { "janus-local": { "model": "qwen/qwen3.8-27b" } }
},
"features": { "route": true },
"routing": [
{ "name": "main", "class": "main", "route": "upstream", "ifDown": "janus-local" },
{ "name": "subagents", "class": "subagent", "route": "upstream", "ifDown": "janus-local" }
]
}

This costs nothing while Claude is healthy: LM Studio is not even asked. During an outage Janus loads the model if it needs to, queues requests for it, and cuts a conversation too long for it down to its start and its latest turns. It is slower and less capable than Claude, but the session keeps going. In auto mode, press Shift+Tab to approve tool calls yourself while it lasts; using Janus shows what an outage looks like. The outage recipe has the details.

Keep secrets and private files on your machine​

The guard replaces credentials it recognises with placeholders before a request leaves, and puts them back in the answer. The files named under localOnly never reach Anthropic, whatever is in them:

{
"features": { "guard": true },
"guard": {
"redact": true,
"localOnly": { "paths": [".env*", "secrets/**", "*.pem"], "action": "withhold" }
}
}

With "action": "withhold", the protected content is cut out of the request and the rest goes to Claude; Claude is told it was withheld. With "action": "local", a request holding protected content is answered by the local model instead, or refused. Then run janus audit -verify to see what left your machine. The privacy recipe and the guard go further.

All of it​

The setup Janus is developed with: subagents tried locally first, a stand-in for the main conversation when Claude is down, and the guard on.

{
"listen": "127.0.0.1:8787",
"local": {
"baseUrl": "http://127.0.0.1:1234",
"models": {
"janus-local": { "model": "qwen/qwen3.8-27b", "contextTokens": 65536, "maxConcurrent": 1 }
}
},
"features": { "route": true, "guard": true },
"routing": [
{ "name": "main", "class": "main", "route": "upstream", "ifDown": "janus-local" },
{ "name": "subagents", "class": "subagent", "route": "local-then-escalate", "local": "janus-local", "maxSeconds": 90 }
],
"guard": {
"redact": true,
"localOnly": { "paths": [".env*", "secrets/**", "*.pem"] }
},
"logging": { "dir": "logs", "capture": true, "retainDays": 7, "maxMegabytes": 1024 }
}

Sizing the local model​

Three settings on a local.models entry decide how hard Janus works the model:

SettingDefaultWhat to know
contextTokensthe loaded context, or 65,536 when Janus loads itThe largest request Janus sends the model, and the context it loads the model with. A bigger context takes more memory. A model already loaded is never reloaded.
maxConcurrent1How many requests the model gets at once. Each runs in your machine's memory; on a 36 GB Mac, two at once took a 27B model past 31 GB.
timeoutSeconds900The longest one request may take.

Claude Code's main conversation starts at 25,000 to 35,000 tokens before any work, and grows. A context much under 65,536 tokens leaves little room.

Logs and captures​

Janus writes to logging.dir, by default a folder per user (macOS ~/Library/Application Support/janus, Linux $XDG_STATE_HOME/janus (or ~/.local/state/janus), Windows %LocalAppData%\janus). Below, and in the examples elsewhere, logs stands for that folder:

WhatWhereKept
The ledger: one line per request, no contentlogs/ledger/Forever; it is small
Captures: every request and response in fulllogs/captures/retainDays (7) days, and at most maxMegabytes (1,024 MB)
Originals of digested tool outputlogs/originals/retainDays days
The guard's placeholder keylogs/redact.keyUntil you delete it

Captures hold your prompts and code, readable by your user only. Set logging.capture to false if you don't want them; janus stats works without them, but janus replay and janus route need them.

Next: using Janus.