feat: stamp API key into model override at save time to survive global provider change (#362)

* fix: stamp API key into model override at save time to survive global provider change

When a workflow overrides the TTS/LLM/STT provider to match the current
global config, the override dict only stores model/voice fields, not the
API key. If the global config later switches to a different provider, the
override can no longer inherit the API key and calls fail.

Fix: enrich_overrides_with_api_keys() copies the global provider's API
key (and other secret fields) into the override dict at workflow-save
time, making the override self-contained regardless of future global
config changes.

* feat: add test coverage and masking logic

---------

Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
nuthalapativarun 2026-05-27 01:31:14 -07:00 committed by GitHub
parent 8a58b0992d
commit 5b61ad645f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 451 additions and 39 deletions

View file

@ -2,6 +2,8 @@
from __future__ import annotations
import copy
from api.schemas.user_configuration import UserConfiguration
from api.services.configuration.registry import (
REGISTRY,
@ -29,6 +31,48 @@ def _build_section_from_override(service_type: ServiceType, override: dict):
return config_cls(**override)
_SECRET_FIELDS = ("api_key", "credentials", "aws_access_key", "aws_secret_key")
def enrich_overrides_with_api_keys(
model_overrides: dict,
user_config: UserConfiguration,
) -> dict:
"""Copy API keys from the global config into model_overrides where missing.
When a workflow override selects the same provider as the current global
config but omits the API key, the override becomes broken if the global
config later switches to a different provider. This function stamps the
global provider's API key (and other secret fields) into the override at
save time so the override is self-contained.
"""
result = copy.deepcopy(model_overrides)
for section_key in _SECTION_MAP:
if section_key not in result:
continue
override = result[section_key]
override_provider = override.get("provider")
if not override_provider:
continue
global_section = getattr(user_config, section_key, None)
if global_section is None:
continue
if getattr(global_section, "provider", None) != override_provider:
continue
for field in _SECRET_FIELDS:
if override.get(field):
continue
if field == "api_key" and hasattr(global_section, "get_all_api_keys"):
all_keys = global_section.get_all_api_keys()
if all_keys:
override[field] = all_keys[0] if len(all_keys) == 1 else all_keys
else:
global_value = getattr(global_section, field, None)
if global_value is not None:
override[field] = global_value
return result
def resolve_effective_config(
user_config: UserConfiguration,
model_overrides: dict | None,