Summary
Changing a models.embedding.* / models.generative.* entry in the root config requires a full process restart to take effect. Every other consumer of hot-reloadable config picks changes up in place. The two pieces needed to close that gap already exist and simply aren't connected.
What already works
RootConfigWatcher (config/RootConfigWatcher.ts) watches the root config file, reparses it, and emits change with the new config object:
this.emit('change', (this.#config = config));
bootstrapModels(rootConfig) (resources/models/bootstrap.ts) is exported and already documented as safe to re-run:
* Populate the model registry from `rootConfig.models`. No-op if the block
* is absent or empty. Idempotent within a process: each entry overwrites any
* prior registration under the same logical name (registry uses `.set()`).
It also calls clearFallbackGroups() first, explicitly so that "a removed/changed fallback: (or a removed models: block) doesn't leave stale routing behind" (#1326) — i.e. it was written with re-loading in mind.
Registration itself is pure. registerOpenAIBackend constructs an OpenAIBackend and calls setEmbedding/setGenerative; the constructor stores apiKey/baseUrl and does no I/O, no connection, and no health check. So re-registering costs nothing and cannot fail on an unreachable endpoint.
There is precedent for the wiring, one line of it, in the logger (utility/logging/harper_logger.ts:149):
rootConfig.on('change', updateLogSettings);
Ask
Subscribe to the watcher's change event and re-run bootstrapModels with the new config, so a changed models block applies to the running process. bootstrapModels is currently invoked exactly once, from components/componentLoader.ts:367, gated on isRoot during component load.
Points worth deciding rather than assuming:
- Removal semantics.
bootstrapModels returns early when the block is absent, so a config that drops models entirely would leave the previous backends registered. clearFallbackGroups() handles fallback groups but not the backend maps. If reload is to be truthful, a removed entry should probably deregister — which needs either a clear-then-repopulate, or a diff.
- Interaction with programmatic registration.
models.registerBackend(...) is a public API that components call from their own code. A reload that clears the registry wholesale would drop those too. Distinguishing config-sourced registrations from component-sourced ones may be necessary.
- In-flight calls. Replacing a backend under a concurrent
models.embed() is safe today because resolveCandidates resolves once per call and the backend object is immutable after construction — worth confirming that stays true.
Why it matters
An orchestrator that manages Harper instances can currently only deliver a models-config change by recreating the container, because the config reaches the process through environment. That makes a routine credential rotation — a bearer token on an embedding backend, rotated on a fixed schedule — cost a restart of a live instance, permanently and on a rolling basis. With reload in place the orchestrator can write the config file and the change applies in place, which removes the restart entirely rather than making it safer.
This also benefits anyone self-hosting: rotating an API key for a configured backend, or repointing a baseUrl, currently means a restart.
Summary
Changing a
models.embedding.*/models.generative.*entry in the root config requires a full process restart to take effect. Every other consumer of hot-reloadable config picks changes up in place. The two pieces needed to close that gap already exist and simply aren't connected.What already works
RootConfigWatcher(config/RootConfigWatcher.ts) watches the root config file, reparses it, and emitschangewith the new config object:bootstrapModels(rootConfig)(resources/models/bootstrap.ts) is exported and already documented as safe to re-run:It also calls
clearFallbackGroups()first, explicitly so that "a removed/changedfallback:(or a removedmodels:block) doesn't leave stale routing behind" (#1326) — i.e. it was written with re-loading in mind.Registration itself is pure.
registerOpenAIBackendconstructs anOpenAIBackendand callssetEmbedding/setGenerative; the constructor storesapiKey/baseUrland does no I/O, no connection, and no health check. So re-registering costs nothing and cannot fail on an unreachable endpoint.There is precedent for the wiring, one line of it, in the logger (
utility/logging/harper_logger.ts:149):Ask
Subscribe to the watcher's
changeevent and re-runbootstrapModelswith the new config, so a changedmodelsblock applies to the running process.bootstrapModelsis currently invoked exactly once, fromcomponents/componentLoader.ts:367, gated onisRootduring component load.Points worth deciding rather than assuming:
bootstrapModelsreturns early when the block is absent, so a config that dropsmodelsentirely would leave the previous backends registered.clearFallbackGroups()handles fallback groups but not the backend maps. If reload is to be truthful, a removed entry should probably deregister — which needs either a clear-then-repopulate, or a diff.models.registerBackend(...)is a public API that components call from their own code. A reload that clears the registry wholesale would drop those too. Distinguishing config-sourced registrations from component-sourced ones may be necessary.models.embed()is safe today becauseresolveCandidatesresolves once per call and the backend object is immutable after construction — worth confirming that stays true.Why it matters
An orchestrator that manages Harper instances can currently only deliver a models-config change by recreating the container, because the config reaches the process through environment. That makes a routine credential rotation — a bearer token on an embedding backend, rotated on a fixed schedule — cost a restart of a live instance, permanently and on a rolling basis. With reload in place the orchestrator can write the config file and the change applies in place, which removes the restart entirely rather than making it safer.
This also benefits anyone self-hosting: rotating an API key for a configured backend, or repointing a
baseUrl, currently means a restart.