diff --git a/src/embedding_client.py b/src/embedding_client.py index d7f188a5..7ceda6db 100644 --- a/src/embedding_client.py +++ b/src/embedding_client.py @@ -805,9 +805,23 @@ class EmbeddingClient: ``hf:`` tokenizer specs fetch from the Hugging Face Hub; doing that on the first request would stall the event loop under the singleton lock. Calling this from the API and deriver lifespans moves any tokenizer - download off the request path and fails the process fast on a bad spec. + download off the request path and fails the process fast on a bad + tokenizer spec. Any other construction error (e.g. a missing API key) + is logged and deferred to first use, so deployments that never embed + keep starting up exactly as they did before warmup existed. """ - self._get_client() + try: + self._get_client() + except ValidationException: + # An explicitly configured tokenizer spec that cannot load is a + # misconfiguration of this feature; keep failing fast. + raise + except Exception: + logger.warning( + "Embedding client warmup failed; embedding calls will fail " + + "until the embedding configuration is fixed", + exc_info=True, + ) def _get_settings_signature(self) -> tuple[object, ...]: runtime_config = self._resolve_runtime_config() diff --git a/tests/llm/test_embedding_client.py b/tests/llm/test_embedding_client.py index d45a0ee2..185b1c95 100644 --- a/tests/llm/test_embedding_client.py +++ b/tests/llm/test_embedding_client.py @@ -1282,3 +1282,56 @@ def test_settings_signature_tracks_tokenizer( after = wrapper._get_settings_signature() # pyright: ignore[reportPrivateUsage] assert before != after + + +def test_warmup_defers_missing_api_key_instead_of_failing_startup( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """A keyless embedding config must not crash startup: warmup logs and + defers the failure to first use, matching pre-warmup lazy behavior.""" + from src.embedding_client import EmbeddingClient + + wrapper = EmbeddingClient() + monkeypatch.setattr(EmbeddingClient, "_instance", None) + monkeypatch.setattr(EmbeddingClient, "_instance_signature", None) + monkeypatch.setattr( + wrapper, + "_resolve_runtime_config", + lambda: EmbeddingModelConfig( + transport="openai", + model="text-embedding-3-small", + api_key=None, + ), + ) + + with caplog.at_level("WARNING", logger="src.embedding_client"): + wrapper.warmup() + + assert EmbeddingClient._instance is None # pyright: ignore[reportPrivateUsage] + assert "warmup failed" in caplog.text + + +def test_warmup_still_fails_fast_on_bad_tokenizer_spec( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An explicitly configured tokenizer spec that cannot load is a + misconfiguration of the tokenizer feature and must keep failing startup.""" + from src.embedding_client import EmbeddingClient + + wrapper = EmbeddingClient() + monkeypatch.setattr(EmbeddingClient, "_instance", None) + monkeypatch.setattr(EmbeddingClient, "_instance_signature", None) + monkeypatch.setattr( + wrapper, + "_resolve_runtime_config", + lambda: EmbeddingModelConfig( + transport="openai", + model="text-embedding-3-small", + api_key="test-key", + tokenizer="tiktoken:", + ), + ) + + with pytest.raises(ValidationException, match="requires an encoding name"): + wrapper.warmup()