from types import SimpleNamespace from typing import Any import pytest from src.config import EmbeddingModelConfig from src.embedding_client import _EmbeddingClient # pyright: ignore[reportPrivateUsage] class FakeOpenAIEmbeddingsAPI: def __init__(self, embedding: list[float]) -> None: self.embedding: list[float] = embedding self.calls: list[dict[str, Any]] = [] async def create(self, *, model: str, input: str | list[str]) -> SimpleNamespace: self.calls.append({"model": model, "input": input}) if isinstance(input, list): data = [SimpleNamespace(embedding=self.embedding) for _ in input] else: data = [SimpleNamespace(embedding=self.embedding)] return SimpleNamespace(data=data) @pytest.mark.asyncio async def test_openai_embedding_client_uses_configured_model_and_dimensions( monkeypatch: pytest.MonkeyPatch, ) -> None: fake_embeddings = FakeOpenAIEmbeddingsAPI([0.1] * 8) class FakeOpenAIClient: def __init__(self, *, api_key: str | None, base_url: str | None) -> None: self.api_key: str | None = api_key self.base_url: str | None = base_url self.embeddings: FakeOpenAIEmbeddingsAPI = fake_embeddings monkeypatch.setattr("src.embedding_client.AsyncOpenAI", FakeOpenAIClient) client = _EmbeddingClient( EmbeddingModelConfig( transport="openai", model="text-embedding-3-small", api_key="test-key", base_url="http://localhost:8000/v1", ), vector_dimensions=8, max_input_tokens=8192, max_tokens_per_request=300_000, ) embedding = await client.embed("hello world") assert embedding == [0.1] * 8 assert fake_embeddings.calls == [ {"model": "text-embedding-3-small", "input": ["hello world"]} ] @pytest.mark.asyncio async def test_openai_embedding_client_rejects_dimension_mismatch( monkeypatch: pytest.MonkeyPatch, ) -> None: fake_embeddings = FakeOpenAIEmbeddingsAPI([0.1] * 7) class FakeOpenAIClient: def __init__(self, *, api_key: str | None, base_url: str | None) -> None: self.embeddings: FakeOpenAIEmbeddingsAPI = fake_embeddings monkeypatch.setattr("src.embedding_client.AsyncOpenAI", FakeOpenAIClient) client = _EmbeddingClient( EmbeddingModelConfig( transport="openai", model="text-embedding-3-small", api_key="test-key", ), vector_dimensions=8, max_input_tokens=8192, max_tokens_per_request=300_000, ) with pytest.raises(ValueError, match="Embedding dimension mismatch"): await client.embed("hello world") @pytest.mark.asyncio async def test_gemini_embedding_client_uses_output_dimensionality( monkeypatch: pytest.MonkeyPatch, ) -> None: calls: list[dict[str, Any]] = [] class FakeGeminiModels: async def embed_content( self, *, model: str, contents: str | list[str], config: dict[str, Any], ) -> SimpleNamespace: calls.append( { "model": model, "contents": contents, "config": config, } ) return SimpleNamespace( embeddings=[SimpleNamespace(values=[0.2] * 12)], ) class FakeGeminiClient: def __init__(self, *, api_key: str | None, http_options: Any) -> None: self.api_key: str | None = api_key self.http_options: Any = http_options self.aio: Any = SimpleNamespace(models=FakeGeminiModels()) monkeypatch.setattr("src.embedding_client.genai.Client", FakeGeminiClient) client = _EmbeddingClient( EmbeddingModelConfig( transport="gemini", model="gemini-embedding-001", api_key="gemini-key", base_url="https://gemini-proxy.example/v1beta", ), vector_dimensions=12, max_input_tokens=4096, max_tokens_per_request=300_000, ) embedding = await client.embed("hello world") assert embedding == [0.2] * 12 assert calls == [ { "model": "gemini-embedding-001", "contents": "hello world", "config": {"output_dimensionality": 12}, } ]