From 2647b2d4992f28f6553a35732c35693856762d25 Mon Sep 17 00:00:00 2001 From: Daniel Nguyen Date: Sat, 4 Jul 2026 00:29:55 +0800 Subject: [PATCH 1/2] feat(config): accept GROQ_API_KEY for Groq-hosted models Add GROQ_API_KEY as an accepted alias for the LLM key alongside LLM_API_KEY / OPENAI_API_KEY, so STRIX_LLM="groq/" works with a provider-native key. Groq models already route through LiteLLM (which mirrors the configured key to GROQ_API_KEY), so this wires up the alias plus docs and tests. Signed-off-by: Daniel Nguyen --- README.md | 9 +++++++++ strix/config/settings.py | 2 +- tests/test_config_loader.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2882f6e4..a5c9d3005 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,15 @@ export PERPLEXITY_API_KEY="your-api-key" # for search capabilities export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, quick scan: medium) ``` +Provider-native keys are also picked up automatically. For example, to use +[Groq's](https://console.groq.com/) fast inference you can set `GROQ_API_KEY` +directly instead of `LLM_API_KEY`: + +```bash +export STRIX_LLM="groq/llama-3.3-70b-versatile" +export GROQ_API_KEY="your-api-key" +``` + > [!NOTE] > Strix automatically saves your configuration to `~/.strix/cli-config.json`, so you don't have to re-enter it on every run. diff --git a/strix/config/settings.py b/strix/config/settings.py index 91fbdef14..04651d877 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -23,7 +23,7 @@ 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"), + validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY", "GROQ_API_KEY"), ) api_base: str | None = Field( default=None, diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index 247c41ffe..f6b48e1f4 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -20,6 +20,7 @@ "STRIX_LLM", "LLM_API_KEY", "OPENAI_API_KEY", + "GROQ_API_KEY", "LLM_API_BASE", "OPENAI_API_BASE", "OPENAI_BASE_URL", @@ -150,6 +151,22 @@ def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None: assert loader.load_settings().llm.model == "second-model" +# --------------------------------------------------------------------------- # +# LLM key aliases +# --------------------------------------------------------------------------- # + + +def test_groq_api_key_alias_populates_llm_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + assert loader.load_settings().llm.api_key == "gsk-test" + + +def test_llm_api_key_takes_precedence_over_groq(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LLM_API_KEY", "primary") + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + assert loader.load_settings().llm.api_key == "primary" + + # --------------------------------------------------------------------------- # # persist_current # --------------------------------------------------------------------------- # From b2de5cd9ad67d552df1f08e01188258585bd08f5 Mon Sep 17 00:00:00 2001 From: Daniel Nguyen Date: Sat, 4 Jul 2026 00:46:57 +0800 Subject: [PATCH 2/2] 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 --- strix/config/models.py | 4 ++++ strix/config/settings.py | 6 +++++- tests/test_config_loader.py | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/strix/config/models.py b/strix/config/models.py index 3cca9fb38..58385a820 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -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) diff --git a/strix/config/settings.py b/strix/config/settings.py index 04651d877..3e59ee4df 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -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( diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index f6b48e1f4..d9ab8a993 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -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" # --------------------------------------------------------------------------- #