Skip to content

Hot reload the models configuration when the root config changes #2344

Description

@Devin-Holland

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions