From c52418244b75b25c76394e120289792dceb7080f Mon Sep 17 00:00:00 2001 From: ostefani Date: Sun, 4 Oct 2026 22:07:48 -0700 Subject: [PATCH 1/2] fix: use dummy key for loopback otherwise fail fast. --- README.md | 3 +-- agent/embeddings/factory.py | 28 ++++++++++++++++++++++++- config.yaml | 2 +- tests/test_embedding_adapters.py | 36 ++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c4d5c4f..8f4b68a 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,10 @@ 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 pytest tests/ -q diff --git a/agent/embeddings/factory.py b/agent/embeddings/factory.py index d77ce2a..8482936 100644 --- a/agent/embeddings/factory.py +++ b/agent/embeddings/factory.py @@ -1,5 +1,6 @@ import importlib from collections.abc import Callable +from urllib.parse import urlparse from ..config import Config from .adapters import ( @@ -16,16 +17,41 @@ "openai": "agent.embeddings.providers.openai.OpenAIEmbeddingAdapter", } +# Placeholder key for local OpenAI-compatible servers (llama.cpp, Ollama, +# LM Studio) that ignore auth but sit behind an SDK requiring a non-empty key. +LOCAL_DUMMY_API_KEY = "dummy" + + +def _is_local_api_base(api_base: str) -> bool: + """Return True when ``api_base`` targets a loopback host.""" + try: + host = urlparse(api_base).hostname or "" + except ValueError: + return False + host = host.lower() + if host in ("localhost", "::1"): + return True + if host.startswith("127."): + return True + 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 } + + # Only substitute the placeholder for loopback targets so a missing key + # against a remote endpoint still fails fast. + 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/config.yaml b/config.yaml index 7848e3d..3004afc 100644 --- a/config.yaml +++ b/config.yaml @@ -18,7 +18,7 @@ chat_provider_options: {} # Embeddings for semantic retrieval / RAG embedding_provider: openai embedding_api_base: 'http://localhost:8081/v1' -embedding_api_key: '' +embedding_api_key: dummy embedding_model: 'nomic-embed-text' embedding_batch_size: 16 embedding_max_concurrency: 4 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) From 58033da6a6c49ca40b7499924ba207c75188f8ab Mon Sep 17 00:00:00 2001 From: ostefani Date: Sun, 4 Oct 2026 22:23:28 -0700 Subject: [PATCH 2/2] fix: check parsed IP address with ipaddress and accept localhost with an optional trailing dot. --- README.md | 2 +- agent/embeddings/factory.py | 17 +++++++---------- config.yaml | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8f4b68a..6040ff9 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ python3.14 -m venv .venv source .venv/bin/activate # 3. Install dependencies -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 8482936..287438f 100644 --- a/agent/embeddings/factory.py +++ b/agent/embeddings/factory.py @@ -1,5 +1,6 @@ import importlib from collections.abc import Callable +from ipaddress import ip_address from urllib.parse import urlparse from ..config import Config @@ -17,23 +18,21 @@ "openai": "agent.embeddings.providers.openai.OpenAIEmbeddingAdapter", } -# Placeholder key for local OpenAI-compatible servers (llama.cpp, Ollama, -# LM Studio) that ignore auth but sit behind an SDK requiring a non-empty key. LOCAL_DUMMY_API_KEY = "dummy" def _is_local_api_base(api_base: str) -> bool: - """Return True when ``api_base`` targets a loopback host.""" try: host = urlparse(api_base).hostname or "" except ValueError: return False - host = host.lower() - if host in ("localhost", "::1"): + host = host.lower().rstrip(".") + if host == "localhost": return True - if host.startswith("127."): - return True - return False + try: + return ip_address(host).is_loopback + except ValueError: + return False def build_embedding_provider_config(config: Config) -> EmbeddingProviderConfig: @@ -43,8 +42,6 @@ def build_embedding_provider_config(config: Config) -> EmbeddingProviderConfig: if key not in RESERVED_EMBEDDING_PROVIDER_OPTION_KEYS } - # Only substitute the placeholder for loopback targets so a missing key - # against a remote endpoint still fails fast. 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 diff --git a/config.yaml b/config.yaml index 3004afc..7848e3d 100644 --- a/config.yaml +++ b/config.yaml @@ -18,7 +18,7 @@ chat_provider_options: {} # Embeddings for semantic retrieval / RAG embedding_provider: openai embedding_api_base: 'http://localhost:8081/v1' -embedding_api_key: dummy +embedding_api_key: '' embedding_model: 'nomic-embed-text' embedding_batch_size: 16 embedding_max_concurrency: 4