Spaces:
Sleeping
Sleeping
| """ | |
| 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: | |
| def adapter(self): | |
| a = GeminiAdapter() | |
| a.mock_mode = True | |
| return a | |
| 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 | |
| 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 | |
| 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 | |
| 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: | |
| def adapter(self): | |
| a = OllamaAdapter() | |
| a.mock_mode = True | |
| return a | |
| 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 | |
| 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 | |
| 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 | |
| async def test_zero_cost(self, adapter): | |
| cost = adapter._get_cost(100, 200) | |
| assert cost == 0.0 | |
| 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 | |