fix(config): isolate GROQ_API_KEY from the generic LLM key

Folding GROQ_API_KEY into the generic api_key AliasChoices let the wrong
provider credential be selected: with both OPENAI_API_KEY and GROQ_API_KEY
set the OpenAI key won by alias order, and a Groq-only key would be
mirrored into OPENAI_API_KEY on non-Groq runs.

Give Groq a dedicated groq_api_key field (like perplexity_api_key) that
is only ever exported as GROQ_API_KEY, so it never contaminates the
generic key or another provider's env. LiteLLM reads GROQ_API_KEY
natively; the export also lets persisted config reach the provider.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
This commit is contained in:
Daniel Nguyen 2026-07-04 00:46:57 +08:00
parent 2647b2d499
commit b2de5cd9ad
3 changed files with 22 additions and 7 deletions

View file

@ -65,6 +65,10 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
llm = settings.llm
set_tracing_disabled(True)
_configure_litellm_compatibility()
if llm.groq_api_key:
# LiteLLM reads GROQ_API_KEY natively; export it (without touching the
# generic api_key) so persisted config reaches the provider too.
os.environ.setdefault("GROQ_API_KEY", llm.groq_api_key)
if llm.api_key:
set_default_openai_key(llm.api_key, use_for_tracing=False)
_configure_litellm_default("api_key", llm.api_key)

View file

@ -23,8 +23,12 @@ class LlmSettings(BaseSettings):
model: str | None = Field(default=None, alias="STRIX_LLM")
api_key: str | None = Field(
default=None,
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY", "GROQ_API_KEY"),
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"),
)
# Provider-native key kept separate from the generic ``api_key`` so it is only
# ever exported as ``GROQ_API_KEY`` and never mistaken for another provider's
# credential (e.g. mirrored into ``OPENAI_API_KEY``).
groq_api_key: str | None = Field(default=None, alias="GROQ_API_KEY")
api_base: str | None = Field(
default=None,
validation_alias=AliasChoices(

View file

@ -152,19 +152,26 @@ def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
# --------------------------------------------------------------------------- #
# LLM key aliases
# LLM provider keys
# --------------------------------------------------------------------------- #
def test_groq_api_key_alias_populates_llm_key(monkeypatch: pytest.MonkeyPatch) -> None:
def test_groq_api_key_populates_dedicated_field(monkeypatch: pytest.MonkeyPatch) -> None:
# GROQ_API_KEY must land on its own field, never the generic api_key, so it
# can't be mirrored into another provider's credential.
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
assert loader.load_settings().llm.api_key == "gsk-test"
llm = loader.load_settings().llm
assert llm.groq_api_key == "gsk-test"
assert llm.api_key is None
def test_llm_api_key_takes_precedence_over_groq(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LLM_API_KEY", "primary")
def test_groq_and_openai_keys_do_not_collide(monkeypatch: pytest.MonkeyPatch) -> None:
# Both set: each key stays on its own field instead of one overriding the other.
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai")
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
assert loader.load_settings().llm.api_key == "primary"
llm = loader.load_settings().llm
assert llm.api_key == "sk-openai"
assert llm.groq_api_key == "gsk-test"
# --------------------------------------------------------------------------- #