When a provider's 'models' config contains dicts (e.g. {"id": "x", "label": "y"})
instead of plain strings, _apply_provider_prefix() crashes with:
AttributeError: 'dict' object has no attribute 'startswith'
This happens because the list comprehension at line 3505 passes the raw dict
as the model ID. The fix extracts 'id' and 'label' from dict entries while
keeping string entries as-is.
Fixes the /api/models and /api/onboarding/status 500 errors.
fix: guard stale stream writebacks (LumenYoung)
Prevents stale WebUI stream workers from writing old results into a session
after that session has already moved on to another stream. Adds new helper
_stream_writeback_is_current() (a token equality check against the session's
active_stream_id) and short-circuits the two finalize/cancel paths when the
worker no longer owns the session writeback.
(1) compress/status no longer pops the job entry on first read of `done` payload.
Second open tab no longer sees `idle` and a stale-job toast.
(2) compress/start no longer short-circuits to a stale `done` payload when
re-invoked within the 10-minute TTL. Re-running /compress always starts
fresh, so closing-and-reopening a tab mid-compress works correctly.
Third SHOULD-FIX (#2135 cfg["model"] fallback tightening when no custom_providers
entry matches) deferred to follow-up — strictly no-worse-than-master behavior.
tests/test_sprint46.py 10/10 still passes.
#2142 (legeantbleu) added the fr locale to static/i18n.js but didn't update:
1. tests/test_issue1488_composer_voice_buttons.py: two TestComposerVoiceButtonI18n + TestVoiceModePreferenceGate LOCALES tuples needed 'fr'
2. api/routes.py: _LOGIN_LOCALE needed an 'fr' block so the login page localizes for French users (issue #1442 parity contract)
3. tests/test_login_locale_parity.py: the test asserting 'fr' falls-back-to-'en' is inverted — fr now resolves to fr, with sibling assertions for fr-FR and fr-CA
Mirrors the stage-340 fix for the it locale (PR #2067 → maintainer adds tuple entries). 46/46 i18n tests pass after fix.
Worst case 4×5s=20s per polling request on ThreadingHTTPServer pool is risky
given today's _cron_env_lock near-miss on production 8787. Status probes
should fail fast; client can retry. All four call sites use default timeout.
Opus advisor pass on stage-341 found three surgical items:
1. static/i18n.js:it — PR #2064 branched before stage-340 landed the 'it'
locale (#2067), missing 9 session_*worktree* keys. Mechanical mirror of
en/ja position. Italian falls back to English silently without this fix.
2. api/streaming.py — PR #2107's new break short-circuit was silent in both
the aux and agent title-generation paths. Added logger.debug calls before
each break so production logs surface the exit shape.
3. api/streaming.py — Expanded _title_should_skip_remaining_attempts docstring
to document the membership criterion explicitly (vs the implicit
reasoning-only-burn case it ships with today). Future additions
(llm_safety_blocked, llm_oauth_quota) have a clear inclusion test.
CHANGELOG updated under the Stage-341 maintainer fixes section to mirror
the stage-340 pattern. All targeted tests pass (57/57 in the affected
modules).
Reasoning models (Qwen3-thinking via LM Studio, DeepSeek-R1, Kimi-K2,
etc.) can burn their entire output budget on hidden reasoning tokens and
emit no visible content. The previous title-generation retry path
classified that as llm_length and doubled the budget — but the second
call produces the same shape, so the retry only doubled the GPU/credit
burn. Repeated across the two prompts in _title_prompts() this came to
~3000 reasoning tokens of GPU work per new chat. On local LM Studio
servers behind a custom: provider (where is_lmstudio=False means
reasoning_effort: none never reaches the model) it manifested as the GPU
never going idle after a prompt.
Fix:
- _extract_title_response: classify reasoning-bearing empty responses
as llm_empty_reasoning regardless of finish_reason. The presence of
reasoning_content is the diagnostic signal, not finish_reason.
- _title_retry_status: drop llm_empty_reasoning from the retry set.
Length-truncated responses WITHOUT reasoning still retry (those are
legitimately recoverable by a larger budget).
- Add _title_should_skip_remaining_attempts() and break out of the
prompt-iteration loop on empty-reasoning. A second prompt against
the same model would produce the same shape.
- Falls through to _fallback_title_from_exchange for a local-summary
title.
Tests updated to invert the previous reasoning-retry assertions:
- test_aux_short_circuits_on_empty_reasoning_without_retrying
- test_aux_still_retries_finish_length_without_reasoning
- test_agent_route_short_circuits_on_empty_reasoning_without_retrying
- test_agent_route_still_retries_finish_length_without_reasoning
Companion agent-side work (LM Studio classifier for custom: providers)
is tracked separately on the hermes-agent side; this WebUI fix is the
belt-and-braces guard so the loop stops regardless of agent classifier
state.
Reported by @darkopetrovic. Closes#2083.
Co-authored-by: darkopetrovic <darkopetrovic@users.noreply.github.com>
(cherry picked from commit efeae4a86e)
Adds complete Italian translation for all ~280 UI strings in static/i18n.js
and the login page strings in api/routes.py (_LOGIN_LOCALE).
Ordered alphabetically: en → it → ja in both files.
Preserves all JS function templates, template literals, and plural forms.
(cherry picked from commit c66e04b190)
Opus stage-339 review SHOULD-FIX items:
1. server.py: drop 'unsafe-eval' from CSP report-only policy.
Verified by grepping all production JS — zero matches for eval(),
new Function(), or string-form setTimeout/setInterval. Keeping it
was a gratuitous privilege.
2. server.py: add https://cdn.jsdelivr.net to script-src + style-src.
index.html loads Prism/xterm/katex from this CDN with SRI hashes —
without the allowance every page load fires known-good CSP violations
that drown out real signal once a collector is wired.
3. api/commands.py: sanitize plugin command error. Previously returned
f'Plugin command error: {exc}' which would leak paths/env from
FileNotFoundError('/etc/something/secret.key') etc. Now returns only
the exception type name; full traceback goes to server log.
Test asserts updated to match the new policy shape.
Co-authored-by: Opus advisor <opus-advisor@hermes.local>
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>