Skip to content

Profiles: Resolve every setting's value and source in one place - #941

Closed
tobocop2 wants to merge 4 commits into
fix/settings-tab-model-defaultsfrom
feat/settings-resolver
Closed

tobocop2 wants to merge 4 commits into
fix/settings-tab-model-defaultsfrom
feat/settings-resolver

Conversation

@tobocop2

@tobocop2 tobocop2 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

A setting's value can come from an environment variable, config.toml, or the built-in default, and lilbee worked that out in several places that disagreed. A PATCH changed the running value even while a LILBEE_* variable pinned it. Switching data directories could keep a value from the previous one. A --model flag was lost when a subcommand reloaded the saved settings. Nothing could say where a value came from, which profiles and the "set by you" markers need.

Solution

One resolver computes each setting's value and its source, and every write path goes through it: building the config, reloading saved settings, and settings updates. The running value matches what the resolver answers, apart from per-run CLI flags and two writers (placement and linked folders) that follow in a later change.

The MCP lilbee_settings_list and lilbee_settings_get tools gain a source field: env, user, profile, built_in or auto. auto means the setting is at its built-in value and lilbee computes it at runtime.

Notes

GET /api/config keeps its flat shape. A PATCH that sends null now reads back the value the setting falls back to (the profile's value, else the built-in) instead of null.

A setting pinned by a LILBEE_* variable stays pinned. When the pinned chat model cannot be used, the TUI names the variable instead of saving a fallback that would never take effect. The MCP init tool now honours LILBEE_DOCUMENTS_DIR, as the CLI already did.

This stacks on #939.

@tobocop2
tobocop2 added this pull request to stack #942 September 27, 2026 06:18
@tobocop2
tobocop2 force-pushed the feat/settings-resolver branch from 77efd5a to d740362 Compare September 27, 2026 07:44
@tobocop2 tobocop2 changed the title Resolve every setting's value and source in one place Profiles: Resolve every setting's value and source in one place Sep 27, 2026
@tobocop2
tobocop2 marked this pull request as ready for review September 27, 2026 22:30
Each setting's effective value and the layer that supplies it now come
from one function over three layers: a set LILBEE_* variable, a
top-level key in config.toml, and the [profile] table's values. An
empty value in any layer is unset, except on a model role that can be
off (vision_model, reranker_model), where it clears the model. With
none of those the pydantic default applies, reported as built_in, or
as auto for a field flagged derived (its default means lilbee computes
the value at runtime).

Config() construction, the data-root overlay used by the CLI and MCP,
and settings updates all assign cfg from that resolver, so cfg holds
what a fresh Config() for the same root would hold:

- An update under a set LILBEE_X persists the value but leaves cfg on
  the env value, which is what a restart gives.
- A null update resolves to the profile value or the built-in, not
  None.
- The overlay for a new data root resets a key that root does not set,
  instead of keeping the previous root's value.

The two pydantic-settings source classes are replaced by one source
that yields the resolver's env, user and profile values. The [profile]
table is only read by the resolver; it is not a Config field and no
settings surface can write it. An unreadable config.toml or
LILBEE_SKIP_TOML_CONFIG=1 empties both file layers.

CLI flags given before the subcommand are kept and re-applied after the
subcommand's own data-root overlay, which previously replaced --model
with the persisted chat_model.

MCP settings_list and settings_get report each setting's source as
env, user, profile, built_in or auto. The schema route builds its list
off the event loop, since listing now reads config.toml.

The test suite points LILBEE_DATA at each test's own data root instead
of setting the skip flag, so a settings write reads back the file it
wrote.
A settings update derived the embedder's width from the requested
model before the resolver ran. Under LILBEE_EMBEDDING_MODEL the model
stayed on the env value while the width took the new model's. The
width is now derived after the sync, from the resolved model.

When an env var pins an unusable chat or embedding model, TUI startup
no longer persists a fallback or announces a swap. The env value wins,
so the notice names the variable and says how to clear it.

The test suite redirects the platform default data root to a scratch
directory, so --global tests never read the developer's config.toml.
A guard test fails if any config read reaches the real root.

A blank LILBEE_<FIELD> counts as an empty one.
main_gpu reports built_in and chat_n_ctx_target reports auto. The
config source hands pydantic every resolved value, since the built-ins
it filtered out validate to the same defaults.
Every CLI invocation re-resolves each writable setting and model role
from env, config.toml and profile, falling back to the built-in value.
The interactive integration fixture set its models and tuning on cfg
in memory only, so each CLI call reset embedding_model to empty and the
search, rebuild and crawl tests failed with no embedding model.

The fixture now writes those settings to the test root's config.toml,
the layer the CLI reads, and mirrors them on cfg for the in-process
sync. Fields the overlay does not touch stay in memory.
PATCH /api/config and MCP settings_set forward raw JSON straight to
cfg with no blank-stripping of their own, unlike the resolver's
env/TOML layers. A blank string for temperature (or top_p,
repeat_penalty, top_k_sampling, num_ctx, seed) still reaches the
before-validator, which is the only thing turning it into None
instead of a pydantic type error. Add a test through that write
boundary so the branch stays covered.
@tobocop2

tobocop2 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

This change is now part of #1003, which collects every Profiles pull request on one branch. Review and merge happen there. The branch stays in place.

@tobocop2 tobocop2 closed this Oct 3, 2026
@tobocop2
tobocop2 deleted the feat/settings-resolver branch November 1, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant