diff --git a/EVAL_WRITEUP.md b/EVAL_WRITEUP.md index 1a97dc9..3bbbb04 100644 --- a/EVAL_WRITEUP.md +++ b/EVAL_WRITEUP.md @@ -105,18 +105,25 @@ them at all is the obvious next change. ### Reproducing it +Add to `backend/.env`: + +``` +PROVIDER=groq +GROQ_API_KEY=gsk_... +GROQ_MODEL=openai/gpt-oss-120b +``` + +Then: + ```bash -printf 'PROVIDER=openai -OPENAI_API_KEY=gsk_... -OPENAI_MODEL=openai/gpt-oss-120b -OPENAI_BASE_URL=https://api.groq.com/openai/v1 -' >> backend/.env backend/.venv/Scripts/python -m evals.runner ``` -`OPENAI_BASE_URL` points the tested OpenAI adapter at any compatible endpoint -(Groq, Together, OpenRouter, a local server) without touching code. Leave it unset -for OpenAI itself. +Groq is a first-class provider reusing the tested OpenAI adapter over its +OpenAI-compatible endpoint, with its own key and model rather than borrowing +`OPENAI_API_KEY`. For anything else that speaks the same wire format (Together, +OpenRouter, a local server), `OPENAI_BASE_URL` points that adapter wherever you +like without touching code. ## Honest limitations diff --git a/backend/.env.example b/backend/.env.example index 3bf6847..4ad79b8 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,12 +2,18 @@ # Copy to `.env` and fill in. Leave the selected provider's key unset to build # and test with the offline demo model (no network, no credentials required). -# Model provider: "openai", "anthropic", or "google". +# Model provider: "openai", "anthropic", "google", or "groq". PROVIDER=openai # OpenAI (used when PROVIDER=openai). The SDK also reads OPENAI_API_KEY directly. OPENAI_API_KEY= OPENAI_MODEL=gpt-4o +# Optional: point the OpenAI adapter at any other compatible endpoint. +OPENAI_BASE_URL= + +# Groq (used when PROVIDER=groq; OpenAI-compatible, so it reuses that adapter). +GROQ_API_KEY= +GROQ_MODEL=openai/gpt-oss-120b # Anthropic (used when PROVIDER=anthropic). ANTHROPIC_API_KEY= diff --git a/backend/app/agent/llm.py b/backend/app/agent/llm.py index edcc0a2..9ccd63d 100644 --- a/backend/app/agent/llm.py +++ b/backend/app/agent/llm.py @@ -185,6 +185,16 @@ def build_llm(settings) -> LLMClient: base_url=settings.openai_base_url, ) return DemoLLMClient() + if settings.provider == "groq": + if settings.groq_api_key: + from app.agent.openai_llm import OpenAILLMClient + + return OpenAILLMClient( + api_key=settings.groq_api_key, + model=settings.groq_model, + base_url=settings.groq_base_url, + ) + return DemoLLMClient() if settings.provider == "google": if settings.google_api_key: from app.agent.google_llm import GoogleLLMClient diff --git a/backend/app/config.py b/backend/app/config.py index a9431e6..120c8e5 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -28,7 +28,7 @@ class Settings(BaseSettings): # The agent core is written against an LLMClient seam, so the provider is a # config switch. Without the selected provider's key, the app falls back to # the offline DemoLLMClient (deterministic, no network). - provider: str = "openai" # "openai" | "anthropic" | "google" + provider: str = "openai" # "openai" | "anthropic" | "google" | "groq" openai_api_key: str | None = None openai_model: str = "gpt-4o" @@ -37,6 +37,14 @@ class Settings(BaseSettings): # that lets a run reach one without editing code. openai_base_url: str | None = None + # Groq speaks the OpenAI wire format, so it reuses the same tested adapter. + # It gets its own key and model rather than borrowing OPENAI_API_KEY: a + # gsk_ key living under a name that says "openai" is a trap for whoever + # reads the file next. + groq_api_key: str | None = None + groq_model: str = "openai/gpt-oss-120b" + groq_base_url: str = "https://api.groq.com/openai/v1" + anthropic_api_key: str | None = None anthropic_model: str = "claude-opus-5" @@ -57,6 +65,7 @@ def active_model(self) -> str: "openai": self.openai_model, "anthropic": self.anthropic_model, "google": self.google_model, + "groq": self.groq_model, }.get(self.provider, self.openai_model) @property @@ -66,6 +75,7 @@ def has_credentials(self) -> bool: "openai": self.openai_api_key, "anthropic": self.anthropic_api_key, "google": self.google_api_key, + "groq": self.groq_api_key, }.get(self.provider) ) diff --git a/backend/tests/test_agent.py b/backend/tests/test_agent.py new file mode 100644 index 0000000..e92de2d --- /dev/null +++ b/backend/tests/test_agent.py @@ -0,0 +1,29 @@ +"""Which client the provider switch actually builds.""" +from __future__ import annotations + +from app.agent.llm import build_llm +from app.config import Settings + + +class TestGroqIsItsOwnProvider: + """A gsk_ key under a name that says "openai" is a trap for the next reader.""" + + def test_groq_builds_the_openai_adapter_against_groqs_endpoint(self): + settings = Settings(provider="groq", groq_api_key="gsk_test", _env_file=None) + client = build_llm(settings) + + assert type(client).__name__ == "OpenAILLMClient" + assert client.model == "openai/gpt-oss-120b" + + def test_without_its_own_key_it_falls_back_to_the_demo_model(self): + settings = Settings( + provider="groq", groq_api_key=None, openai_api_key="sk-not-this-one", + _env_file=None, + ) + + assert type(build_llm(settings)).__name__ == "DemoLLMClient" + + def test_the_active_model_reports_groqs_model(self): + settings = Settings(provider="groq", groq_model="llama-3.3-70b", _env_file=None) + + assert settings.active_model == "llama-3.3-70b"