PlainSQL / backend /tests /test_groq.py
LalitChaudhari3's picture
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71
Raw
History Blame Contribute Delete
12 kB
"""
Tests for Groq Provider Integration.
Covers:
- GroqProvider initialization
- Sync generation
- Async generation
- Streaming
- ModelRouter Groq routing
- Fallback chain behavior
- Circuit breaker integration
- Fast model routing
"""
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
class TestGroqProvider:
"""Unit tests for the GroqProvider class."""
def _make_provider(self):
"""Create a GroqProvider with a mock API key."""
from app.llm.providers import GroqProvider
return GroqProvider(
api_key="test-groq-key",
model="llama-3.3-70b-versatile",
fast_model="llama-3.1-8b-instant",
base_url="https://api.groq.com/openai/v1",
)
def test_provider_init(self):
"""Test GroqProvider initializes correctly with all parameters."""
provider = self._make_provider()
assert provider.name == "groq"
assert provider.model == "llama-3.3-70b-versatile"
assert provider.fast_model == "llama-3.1-8b-instant"
assert provider.api_key == "test-groq-key"
assert provider.base_url == "https://api.groq.com/openai/v1"
def test_provider_name(self):
"""Test provider name property."""
provider = self._make_provider()
assert provider.name == "groq"
def test_lazy_client_init(self):
"""Test that clients are not created until first use."""
provider = self._make_provider()
assert provider._sync_client is None
assert provider._async_client is None
@patch("app.llm.providers.GroqProvider._get_sync_client")
def test_generate_calls_openai_api(self, mock_client):
"""Test sync generate calls the OpenAI-compatible API."""
# Mock the response
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SELECT * FROM users"
mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=5)
mock_client.return_value.chat.completions.create.return_value = mock_response
provider = self._make_provider()
result = provider.generate(
messages=[{"role": "user", "content": "Show all users"}],
max_tokens=100,
)
assert result == "SELECT * FROM users"
mock_client.return_value.chat.completions.create.assert_called_once()
@patch("app.llm.providers.GroqProvider._get_sync_client")
def test_generate_model_override(self, mock_client):
"""Test model_override kwarg changes the model used."""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SELECT 1"
mock_response.usage = None
mock_client.return_value.chat.completions.create.return_value = mock_response
provider = self._make_provider()
provider.generate(
messages=[{"role": "user", "content": "test"}],
model_override="llama-3.1-8b-instant",
)
call_kwargs = mock_client.return_value.chat.completions.create.call_args
assert call_kwargs.kwargs.get("model") == "llama-3.1-8b-instant"
class TestGroqAsync:
"""Async tests for GroqProvider."""
@pytest.fixture
def provider(self):
from app.llm.providers import GroqProvider
return GroqProvider(
api_key="test-key",
model="llama-3.3-70b-versatile",
fast_model="llama-3.1-8b-instant",
)
@pytest.mark.asyncio
async def test_agenerate_calls_async_client(self, provider):
"""Test async generation uses the async client."""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SELECT name FROM employees"
mock_response.usage = MagicMock(prompt_tokens=15, completion_tokens=8)
mock_client = AsyncMock()
mock_client.chat.completions.create = AsyncMock(return_value=mock_response)
provider._async_client = mock_client
result = await provider.agenerate(
messages=[{"role": "user", "content": "List employees"}]
)
assert result == "SELECT name FROM employees"
mock_client.chat.completions.create.assert_called_once()
@pytest.mark.asyncio
async def test_astream_yields_tokens(self, provider):
"""Test streaming yields individual tokens."""
# Create mock chunks
chunks = []
for token in ["SELECT ", "name ", "FROM ", "users"]:
chunk = MagicMock()
chunk.choices = [MagicMock()]
chunk.choices[0].delta.content = token
chunks.append(chunk)
# Add final chunk with None content
end_chunk = MagicMock()
end_chunk.choices = [MagicMock()]
end_chunk.choices[0].delta.content = None
chunks.append(end_chunk)
mock_client = AsyncMock()
# Create async iterator for the stream
async def mock_stream():
for c in chunks:
yield c
mock_client.chat.completions.create = AsyncMock(return_value=mock_stream())
provider._async_client = mock_client
tokens = []
async for token in provider.astream(
messages=[{"role": "user", "content": "test"}]
):
tokens.append(token)
assert len(tokens) == 4
assert "".join(tokens) == "SELECT name FROM users"
class TestModelRouterGroqIntegration:
"""Integration tests for Groq in ModelRouter."""
def _make_router_config(self, include_groq=True, include_hf=False):
"""Create a minimal router config."""
config = {"default_provider": "groq" if include_groq else "huggingface"}
if include_groq:
config["groq_api_key"] = "test-groq-key"
config["groq_model_primary"] = "llama-3.3-70b-versatile"
config["groq_model_fast"] = "llama-3.1-8b-instant"
if include_hf:
config["huggingface_token"] = "test-hf-token"
return config
def test_groq_becomes_primary(self):
"""Test that Groq is registered as the primary provider."""
from app.llm.router import ModelRouter
with patch("app.llm.providers.GroqProvider.health_check", return_value=True):
router = ModelRouter(self._make_router_config())
assert "groq" in router.providers
assert router.default_provider == "groq"
assert router.routing["fast"] == "groq"
assert router.routing["accurate"] == "groq"
def test_fallback_when_groq_unavailable(self):
"""Test fallback to HF when Groq is not configured."""
from app.llm.router import ModelRouter
config = self._make_router_config(include_groq=False, include_hf=True)
with patch("app.llm.providers.HuggingFaceProvider.health_check", return_value=True):
router = ModelRouter(config)
assert "groq" not in router.providers
assert "huggingface" in router.providers
assert router.default_provider == "huggingface"
def test_groq_in_fallback_chain(self):
"""Test that Groq appears first in fallback chain when generating."""
from app.llm.router import ModelRouter
with patch("app.llm.providers.GroqProvider.health_check", return_value=True):
router = ModelRouter(self._make_router_config())
# The fallback chain for 'accurate' should start with groq
assert router.routing["accurate"] == "groq"
def test_provider_priority_order(self):
"""Test providers are registered in priority order."""
from app.llm.router import ModelRouter
config = self._make_router_config(include_groq=True, include_hf=True)
with patch("app.llm.providers.GroqProvider.health_check", return_value=True):
with patch("app.llm.providers.HuggingFaceProvider.health_check", return_value=True):
router = ModelRouter(config)
providers = list(router.providers.keys())
# Groq should be before HuggingFace
assert providers.index("groq") < providers.index("huggingface")
class TestCircuitBreakerWithGroq:
"""Test circuit breaker integration with Groq."""
def test_circuit_breaker_created_for_groq(self):
"""Test that a circuit breaker is created for Groq."""
from app.llm.router import ModelRouter
with patch("app.llm.providers.GroqProvider.health_check", return_value=True):
router = ModelRouter({
"default_provider": "groq",
"groq_api_key": "test-key",
})
assert "groq" in router.breakers
assert router.breakers["groq"].state == "closed"
def test_circuit_breaker_opens_after_failures(self):
"""Test circuit breaker opens after threshold failures."""
from app.llm.router import CircuitBreaker
breaker = CircuitBreaker(failure_threshold=3)
assert breaker.is_available()
for _ in range(3):
breaker.record_failure()
assert not breaker.is_available()
assert breaker.state == "open"
class TestGroqModelRouting:
"""Test model selection based on routing preference."""
def test_fast_routing_uses_fast_model(self):
"""Test 'fast' preference routes to the 8B model."""
from app.llm.providers import GroqProvider
provider = GroqProvider(
api_key="test",
model="llama-3.3-70b-versatile",
fast_model="llama-3.1-8b-instant",
)
assert provider.model == "llama-3.3-70b-versatile"
assert provider.fast_model == "llama-3.1-8b-instant"
def test_config_settings_parsed(self):
"""Test Groq config fields are properly parsed."""
from app.config import Settings
import os
# Set required env var for Settings
os.environ.setdefault("DB_URI", "mysql://test:test@localhost/test")
settings = Settings(
DB_URI="mysql://test:test@localhost/test",
GROQ_API_KEY="test-key",
GROQ_MODEL_PRIMARY="custom-model",
GROQ_MODEL_FAST="custom-fast",
)
assert settings.GROQ_API_KEY == "test-key"
assert settings.GROQ_MODEL_PRIMARY == "custom-model"
assert settings.GROQ_MODEL_FAST == "custom-fast"
assert settings.DEFAULT_LLM_PROVIDER == "groq"
class TestGroqProviderMetrics:
"""Test that Groq calls are properly tracked in metrics."""
@patch("app.llm.providers.GroqProvider._get_sync_client")
def test_token_usage_logged(self, mock_client):
"""Test that token usage is extracted from Groq responses."""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SELECT 1"
mock_response.usage = MagicMock(prompt_tokens=42, completion_tokens=7)
mock_client.return_value.chat.completions.create.return_value = mock_response
from app.llm.providers import GroqProvider
provider = GroqProvider(api_key="test", model="test-model")
result = provider.generate(
messages=[{"role": "user", "content": "test"}]
)
assert result == "SELECT 1"
def test_token_tracker_tracks_groq(self):
"""Test TokenTracker records Groq usage."""
from app.llm.router import TokenTracker
tracker = TokenTracker()
info = tracker.track(
messages=[{"role": "user", "content": "Show employees"}],
response="SELECT * FROM employees",
provider="groq",
)
assert info["provider"] == "groq"
assert info["input_tokens"] > 0
assert info["output_tokens"] > 0
totals = tracker.get_totals()
assert totals["total_requests"] == 1
assert totals["total_tokens"] > 0