Configuration basics
Everything is one TOML file, plus LUMEN_* environment variable overrides
that use __ for nesting, e.g. LUMEN_SERVER__PORT=9090. In the TOML file
itself, top-level keys must appear before any [table] header. The
exhaustively commented reference is
config.example.toml
on GitHub.
Section tour
log_format - "pretty" (human-readable, default) or "json"
(production).
[server] - the HTTP server: host (bind address; use "0.0.0.0" in a
container) and port (must not be 0), body_limit (max request body in
bytes), first_token_timeout_ms (how long to wait for the upstream’s first
sign of life before failing with LM-3011; for streaming this is time to the
first SSE frame, for non-streaming it is the whole upstream call), and
sse_heartbeat_ms (idle interval after which a : ping SSE comment keeps
proxies from reaping a silent stream).
[auth] - virtual keys, hard budgets, quotas and the usage log.
Disabled by default, in which case the gateway is an open proxy with no
database at all. See Keys & budgets.
[telemetry] - which x-lumen-metadata keys become Prometheus labels
on the token counters. See Usage log.
[resilience] - retries, fallbacks, circuit breaker, timeouts and
health checks. Every value is the built-in default and the whole section is
optional; see config.example.toml for the full set. Details in
Resilience.
[[providers]] / [[providers.models]] - one [[providers]] block per
upstream (id, upstream_id, capabilities, modalities, costs,
per-model fallbacks). See Providers for the full
provider matrix and per-provider notes.
[image_fetch] - server-side fetching of remote image URLs for
multimodal input. See Multimodal input.
API keys
API keys are never written in the config. A provider references the name
of the environment variable that holds its key, via api_key_env.
Hot reload
A SIGHUP, a file watch, or an admin provider-key rotation triggers a
reload: the new config is validated, then the provider registry, price
table, resilience policy and the runtime-safe [auth] knobs are atomically
swapped (the bind address and a few other knobs still need a restart).
Details in Deployment.
Viewing the live config over the admin API
GET /admin/config returns the config file the gateway booted from,
verbatim. It requires the master key AND auth.enabled = true: the whole
/admin/* router is only mounted when auth is on, so on a default
deployment (where [auth] is disabled) this route does not exist at all and
answers 404 rather than 401.
{ "config": "<raw toml, byte for byte>", "hash": "<64 hex chars, BLAKE3>" }
config is the file’s exact bytes, never a re-serialisation of the merged
in-memory config: Config::load overlays LUMEN_* environment variables on
top of the file, and showing that merged view would make environment
overrides look like file content, and a later write would bake them in
permanently. hash is a BLAKE3 content hash of those same bytes, meant to be
echoed as If-Match on the PUT /admin/config that applies a new one (ADR
010), so two operators editing at once cannot silently clobber each other.
Applying a new config over the admin API
PUT /admin/config is the highest-privilege route in the gateway: it can
repoint any provider’s base_url (or add a new provider entirely) and
thereby redirect customer traffic to a different upstream. Master key
required, same as every other /admin/* route, and likewise only mounted
when auth.enabled = true: with auth off the route is absent and returns
404, not 401.
The If-Match contract:
- Send the submitted document as the raw request body (
Content-Typeis irrelevant; the body is treated as the TOML file’s new contents). - Send the
hashfrom a priorGET /admin/configas theIf-Matchheader. A missingIf-Matchheader is rejected with400(LM-1001) - the request is malformed like any other missing-required-input case. - If
If-Matchdoes not equal the config file’s current hash (someone else applied a change since you last read it), the request is rejected with412(LM-1004) and the file is left untouched.GET /admin/configagain to see what changed, then re-apply against the fresh hash.
What happens on a successful apply:
- The submitted bytes are staged in a temporary file next to the real one (same directory, so the final rename is atomic).
- The staged file is validated exactly like a hot reload would: parsed,
merged with
LUMEN_*env vars, and used to build a candidate provider registry. A parse failure or a registry-build failure (e.g. a provider missing a requiredbase_url) is rejected with400(LM-1001); the staging file is removed and the real config file is never touched. The error message names the offending field or setting (e.g."server.first_token_timeout_ms must not be 0") but never the staging file’s own filesystem path - that path is an implementation detail of this route, not something the operator wrote or needs to see, and it is logged server-side instead. - Only once validation succeeds is the current file copied to a
.baksibling (e.g.lumen.toml.bak) - one generation of history, enough to revert a bad apply by hand - and the staged file renamed into place. - The hot-reload trigger fires, so the new config takes effect without a restart (see Hot reload).
A rejected apply (400 or 412) is guaranteed to leave the config file
byte-for-byte unchanged and to leave no temporary file behind. Concurrent
PUTs are serialised gateway-side, so two operators racing the same
pre-apply hash can never both land: exactly one wins, and the other sees a
412 for a hash that moved out from under it, never a silently corrupted
mix of the two documents.
Not every field actually takes effect without a restart. A 204 means
the write and the reload trigger both succeeded, not that every field you
changed is now live: the restart-only settings named in Hot
reload - the bind address, auth.enabled, auth.db_path, and
the bounded usage-log channel knobs (usage_channel_capacity,
usage_batch_max, usage_flush_ms) - are silently unaffected by a PUT
just as they are by SIGHUP, with no separate signal in the response. Check
reload.rs’s module documentation (or this page’s Hot
reload section) for the authoritative restart-only list before
relying on a config change through this route.
Security note: the master key is equivalent to host filesystem access.
api_key_env accepts ANY environment variable name, not just ones a
provider convention would suggest. Because PUT /admin/config lets a
master-key holder add a provider with an attacker-controlled base_url and
api_key_env pointed at any variable present in the gateway’s own process
environment - LUMEN_MASTER_KEY itself, or any cloud credential the process
happens to carry - a single crafted config plus one request to that
provider’s model returns the named secret back to the caller as a Bearer
token. Before remote config apply existed, reaching this required
filesystem write access on the gateway host; with this route, the master
key alone is enough. In practice: holding the master key is now equivalent
to filesystem write access on the gateway host, plus read access to its
entire process environment. Mitigate with a separate master key per
gateway, exposing the admin surface on a private network only, and using the
read-only config mount opt-out (see
Deployment) on any gateway that
does not need remote apply.
Validate before you boot
Run lumen --check-config --config config.toml to validate a config file
without starting the server. See Installation.