Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions EVAL_WRITEUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 10 additions & 0 deletions backend/app/agent/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand All @@ -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
Expand All @@ -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)
)

Expand Down
29 changes: 29 additions & 0 deletions backend/tests/test_agent.py
Original file line number Diff line number Diff line change
@@ -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"
Loading