fix(transcription): resolve ${VAR} env refs in transcription api_key/api_base

config.loader.load_config() intentionally returns the raw config with ${VAR}
references intact — env interpolation is a separate, explicit step
(resolve_config_env_vars) so that settings read/edit/save paths never
materialize secrets to disk or to the UI.

The transcription config path does not apply that step: both
channels/base.py (channel voice notes) and webui/transcription_ws.py (WebUI
recording) build their effective config via
resolve_transcription_config(load_config()). As a result a configured
api_key of "${GROQ_API_KEY}" (the documented way to reference secrets) is
passed to the provider verbatim, which fails with 401 Invalid API Key. No
amount of rotating the real key helps, because the literal placeholder
string is what gets sent.

Resolve the reference at the single choke point both callers share —
_resolve_transcription_api_key / _resolve_transcription_api_base — using a
new lenient loader.resolve_env_refs() helper (unset var -> empty string, so
a missing variable degrades to "not configured" rather than raising or
leaking). This fixes both entry points at once and cannot drift the way a
per-call-site fix does. Resolving inside load_config() was rejected: the
~20 settings-UI callers depend on it returning raw ${VAR} placeholders.

Literal keys are unaffected; the settings API only reads the derived
`configured` flag (never the key), which now reflects the resolved value.

Claude-Session: https://claude.ai/code/session_01Q3HuVaJAAQJA3kgVQVJ2Zt
This commit is contained in:
Ben Lenarts
2026-07-21 17:35:16 +08:00
committed by Xubin Ren
parent b2cf37da4a
commit 4cfc99f4b3
3 changed files with 58 additions and 2 deletions
+3 -2
View File
@@ -20,6 +20,7 @@ from nanobot.audio.transcription_registry import (
get_transcription_provider,
resolve_transcription_provider,
)
from nanobot.config.loader import resolve_env_refs
from nanobot.config.paths import get_media_dir
from nanobot.providers.registry import find_by_name
from nanobot.utils.media_decode import FileSizeExceeded, save_base64_data_url
@@ -82,7 +83,7 @@ def _provider_default_api_base(provider: str) -> str | None:
def _resolve_transcription_api_key(provider: str, provider_cfg: Any) -> str:
api_key = getattr(provider_cfg, "api_key", None) if provider_cfg else None
api_key = resolve_env_refs(getattr(provider_cfg, "api_key", None) or "") if provider_cfg else ""
if api_key:
return api_key
@@ -97,7 +98,7 @@ def _resolve_transcription_api_key(provider: str, provider_cfg: Any) -> str:
def _resolve_transcription_api_base(provider: str, provider_cfg: Any) -> str:
api_base = getattr(provider_cfg, "api_base", None) if provider_cfg else None
api_base = resolve_env_refs(getattr(provider_cfg, "api_base", None) or "") if provider_cfg else ""
if api_base:
return api_base
return _provider_default_api_base(provider) or ""