honcho/tests/llm/test_embedding_client.py

140 lines
4.3 KiB
Python

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},
}
]