From 04552996b5625de1d3dab8048051b31cce2af145 Mon Sep 17 00:00:00 2001 From: aiedwardyi <41576951+aiedwardyi@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:46:02 +0900 Subject: [PATCH 1/2] fix: defer fastembed import errors to use time instead of import time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace import-time ConfigurationError raises with conditional imports using has_package() sentinels, following the existing pattern in service_cards.py. Modules can now be safely imported even when fastembed is unavailable — errors are raised only when fastembed functionality is actually invoked. Files changed: - fastembed_extensions.py: guard imports + model constants, add _require_fastembed() check in provider functions - embedding/providers/fastembed.py: conditional import with Any fallbacks - reranking/providers/fastembed.py: conditional import with Any fallbacks Closes #279 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../embedding/fastembed_extensions.py | 184 ++++++++++-------- .../embedding/providers/fastembed.py | 24 ++- .../reranking/providers/fastembed.py | 19 +- 3 files changed, 124 insertions(+), 103 deletions(-) diff --git a/src/codeweaver/providers/embedding/fastembed_extensions.py b/src/codeweaver/providers/embedding/fastembed_extensions.py index eea6304f0..15136bf2f 100644 --- a/src/codeweaver/providers/embedding/fastembed_extensions.py +++ b/src/codeweaver/providers/embedding/fastembed_extensions.py @@ -8,12 +8,14 @@ from __future__ import annotations -from typing import overload +from typing import TYPE_CHECKING, Any, overload from codeweaver.core.di import dependency_provider +from codeweaver.core.utils import has_package +_FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") -try: +if TYPE_CHECKING or _FASTEMBED_AVAILABLE: from fastembed.common.model_description import ( BaseModelDescription, DenseModelDescription, @@ -24,87 +26,104 @@ from fastembed.rerank.cross_encoder import TextCrossEncoder from fastembed.sparse import SparseTextEmbedding from fastembed.text import TextEmbedding +else: + BaseModelDescription = Any + DenseModelDescription = Any + ModelSource = Any + PoolingType = Any + TextCrossEncoder = Any + SparseTextEmbedding = Any + TextEmbedding = Any -except ImportError as e: - from codeweaver.core import ConfigurationError - - raise ConfigurationError( - "fastembed is not installed. Please install it with `pip install code-weaver[fastembed]` or `codeweaver[fastembed-gpu]`." - ) from e - -""" -SPARSE_MODELS = ( - SparseModelDescription( - model="prithivida/Splade_PP_en_v2", - vocab_size=30522, # BERT base uncased vocab - description="SPLADE++ v2", - license="apache-2.0", - size_in_GB=0.6, - sources=ModelSource(hf="prithivida/Splade_PP_en_v2"), - model_file="model.onnx", - ), -) -""" -DENSE_MODELS = ( - DenseModelDescription( - model="Alibaba-NLP/gte-modernbert-base", - license="apache-2.0", - sources=ModelSource(hf="Alibaba-NLP/gte-modernbert-base"), - description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year.""", - model_file="onnx/model.onnx", - size_in_GB=0.60, - dim=768, - ), - DenseModelDescription( - model="BAAI/bge-m3", - license="mit", - sources=ModelSource(hf="BAAI/bge-m3"), - # if this seems like a strange description, it's because it mirrors the FastEmbed format, which gets parsed - description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year.""", - model_file="onnx/model.onnx", - additional_files=["onnx/model.onnx_data"], - size_in_GB=2.27, - dim=1024, - ), - DenseModelDescription( - model="WhereIsAI/UAE-Large-V1", - license="mit", - sources=ModelSource(hf="WhereIsAI/UAE-Large-V1"), - description="""Text embeddings, Unimodal (text), multilingual, 512 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", - model_file="onnx/model.onnx", - size_in_GB=1.23, - dim=1024, - ), - DenseModelDescription( - model="snowflake/snowflake-arctic-embed-l-v2.0", - license="apache-2.0", - sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-l-v2.0"), - description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", - model_file="onnx/model.onnx", - size_in_GB=1.79, - dim=1024, - ), - DenseModelDescription( - model="snowflake/snowflake-arctic-embed-m-v2.0", - license="apache-2.0", - sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-m-v2.0"), - description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", - model_file="onnx/model.onnx", - size_in_GB=1.23, - dim=768, - ), -) -RERANKING_MODELS: tuple[BaseModelDescription, ...] = ( - BaseModelDescription( - model="Alibaba-NLP/gte-reranker-modernbert-base", - license="apache-2.0", - sources=ModelSource(hf="Alibaba-NLP/gte-reranker-modernbert-base"), - description="""A lightweight high-performance cross-encoder with 8192 token context length.""", - model_file="onnx/model_fp16.onnx", - size_in_GB=0.3, - ), -) +def _require_fastembed() -> None: + """Raise ConfigurationError if fastembed is not installed.""" + if not _FASTEMBED_AVAILABLE: + from codeweaver.core import ConfigurationError + + raise ConfigurationError( + "fastembed is not installed. Please install it with " + "`pip install code-weaver[fastembed]` or `codeweaver[fastembed-gpu]`." + ) + + +if _FASTEMBED_AVAILABLE: + """ + SPARSE_MODELS = ( + SparseModelDescription( + model="prithivida/Splade_PP_en_v2", + vocab_size=30522, # BERT base uncased vocab + description="SPLADE++ v2", + license="apache-2.0", + size_in_GB=0.6, + sources=ModelSource(hf="prithivida/Splade_PP_en_v2"), + model_file="model.onnx", + ), + ) + """ + DENSE_MODELS = ( + DenseModelDescription( + model="Alibaba-NLP/gte-modernbert-base", + license="apache-2.0", + sources=ModelSource(hf="Alibaba-NLP/gte-modernbert-base"), + description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year.""", + model_file="onnx/model.onnx", + size_in_GB=0.60, + dim=768, + ), + DenseModelDescription( + model="BAAI/bge-m3", + license="mit", + sources=ModelSource(hf="BAAI/bge-m3"), + # if this seems like a strange description, it's because it mirrors the FastEmbed format, which gets parsed + description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year.""", + model_file="onnx/model.onnx", + additional_files=["onnx/model.onnx_data"], + size_in_GB=2.27, + dim=1024, + ), + DenseModelDescription( + model="WhereIsAI/UAE-Large-V1", + license="mit", + sources=ModelSource(hf="WhereIsAI/UAE-Large-V1"), + description="""Text embeddings, Unimodal (text), multilingual, 512 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", + model_file="onnx/model.onnx", + size_in_GB=1.23, + dim=1024, + ), + DenseModelDescription( + model="snowflake/snowflake-arctic-embed-l-v2.0", + license="apache-2.0", + sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-l-v2.0"), + description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", + model_file="onnx/model.onnx", + size_in_GB=1.79, + dim=1024, + ), + DenseModelDescription( + model="snowflake/snowflake-arctic-embed-m-v2.0", + license="apache-2.0", + sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-m-v2.0"), + description="""Text embeddings, Unimodal (text), multilingual, 8192 input tokens truncation, Prefixes for queries/documents: necessary, 2024 year.""", + model_file="onnx/model.onnx", + size_in_GB=1.23, + dim=768, + ), + ) + + RERANKING_MODELS: tuple[BaseModelDescription, ...] = ( + BaseModelDescription( + model="Alibaba-NLP/gte-reranker-modernbert-base", + license="apache-2.0", + sources=ModelSource(hf="Alibaba-NLP/gte-reranker-modernbert-base"), + description="""A lightweight high-performance cross-encoder with 8192 token context length.""", + model_file="onnx/model_fp16.onnx", + size_in_GB=0.3, + ), + ) +else: + DENSE_MODELS = () + RERANKING_MODELS = () @overload @@ -161,6 +180,7 @@ def get_cross_encoder() -> type[TextCrossEncoder]: """ Get the cross encoder with added custom models. """ + _require_fastembed() return add_models(TextCrossEncoder, RERANKING_MODELS) @@ -171,6 +191,7 @@ def get_sparse_embedder() -> type[SparseTextEmbedding]: TODO: Temporarily disabled until we can work out the bugs on added sparse models in FastEmbed. """ + _require_fastembed() # splade_pp.supported_splade_models.append(SPARSE_MODELS[0]) return SparseTextEmbedding @@ -182,6 +203,7 @@ def get_text_embedder() -> type[TextEmbedding]: Only adds models that aren't already in FastEmbed's native registry to avoid conflicts. """ + _require_fastembed() from fastembed.common.model_description import PoolingType # we don't add these yet, but they're here for when we do diff --git a/src/codeweaver/providers/embedding/providers/fastembed.py b/src/codeweaver/providers/embedding/providers/fastembed.py index 83a2dd02f..af4a77bca 100644 --- a/src/codeweaver/providers/embedding/providers/fastembed.py +++ b/src/codeweaver/providers/embedding/providers/fastembed.py @@ -17,7 +17,7 @@ import logging from collections.abc import Callable, Iterable, Sequence -from typing import Any, ClassVar, Literal, cast, override +from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, override import numpy as np @@ -26,10 +26,10 @@ from codeweaver.core import ( CodeChunk, CodeWeaverSparseEmbedding, - ConfigurationError, Provider, rpartial, ) +from codeweaver.core.utils import has_package from codeweaver.providers.embedding.capabilities.base import SparseEmbeddingModelCapabilities from codeweaver.providers.embedding.providers.base import ( EmbeddingCustomDeps, @@ -38,8 +38,9 @@ SparseEmbeddingProvider, ) +_FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") -try: +if TYPE_CHECKING or _FASTEMBED_AVAILABLE: from fastembed.sparse import SparseTextEmbedding from fastembed.text import TextEmbedding @@ -47,13 +48,16 @@ get_sparse_embedder, get_text_embedder, ) -except ImportError as e: - raise ConfigurationError( - r"FastEmbed is not installed. Please install it with `pip install code-weaver\[fastembed]` or `code-weaver\[fastembed-gpu]`." - ) from e - -_TextEmbedding = get_text_embedder() -_SparseTextEmbedding = get_sparse_embedder() +else: + TextEmbedding = Any + SparseTextEmbedding = Any + +if _FASTEMBED_AVAILABLE: + _TextEmbedding = get_text_embedder() + _SparseTextEmbedding = get_sparse_embedder() +else: + _TextEmbedding = None + _SparseTextEmbedding = None logger = logging.getLogger(__name__) diff --git a/src/codeweaver/providers/reranking/providers/fastembed.py b/src/codeweaver/providers/reranking/providers/fastembed.py index b6fd7f974..bebd12ac4 100644 --- a/src/codeweaver/providers/reranking/providers/fastembed.py +++ b/src/codeweaver/providers/reranking/providers/fastembed.py @@ -11,29 +11,24 @@ from collections.abc import Callable, Sequence from functools import partial -from typing import Any, ClassVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, cast import numpy as np from codeweaver.core import Provider, ProviderError from codeweaver.core.constants import DEFAULT_RERANKING_MAX_RESULTS +from codeweaver.core.utils import has_package from codeweaver.providers.reranking.providers.base import RerankingProvider logger = logging.getLogger(__name__) -try: - from fastembed.rerank.cross_encoder import TextCrossEncoder - -except ImportError as e: - logger.warning( - "Failed to import TextCrossEncoder from fastembed.rerank.cross_encoder", exc_info=True - ) - from codeweaver.core import ConfigurationError +_FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") - raise ConfigurationError( - r"FastEmbed is not installed. Please install it with `pip install code-weaver\[fastembed]` or `codeweaver\[fastembed-gpu]`." - ) from e +if TYPE_CHECKING or _FASTEMBED_AVAILABLE: + from fastembed.rerank.cross_encoder import TextCrossEncoder +else: + TextCrossEncoder = Any class FastEmbedRerankingProvider(RerankingProvider[TextCrossEncoder]): From 93586417f7cd3d113661f89048df4a7d6de97015 Mon Sep 17 00:00:00 2001 From: aiedwardyi <41576951+aiedwardyi@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:12:24 +0900 Subject: [PATCH 2/2] fix: add runtime guards and try/except for broken fastembed installs Address review feedback from Sourcery and Copilot: - Wrap fastembed imports in try/except ImportError so broken installs (missing binary deps, incompatible Python) don't crash at import time - Add _require_fastembed() guards in embedding and reranking providers so missing fastembed raises a clear ConfigurationError instead of opaque AttributeError/TypeError from None placeholders - Fix install hint typo: codeweaver[fastembed-gpu] -> code-weaver[fastembed-gpu] --- .../embedding/fastembed_extensions.py | 20 +++++++++++-- .../embedding/providers/fastembed.py | 29 +++++++++++++++++-- .../reranking/providers/fastembed.py | 22 ++++++++++++-- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/codeweaver/providers/embedding/fastembed_extensions.py b/src/codeweaver/providers/embedding/fastembed_extensions.py index 15136bf2f..e86102523 100644 --- a/src/codeweaver/providers/embedding/fastembed_extensions.py +++ b/src/codeweaver/providers/embedding/fastembed_extensions.py @@ -15,7 +15,7 @@ _FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") -if TYPE_CHECKING or _FASTEMBED_AVAILABLE: +if TYPE_CHECKING: from fastembed.common.model_description import ( BaseModelDescription, DenseModelDescription, @@ -26,7 +26,21 @@ from fastembed.rerank.cross_encoder import TextCrossEncoder from fastembed.sparse import SparseTextEmbedding from fastembed.text import TextEmbedding -else: +elif _FASTEMBED_AVAILABLE: + try: + from fastembed.common.model_description import ( + BaseModelDescription, + DenseModelDescription, + ModelSource, + PoolingType, + ) + from fastembed.rerank.cross_encoder import TextCrossEncoder + from fastembed.sparse import SparseTextEmbedding + from fastembed.text import TextEmbedding + except ImportError: + _FASTEMBED_AVAILABLE = False + +if not (TYPE_CHECKING or _FASTEMBED_AVAILABLE): BaseModelDescription = Any DenseModelDescription = Any ModelSource = Any @@ -43,7 +57,7 @@ def _require_fastembed() -> None: raise ConfigurationError( "fastembed is not installed. Please install it with " - "`pip install code-weaver[fastembed]` or `codeweaver[fastembed-gpu]`." + "`pip install code-weaver[fastembed]` or `pip install code-weaver[fastembed-gpu]`." ) diff --git a/src/codeweaver/providers/embedding/providers/fastembed.py b/src/codeweaver/providers/embedding/providers/fastembed.py index af4a77bca..55111ce0b 100644 --- a/src/codeweaver/providers/embedding/providers/fastembed.py +++ b/src/codeweaver/providers/embedding/providers/fastembed.py @@ -40,7 +40,7 @@ _FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") -if TYPE_CHECKING or _FASTEMBED_AVAILABLE: +if TYPE_CHECKING: from fastembed.sparse import SparseTextEmbedding from fastembed.text import TextEmbedding @@ -48,7 +48,19 @@ get_sparse_embedder, get_text_embedder, ) -else: +elif _FASTEMBED_AVAILABLE: + try: + from fastembed.sparse import SparseTextEmbedding + from fastembed.text import TextEmbedding + + from codeweaver.providers.embedding.fastembed_extensions import ( + get_sparse_embedder, + get_text_embedder, + ) + except ImportError: + _FASTEMBED_AVAILABLE = False + +if not (TYPE_CHECKING or _FASTEMBED_AVAILABLE): TextEmbedding = Any SparseTextEmbedding = Any @@ -60,6 +72,17 @@ _SparseTextEmbedding = None +def _require_fastembed() -> None: + """Raise ConfigurationError if fastembed is not installed.""" + if not _FASTEMBED_AVAILABLE: + from codeweaver.core import ConfigurationError + + raise ConfigurationError( + "fastembed is not installed. Please install it with " + "`pip install code-weaver[fastembed]` or `pip install code-weaver[fastembed-gpu]`." + ) + + logger = logging.getLogger(__name__) @@ -113,6 +136,7 @@ def _initialize( **kwargs: Any, ) -> None: """Initialize the FastEmbed client.""" + _require_fastembed() @property def base_url(self) -> str | None: @@ -180,6 +204,7 @@ def _initialize( **kwargs: Any, ) -> None: """Initialize the FastEmbed sparse client.""" + _require_fastembed() # impl_deps and custom_deps are ignored for FastEmbed sparse provider; # caps may be passed as a keyword argument via **kwargs from the base class. # 1. Set caps using object.__setattr__ because pydantic model isn't fully initialized yet diff --git a/src/codeweaver/providers/reranking/providers/fastembed.py b/src/codeweaver/providers/reranking/providers/fastembed.py index bebd12ac4..41a959415 100644 --- a/src/codeweaver/providers/reranking/providers/fastembed.py +++ b/src/codeweaver/providers/reranking/providers/fastembed.py @@ -25,12 +25,29 @@ _FASTEMBED_AVAILABLE = has_package("fastembed") or has_package("fastembed-gpu") -if TYPE_CHECKING or _FASTEMBED_AVAILABLE: +if TYPE_CHECKING: from fastembed.rerank.cross_encoder import TextCrossEncoder -else: +elif _FASTEMBED_AVAILABLE: + try: + from fastembed.rerank.cross_encoder import TextCrossEncoder + except ImportError: + _FASTEMBED_AVAILABLE = False + +if not (TYPE_CHECKING or _FASTEMBED_AVAILABLE): TextCrossEncoder = Any +def _require_fastembed() -> None: + """Raise ConfigurationError if fastembed is not installed.""" + if not _FASTEMBED_AVAILABLE: + from codeweaver.core import ConfigurationError + + raise ConfigurationError( + "fastembed is not installed. Please install it with " + "`pip install code-weaver[fastembed]` or `pip install code-weaver[fastembed-gpu]`." + ) + + class FastEmbedRerankingProvider(RerankingProvider[TextCrossEncoder]): """ FastEmbed implementation of the reranking provider. @@ -50,6 +67,7 @@ async def _execute_rerank( **kwargs: Any, ) -> Any: """Execute the reranking process.""" + _require_fastembed() try: # our batch_size needs to be the number of documents because we only get back the scores. # If we set it to a lower number, we wouldn't know what documents the scores correspond to without some extra setup.