diff --git a/README.md b/README.md index c4d5c4f..6040ff9 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,11 @@ git clone https://github.com/ostefani/wepaircode cd wepaircode # 2. Create virtual environment -python3 -m venv .venv +python3.14 -m venv .venv source .venv/bin/activate # 3. Install dependencies -pip install -r requirements.txt -python3.14 -m pip install -r requirements.txt +python -m pip install -r requirements.txt python -m pytest tests/ -q diff --git a/agent/embeddings/factory.py b/agent/embeddings/factory.py index d77ce2a..287438f 100644 --- a/agent/embeddings/factory.py +++ b/agent/embeddings/factory.py @@ -1,5 +1,7 @@ import importlib from collections.abc import Callable +from ipaddress import ip_address +from urllib.parse import urlparse from ..config import Config from .adapters import ( @@ -16,16 +18,37 @@ "openai": "agent.embeddings.providers.openai.OpenAIEmbeddingAdapter", } +LOCAL_DUMMY_API_KEY = "dummy" + + +def _is_local_api_base(api_base: str) -> bool: + try: + host = urlparse(api_base).hostname or "" + except ValueError: + return False + host = host.lower().rstrip(".") + if host == "localhost": + return True + try: + return ip_address(host).is_loopback + except ValueError: + return False + + def build_embedding_provider_config(config: Config) -> EmbeddingProviderConfig: options = { key: value for key, value in dict(config.embedding_provider_options).items() if key not in RESERVED_EMBEDDING_PROVIDER_OPTION_KEYS } + + api_key = config.embedding_api_key or config.api_key + if not api_key and _is_local_api_base(config.embedding_api_base): + api_key = LOCAL_DUMMY_API_KEY return EmbeddingProviderConfig( provider=config.embedding_provider, api_base=config.embedding_api_base, - api_key=config.embedding_api_key or config.api_key, + api_key=api_key, model=config.embedding_model, options=options, ) diff --git a/tests/test_embedding_adapters.py b/tests/test_embedding_adapters.py index 3811d5e..ea60be0 100644 --- a/tests/test_embedding_adapters.py +++ b/tests/test_embedding_adapters.py @@ -296,6 +296,42 @@ def test_embedding_factory_falls_back_to_primary_api_key(monkeypatch) -> None: assert instance.calls[0]["input"] == ["hello"] +def test_embedding_factory_defaults_to_dummy_api_key_for_local_servers( + monkeypatch, +) -> None: + AsyncOpenAI, _, _ = _install_fake_openai(monkeypatch) + + config = Config( + embedding_provider="openai", + api_key="", + embedding_api_key="", + embedding_api_base="http://localhost:8082/v1", + embedding_model="embed-model", + ) + + adapter = asyncio.run(create_embedding_adapter(config)) + asyncio.run(adapter.embed(["hello"])) + + instance = AsyncOpenAI.instances[0] + assert instance.kwargs["api_key"] == embeddings_factory.LOCAL_DUMMY_API_KEY + assert instance.calls[0]["input"] == ["hello"] + asyncio.run(adapter.aclose()) + + +def test_embedding_factory_preserves_empty_api_key_for_remote_servers() -> None: + config = Config( + embedding_provider="openai", + api_key="", + embedding_api_key="", + embedding_api_base="https://api.openai.com/v1", + embedding_model="embed-model", + ) + + provider_config = embeddings_factory.build_embedding_provider_config(config) + + assert provider_config.api_key == "" + + def test_embedding_factory_probes_when_configured(monkeypatch) -> None: AsyncOpenAI, _, _ = _install_fake_openai(monkeypatch)