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/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 91fbdef14..3e59ee4df 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -25,6 +25,10 @@ class LlmSettings(BaseSettings): default=None, 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 247c41ffe..d9ab8a993 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,29 @@ def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None: assert loader.load_settings().llm.model == "second-model" +# --------------------------------------------------------------------------- # +# LLM provider keys +# --------------------------------------------------------------------------- # + + +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") + llm = loader.load_settings().llm + assert llm.groq_api_key == "gsk-test" + assert llm.api_key is None + + +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") + llm = loader.load_settings().llm + assert llm.api_key == "sk-openai" + assert llm.groq_api_key == "gsk-test" + + # --------------------------------------------------------------------------- # # persist_current # --------------------------------------------------------------------------- #