InferRoute / tests /test_adapters.py
Ypeng12's picture
feat: Kubernetes-ready multi-provider LLM gateway v0.2.0
8a841b2
Raw
History Blame Contribute Delete
6.27 kB
"""
Adapter unit tests for Gemini and Ollama (mock mode).
Tests:
- GeminiAdapter.generate() mock returns correct shape
- GeminiAdapter.generate_stream() mock yields correct chunks
- OllamaAdapter.generate() mock returns correct shape
- OllamaAdapter.generate_stream() mock yields correct chunks
- Both adapters handle json_schema response_format
- Cost calculation is correct per model
"""
import asyncio
import pytest
from inferroute.adapters.gemini import GeminiAdapter, MODEL_PRICING
from inferroute.adapters.ollama import OllamaAdapter
SIMPLE_REQ = {
"model": "gemini-1.5-flash",
"messages": [{"role": "user", "content": "Hello, world!"}],
"tenant_id": "test_tenant",
}
JSON_SCHEMA_REQ = {
"model": "gemini-1.5-flash",
"messages": [{"role": "user", "content": "Extract invoice fields"}],
"tenant_id": "test_tenant",
"response_format": {"type": "json_schema", "schema": {"type": "object"}},
}
# ── Gemini Adapter Tests ──────────────────────────────────────────────────────
class TestGeminiAdapterMock:
@pytest.fixture
def adapter(self):
a = GeminiAdapter()
a.mock_mode = True
return a
@pytest.mark.asyncio
async def test_generate_returns_valid_shape(self, adapter):
resp = await adapter.generate(SIMPLE_REQ)
assert resp["object"] == "chat.completion"
assert len(resp["choices"]) == 1
assert resp["choices"][0]["message"]["role"] == "assistant"
assert isinstance(resp["choices"][0]["message"]["content"], str)
assert "usage" in resp
assert resp["usage"]["total_tokens"] > 0
assert "timing" in resp
assert resp["timing"]["ttft_ms"] > 0
@pytest.mark.asyncio
async def test_generate_stream_yields_chunks(self, adapter):
chunks = []
async for chunk in adapter.generate_stream(SIMPLE_REQ):
chunks.append(chunk)
assert len(chunks) > 0
# Last chunk should be the stats chunk (no choices, has usage)
stats_chunk = chunks[-1]
assert "usage" in stats_chunk
assert "timing" in stats_chunk
assert stats_chunk["usage"]["total_tokens"] > 0
# At least one chunk should have content
content_chunks = [
c for c in chunks
if c.get("choices") and c["choices"][0].get("delta", {}).get("content")
]
assert len(content_chunks) > 0
@pytest.mark.asyncio
async def test_generate_json_schema_format(self, adapter):
resp = await adapter.generate(JSON_SCHEMA_REQ)
content = resp["choices"][0]["message"]["content"]
assert "invoice_id" in content or "GEMINI" in content
@pytest.mark.asyncio
async def test_generate_stream_json_schema_format(self, adapter):
all_content = []
async for chunk in adapter.generate_stream(JSON_SCHEMA_REQ):
if chunk.get("choices"):
delta = chunk["choices"][0].get("delta", {})
if delta.get("content"):
all_content.append(delta["content"])
full = "".join(all_content)
assert "GEMINI" in full or "invoice_id" in full
def test_cost_calculation_flash(self, adapter):
cost = adapter._get_cost("gemini-1.5-flash", 1000, 500)
expected = 1000 * MODEL_PRICING["gemini-1.5-flash"]["input"] + 500 * MODEL_PRICING["gemini-1.5-flash"]["output"]
assert abs(cost - expected) < 1e-10
def test_cost_calculation_pro(self, adapter):
cost = adapter._get_cost("gemini-1.5-pro", 1000, 500)
expected = 1000 * MODEL_PRICING["gemini-1.5-pro"]["input"] + 500 * MODEL_PRICING["gemini-1.5-pro"]["output"]
assert abs(cost - expected) < 1e-10
# Pro should be more expensive than Flash
flash_cost = adapter._get_cost("gemini-1.5-flash", 1000, 500)
assert cost > flash_cost
# ── Ollama Adapter Tests ──────────────────────────────────────────────────────
OLLAMA_REQ = {
"model": "llama3",
"messages": [{"role": "user", "content": "What is 2+2?"}],
"tenant_id": "test_tenant",
}
OLLAMA_JSON_REQ = {
"model": "llama3",
"messages": [{"role": "user", "content": "Parse this invoice"}],
"tenant_id": "test_tenant",
"response_format": {"type": "json_schema", "schema": {"type": "object"}},
}
class TestOllamaAdapterMock:
@pytest.fixture
def adapter(self):
a = OllamaAdapter()
a.mock_mode = True
return a
@pytest.mark.asyncio
async def test_generate_returns_valid_shape(self, adapter):
resp = await adapter.generate(OLLAMA_REQ)
assert resp["object"] == "chat.completion"
assert len(resp["choices"]) == 1
assert resp["choices"][0]["finish_reason"] == "stop"
assert resp["usage"]["estimated_cost_usd"] == 0.0 # local = free
@pytest.mark.asyncio
async def test_generate_stream_yields_chunks(self, adapter):
chunks = []
async for chunk in adapter.generate_stream(OLLAMA_REQ):
chunks.append(chunk)
assert len(chunks) > 0
stats = chunks[-1]
assert stats["usage"]["estimated_cost_usd"] == 0.0
assert "timing" in stats
@pytest.mark.asyncio
async def test_generate_json_schema_format(self, adapter):
resp = await adapter.generate(OLLAMA_JSON_REQ)
content = resp["choices"][0]["message"]["content"]
assert "OLLAMA" in content or "invoice_id" in content
@pytest.mark.asyncio
async def test_zero_cost(self, adapter):
cost = adapter._get_cost(100, 200)
assert cost == 0.0
@pytest.mark.asyncio
async def test_stream_ttft_faster_than_openai(self, adapter):
"""Ollama mock should have faster TTFT than OpenAI's 250ms mock."""
import time
start = time.time()
async for _ in adapter.generate_stream(OLLAMA_REQ):
break
elapsed = (time.time() - start) * 1000.0
# Ollama TTFT is ~80ms mock delay β€” should be well under OpenAI's 250ms
assert elapsed < 250.0