So nobody ever has to hand-write the OLLAMA_BACKENDS JSON again.
# add a backend, probe it, print the resulting .env line:
neuronetz-gateway add-backend embedded http://ollama:11434
neuronetz-gateway add-backend neuro-ollama http://neuro-ollama:11434 --token ABC
# update one (e.g. rotate token):
neuronetz-gateway add-backend neuro-ollama http://neuro-ollama:11434 --token XYZ --replace
# remove:
neuronetz-gateway remove-backend neuro-ollama
# peek (tokens redacted):
neuronetz-gateway list-backends
# write directly to a .env file (atomic temp-file + rename):
neuronetz-gateway add-backend foo http://foo:11434 --token T --write-env /app/.env
# show what would change without doing it:
neuronetz-gateway add-backend foo http://foo:11434 --token T --dry-run
What each command does:
- `add-backend NAME URL` (+ optional --token / --header / --scheme / --replace
/ --no-validate / --write-env / --dry-run): builds a new backend list (current
list parsed from OLLAMA_BACKENDS env, or synthesized from the single-backend
fallback if unset), validates the new backend by probing /api/tags with the
same headers the gateway will use at runtime (`build_backend_headers`), then
prints the resulting OLLAMA_BACKENDS=... line ready to paste — or writes it
in place if --write-env is given. Refuses to overwrite an existing name
unless --replace is passed.
- `remove-backend NAME` (+ --write-env / --dry-run): mirror of add-backend for
removal.
- `list-backends`: shows the configured backends with tokens redacted to
"***" via `redacted_dump`. Useful sanity check after editing .env.
All the JSON manipulation is in a new pure-helpers module
`cli/backends.py` (parse / serialize / add_or_replace / remove /
update_env_file). The Typer commands in `cli/manage.py` are thin shells
on top — the logic is unit-tested directly without spinning up Typer or
the network. The token is unwrapped from SecretStr exactly once at the
serialization boundary (`to_dict`) and never logged.
New tests (16): full coverage of the helpers — round-trip serialize/parse,
duplicate-name rejection, replace-in-place order preservation, remove
on unknown name, redaction, atomic env-file rewrite (insert / replace /
idempotent re-apply / create-when-missing).
ruff (incl. the per-file ignore add for tests' S105/S106 — placeholder
"tok123"-style strings are inputs, not credentials) + mypy --strict (68
source files) clean. pytest: 76 passed + 39 skipped (the 16 new tests +
no regressions on the existing 60).