Provider scoping (items 1/2/6):
- Key visibility maps on (model_id.lower(), upstream_provider_id), matching
refresh_model_maps, so a disable/override row on one provider never leaks
onto another provider's model, and matching is case-insensitive.
Data safety (items 3/5):
- Degraded OpenRouter fetches (network error, 429, non-200, bad payload)
return None (unknown) instead of []; a provider whose path set is unknown
keeps its previously persisted rows instead of being wiped.
- Endpoint payload parsing moved fully inside try, with a list guard, so
endpoints:null or non-list shapes are swallowed as documented.
- refresh with an empty live upstream list is a no-op; the unfiltered
DELETE in the prune path is gone (prune now keys off enabled DB rows).
Hot path (items 4/12/14):
- Persist uses chunked bulk INSERTs (one statement per 500 rows) instead of
per-row ORM adds; redundant ix_model_paths_model_id index dropped.
- Read routes filter in SQL instead of materializing the whole table, and
output ordering is deterministic (public id + path), independent of rowid.
- Visibility no longer rebuilds fully priced Model objects per override row;
it reads id/forwarded_model_id/canonical_slug straight off ModelRow.
Path/id contract (items 7/8/9/11):
- discovery_path_for_subprovider/discovery_base_paths hooks on
BaseUpstreamProvider, overridden by OpenRouterUpstreamProvider, mirror
_apply_provider_field so discovery and response stamping cannot drift
(openrouter:OpenRouter now correctly maps to unknown).
- openrouter_author_slug falls back to a slash-containing forwarded_model_id,
so admin-created alias rows are discoverable.
- public_model_id splits on the first slash, same as get_base_model_id, so
discovery ids can be sent to chat completions verbatim.
Lifecycle (items 10/13):
- ENABLE_MODEL_PATHS_REFRESH kill switch; interval and flag re-read every
loop iteration, and the task idles (not exits) while disabled.
- First 429 latches and aborts the remaining fan-out for the cycle; a
per-cycle cache dedupes fetches across providers sharing a base URL.
- refresh_model_maps prunes paths of disabled/deleted providers so admin
mutations take effect immediately; rows carry updated_at and both
endpoints expose it.
Tests (item 15) rewritten through the public refresh entry point with
transport-level httpx.MockTransport fakes, FK enforcement on, and coverage
for the periodic loop. Migration re-chained onto 9c4d8e2f1a6b.
The admin password is generated and logged on first start and the nsec is
set from the admin UI; ADMIN_PASSWORD/NSEC in .env are only a legacy seed.
Update the README, quickstart, configuration, and deployment docs to match,
and drop the unused ADMIN_KEY environment variable.
Several compose/.env examples mounted /app/data but left DATABASE_URL at the
relative default, so the database — and the master key file generated beside it —
landed off the persisted volume and would be lost on a container recreate. Point
DATABASE_URL inside /app/data in those examples and list routstr_secret.key in
the persistence table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The secret-key docs still said ROUTSTR_SECRET_KEY was required and that
the node would not start without it. Update the README, .env.example, and
the provider docs (configuration, quickstart, deployment) to the current
behaviour: the key is optional; when unset the node generates one beside
the database, so it persists on the same volume as the data, and prints a
one-time back-it-up notice; set it explicitly to manage the key yourself.
Switch the generation and reset snippets to `uv run python`, and add the
containerised reset variant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>