Addresses both #1499 (onboarding wizard never probes the configured base URL)
and #1500 (cross-tool env-var name divergence between webui and agent CLI).
Surfaced together because they're both LM-Studio onboarding bugs that pile
on top of each other — fixing only one leaves the broken UX.
#1499 — Onboarding wizard probes <base_url>/models before persisting
Pre-fix, `apply_onboarding_setup` accepted whatever `base_url` the user typed
without ever fetching `<base_url>/models`. @chwps's log timeline in #1420
showed the wizard finishing in 239ms with zero outbound HTTP — onboarding
silently persisted unreachable URLs and left users with empty model
dropdowns they had to populate by hand-editing config.yaml.
Backend:
* New `probe_provider_endpoint(provider, base_url, api_key, timeout=5.0)`
in `api/onboarding.py`. Stdlib-only (urllib + socket — no httpx dep).
Returns `{ok, models}` on success; `{ok: False, error: <code>, detail}`
on failure with stable error codes the frontend can switch on:
invalid_url, dns, connect_refused, timeout, http_4xx, http_5xx, parse,
unreachable. 256 KB response cap and 5s timeout keep a hostile or mis-
pointed endpoint from blocking the wizard.
* New `POST /api/onboarding/probe` route — thin JSON wrapper around the
function above. Same local-network gate as `/api/onboarding/setup`
because the body carries an `api_key` the user typed.
* The probe response is NEVER persisted. Only the user's typed selection
ends up in config.yaml; the probed model list just populates the
wizard's dropdown.
* SSRF: deliberately does NOT block private-IP ranges. The wizard is
gated behind WebUI auth and the legitimate target IS a local LM Studio
/ Ollama / vLLM server. A "block private IPs" SSRF defense would make
the feature useless for its primary use case.
Frontend:
* `static/onboarding.js`:
- New `ONBOARDING.probe` state ({status, error, detail, models, probedKey}).
- `_runOnboardingProbe()` — POSTs to /api/onboarding/probe, idempotent
& cached on (provider, baseUrl, apiKey).
- Debounced (400ms) on `oninput` of the base URL field.
- Explicit "Test connection" button.
- `nextOnboardingStep` blocks Continue at the setup step for any
provider with `requires_base_url=True` until the probe succeeds.
Same localized error renders inline.
* `static/i18n.js`: 13 new keys × 9 locales (canonical English in `en`,
English fallback with `// TODO: translate` markers in the other 8 —
same convention as v0.50.271 #1488 voice-buttons).
* `static/style.css`: probe banner + Test button styling (red-tinted
error variant, green-tinted success variant, neutral probing state).
Verified via manual repro on port 8789:
* connect_refused → red banner, helpful "from Docker, try the host IP"
hint, blocks Continue.
* DNS failure → red banner, "could not resolve host '...'", blocks Continue.
* Success against a mock /v1/models server → green banner, model dropdown
populates from the probed list, Continue advances normally.
#1500 — webui env var aligned with agent CLI (LM_API_KEY)
The webui has long used `LMSTUDIO_API_KEY` for LM Studio's API key in
both onboarding and Settings detection. The agent CLI runtime
(hermes_cli/auth.py:177-183) reads `LM_API_KEY`. So a user who configured
auth on their LM Studio instance got Settings → Providers reporting
has_key=True (because webui saw its own LMSTUDIO_API_KEY) but the agent
runtime ignored the key and fell back to LMSTUDIO_NOAUTH_PLACEHOLDER →
401 against the auth-enabled LM Studio server. Masked in practice for
the no-auth majority.
Picked Option B from the issue (defer to the agent — single source of
truth) but mitigated the migration cliff by reading the legacy name as
a fallback:
* `api/onboarding.py:_SUPPORTED_PROVIDER_SETUPS["lmstudio"]`:
- `env_var: "LM_API_KEY"` (canonical, what onboarding writes going forward).
- `env_var_aliases: ["LMSTUDIO_API_KEY"]` (read-only fallback for
pre-#1500 users so detection keeps working without forcing an
.env rewrite).
* `api/onboarding.py:_provider_api_key_present` reads aliases too.
* `api/providers.py:_PROVIDER_ENV_VAR["lmstudio"] = "LM_API_KEY"`.
* `api/providers.py:_PROVIDER_ENV_VAR_ALIASES["lmstudio"] = ("LMSTUDIO_API_KEY",)`
— new dict, used by `_provider_has_key` and `get_providers`'s
key_source resolution. Drops in cleanly when other providers later
rename their env vars too.
Verified:
```
before fix: webui writes LMSTUDIO_API_KEY → agent ignores it → 401 on chat
after fix: webui writes LM_API_KEY → agent picks it up → chat works
pre-#1500 .env with LMSTUDIO_API_KEY → still has_key=True in Settings
→ key_source='env_file'
```
Tests
* `tests/test_issue1499_onboarding_probe.py` — 17 tests:
3 invalid_url variants, dns, connect_refused, success (OpenAI shape),
success (bare-list shape), http_4xx, http_5xx, parse non-JSON, parse
wrong-shape, api_key authorization header passthrough, "probe must
not write to config.yaml or .env", PROBE_ERROR_CODES contract pin,
3 end-to-end route-level smoke tests against the live server fixture.
* `tests/test_issue1500_lmstudio_env_var_alignment.py` — 5 tests:
onboarding declares LM_API_KEY canonical with LMSTUDIO_API_KEY alias,
onboarding writes ONLY the canonical name, legacy env var still
detected post-migration, canonical takes precedence when both are
set, _provider_api_key_present reads aliases.
* `tests/test_issue1420_lmstudio_provider_env_var.py` — updated:
the original 5-test #1420 suite now pins LM_API_KEY as canonical
and LMSTUDIO_API_KEY as alias.
Full suite: 3879 → 3901 passing (+22), 0 failures.
Out of scope (explicitly NOT addressed here)
The third LM Studio onboarding sub-bug from #1420's thread — that
`apply_onboarding_setup` requires a non-empty api_key for lmstudio
even though most LM Studio installs run keyless — remains. The agent's
`LMSTUDIO_NOAUTH_PLACEHOLDER` substitution kicks in at runtime, but
the onboarding wizard rejects the empty-key case at submit. Fixing
this requires a UX decision (auto-write a sentinel? loosen the
required-key check for self-hosted providers?) and is left as a
separate follow-up.
Closes#1499Closes#1500
Co-authored-by: chwps <106549456+chwps@users.noreply.github.com>
Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
After completing the onboarding wizard with the LM Studio provider, users
saw LM Studio in the model picker and could chat normally, but Settings →
Providers showed no LM Studio entry — or rendered it with has_key=False
and configurable=False even when LMSTUDIO_API_KEY was already in
~/.hermes/.env. There was no UI surface to add or update the key.
Root cause:
api/providers.py:_PROVIDER_ENV_VAR — the dict that maps each provider id
to its env-var name — is missing an "lmstudio: LMSTUDIO_API_KEY" entry.
That dict drives two things:
1. _provider_has_key(pid) — env-var-based key detection. Returns False
and sets key_source='none' if the pid isn't in the dict, regardless
of what's in .env or os.environ.
2. get_providers() line 364:
"configurable": not is_oauth and pid in _PROVIDER_ENV_VAR,
Without the entry, configurable=False, hiding the "Add API key"
form in the UI.
So with no map entry, an LM Studio user with a working LMSTUDIO_API_KEY
gets has_key=False (wrong) AND no UI to fix it (wrong-er).
Same bug shape as #1410 (Ollama Cloud / local Ollama env-var collision).
The #1410 fix dropped bare "ollama" from _PROVIDER_ENV_VAR because
OLLAMA_API_KEY was shared with ollama-cloud and the runtime semantics
made the local key detection ambiguous. LMSTUDIO_API_KEY has no such
collision — it's only consumed by the lmstudio runtime.
Verified via reproduction:
before fix: lmstudio.has_key=False, configurable=False, key_source='none'
after fix: lmstudio.has_key=True, configurable=True, key_source='env_file'
5 regression tests in tests/test_issue1420_lmstudio_provider_env_var.py:
1. _PROVIDER_ENV_VAR['lmstudio'] == 'LMSTUDIO_API_KEY'
2. LMSTUDIO_API_KEY in env → has_key=True + configurable=True
3. providers.lmstudio.api_key in config.yaml → has_key=True (fallback path)
4. No env, no config → has_key=False but configurable=True (UI fix surface)
5. LMSTUDIO_API_KEY doesn't cross-detect any other provider
Mutation-verified: reverting the map entry causes 4 of 5 tests to fail
with clear assertion messages naming the bug (the 5th — config.yaml
fallback — is independent of the env-var path and intentionally remains
green to pin that the existing path keeps working).
Scope discipline:
#1420's broader thread surfaces a sibling bug — the onboarding wizard
never probes the configured <base_url>/v1/models endpoint before
persisting (the wizard accepts unreachable URLs silently with no
model-list dropdown population). That bug is being filed separately
and is NOT addressed here. Adding a probe touches the wizard UX flow,
has timeout / error-handling implications, and warrants its own design
pass.
Closes#1420 (the "LM Studio missing from Settings" half — feature-
request half about provider catalog support is already shipped: LM
Studio has been a first-class provider in api/onboarding.py since long
before this issue).
Co-authored-by: chwps <106549456+chwps@users.noreply.github.com>
Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>