Commit Graph

30 Commits

Author SHA1 Message Date
nesquena-hermes 83de9d0cf0 fix(providers): log warning when custom provider entry yields empty slug
Opus stage-338 review SHOULD-FIX: silent drop at api/providers.py:1049
was diagnostically opaque. logger.warning() now surfaces the bad
config entry so operators can spot misconfigurations.

Co-authored-by: Opus advisor <opus-advisor@hermes.local>
2026-05-11 17:30:56 +00:00
Frank Song f6115b78c6 Fix custom provider name slugs with ports 2026-05-11 17:24:53 +08:00
nesquena-hermes e0ecf2a035 Merge PR #1970: feat: LM Studio provider with live model discovery 2026-05-11 05:12:04 +00:00
nesquena-hermes 9c471be4a3 Merge PR #2034 into stage-334
# Conflicts:
#	CHANGELOG.md
2026-05-10 23:38:05 +00:00
Frank Song 128e734df4 Fix Xiaomi API key env detection 2026-05-11 07:33:52 +08:00
Michael Lam cb3284b73f fix: harden quota probe subprocess handling 2026-05-10 12:18:02 -07:00
dobby-d-elf 35cf332c9a feat: add LM Studio provider support with live model discovery
- api/config.py: resolve merge conflict, keep both _custom_slug_rest_looks_like_host_port
  and new _get_provider_base_url helper. Custom providers now return their configured
  base_url in resolve_model_provider(). Add 'Configured' badge for explicitly configured
  providers in the models dropdown. Detect LM Studio via LM_API_KEY+LM_BASE_URL env vars.
  Fetch live loaded models from LM Studio with fallback to direct HTTP requests.

- api/providers.py: fetch live LM Studio model list via hermes_cli for the providers card.

- static/style.css: add purple 'Configured' badge style.
2026-05-09 13:20:01 -06:00
Frank Song 6808e06083 fix: isolate profile quota usage probes 2026-05-08 15:37:07 +00:00
hermes-agent 0ed63968b6 Stage 314: PR #1827 — sync Codex provider card models with picker by @Michaelyklam
Note: PR #1827 was branched before v0.51.19 shipped #1812, which
introduced an initial (pure live-fetch) Codex provider card hook in
api/providers.py at the same line range. The contributor's PR was
filed AFTER #1812 shipped but their diff didn't yet account for it.
Stage 314 absorbs the contributor's intent (visible Codex cache
merge for gpt-5.3-codex-spark visibility) by replacing the v0.51.19
hook with the richer merged version directly in stage. Production
code change ≡ what the contributor's PR would have produced if
rebased onto current master. Test file + pr-media adopted verbatim.
Marker commit so the stage log makes the absorption visible.
2026-05-07 17:58:52 +00:00
Frank Song f7902776d4 fix: use live Codex models in providers card 2026-05-07 16:58:16 +00:00
Frank Song a6b88c8c1e feat: show account limits in provider quota 2026-05-07 17:36:04 +08:00
Michael Lam 22df075b8a feat: add active provider quota status 2026-05-05 02:26:52 +00:00
Michael Lam b6c695e1ab test: cover provider config cleanup path 2026-05-04 09:04:07 -07:00
Michael Lam 22187d2b4c fix: resolve provider config cleanup path 2026-05-03 23:13:10 -07:00
nesquena-hermes a2b793be4f fix(picker): Nous Portal featured-set cap + endpoint symmetry (closes #1567)
Two related dropdown bugs in one PR — same root shape (model-picker
endpoints disagreeing about which Nous Portal models exist) plus the
preemptive UX guard against the picker becoming unusable on large-tier
Nous accounts.

#1567 — Endpoint disagreement
=============================
Reporter (Deor, Discord, May 03 2026) saw Settings → Providers card
showing "Nous Portal — 396 models · OAuth" while the in-conversation
picker dropdown listed only the four hardcoded curated entries.

Two structural causes:

1. ``api/providers.py:get_providers`` iterates ALL OAuth providers
   regardless of authentication state and unconditionally live-fetches
   the catalog.
2. ``api/config.py:_build_available_models_uncached`` only iterates
   providers in ``detected_providers``, gated on
   ``hermes_cli.models.list_available_providers().authenticated``.
   That flag can disagree with ``get_auth_status(<id>).logged_in`` on
   some hermes_cli versions.

When the disagreement happens for Nous, the picker silently falls
through to the curated 4-entry static list while the providers card
keeps showing the live catalog — exactly the asymmetry users report.

Plus: the Nous live-fetch branch in `_build_available_models_uncached`
fell back to the same curated 4-entry list when `provider_model_ids`
returned an empty list (transient failure / OAuth refresh in flight),
which doubles down on the disagreement instead of healing it.

UX cap (the design concern Nathan flagged on triage)
====================================================
Even with the disagreement fixed, dumping a 397-model catalog into a
flat dropdown is unusable. We trim the visible picker to a curated
~15-entry featured set when the catalog exceeds 25 models, and surface
the rest under a new ``extra_models`` field so:

- ``/model`` slash autocomplete (commands.js) covers the full catalog
- ``_dynamicModelLabels`` (ui.js) hydrates from both lists, so a model
  selected from outside the featured slice still gets a proper label
- The optgroup label gets ``" (15 of 397)"`` appended so the user
  understands the dropdown is intentionally trimmed, not broken
- The providers card surfaces ``models_total`` separately so the
  header still reads "397 models · OAuth"
- A small "+N more" disclosure pill appears at the end of the rendered
  pill list (only fires for non-OAuth providers — OAuth cards never
  render pills) with a tooltip pointing at the slash command

Featured selection rules
------------------------
Deterministic; same algorithm runs in both `/api/models` and
`/api/models/live` so background enrichment doesn't undo the trim:

1. Always include the user's currently-selected model (sticky — no
   orphan IDs in the dropdown after a refresh)
2. Always include every entry from the curated static
   ``_PROVIDER_MODELS["nous"]`` list whose id maps onto a live id
3. Top up to 15 by walking ``_NOUS_VENDOR_PRIORITY`` round-robin
   (one model per vendor each pass) so no vendor monopolises the slots

Changes by file
===============

api/config.py
- New `_format_nous_label` neighbour: `_NOUS_FEATURED_THRESHOLD = 25`,
  `_NOUS_FEATURED_TARGET = 15`, `_NOUS_VENDOR_PRIORITY` tuple,
  `_build_nous_featured_set()` helper (~80 LOC)
- `_build_available_models_uncached` Nous branch:
  - Apply featured-set cap with sticky-selection signal
  - Return `extra_models` alongside `models` for the catalog tail
  - Decorate optgroup label with truncation count
  - Drop stale-4 fallback when authenticated but live-fetch empty
    (omit the group entirely; truth lives in the providers card and
    the next cache rebuild will heal it)
  - Keep stale-4 fallback when hermes_cli is unavailable (test envs,
    package mismatches) — that's a different failure mode
- Detection symmetry: explicit `get_auth_status("nous").logged_in`
  check after the existing `list_available_providers()` loop, so the
  picker matches the providers card on hermes_cli versions where the
  two signals disagree

api/providers.py:get_providers
- Apply same featured-set cap so card body doesn't render 397 pills
- Add `models_total` field reporting full catalog size (used by
  frontend for the "N models · OAuth" header text)

api/routes.py:_handle_live_models
- Apply same featured-set cap for `/api/models/live` so background
  enrichment via `_fetchLiveModels()` doesn't undo the dropdown trim
- Use sticky-selection from `cfg["model"]["model"]` matching the main
  endpoint's logic

static/ui.js:populateModelDropdown
- Hydrate `_dynamicModelLabels` from `g.extra_models` so a selection
  outside the visible dropdown still renders with its proper label

static/commands.js:_loadSlashModelSubArgs
- Iterate `group.extra_models` so `/model` autocomplete covers the
  full catalog (not just the trimmed featured slice)

static/panels.js:_buildProviderCard
- Header count uses `p.models_total` (full catalog size) instead of
  `p.models.length` (trimmed slice)
- Render trailing "+N more" disclosure pill when `models.length <
  models_total` with a tooltip pointing at the slash command

static/style.css
- New `.provider-card-model-tag-more` rule (italic, dashed border,
  cursor:help, no select) — visually distinct from real model pills

Tests
=====

`tests/test_issue1567_nous_picker_capacity_and_symmetry.py` (20 tests):

- TestBuildNousFeaturedSet (8): unit tests on the helper —
  small-catalog no-op, large-catalog cap to target, disjoint+complete
  invariants, priority-vendor round-robin guarantee, sticky selection
  with and without `@nous:` prefix, curated-flagship preservation,
  empty-catalog handling, determinism
- TestApiModelsLargeCatalog (2): /api/models cap behavior end-to-end
  on a synthetic 397-model catalog vs a 20-model catalog
- TestNousDetectionSymmetry (2): picker includes Nous when
  `get_auth_status` agrees but `list_available_providers` disagrees;
  picker omits Nous when both disagree
- TestNousLiveFetchEmpty (2): authenticated + empty-fetch omits group;
  hermes_cli unavailable still falls back to static-4
- TestProvidersCardPickerSymmetry (1): both endpoints agree on
  exactly the same featured-set IDs + total catalog count
- TestFrontendExtrasContract (4): static-source assertions pinning
  the JS contract for `extra_models`, `models_total`, and the "+N more"
  disclosure

Verified live on port 8789 (30-model catalog):
- /api/models Nous group: provider="Nous Portal (15 of 30)", 15 models,
  15 extra_models
- /api/models/live?provider=nous: 15 entries (matches main path)
- /api/providers Nous card: models_total=30, models=15
- Browser dropdown after backfill: 15 options, 30 entries in
  _dynamicModelLabels
- Sticky selection: Claude Opus 4.7 (the active model) in the featured
  slice as expected

4073 pytest passed (was 4053 → 4073, +20 from this PR).
3 CI test runs (3.11/3.12/3.13) green.
QA harness 11/11 passed.

Reporter: Deor (Discord #report-bugs, May 03 2026 14:15 PT)
Relayed by: AvidFuturist
2026-05-03 21:44:22 +00:00
nesquena-hermes bff8cb2b58 fix: Nous Portal full live catalog + dropdown cache invalidation on provider remove
Closes #1538, #1539. Two related dropdown-staleness bugs reported by Deor
(Discord, May 03 2026).

#1538 — Nous Portal picker showed only 4 hardcoded models
=========================================================
The Settings → Default Model picker, the composer model dropdown, the
/model slash command, and the Settings → Providers card all showed only
four Nous models (Claude Opus 4.6, Claude Sonnet 4.6, GPT-5.4 Mini, Gemini
3.1 Pro Preview) because `_PROVIDER_MODELS["nous"]` had four hardcoded
entries and `_build_available_models_uncached()` fell through to the
generic `pid in _PROVIDER_MODELS` branch.

The actual Nous Portal catalog has 30 models live — Claude Opus 4.7, GPT-5.5,
Kimi K2.6, MiniMax M2.7, Gemini 3.1 Pro/Flash, several Xiaomi/Tencent/StepFun
entries, and more.

Fix:
- New `_format_nous_label()` helper in `api/config.py` — reuses the
  `_format_ollama_label()` token rules, drops the vendor namespace, and
  appends ` (via Nous)` so labels disambiguate from same-named direct-
  provider entries (e.g. "Claude Opus 4.7" via direct Anthropic).
- New `elif pid == "nous":` branch in `_build_available_models_uncached()`
  mirroring the Ollama Cloud pattern: live-fetch through
  `hermes_cli.models.provider_model_ids("nous")`, prefix every id with
  `@nous:` (matches the existing routing convention from PR-era #854 and
  pinned in tests/test_nous_portal_routing.py), fall back to the curated
  4-entry static list when hermes_cli is unavailable.
- Same fix applied to `api/providers.py:get_providers()` — that's the
  separate code path that builds Settings → Providers card models, and
  it had the identical bug shape.

#1539 — Removed provider lingered in dropdowns until restart
============================================================
After Settings → Providers → Remove, the provider's models still appeared
in every model dropdown until the page was reloaded. The server-side
TTL cache was correctly flushed (`set_provider_key()` calls
`invalidate_models_cache()` on both add and remove) but JS-side caches
were never dropped:

- `_slashModelCache` / `_slashModelCachePromise` (commands.js) — feeds
  the `/model` slash-command suggestions.
- `_dynamicModelLabels` / `window._configuredModelBadges` (ui.js) —
  populated by `populateModelDropdown()` on app boot and profile switch.

Pre-fix, `_removeProviderKey()` only called `loadProvidersPanel()`
which refreshed the providers card list but never asked any consumer
to re-fetch /api/models.

Fix:
- `static/commands.js`: new `_invalidateSlashModelCache()` helper that
  nulls both cache slots, exposed on `window` (typeof-guarded so the
  module remains importable in headless vm contexts — needed by the
  existing tests/test_cli_only_slash_commands.py harness).
- `static/panels.js`: new `_refreshModelDropdownsAfterProviderChange()`
  helper that calls the invalidator + `populateModelDropdown()`, wrapped
  in try/catch so the providers panel update never breaks if a
  downstream module hasn't loaded yet. Both `_saveProviderKey` and
  `_removeProviderKey` invoke it (defense-in-depth: same staleness shape
  applies to the add path too).

Tests
-----
- `tests/test_issue1538_nous_live_catalog.py` (12 tests): live-fetch
  surfaces ≥20 entries, every id starts with `@nous:`, every label ends
  with ` (via Nous)`, recent flagships (Opus 4.7, GPT-5.5, Kimi K2.6,
  Gemini 3.1 Pro, MiniMax M2.7) reach the dropdown, static fallback
  works when hermes_cli raises, label formatter unit tests (vendor
  namespace stripping, variant rendering, MiniMax mixed-case), the
  curated static list and its routing invariants are preserved.
- `tests/test_issue1539_provider_removal_dropdown_invalidation.py`
  (11 tests): invalidator helper exists and clears both cache slots,
  exposed on window with typeof guard, both save and remove paths
  invoke the dropdown flush, helper calls both invalidator and
  populateModelDropdown, helper is resilient to missing modules,
  helper does not block panel refresh, server-side
  `set_provider_key → invalidate_models_cache` invariant pinned.

Verified live on port 8789: `/api/models` Nous group returns 30
models (was 4); browser `document.getElementById('modelSelect')`
exposes 30 options under the "Nous Portal" group; the dropdown-flush
helper is callable from the browser and round-trip rebuild keeps the
dropdown at 30 options.

Test counts:
- Full pytest: 4013 passed, 2 skipped, 3 xpassed, 0 failures
  (was 3990 → 4013, +23 from this PR).
- QA harness pytest: 20 passed.
- Browser API sanity: 11/11 passed.
- Agent Browser CDP: 21/23 passed (the 2 SSE liveness failures
  reproduce on master and are unrelated to this PR).
2026-05-03 18:12:01 +00:00
Hermes Bot 8616033605 fix(onboarding,providers): probe LM Studio /models + align env var with agent CLI (#1499 #1500)
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 #1499
Closes #1500

Co-authored-by: chwps <106549456+chwps@users.noreply.github.com>
Co-authored-by: AdoneyGalvan <25235323+AdoneyGalvan@users.noreply.github.com>
2026-05-03 02:46:24 +00:00
Hermes Bot d3c7ac182b fix(providers): map lmstudio to LMSTUDIO_API_KEY in _PROVIDER_ENV_VAR (#1420)
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>
2026-05-03 02:06:19 +00:00
nesquena-hermes 0e9bd651a4 fix: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410)
Two unrelated UX/Settings bugs, both small surgical fixes with regression
tests.

Issue #1409 — TTS toggle has no effect
=======================================
Reported via Discord: ticking Settings → Voice → "Text-to-Speech for
responses" did nothing. The speaker icon never appeared on assistant
messages despite the checkbox saving to localStorage correctly.

Root cause (CSS specificity collision):
  static/panels.js _applyTtsEnabled() set
    btn.style.display = enabled ? '' : 'none'
  on every .msg-tts-btn. The '' branch removes the inline override, after
  which the .msg-tts-btn { display:none; } rule from style.css re-hides the
  button. Both branches left the icon hidden, so the toggle has been
  silently broken since #499 first shipped the TTS feature.

Fix (body-class toggle, Option B from the issue):
  - panels.js: _applyTtsEnabled now toggles body.classList('tts-enabled')
  - style.css: new compound selector
      body.tts-enabled .msg-tts-btn { display:inline-flex; align-items:center; }
  - default-hidden rule (.msg-tts-btn{display:none;}) preserved so the icon
    stays hidden by default (CSS-only state)
  - boot.js paths that already call _applyTtsEnabled(localStorage…) work
    unchanged — the new function applies state at the body level instead of
    inline-styling individual buttons, so the rule survives renderMd()
    re-renders without re-querying every button

Verified end-to-end against live server: getComputedStyle on a probe
.msg-tts-btn returns display:flex when body has tts-enabled, display:none
when it doesn't. Two regression tests in TestIssue1409TtsToggleBodyClass
explicitly check for the body-class shape and forbid the broken inline-style
pattern.

Issue #1410 — Ollama (local) shows "API key configured" when only
              Ollama Cloud key is set
=================================================================
Reported via Discord: configuring Ollama Cloud lit up the local Ollama card
too. Both providers were mapped to OLLAMA_API_KEY in api/providers.py
_PROVIDER_ENV_VAR.

Root cause:
  api/providers.py:47-48
    "ollama":       "OLLAMA_API_KEY",
    "ollama-cloud": "OLLAMA_API_KEY",
  _provider_has_key("ollama") found the value the user set for Ollama Cloud
  and returned True. But the runtime code path in
  hermes_cli/runtime_provider.py only consumes OLLAMA_API_KEY when the base
  URL hostname is ollama.com (Ollama Cloud) — local Ollama is keyless by
  default and reaches a custom base URL with no auth. The WebUI was
  reporting "configured" for a key local Ollama doesn't even read.

Fix (Option A from the issue body, preferred):
  - Drop bare "ollama" from _PROVIDER_ENV_VAR with an inline comment
    explaining why
  - _provider_has_key("ollama") falls through to the config.yaml branch,
    which already supports providers.ollama.api_key for local users who
    genuinely need to set a token
  - ollama-cloud retains its OLLAMA_API_KEY mapping unchanged

Verified end-to-end against live server with OLLAMA_API_KEY=sk-cloud-key-test
in env: GET /api/providers reports has_key=True only for ollama-cloud, and
has_key=False for bare ollama. Two regression tests in
TestIssue1410OllamaEnvVarBleed cover the bleed-prevention case AND the
"local user with config.yaml api_key still reports configured" case to
guard against over-correction.

Tests
-----
3572 passed, 2 skipped, 3 xpassed (was 3567 — added 5 new regression tests).

Closes #1409
Closes #1410

Reported by @AvidFuturist (Discord, May 1 2026)
2026-05-01 17:14:51 +00:00
Frank Song b277e195fe Fix MiniMax China provider visibility 2026-04-29 15:50:32 +08:00
Hermes Agent 867f2a3f81 absorb: address Opus review findings (security + correctness)
B1: fix stored XSS in MCP delete button — replace inline onclick with
    data-mcp-name attribute + event delegation (panels.js)
B2: fix zip/tar-slip via startswith prefix collision — use
    is_relative_to(); track actual extracted bytes instead of trusting
    member.file_size (upload.py)
B3: add NVIDIA NIM endpoint to _OPENAI_COMPAT_ENDPOINTS and
    _SUPPORTED_PROVIDER_SETUPS so provider is reachable (routes.py,
    onboarding.py)
H1: add terminalResizeHandle element to index.html and return it from
    _terminalEls() so resize-by-drag works (index.html, terminal.js)
H2: fix dead get_terminal() branch — return None for dead terminals
    instead of always returning term (terminal.py)
H3: replace os.environ.copy() with a safe allowlist in PTY shell env
    so API keys are not exposed inside the terminal (terminal.py)
H5: make model dedup deterministic — sort groups by provider_id
    alphabetically before first-occurrence assignment (config.py)
H7: add pid regex validation before OAuth probe; constrain key_source
    to a closed set of safe values (providers.py)
M8: add double-run guard for cron run-now — reject if job is already
    tracked as running (routes.py)
2026-04-29 05:06:34 +00:00
fxd-jason 25958139da feat: show model names in provider cards + scan custom_providers
Provider card improvements:
- Show model name tags when a provider card is expanded (panels.js)
- Add .provider-card-model-tag styling (style.css)

Custom providers in providers panel:
- Scan config.yaml custom_providers (e.g. glmcode, timicc) and list
  them as providers with their configured models (api/providers.py)
- Detect API key status from env var references (${ENV_VAR})
2026-04-29 04:31:15 +00:00
bergeouss 0741a2ab9f fix: skip get_auth_status() fallback for known API-key providers
Avoids unnecessary latency on the Settings page by restricting the
OAuth auth-status fallback to providers that are not in _PROVIDER_ENV_VAR.

Review feedback (PR #1221): the get_auth_status() call in the else branch
was firing for every unconfigured API-key provider (openai, anthropic, etc.),
adding a network round-trip per provider. Now it only runs for providers
that are not known API-key providers (custom/OAuth-capable providers).
2026-04-29 04:31:11 +00:00
bergeouss ae2ed1a4e7 Fix #1214: refresh workspace on profile switch when session is empty
Add loadDir('.') call in switchToProfile() Case B so the workspace file
tree panel reflects the new profile's workspace instead of showing stale
files from the previous profile.

Fix #1212: detect OAuth providers not in hardcoded set

Expand _OAUTH_PROVIDERS with copilot-acp and qwen-oauth.
Add fallback in get_providers() that checks hermes auth live status
for providers that have no API key and are not in the hardcoded set
(e.g. Anthropic connected via OAuth), so the Providers tab shows
them as configured.
2026-04-29 04:31:11 +00:00
JinYue-GitHub 24d65a1efa Fix nvidia provider support in WebUI
- Add nvidia to _PROVIDER_DISPLAY, _PROVIDER_MODELS, and _PROVIDER_ALIASES
- Add nvidia to _PORTAL_PROVIDERS to preserve full model paths (e.g. qwen/qwen3-next-80b-a3b-instruct)
- Add NVIDIA_API_KEY to _PROVIDER_ENV_VAR for API key management
- Fixes 404 errors when using nvidia provider with models from multiple namespaces
2026-04-29 04:30:55 +00:00
nesquena-hermes 24b1e6f3fc fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)

Merges PRs #1208, #1209, #1210 (#1152 rebased):

- fix(providers): OAuth provider cards show correct Configured status in Settings.
  get_providers() was discarding has_key=True from _provider_has_key() for OAuth
  providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
  from the Settings panel. Surfaces auth_error string. (closes #1202)

- ux(profiles): profile chip shows spinner and new name immediately on switch.
  Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
  populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.

- feat: YOLO mode toggle — skip all approvals per session.
  /yolo slash command, "Skip all this session" button on approval cards,
  amber  pill indicator in composer footer. Session-scoped, in-memory.
  Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
  Original author: @bergeouss (PR #1152)

Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-27 22:56:12 -07:00
nesquena-hermes ef26d19549 fix: batch v0.50.228 — renderer, model race, tool card, empty session, .env (#1179)
Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser.

**Fix verifications:**
- #1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite 
- #1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard 
- #1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px   
- #1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused 
- #1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass 

Thanks @bsgdigital (#1150) and @bergeouss (#1178)!
2026-04-27 15:28:19 -07:00
nesquena-hermes 4528c6c848 v0.50.222: Korean locale, provider fixes, reasoning chip boot, Prism SRI (#1119)
* feat: add Korean locale support (#1093, @jundev0001) — 615 keys, copy_failed added

* fix(#1094): provider deletion + false positive API key + threading deadlock (#1102, @bergeouss)

* fix(#1103): show reasoning chip on page load not only after session load (#1114, @bergeouss)

* fix(#1100): remove Prism CSS SRI integrity to fix intermittent blocking (#1115, @bergeouss)

* fix(tests): update copy_failed locale count for 7 locales (Korean added)

* fix: drop unused _cfg_cache import; update locale count comment

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 14:24:20 -07:00
nesquena-hermes 0f1b232c12 fix(ci): eliminate test_set_key flakiness — v0.50.161
Root cause: test_profile_env_isolation.py and test_profile_path_security.py called sys.modules.pop() without restoring, poisoning subsequent tests. Fix: monkeypatch.delitem so pytest auto-restores. Also holds _ENV_LOCK for full I/O cycle in _write_env_file and creates .env at 0600 via os.open. Reviewed by Opus (no independent review needed — test/providers fix only).
2026-04-23 02:09:37 +00:00
nesquena-hermes 04b00065f9 feat: provider key management from Settings — v0.50.159 (PR #867 by @bergeouss, closes #586)
New Providers tab in Settings lets users add/update/remove API keys without editing .env. Six review fixes applied. 18 tests.
2026-04-23 01:09:22 +00:00