fix(config): invalidate fields with missing env refs

This commit is contained in:
Xubin Ren
2026-07-21 17:35:16 +08:00
parent 4cfc99f4b3
commit b46e7f4377
2 changed files with 33 additions and 6 deletions
+9 -6
View File
@@ -121,15 +121,18 @@ def resolve_env_refs(value: str) -> str:
"""Resolve ``${VAR}`` references in a single string, leniently. """Resolve ``${VAR}`` references in a single string, leniently.
Unlike :func:`resolve_config_env_vars` (which walks a whole ``Config`` and Unlike :func:`resolve_config_env_vars` (which walks a whole ``Config`` and
raises on a missing variable), this resolves one value and substitutes an raises on a missing variable), this resolves one value and returns an empty
empty string for any unset reference. It is meant for individual, lazily string if any reference is unset. It is meant for individual, lazily consumed
consumed secret fields — e.g. a transcription provider's ``api_key`` — so a fields — e.g. a transcription provider's ``api_key`` or ``api_base`` — so a
missing variable degrades to "not configured" instead of sending the literal missing variable degrades to "not configured" instead of producing a partial
``${VAR}`` text to the provider. Non-string input is returned unchanged. value. Non-string input is returned unchanged.
""" """
if not isinstance(value, str): if not isinstance(value, str):
return value return value
return _ENV_REF_PATTERN.sub(lambda m: os.environ.get(m.group(1), ""), value) names = _ENV_REF_PATTERN.findall(value)
if any(name not in os.environ for name in names):
return ""
return _ENV_REF_PATTERN.sub(lambda m: os.environ[m.group(1)], value)
def _resolve_in_place(obj: Any) -> Any: def _resolve_in_place(obj: Any) -> Any:
+24
View File
@@ -185,6 +185,18 @@ def test_resolver_env_ref_missing_var_degrades_to_not_configured() -> None:
assert resolved.configured is False assert resolved.configured is False
def test_resolver_missing_embedded_api_key_ref_degrades_to_not_configured() -> None:
config = Config()
config.transcription.provider = "groq"
config.providers.groq.api_key = "Bearer ${MISSING_GROQ_KEY}"
with patch.dict(os.environ, {}, clear=True):
resolved = resolve_transcription_config(config)
assert not resolved.api_key
assert resolved.configured is False
def test_resolver_interpolates_env_ref_in_api_base() -> None: def test_resolver_interpolates_env_ref_in_api_base() -> None:
config = Config() config = Config()
config.transcription.provider = "groq" config.transcription.provider = "groq"
@@ -197,6 +209,18 @@ def test_resolver_interpolates_env_ref_in_api_base() -> None:
assert resolved.api_base == "https://groq.example/v1" assert resolved.api_base == "https://groq.example/v1"
def test_resolver_missing_embedded_api_base_ref_uses_provider_default() -> None:
config = Config()
config.transcription.provider = "groq"
config.providers.groq.api_key = "gsk-test"
config.providers.groq.api_base = "https://${MISSING_GROQ_HOST}/openai/v1"
with patch.dict(os.environ, {}, clear=True):
resolved = resolve_transcription_config(config)
assert resolved.api_base == "https://api.groq.com/openai/v1"
def test_resolver_supports_xiaomi_mimo_transcription_provider() -> None: def test_resolver_supports_xiaomi_mimo_transcription_provider() -> None:
config = Config() config = Config()
config.transcription.provider = "xiaomi_mimo" config.transcription.provider = "xiaomi_mimo"