Troubleshooting
Common snags and how to clear them.
Start with lev doctor
Before reading further, run lev doctor. It checks the config file, model
resolution, one real inference, and the daemon handoff, in that order, and reports each one. The
check that fails tells you which section below you need. In particular it separates "my keys are
wrong" from "the daemon is stuck", which look identical from the outside.
If you get stuck, lev rage packs the logs, the config with its keys removed, and the run into
one zip you can attach to an issue. See Reporting issues.
I edited config.toml and nothing changed
The daemon picks up ~/.leviath/config.toml on its own, so an edit that does nothing almost always
means the file no longer parses. When that happens Leviath keeps serving the last version of the
file that loaded: runs still start and still work, on the settings from before your edit. That
is deliberate, because a half-typed save should not break a spawn. It is also why the symptom is
"nothing changed" rather than an error.
Any of these will tell you:
lev doctor # the `config` check fails, with the line and column, or the key
lev ps # a line under the run table
lev run ... # one line before the run starts
lev dash # a warning across the top of the screen, for as long as it lastsThe message names the file and where in it the problem is. A syntax error, or a value of the wrong
type, comes with a line and a column. A value that parsed and was then refused comes with the key
instead, such as model_providers.local. That covers an openai-compatible gateway with no
base_url, and an [[mcp_servers]] entry with neither a command nor a url.
Fix the file and save it. Everything above clears on its own and the next run uses the new config;
nothing needs restarting. If you would rather see it over HTTP, GET /api/config carries the same
thing as a config_error object, and /ws sends a config_health frame each time the answer
changes.
A key Leviath simply does not recognize is a different problem: the file still loads, and the key is reported as one nothing reads. See keys nothing reads.
The install worked, but lev is not found
The installer finished, and lev --version says command not found. Almost always the shell just
has not picked up the new PATH entry yet: open a new terminal (or re-source your shell profile)
and try again. The installer prints where it put the binary; if that directory is not on your
PATH, add it. On Windows, a new PowerShell window picks up the PATH change made by the installer.
Install fails with an auth error
No token is needed, because the repos and release assets are public. A 401/403 during install usually
means leftovers from the private alpha: remove any
url."https://…@github.com/GEMISIS/".insteadOf rewrite from ~/.gitconfig, unset stale
GITHUB_TOKEN / HOMEBREW_GITHUB_API_TOKEN exports (an expired token fails requests that
would succeed anonymously), and retry.
Note
Still stuck? Open an issue or a private security advisory on GitHub.
No provider configured
An agent needs at least one provider. Run lev setup: an API key, a ChatGPT or
Claude subscription you sign in to, or a local Ollama all count, and the last
two need no key.
lev doctor says which provider your defaults actually resolve to. With no provider configured
its resolve line fails and names the one your config asked for, default_provider or the first
entry of provider_order, and lists what is registered. A config that names a provider but no
override_model or fallback_model is fine. The line passes on that provider, and the inference
check picks a model from its catalogue to probe with. Each stage of a real run uses the model its
blueprint names. When your configured default_provider is being passed over for one
that is configured, the resolve line says that too.
Every run dies immediately with a payment or auth error
The account behind that provider is out of credits, or its key was rejected. Every run starts, fails on its first request, and ends at iteration 0 with no tool calls.
Run lev ps. A provider in this state is listed under the table with the reason and how long until
it is tried again, and lev ps --json carries the same under health.providers_down. After a few
failures in a row Leviath stops sending work there at all, so the flood of dead runs stops.
To keep working through it, give the host somewhere else to go:
[providers]
fallback_order = ["anthropic/claude-sonnet-5"]Runs then move to that model instead of failing. This is read per run, so it takes effect on the
next lev run without a restart, and so does topping the original account back up. See
Providers for the thresholds.
A run that has nowhere left to go is failed after [limits] stall_timeout_secs with a message
saying every provider it could use is out of service, rather than being left to sit there.
Not sure the credential is the problem at all? lev doctor checks the provider wiring layer by
layer and names the first one that breaks.
has no usable provider, but I do have a key
The message names what it tried: stage 'parallel_fix' has no usable provider (tried: anthropic).
That stage's models list never mentions the provider you configured. A blueprint only
starts on a provider it lists. The bundled agents list all five providers,
so with them any key qualifies; a blueprint you downloaded or wrote may list fewer.
Set fallback_model beside your default_provider: a stage none of whose models is configured
here then runs on that. override_model is the heavier tool, putting one model ahead of the
blueprint's list for every stage that has not opted out. --model <provider>/<model> does it for
one run, and copying the blueprint does it per stage. See
which entry a stage starts on.
My run went to Ollama and I never asked for it
If a run started on Ollama when you did not mean it to, Ollama is enabled in your config: either
[providers] ollama_enabled = true or an ollama_base_url is set. One of them may have carried
over from before Ollama became opt-in, since an address that used to configure it still does. Once
it is on, every bundled agent lists it last, so on a stage that names no model of its own it is the
first entry that matches, and the run starts against http://localhost:11434.
Two ways to stop it. Turn Ollama off if you did not mean to enable it. Or set fallback_model
alongside your default_provider: without a model to send, default_provider is never consulted
and Ollama wins by default. lev doctor says so in its resolve line when your configured provider
is being passed over.
A run that starts on a dead Ollama does not die there. An unreachable provider is treated the same as one out of credits, so the stage moves to its next candidate. You will see the swap in the stage log:
[failover] ollama/qwen3.5:9b is unusable (Request failed: error sending request for url
(http://localhost:11434/api/chat)); retrying on openrouter/openai/gpt-4o-miniA run vanished when I closed my terminal
It didn't. lev run hands the agent to the daemon, which keeps hosting it. Bring
it back with lev ps or lev dash. If the daemon itself was stopped, it reloads interrupted runs
on its next start.
A run says running but never does anything
Check its provider first, with lev doctor. If a stage's model list names only providers you
haven't configured, lev run refuses the spawn outright and tells you which ones it tried.
Configure one with lev setup, or add it to config.toml. Either way the next run you start
uses it: the daemon rebuilds its providers from the file, so there is nothing to restart. A run
that parked because its provider ran out of credits moves to whatever the file names now when you
lev resume it.
A run that gets past that and still can't dispatch is paused after [limits] stall_timeout_secs,
60 seconds by default, so nothing it did is lost. That covers removing a provider key after the
run started, and every provider it can use going out of service. Set the limit to 0 to wait
indefinitely instead.
A run says paused and I did not pause it
Leviath pauses a run when something outside it has to change first. lev ps shows which kind of
problem it is in the STATUS column and, under the table, what happened and what to do, with the
provider's own error. The stage log in lev dash has the same line, starting [paused]. Fix it,
then lev resume the run.
| STATUS | What happened | What to do |
|---|---|---|
paused: needs provider |
The stage names a provider this install does not have | Add it with lev setup |
paused: needs credits |
The account is out of credits | Top it up |
paused: needs key |
The provider rejected the API key | Replace it with lev setup |
paused: needs access |
The key cannot use this model | Check the account's plan |
paused: provider unreachable |
No connection: the name did not resolve, the port refused, or TLS failed | Check the network and the provider's base_url |
paused: provider timed out |
The provider was reached and did not answer in time | Resume to try again; a very large request may need a longer request_timeout_secs |
paused: provider failed |
The provider was reached and failed: a server error, or a reply that stopped part-way | Resume once the provider recovers |
paused: needs providers |
Every provider the stage can use is out of service, for reasons that do not agree | Read the line under the table |
lev doctor, lev setup and lev models list print a provider failure in the same words, so the
error in a paused run's line is the one to look up.
Waiting for a busy model is not this. An agent queued behind other in-flight requests to the same
model is working as intended and is never failed, however long the queue takes. Raise
[limits] max_concurrent_inferences if you want more of them running at once.
A run I spawned is not in lev ps
There are three of these, and they need different answers.
It is still starting. A spawn is not a start. Loading the blueprint, running any seed
commands, and getting through the first inference all happen before an agent can call its first
tool, and a cold model can take several seconds on its own. A run in this state is in lev ps
already, at iteration 0. It is there; give it a moment.
It finished, and you looked within the retention window. It is still listed, with the
status it ended on. An error row at ITER 0 and TOOLS 0 is a run that never got a turn,
and the message says why. HTTP 402 there means the provider account is out of credit, not
that Leviath lost the run.
It finished longer ago than the window. Now it really is gone from the listing, and the
answer is on disk: ~/.leviath/runs/<run-id>/meta.json, or GET /api/agents, which reads the
same records and does not expire them. Widen [limits] finished_retention_secs if you are
polling less often than the default five minutes.
If it is none of those, the spawn itself failed and no run was ever created. lev run reports
that on the spot, and the daemon logs it at error level in ~/.leviath/daemon.log, so check
there rather than in the listing.
This matters most to anything that schedules work by spawning agents and watching for them. Poll the listing rather than timing how long a run "should" take: a wall-clock deadline that is shorter than a cold start will keep giving up on runs that were about to work.
My OpenRouter agent finishes without saying anything
Reasoning models on OpenRouter answer with content: null and put their text under reasoning.
Leviath reads that field when the message carries nothing else, so a reasoning-only turn counts
as a real answer rather than an empty response.
The reasoning text is only used when the turn has no content and no tool calls of its own, so it never displaces real output or a tool call.
The provider says my model doesn't exist
Model strings are passed to the provider exactly as written and are never checked locally. So a
typo, a missing OpenRouter vendor/ prefix, or an identifier the provider has retired all show up
the same way: an API error on the first call, not a lev validate failure.
If the name in the error carries a provider prefix, as in Ollama's
model 'ollama/qwen3.8:latest' not found, the model was written as provider/model where a bare
id was expected. override_model and fallback_model in config.toml and a model in a
blueprint's models list pair with a provider that is named separately, so they take
qwen3.8:latest; only --model and [providers] fallback_order take ollama/qwen3.8:latest. An
override_model or fallback_model prefixed with its own default_provider is read bare and
lev doctor says so; a blueprint entry is sent as written.
Check the spelling against lev models list --provider <name>, which asks the provider rather
than Leviath's built-in table. Asking is the default; --offline is what limits the answer to the
table. A provider name it cannot reach at all fails the command outright, so a typo there is
answered rather than shown as an empty table.
A valid dated identifier such as deepseek/deepseek-v4-flash-0731 may be absent from the offline
table while still working, so absence there is not proof of a bad name. See
model identifiers.
Bedrock answers with an access error
A 403 from Bedrock says one of two things, and the message names which. UnrecognizedClientException
or ExpiredTokenException is the key itself: it was mistyped, deactivated in the Bedrock console,
or has expired. AccessDeniedException is a key that works but is not allowed this call. That
is nearly always a model the account has not enabled in the region the request went to. It can
also be a region that is not the one the key was made for. lev models list --provider bedrock
lists what the key can reach where it is pointed; the region comes from bedrock_region, then
AWS_REGION, then us-east-1. A 404 ResourceNotFoundException saying model use case details
have not been submitted is AWS's Anthropic use case form. The account fills it in once, in the
Bedrock console under model access, and AWS says to allow fifteen minutes after. A shell or Rhai
tool does not see AWS_* variables unless [security] allow_env_vars names them; that is
deliberate and does not affect the provider.
Grok answers 401, 403 or 429
A 401 that survives Leviath's automatic refresh means the sign-in was revoked or has lapsed:
lev auth login grok signs in again. A 403 is an account the Grok route will not serve, usually a
plan without SuperGrok or X Premium+; lev auth status shows the plan the sign-in carries. A 429
with no wait time is the subscription's limit for the period: lev providers quota shows each
window and when it resets, and the run waits for that reset rather than retrying blind.
Meta refuses a request with a 400
Meta's Model API refuses several parameters other APIs take: a reasoning effort of none, stop,
logit_bias, n and log probabilities. Leviath removes those from a stage's parameters, so a 400
that still comes back names something else in the message. muse-voice-transcribe-1.0 takes WAV
audio only (mono, 16-bit, 16 or 24 kHz, at most 32 MB and ten minutes); any other audio is refused
before it is sent, with the format it needs.
A large file reached the model as a line of text
A part the model takes natively reaches it as a one-line stand-in when it is too large to send.
The line says why: over the provider's inline limit and not uploaded (zero data retention is on,
or [providers] file_uploads = false), or over [mime] max_part_bytes when it arrived. Upload is
the usual fix for the first; raise max_part_bytes for the second. A generated video or a long
speech file can be large too. See Files and size limits.
Windows quoting and environment variables
lev itself is the same on every platform, and the commands in these docs work unchanged in
PowerShell. Two things around it differ.
Environment variables. The shell examples in these docs use the Unix VAR=value command prefix,
which PowerShell and cmd do not have:
$env:ANTHROPIC_API_KEY = "sk-ant-..." # PowerShell
lev run coder --task "Fix the failing test"set ANTHROPIC_API_KEY=sk-ant-...
lev run coder --task "Fix the failing test"Quoting. PowerShell strips the outer quotes before lev sees the argument, so a task containing a
literal quote needs escaping. Single quotes are safest when the text contains $:
lev run coder --task 'Handle the $HOME case'The agent's own shell is a separate matter: it runs through cmd.exe, not a POSIX shell, and
Leviath tells the model so. See which shell you get.
A Windows agent keeps trying cat, ls, and grep
The shell tool runs through cmd.exe on Windows, where those do not exist. Leviath names the
resolved shell in the tool's description and, on Windows, prepends a short system block giving the
PowerShell stand-ins, so an up-to-date install should not do this.
If you see it anyway, check that shell_hint has not been turned off in config.toml or in the
blueprint's [agent] / [stages.<name>] block. A blueprint that spells out POSIX commands in its
own prompt will still ask for them; that text has to change in the blueprint. See
which shell you get.
Console windows keep flashing on my Windows desktop
Every child process Leviath starts is asked for no console window, so this should not happen at
all. A shell call, an MCP server, and a provider that shells out are all covered.
If you still see it, check what is spawning. lev itself run from a terminal
shares that terminal and draws nothing extra. The one child that is still meant
to be visible is your editor: lev run with no --task opens $EDITOR in the
console on purpose, and that is not this bug.
Every run looks busy but nothing finishes
Run lev ps and read the line under the table. If there isn't one, the daemon thinks it is
getting somewhere and the problem is in a particular run rather than the daemon as a whole.
A lanes: line means the tool lane is full with batches queued behind it. On its own that is a
normal sign of a busy daemon. What matters is the second half:
lanes: tools 8/8 busy, 3 parked, 12 queued · no progress for 14 cycles (7m)A cycle is 30 seconds, so that reads as seven minutes in which work was waiting for the tool lane
and not one run moved. parked is not part of the problem: a batch waiting on a person or on
another run holds no capacity. busy with a queued figure that never falls is.
The usual cause is too little tool capacity for the shape of the workload. Raise
[limits] max_concurrent_tools and save the file. The daemon picks the new width up on its own,
and a raised lane is usable at once, so nothing needs restarting.
Left alone, the daemon widens the lane itself after [limits] dead_cycles_before_relief cycles, 10
by default, and logs it at error level. It never cancels anything, and it stops after granting one
extra lane's worth.
The same numbers are exported as leviath.tool_lane.* and
leviath.scheduler.dead_cycles.total if you have observability on. A
healthy daemon sits at zero dead cycles.
My work queue thinks runs are still running
A scheduler that hands work to Leviath and marks a slot busy has to learn when the run ends, and
two things get in the way of the obvious approach. updated_at in meta.json is a 30-second
heartbeat, so it stays fresh on a run that has stopped dead. pid is 0 for every run, live or
finished, so a sweeper that reverts on pid == 0 reverts everything.
Poll lev ps --all --json instead, and read last_progress_at rather than updated_at. The
reconciliation recipe covers the four cases and,
importantly, what to do when the daemon does not answer, which is nothing.
A run says running but nothing in it is moving at all
Not the stall above, and not a slow one. A run can end up in a state no part of the engine can
reach: no inference in flight, no tool batch, nothing waiting on it. It has stopped for good and
still reports running.
Set [limits] wedge_timeout_secs = 300 and the daemon fails such a run instead of leaving it. It
is off by default because it fails runs. A run it fails logs at error level and its meta.json
carries the reason, which begins [wedged] in the stage log. Nothing else in Leviath produces that
line, so it means the engine lost track of a run. Please report it.
A slow run never trips it. An agent waiting on the model, on a tool, on its sub-agents, or on a person is holding the marker that says so and is exempt however long it takes.
An agent seems stuck in a loop
That's what stuck detection is for. Add a condition = "stuck" transition
with thresholds (stuck_after_iterations, stuck_after_same_file_edits, …) so the runtime escapes
the stage automatically instead of burning tokens.
The Lair can't reach my server
The Lair, the browser console, talks straight to your lev serve
endpoint, so three things must line up:
- The server is running with a token:
lev serve --token <t>. - CORS allows the site's origin: add
--cors https://leviath.dev(or--cors "*"). Without it, the browser blocks the request before your server ever sees it. - The token matches the one you entered in The Lair.
lev serve --token 6618… --cors https://leviath.dev --allow-adminWarning
A page served over https can't call an http endpoint (mixed content). http://127.0.0.1
is exempt, so localhost works; for a remote box use TLS, an SSH tunnel, or the Docker image.
mkcert stops with keytool -list
ERROR: failed to execute "keytool -list": exit status 1
keytool error: java.lang.Exception: Keystore file does not exist: /Users/you/.keystoreBoth mkcert -install and the command that makes a certificate can stop like this. It is a bug in
mkcert 1.4.4: when JAVA_HOME points at a directory with a keytool but no
lib/security/cacerts, which is what brew --prefix openjdk gives you, mkcert runs keytool with
an empty keystore path. The Lair never needs the Java trust store. Put TRUST_STORES=system,nss in
front of both commands:
TRUST_STORES=system,nss mkcert -install
TRUST_STORES=system,nss mkcert 127.0.0.1 localhostIf the install printed "now installed in the system trust store" before it stopped, the CA is in
place. Only the certificate command needs running again. Setting JAVA_HOME to
$(brew --prefix openjdk)/libexec/openjdk.jdk/Contents/Home fixes it for good. The full recipe is
on the API page.
I get 401 Unauthorized
The token is missing or wrong. REST clients send Authorization: Bearer <token>; WebSocket clients
pass ?token=<token> in the URL (browsers can't set WS headers). Confirm the value matches what you
passed to --token.
An admin action returns 405 or 404
Config-write and MCP add/remove live behind --allow-admin. Without that flag the mutating route is
not mounted, and you get whichever error fits the path. PUT /api/config and
POST /api/mcp/servers return 405, because those paths still answer GET.
DELETE /api/mcp/servers/{name} returns 404, because nothing is mounted there at all. The read routes keep
working either way. Restart with lev serve … --allow-admin.