Skip to content

fix(agent): persist /model selection to .env, TOML, and DB - #1581

Merged
ilblackdragon merged 2 commits into
stagingfrom
fix/model-persist-between-runs
Mar 24, 2026
Merged

ilblackdragon merged 2 commits into
stagingfrom
fix/model-persist-between-runs

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

  • Root cause: /model wrote selected_model to the DB and config.toml, but the ~/.ironclaw/.env file (e.g. NEARAI_MODEL=old-model) has the highest priority in LlmConfig::resolve_model() and was never updated — so it always shadowed the new model on restart.
  • persist_selected_model now updates all three persistence layers: .env (backend-specific model var), config.toml (created if absent), and DB settings.
  • Adds diagnostic logging when the DB store is unavailable.

Test plan

  • db_single_key_model_update_survives_roundtrip — verifies /model's single-key DB write survives from_db_map() roundtrip
  • toml_overlay_preserves_matching_model — TOML overlay doesn't clobber when DB and TOML match
  • stale_toml_overwrites_db_model — documents TOML > DB priority (proves why TOML must be kept in sync)
  • toml_created_when_missing_for_model_persist — verifies config.toml creation when absent
  • Manual: run /model <name>, quit, restart, verify model persists

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 23, 2026 06:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@github-actions github-actions Bot added scope: agent Agent core (agent loop, router, scheduler) scope: channel Channel infrastructure scope: channel/cli TUI / CLI channel scope: channel/web Web gateway channel scope: channel/wasm WASM channel runtime scope: tool Tool infrastructure scope: tool/builtin Built-in tools scope: tool/wasm WASM tool sandbox scope: tool/mcp MCP client scope: db Database trait / abstraction scope: db/postgres PostgreSQL backend scope: db/libsql libSQL / Turso backend scope: llm LLM integration scope: workspace Persistent memory / workspace scope: orchestrator Container orchestrator scope: worker Container worker scope: config Configuration scope: extensions Extension management scope: setup Onboarding / setup scope: ci CI/CD workflows scope: docs Documentation scope: dependencies Dependency updates size: XL 500+ changed lines risk: high Safety, secrets, auth, or critical infrastructure contributor: core 20+ merged PRs labels Mar 23, 2026
The /model command only wrote selected_model to the DB and config.toml,
but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest
priority in LlmConfig::resolve_model(). The .env value was never
updated, so it always shadowed the new model on restart.

Now persist_selected_model updates all three persistence layers:
1. The backend-specific model env var in ~/.ironclaw/.env (only if the
   var already exists, to avoid injecting new vars)
2. The config.toml file (created if absent, since TOML > DB priority)
3. The DB settings table (for completeness)

Also adds diagnostic logging when the DB store is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon
ilblackdragon changed the base branch from main to staging March 23, 2026 07:02
@ilblackdragon
ilblackdragon force-pushed the fix/model-persist-between-runs branch from 3b3442c to 0c4d497 Compare March 23, 2026 07:03
Comment thread src/agent/commands.rs
let env_path = crate::bootstrap::ironclaw_env_path();
let env_has_var = std::fs::read_to_string(&env_path)
.ok()
.is_some_and(|content| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Severity — .env line detection uses starts_with without = delimiter

The check for whether the env var exists in .env uses:

line.trim_start().starts_with(model_env)

Where model_env is e.g. "NEARAI_MODEL". This matches any line that starts with that prefix, including unrelated vars like NEARAI_MODEL_VERSION=foo.

Meanwhile, upsert_bootstrap_var correctly uses format!("{}=", key) as its prefix match. So the detection and update use different matching strategies — the detection could false-positive, causing the code to update the wrong var or trigger an update when it shouldn't.

Fix:

let prefix = format!("{model_env}=");
content.lines().any(|line| line.trim_start().starts_with(&prefix))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d824875. Now uses format\!("{}=", model_env) prefix and skips #-commented lines, matching the same logic as upsert_bootstrap_var_to.

Comment thread src/agent/commands.rs
if let Err(e) = tokio::task::spawn_blocking(move || {
// 2a. Update the backend-specific model env var in ~/.ironclaw/.env.
//
// Env vars have the HIGHEST priority in LlmConfig::resolve_model()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity — concurrent /model commands could corrupt .env or TOML

The .env read-then-conditionally-write and the TOML load-then-write are not atomic. If two /model commands execute concurrently (e.g., from different channels — Telegram + web gateway), they could interleave reads/writes and corrupt either file.

Low likelihood for a single-user assistant, but worth noting since the agent supports multi-channel access. Consider flock-style advisory locking, or at minimum a comment documenting this as a known limitation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. This is a single-user assistant and concurrent /model commands are extremely unlikely. The TOML write is a full-file replace (atomic on most filesystems), and upsert_bootstrap_var does a read-modify-write that is also a single fs::write. Adding flock would add complexity disproportionate to the risk here.

Comment thread src/agent/commands.rs Outdated
// update this var or the /model change is invisible on restart.
let registry = crate::llm::ProviderRegistry::load();
let backend = std::env::var("LLM_BACKEND")
.ok()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity — backend re-resolved from disk instead of using in-memory value

The closure re-resolves the LLM backend by reading LLM_BACKEND env var then re-loading config.toml from the default path. But the agent already has the resolved backend in memory (in its Config). This creates two issues:

  1. Redundant I/O — ProviderRegistry::load() re-reads providers.json from disk, and Settings::load_toml() re-reads the TOML.
  2. Custom config path — if the user specified a non-default config path, this reads the wrong file (or no file), potentially resolving a different backend than the one actually in use.

Suggestion: Capture the already-resolved backend and model_env var before entering spawn_blocking, then move the strings into the closure:

let registry = crate::llm::ProviderRegistry::load();
let backend = self.config().llm.backend.clone(); // or however the agent exposes it
let model_env = registry.model_env_var(&backend).to_string();
let model_owned = model.to_string();
tokio::task::spawn_blocking(move || {
    // use model_env directly, no re-resolution needed
    ...
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d824875. Backend is now read from AgentDeps.llm_backend — the already-resolved value from startup (env > DB > TOML > default). This eliminates the redundant ProviderRegistry::load() and Settings::load_toml() calls, and correctly handles custom --config paths and DB-only backends.

Comment thread src/agent/commands.rs
Ok(None) => {
// No config file on disk; nothing to update.
// No config file yet — create one so the model choice
// survives restarts even when the DB is unavailable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity — newly created config.toml from Default::default() may leak non-default values

When no config.toml exists, this creates one with Settings { selected_model: Some(...), ..Default::default() }. On next startup, merge_from overlays the TOML onto DB settings.

merge_from skips fields that match Default::default(), so this is probably safe. But if any `Settings` field has a non-trivial default (e.g., `gateway_enabled` defaults to `true` via `default_true`), the newly created TOML could unintentionally override DB settings for those fields.

The existing test `toml_created_when_missing_for_model_persist` only verifies `selected_model` round-trips — it doesn't verify that other fields in the created TOML don't interfere with `merge_from`.

Suggestion: Add a test that creates TOML via this code path, then `merge_from`s it onto a Settings with different values, and asserts the non-model fields are unchanged.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe by construction. merge_non_default compares each field against Default::default() and only copies values that differ. The created TOML has Settings { selected_model: Some("..."), ..Default::default() }, so every field except selected_model matches the default and is skipped during merge. Fields with non-trivial defaults (e.g. default_true()) are equal to Default::default() by definition, so they are also skipped.

@serrrfirat

Copy link
Copy Markdown
Collaborator

Overall Review

Good fix for a real and subtle persistence bug — the root cause analysis (.env env var shadowing TOML and DB) is spot-on, and writing to all three layers is the correct approach.

One real bug: The .env line detection at line 878 uses starts_with(model_env) without an = delimiter, which could false-positive on similarly-prefixed vars. This should be a quick fix.

Three design considerations:

  • The backend is re-resolved from disk inside spawn_blocking instead of using the already-resolved in-memory value — minor inefficiency but also a correctness risk with custom config paths.
  • Creating a fresh config.toml from Default::default() needs verification that merge_from correctly ignores the default-valued fields on next startup.
  • The file read/write sequence isn't atomic, so concurrent /model from different channels could race (low risk in practice).

Tests are good — the regression tests clearly document the priority model (env > TOML > DB) and verify the round-trip behavior. Missing: a test for the .env update path itself.

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — persist /model selection to .env, TOML, and DB

+164 / -3 across 2 files. Fixes the root cause where .env model var shadowed DB/TOML on restart. Good test coverage of the priority chain.


Issues

1. .env line matching is prefix-only (high — already flagged)

line.trim_start().starts_with(model_env) (commands.rs:881) matches prefixed vars — e.g., NEARAI_MODEL_FALLBACK would match when looking for NEARAI_MODEL. Also matches commented-out lines like # NEARAI_MODEL=....

Fix: check for starts_with(&format!("{model_env}=")) or starts_with(&format!("{model_env} =")) and skip lines starting with #.

2. Backend resolved from disk, not in-memory config (medium — already flagged)

The spawn_blocking closure re-resolves the LLM backend by reading LLM_BACKEND env var then re-parsing config.toml (commands.rs:862-870). The Agent already has the resolved backend in its config. Pass it into the closure instead of re-deriving it — avoids the double TOML parse and potential divergence.

3. Default::default() in new TOML may include non-default values (medium)

When no config.toml exists (commands.rs:901-908), the code creates one with Settings { selected_model: Some(model), ..Default::default() }. If Default::default() for Settings ever includes non-empty values for other fields, they'll be persisted to disk as if the user configured them. Consider using a minimal TOML write that only contains selected_model, or use save_toml with a settings struct that only serializes non-None fields.

4. No concurrency protection (low-medium)

Two concurrent /model commands could interleave reads and writes to .env and config.toml. Unlikely in practice (CLI single-user) but upsert_bootstrap_var should ideally use atomic write (write to temp + rename). Documenting this as a known limitation is also fine.

5. Double TOML load in the closure (low)

Settings::load_toml is called twice in the same spawn_blocking closure — once to infer llm_backend (line 866) and once to update selected_model (line 897). Load once, use both.


What's good

  • Root cause analysis is correct: .env > TOML > DB priority chain meant only updating DB was useless
  • Conservative .env update: only writes if the var already exists (line 882), avoiding injecting new vars
  • Tests cover the priority chain: stale_toml_overwrites_db_model documents the TOML > DB priority and proves why the fix is necessary
  • TOML creation on missing file: solves the fresh-install case where no wizard ran
  • Diagnostic logging: warns when DB store is unavailable

Verdict

Approve with required changes. Fix the .env prefix matching (item 1) before merge — it's a real bug that could corrupt unrelated env vars. Items 2 and 5 (double TOML load) are easy wins.

Review feedback:
- Use resolved llm_backend from AgentDeps instead of re-reading from
  disk/env (fixes DB-only backend detection, eliminates redundant I/O)
- Match .env var with exact "KEY=" prefix and skip commented lines
  (prevents false matches on NEARAI_MODEL_VERSION etc.)
- TOML is now loaded once (no double-read for backend + model update)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon

Copy link
Copy Markdown
Member Author

All three required changes from @zmanian's review are addressed in d824875:

Item 1 (.env prefix matching): Now uses format!("{}=", model_env) and skips #-commented lines — matches upsert_bootstrap_var_to logic exactly.

Item 2 (backend from in-memory config): Added llm_backend: String to AgentDeps, populated from config.llm.backend at startup. The closure captures it directly — no disk re-read.

Item 5 (double TOML load): Eliminated. Backend comes from AgentDeps, TOML is loaded once for the model update.

Item 3 (Default::default() in new TOML): Safe by construction — merge_non_default compares each field against Default::default() and only copies values that differ. Every field except selected_model matches the default and is skipped.

Item 4 (concurrency): Acknowledged as known limitation. Single-user assistant makes this extremely unlikely.

@ilblackdragon
ilblackdragon merged commit 5847479 into staging Mar 24, 2026
14 checks passed
@ilblackdragon
ilblackdragon deleted the fix/model-persist-between-runs branch March 24, 2026 05:24
ilblackdragon added a commit that referenced this pull request Mar 28, 2026
- Add server-side validation of custom provider ID format (lowercase
  alphanumeric + hyphens, 1-64 chars) to match frontend regex
- Tighten is_nearai_private_endpoint to exact-match private.near.ai
  or *.private.near.ai, rejecting lookalikes like private-evil.near.ai
- Fix misleading priority doc comments in config/mod.rs and settings.rs
  to reflect the split model: LLM uses DB > env, others use env > DB
- Clean up #1581 artifacts: remove TOML file creation from
  persist_selected_model (DB is sufficient), update stale priority
  comments in commands.rs, fix contradictory test assertions
- Add 18 new tests for provider ID validation, adapter validation,
  and nearai private endpoint matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
* fix(agent): persist /model selection to .env, TOML, and DB

The /model command only wrote selected_model to the DB and config.toml,
but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest
priority in LlmConfig::resolve_model(). The .env value was never
updated, so it always shadowed the new model on restart.

Now persist_selected_model updates all three persistence layers:
1. The backend-specific model env var in ~/.ironclaw/.env (only if the
   var already exists, to avoid injecting new vars)
2. The config.toml file (created if absent, since TOML > DB priority)
3. The DB settings table (for completeness)

Also adds diagnostic logging when the DB store is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): address PR review — backend from deps, exact .env match

Review feedback:
- Use resolved llm_backend from AgentDeps instead of re-reading from
  disk/env (fixes DB-only backend detection, eliminates redundant I/O)
- Match .env var with exact "KEY=" prefix and skip commented lines
  (prevents false matches on NEARAI_MODEL_VERSION etc.)
- TOML is now loaded once (no double-read for backend + model update)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ilblackdragon added a commit that referenced this pull request Mar 29, 2026
* feat: support custom LLM provider configuration via web UI

Users can now define custom LLM providers through the web UI and have
them take effect without modifying environment variables or config files.

- Add `CustomLlmProviderSettings` struct and `llm_custom_providers`
  field to `Settings` so custom provider definitions are persisted and
  loaded from the DB settings table
- Add `LlmConfig::resolve_custom_provider()` to build a
  `RegistryProviderConfig` from user-defined provider data (base_url,
  adapter, model, api_key)
- Flip resolution priority to `db > env > default` so active provider
  set through the UI takes precedence over deployment env vars
- Warn when a custom provider is missing base_url or model
- Add startup info logs for backend source and provider creation
- Add regression tests for custom provider resolution and DB priority

* feat: add test connection for custom LLM providers

- Add POST /api/llm/test_connection endpoint that validates
  connectivity and auth for OpenAI-compatible, Anthropic, and
  Ollama adapters (10s timeout, per-adapter request logic)
- Add "Test" button next to Save/Cancel in the add-provider form;
  result shown inline with green/red styling
- Hide delete button for the active provider instead of showing
  an error toast
- Sort the active provider to the top of the provider list
- Clear selected_model when switching providers to avoid
  model-not-supported errors on the new provider
- Add i18n keys for test/testing states (en + zh-CN)

* feat: add built-in provider API key and model configuration

- Add Configure button on built-in provider cards (openai, anthropic,
  gemini, ollama, etc.) to set API key and default model via web UI
- Store overrides as `llm_builtin_overrides` setting (per-provider
  key/model map) using the existing generic settings k/v API
- Add LlmBuiltinOverride struct in settings.rs; resolve in
  resolve_registry_provider() with priority:
  env var > selected_model > llm_builtin_overrides[id] > default
- Restore provider's configured model to selected_model on provider
  switch, so /model command always takes precedence at runtime
- Fix fetch-models button in built-in configure mode: use hardcoded
  base_url from BUILTIN_PROVIDERS instead of the hidden form field
- Add edit support for custom providers with pre-filled dialog
- Show current model on active and configured provider cards
- Convert add/edit provider form to a modal dialog
- Sync selected_model when editing or deleting an active custom provider

* feat: move Config tab into Settings as Providers subtab

* feat(web): merge Providers into Inference tab with UX improvements

* chore: resolve conflicts

* fix(llm): address security and correctness issues in custom LLM provider

* fix(llm): address security and correctness issues in custom LLM provider

* feat(web): fall back to env vars for LLM provider config in UI

* fix(llm): enforce db > env > default config priority for provider setting

* fix: address review feedback on provider config priority

* feat: extract BUILTIN_PROVIDERS into providers.js

* fix(security): store LLM API keys in encrypted secrets store instead of plaintext

* fix(security): harden LLM API key handling across settings and LLM endpoints

* fix: test_connection sends actual chat completion

* refactor(web): derive LLM Provider display from active Model Provider

* fix(settings): language switch not working for llm provider

* feat(web): add restart notice to LLM Provider settings

* fix: review fixes for custom LLM provider PR

- Add server-side validation of custom provider ID format (lowercase
  alphanumeric + hyphens, 1-64 chars) to match frontend regex
- Tighten is_nearai_private_endpoint to exact-match private.near.ai
  or *.private.near.ai, rejecting lookalikes like private-evil.near.ai
- Fix misleading priority doc comments in config/mod.rs and settings.rs
  to reflect the split model: LLM uses DB > env, others use env > DB
- Clean up #1581 artifacts: remove TOML file creation from
  persist_selected_model (DB is sufficient), update stale priority
  comments in commands.rs, fix contradictory test assertions
- Add 18 new tests for provider ID validation, adapter validation,
  and nearai private endpoint matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments for custom LLM provider

- Move LLM handlers (test_connection, list_models, env_defaults) from
  server.rs to handlers/llm.rs for consistency with other handler modules
- Merge validate_custom_providers into single pass (ID + adapter check)
- Allow underscores in custom provider IDs to match builtin naming
- Add missing i18n key config.fetchingModels (en + zh-CN)
- Fix optional_env().ok().flatten() error swallowing in config/llm.rs;
  propagate ConfigError with ? instead of silently discarding
- Narrow settings.rs module docs to scope DB>env precedence to LLM
- Add unit tests for hydrate_llm_keys_from_secrets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace static providers.js with API endpoint from registry

- Delete providers.js; serve provider list from /api/llm/providers
  endpoint that reads from the embedded ProviderRegistry (providers.json)
- Centralize secret naming (builtin_secret_name, custom_secret_name)
  into settings.rs; replace 8 duplicated format! calls across 4 files
- Extract JS API_KEY_UNCHANGED constant; replace 6 magic string literals
- Replace hard-coded API key placeholder strings with i18n keys
  (config.apiKeyConfigured, config.apiKeyFromEnv, config.apiKeyEnter)
- Simplify apiFetchVoid to delegate to apiFetch
- Remove unnecessary Vec clones in guard_active_provider_not_removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
* fix(agent): persist /model selection to .env, TOML, and DB

The /model command only wrote selected_model to the DB and config.toml,
but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest
priority in LlmConfig::resolve_model(). The .env value was never
updated, so it always shadowed the new model on restart.

Now persist_selected_model updates all three persistence layers:
1. The backend-specific model env var in ~/.ironclaw/.env (only if the
   var already exists, to avoid injecting new vars)
2. The config.toml file (created if absent, since TOML > DB priority)
3. The DB settings table (for completeness)

Also adds diagnostic logging when the DB store is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): address PR review — backend from deps, exact .env match

Review feedback:
- Use resolved llm_backend from AgentDeps instead of re-reading from
  disk/env (fixes DB-only backend detection, eliminates redundant I/O)
- Match .env var with exact "KEY=" prefix and skip commented lines
  (prevents false matches on NEARAI_MODEL_VERSION etc.)
- TOML is now loaded once (no double-read for backend + model update)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
* feat: support custom LLM provider configuration via web UI

Users can now define custom LLM providers through the web UI and have
them take effect without modifying environment variables or config files.

- Add `CustomLlmProviderSettings` struct and `llm_custom_providers`
  field to `Settings` so custom provider definitions are persisted and
  loaded from the DB settings table
- Add `LlmConfig::resolve_custom_provider()` to build a
  `RegistryProviderConfig` from user-defined provider data (base_url,
  adapter, model, api_key)
- Flip resolution priority to `db > env > default` so active provider
  set through the UI takes precedence over deployment env vars
- Warn when a custom provider is missing base_url or model
- Add startup info logs for backend source and provider creation
- Add regression tests for custom provider resolution and DB priority

* feat: add test connection for custom LLM providers

- Add POST /api/llm/test_connection endpoint that validates
  connectivity and auth for OpenAI-compatible, Anthropic, and
  Ollama adapters (10s timeout, per-adapter request logic)
- Add "Test" button next to Save/Cancel in the add-provider form;
  result shown inline with green/red styling
- Hide delete button for the active provider instead of showing
  an error toast
- Sort the active provider to the top of the provider list
- Clear selected_model when switching providers to avoid
  model-not-supported errors on the new provider
- Add i18n keys for test/testing states (en + zh-CN)

* feat: add built-in provider API key and model configuration

- Add Configure button on built-in provider cards (openai, anthropic,
  gemini, ollama, etc.) to set API key and default model via web UI
- Store overrides as `llm_builtin_overrides` setting (per-provider
  key/model map) using the existing generic settings k/v API
- Add LlmBuiltinOverride struct in settings.rs; resolve in
  resolve_registry_provider() with priority:
  env var > selected_model > llm_builtin_overrides[id] > default
- Restore provider's configured model to selected_model on provider
  switch, so /model command always takes precedence at runtime
- Fix fetch-models button in built-in configure mode: use hardcoded
  base_url from BUILTIN_PROVIDERS instead of the hidden form field
- Add edit support for custom providers with pre-filled dialog
- Show current model on active and configured provider cards
- Convert add/edit provider form to a modal dialog
- Sync selected_model when editing or deleting an active custom provider

* feat: move Config tab into Settings as Providers subtab

* feat(web): merge Providers into Inference tab with UX improvements

* chore: resolve conflicts

* fix(llm): address security and correctness issues in custom LLM provider

* fix(llm): address security and correctness issues in custom LLM provider

* feat(web): fall back to env vars for LLM provider config in UI

* fix(llm): enforce db > env > default config priority for provider setting

* fix: address review feedback on provider config priority

* feat: extract BUILTIN_PROVIDERS into providers.js

* fix(security): store LLM API keys in encrypted secrets store instead of plaintext

* fix(security): harden LLM API key handling across settings and LLM endpoints

* fix: test_connection sends actual chat completion

* refactor(web): derive LLM Provider display from active Model Provider

* fix(settings): language switch not working for llm provider

* feat(web): add restart notice to LLM Provider settings

* fix: review fixes for custom LLM provider PR

- Add server-side validation of custom provider ID format (lowercase
  alphanumeric + hyphens, 1-64 chars) to match frontend regex
- Tighten is_nearai_private_endpoint to exact-match private.near.ai
  or *.private.near.ai, rejecting lookalikes like private-evil.near.ai
- Fix misleading priority doc comments in config/mod.rs and settings.rs
  to reflect the split model: LLM uses DB > env, others use env > DB
- Clean up nearai#1581 artifacts: remove TOML file creation from
  persist_selected_model (DB is sufficient), update stale priority
  comments in commands.rs, fix contradictory test assertions
- Add 18 new tests for provider ID validation, adapter validation,
  and nearai private endpoint matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments for custom LLM provider

- Move LLM handlers (test_connection, list_models, env_defaults) from
  server.rs to handlers/llm.rs for consistency with other handler modules
- Merge validate_custom_providers into single pass (ID + adapter check)
- Allow underscores in custom provider IDs to match builtin naming
- Add missing i18n key config.fetchingModels (en + zh-CN)
- Fix optional_env().ok().flatten() error swallowing in config/llm.rs;
  propagate ConfigError with ? instead of silently discarding
- Narrow settings.rs module docs to scope DB>env precedence to LLM
- Add unit tests for hydrate_llm_keys_from_secrets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace static providers.js with API endpoint from registry

- Delete providers.js; serve provider list from /api/llm/providers
  endpoint that reads from the embedded ProviderRegistry (providers.json)
- Centralize secret naming (builtin_secret_name, custom_secret_name)
  into settings.rs; replace 8 duplicated format! calls across 4 files
- Extract JS API_KEY_UNCHANGED constant; replace 6 magic string literals
- Replace hard-coded API key placeholder strings with i18n keys
  (config.apiKeyConfigured, config.apiKeyFromEnv, config.apiKeyEnter)
- Simplify apiFetchVoid to delegate to apiFetch
- Remove unnecessary Vec clones in guard_active_provider_not_removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: channel/cli TUI / CLI channel scope: channel/wasm WASM channel runtime scope: channel/web Web gateway channel scope: channel Channel infrastructure scope: ci CI/CD workflows scope: config Configuration scope: db/libsql libSQL / Turso backend scope: db/postgres PostgreSQL backend scope: db Database trait / abstraction scope: dependencies Dependency updates scope: docs Documentation scope: extensions Extension management scope: llm LLM integration scope: orchestrator Container orchestrator scope: setup Onboarding / setup scope: tool/builtin Built-in tools scope: tool/mcp MCP client scope: tool/wasm WASM tool sandbox scope: tool Tool infrastructure scope: worker Container worker scope: workspace Persistent memory / workspace size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants