Repository: deepset-ai/haystack

Issue:
SASEvaluator returns score even when `predicted_answers` contains `None`
In a RAG pipeline, there are cases where the prompt instructs the LLM to return a placeholder answer if there is missing information in the context, such as "No answer". See an [example](https://github.com/deepset-ai/haystack-evaluation/blob/cbfcde5afcaf7574aa82b1ad6b5ce307c8a7c97e/evaluations/architectures/basic_rag.py#L10) in our evaluation repository.

In that case, it makes sense to replace the predicted answer in the RAG pipeline with `None`, instead of computing any metric on the placeholder answer. However, the SASEvaluator returns a score even if `None` is passed.

**Example**:
```
from haystack.components.evaluators import SASEvaluator

sas = SASEvaluator(model="sentence-transformers/all-mpnet-base-v2")
sas.warm_up()
sas.run(predicted_answers=[None, "Whatever"], ground_truth_answers=["Whatever", "Another whatever"])
```

the output is:

```
{'score': 0.46286168694496155,
 'individual_scores': [0.26153379678726196, 0.6641895771026611]}
```

**Expected**

```
{'score': 0.6641895771026611,
 'individual_scores': [None, 0.6641895771026611]}
```

Discussion:

Patch:
diff --git a/docs/pydoc/config/embedders_api.yml b/docs/pydoc/config/embedders_api.yml
index 5e11a9f1..9528039a 100644
--- a/docs/pydoc/config/embedders_api.yml
+++ b/docs/pydoc/config/embedders_api.yml
@@ -5,8 +5,6 @@ loaders:
       [
         "azure_document_embedder",
         "azure_text_embedder",
-        "hugging_face_tei_document_embedder",
-        "hugging_face_tei_text_embedder",
         "hugging_face_api_document_embedder",
         "hugging_face_api_text_embedder",
         "openai_document_embedder",
diff --git a/docs/pydoc/config/generators_api.yml b/docs/pydoc/config/generators_api.yml
index 655498e9..4fcfa748 100644
--- a/docs/pydoc/config/generators_api.yml
+++ b/docs/pydoc/config/generators_api.yml
@@ -5,12 +5,10 @@ loaders:
       [
         "azure",
         "hugging_face_local",
-        "hugging_face_tgi",
         "hugging_face_api",
         "openai",
         "chat/azure",
         "chat/hugging_face_local",
-        "chat/hugging_face_tgi",
         "chat/hugging_face_api",
         "chat/openai",
       ]
diff --git a/haystack/components/caching/cache_checker.py b/haystack/components/caching/cache_checker.py
index 8da3d322..f4630b38 100644
--- a/haystack/components/caching/cache_checker.py
+++ b/haystack/components/caching/cache_checker.py
@@ -108,7 +108,7 @@ class CacheChecker:
         misses = []
 
         for item in items:
-            filters = {self.cache_field: item}
+            filters = {"field": self.cache_field, "operator": "==", "value": item}
             found = self.document_store.filter_documents(filters=filters)
             if found:
                 found_documents.extend(found)
diff --git a/haystack/components/embedders/__init__.py b/haystack/components/embedders/__init__.py
index 1e7f2485..2b6cf430 100644
--- a/haystack/components/embedders/__init__.py
+++ b/haystack/components/embedders/__init__.py
@@ -6,16 +6,12 @@ from haystack.components.embedders.azure_document_embedder import AzureOpenAIDoc
 from haystack.components.embedders.azure_text_embedder import AzureOpenAITextEmbedder
 from haystack.components.embedders.hugging_face_api_document_embedder import HuggingFaceAPIDocumentEmbedder
 from haystack.components.embedders.hugging_face_api_text_embedder import HuggingFaceAPITextEmbedder
-from haystack.components.embedders.hugging_face_tei_document_embedder import HuggingFaceTEIDocumentEmbedder
-from haystack.components.embedders.hugging_face_tei_text_embedder import HuggingFaceTEITextEmbedder
 from haystack.components.embedders.openai_document_embedder import OpenAIDocumentEmbedder
 from haystack.components.embedders.openai_text_embedder import OpenAITextEmbedder
 from haystack.components.embedders.sentence_transformers_document_embedder import SentenceTransformersDocumentEmbedder
 from haystack.components.embedders.sentence_transformers_text_embedder import SentenceTransformersTextEmbedder
 
 __all__ = [
-    "HuggingFaceTEITextEmbedder",
-    "HuggingFaceTEIDocumentEmbedder",
     "HuggingFaceAPITextEmbedder",
     "HuggingFaceAPIDocumentEmbedder",
     "SentenceTransformersTextEmbedder",
diff --git a/haystack/components/embedders/hugging_face_tei_document_embedder.py b/haystack/components/embedders/hugging_face_tei_document_embedder.py
deleted file mode 100644
index 4a7f6018..00000000
--- a/haystack/components/embedders/hugging_face_tei_document_embedder.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import json
-import warnings
-from typing import Any, Dict, List, Optional
-from urllib.parse import urlparse
-
-from tqdm import tqdm
-
-from haystack import component, default_from_dict, default_to_dict, logging
-from haystack.dataclasses import Document
-from haystack.lazy_imports import LazyImport
-from haystack.utils import Secret, deserialize_secrets_inplace
-from haystack.utils.hf import HFModelType, check_valid_model
-
-with LazyImport(message="Run 'pip install \"huggingface_hub>=0.23.0\"'") as huggingface_hub_import:
-    from huggingface_hub import InferenceClient
-
-logger = logging.getLogger(__name__)
-
-
-@component
-class HuggingFaceTEIDocumentEmbedder:
-    """
-    A component for computing Document embeddings using HuggingFace Text-Embeddings-Inference endpoints.
-
-    This component can be used with embedding models hosted on Hugging Face Inference endpoints, the rate-limited
-    Inference API tier, for embedding models hosted on [the paid inference endpoint](https://huggingface.co/inference-endpoints)
-    and/or your own custom TEI endpoint.
-
-    Usage example:
-    ```python
-    from haystack.dataclasses import Document
-    from haystack.components.embedders import HuggingFaceTEIDocumentEmbedder
-    from haystack.utils import Secret
-
-    doc = Document(content="I love pizza!")
-
-    document_embedder = HuggingFaceTEIDocumentEmbedder(
-        model="BAAI/bge-small-en-v1.5", token=Secret.from_token("<your-api-key>")
-    )
-
-    result = document_embedder.run([doc])
-    print(result["documents"][0].embedding)
-
-    # [0.017020374536514282, -0.023255806416273117, ...]
-    ```
-    """
-
-    def __init__(
-        self,
-        model: str = "BAAI/bge-small-en-v1.5",
-        url: Optional[str] = None,
-        token: Optional[Secret] = Secret.from_env_var("HF_API_TOKEN", strict=False),
-        prefix: str = "",
-        suffix: str = "",
-        truncate: bool = True,
-        normalize: bool = False,
-        batch_size: int = 32,
-        progress_bar: bool = True,
-        meta_fields_to_embed: Optional[List[str]] = None,
-        embedding_separator: str = "\n",
-    ):
-        """
-        Create a HuggingFaceTEIDocumentEmbedder component.
-
-        :param model:
-            ID of the model on HuggingFace Hub.
-        :param url:
-            The URL of your self-deployed Text-Embeddings-Inference service or the URL of your paid HF Inference
-            Endpoint.
-        :param token:
-            The HuggingFace Hub token. This is needed if you are using a paid HF Inference Endpoint or serving
-            a private or gated model.
-        :param prefix:
-            A string to add at the beginning of each text.
-        :param suffix:
-            A string to add at the end of each text.
-        :param truncate:
-            Truncate input text from the end to the maximum length supported by the model. This option is only available
-            for self-deployed Text Embedding Inference (TEI) endpoints and paid HF Inference Endpoints deployed with
-            TEI. It will be ignored when used with free HF Inference endpoints or paid HF Inference endpoints deployed
-            without TEI.
-        :param normalize:
-            Normalize the embeddings to unit length. This option is only available for self-deployed Text Embedding
-            Inference (TEI) endpoints and paid HF Inference Endpoints deployed with TEI. It will be ignored when used
-            with free HF Inference endpoints or paid HF Inference endpoints deployed without TEI.
-        :param batch_size:
-            Number of Documents to encode at once.
-        :param progress_bar:
-            If True shows a progress bar when running.
-        :param meta_fields_to_embed:
-            List of meta fields that will be embedded along with the Document text.
-        :param embedding_separator:
-            Separator used to concatenate the meta fields to the Document text.
-        """
-        warnings.warn(
-            "`HuggingFaceTEIDocumentEmbedder` is deprecated and will be removed in Haystack 2.3.0."
-            "Use `HuggingFaceAPIDocumentEmbedder` instead.",
-            DeprecationWarning,
-        )
-
-        huggingface_hub_import.check()
-
-        if url:
-            r = urlparse(url)
-            is_valid_url = all([r.scheme in ["http", "https"], r.netloc])
-            if not is_valid_url:
-                raise ValueError(f"Invalid TEI endpoint URL provided: {url}")
-
-        check_valid_model(model, HFModelType.EMBEDDING, token)
-
-        self.model = model
-        self.url = url
-        self.token = token
-        self.client = InferenceClient(url or model, token=token.resolve_value() if token else None)
-        self.prefix = prefix
-        self.suffix = suffix
-        self.truncate = truncate
-        self.normalize = normalize
-        self.batch_size = batch_size
-        self.progress_bar = progress_bar
-        self.meta_fields_to_embed = meta_fields_to_embed or []
-        self.embedding_separator = embedding_separator
-
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Serializes the component to a dictionary.
-
-        :returns:
-            Dictionary with serialized data.
-        """
-        return default_to_dict(
-            self,
-            model=self.model,
-            url=self.url,
-            prefix=self.prefix,
-            suffix=self.suffix,
-            truncate=self.truncate,
-            normalize=self.normalize,
-            batch_size=self.batch_size,
-            progress_bar=self.progress_bar,
-            meta_fields_to_embed=self.meta_fields_to_embed,
-            embedding_separator=self.embedding_separator,
-            token=self.token.to_dict() if self.token else None,
-        )
-
-    @classmethod
-    def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTEIDocumentEmbedder":
-        """
-        Deserializes the component from a dictionary.
-
-        :param data:
-            Dictionary to deserialize from.
-        :returns:
-            Deserialized component.
-        """
-        deserialize_secrets_inplace(data["init_parameters"], keys=["token"])
-        return default_from_dict(cls, data)
-
-    def _get_telemetry_data(self) -> Dict[str, Any]:
-        """
-        Data that is sent to Posthog for usage analytics.
-        """
-        # Don't send URL as it is sensitive information
-        return {"model": self.model}
-
-    def _prepare_texts_to_embed(self, documents: List[Document]) -> List[str]:
-        """
-        Prepare the texts to embed by concatenating the Document text with the metadata fields to embed.
-        """
-        texts_to_embed = []
-        for doc in documents:
-            meta_values_to_embed = [
-                str(doc.meta[key]) for key in self.meta_fields_to_embed if key in doc.meta and doc.meta[key] is not None
-            ]
-
-            text_to_embed = (
-                self.prefix + self.embedding_separator.join(meta_values_to_embed + [doc.content or ""]) + self.suffix
-            )
-
-            texts_to_embed.append(text_to_embed)
-        return texts_to_embed
-
-    def _embed_batch(self, texts_to_embed: List[str], batch_size: int) -> List[List[float]]:
-        """
-        Embed a list of texts in batches.
-        """
-
-        all_embeddings = []
-        for i in tqdm(
-            range(0, len(texts_to_embed), batch_size), disable=not self.progress_bar, desc="Calculating embeddings"
-        ):
-            batch = texts_to_embed[i : i + batch_size]
-            response = self.client.post(
-                json={"inputs": batch, "truncate": self.truncate, "normalize": self.normalize},
-                task="feature-extraction",
-            )
-            embeddings = json.loads(response.decode())
-            all_embeddings.extend(embeddings)
-
-        return all_embeddings
-
-    @component.output_types(documents=List[Document])
-    def run(self, documents: List[Document]):
-        """
-        Embed a list of Documents.
-
-        :param documents:
-            Documents to embed.
-
-        :returns:
-            A dictionary with the following keys:
-            - `documents`: Documents with embeddings
-        """
-        if not isinstance(documents, list) or documents and not isinstance(documents[0], Document):
-            raise TypeError(
-                "HuggingFaceTEIDocumentEmbedder expects a list of Documents as input."
-                " In case you want to embed a string, please use the HuggingFaceTEITextEmbedder."
-            )
-
-        texts_to_embed = self._prepare_texts_to_embed(documents=documents)
-
-        embeddings = self._embed_batch(texts_to_embed=texts_to_embed, batch_size=self.batch_size)
-
-        for doc, emb in zip(documents, embeddings):
-            doc.embedding = emb
-
-        return {"documents": documents}
diff --git a/haystack/components/embedders/hugging_face_tei_text_embedder.py b/haystack/components/embedders/hugging_face_tei_text_embedder.py
deleted file mode 100644
index 04ec8142..00000000
--- a/haystack/components/embedders/hugging_face_tei_text_embedder.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import json
-import warnings
-from typing import Any, Dict, List, Optional
-from urllib.parse import urlparse
-
-from haystack import component, default_from_dict, default_to_dict, logging
-from haystack.lazy_imports import LazyImport
-from haystack.utils import Secret, deserialize_secrets_inplace
-from haystack.utils.hf import HFModelType, check_valid_model
-
-with LazyImport(message="Run 'pip install \"huggingface_hub>=0.23.0\"'") as huggingface_hub_import:
-    from huggingface_hub import InferenceClient
-
-logger = logging.getLogger(__name__)
-
-
-@component
-class HuggingFaceTEITextEmbedder:
-    """
-    A component for embedding strings using HuggingFace Text-Embeddings-Inference endpoints.
-
-    This component can be used with embedding models hosted on Hugging Face Inference endpoints, the rate-limited
-    Inference API tier, for embedding models hosted on [the paid inference endpoint](https://huggingface.co/inference-endpoints)
-    and/or your own custom TEI endpoint.
-
-    Usage example:
-    ```python
-    from haystack.components.embedders import HuggingFaceTEITextEmbedder
-    from haystack.utils import Secret
-
-    text_to_embed = "I love pizza!"
-
-    text_embedder = HuggingFaceTEITextEmbedder(
-        model="BAAI/bge-small-en-v1.5", token=Secret.from_token("<your-api-key>")
-    )
-
-    print(text_embedder.run(text_to_embed))
-
-    # {'embedding': [0.017020374536514282, -0.023255806416273117, ...],
-    ```
-    """
-
-    def __init__(
-        self,
-        model: str = "BAAI/bge-small-en-v1.5",
-        url: Optional[str] = None,
-        token: Optional[Secret] = Secret.from_env_var("HF_API_TOKEN", strict=False),
-        prefix: str = "",
-        suffix: str = "",
-        truncate: bool = True,
-        normalize: bool = False,
-    ):
-        """
-        Create an HuggingFaceTEITextEmbedder component.
-
-        :param model:
-            ID of the model on HuggingFace Hub.
-        :param url:
-            The URL of your self-deployed Text-Embeddings-Inference service or the URL of your paid HF Inference
-            Endpoint.
-        :param token:
-            The HuggingFace Hub token. This is needed if you are using a paid HF Inference Endpoint or serving
-            a private or gated model.
-        :param prefix:
-            A string to add at the beginning of each text.
-        :param suffix:
-            A string to add at the end of each text.
-        :param truncate:
-            Truncate input text from the end to the maximum length supported by the model. This option is only available
-            for self-deployed Text Embedding Inference (TEI) endpoints and paid HF Inference Endpoints deployed with
-            TEI. It will be ignored when used with free HF Inference endpoints or paid HF Inference endpoints deployed
-            without TEI.
-        :param normalize:
-            Normalize the embeddings to unit length. This option is only available for self-deployed Text Embedding
-            Inference (TEI) endpoints and paid HF Inference Endpoints deployed with TEI. It will be ignored when used
-            with free HF Inference endpoints or paid HF Inference endpoints deployed without TEI.
-        """
-        warnings.warn(
-            "`HuggingFaceTEITextEmbedder` is deprecated and will be removed in Haystack 2.3.0."
-            "Use `HuggingFaceAPITextEmbedder` instead.",
-            DeprecationWarning,
-        )
-
-        huggingface_hub_import.check()
-
-        if url:
-            r = urlparse(url)
-            is_valid_url = all([r.scheme in ["http", "https"], r.netloc])
-            if not is_valid_url:
-                raise ValueError(f"Invalid TEI endpoint URL provided: {url}")
-
-        check_valid_model(model, HFModelType.EMBEDDING, token)
-
-        self.model = model
-        self.url = url
-        self.token = token
-        self.client = InferenceClient(url or model, token=token.resolve_value() if token else None)
-        self.prefix = prefix
-        self.suffix = suffix
-        self.truncate = truncate
-        self.normalize = normalize
-
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Serializes the component to a dictionary.
-
-        :returns:
-            Dictionary with serialized data.
-        """
-        return default_to_dict(
-            self,
-            model=self.model,
-            url=self.url,
-            prefix=self.prefix,
-            suffix=self.suffix,
-            token=self.token.to_dict() if self.token else None,
-            truncate=self.truncate,
-            normalize=self.normalize,
-        )
-
-    @classmethod
-    def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTEITextEmbedder":
-        """
-        Deserializes the component from a dictionary.
-
-        :param data:
-            Dictionary to deserialize from.
-        :returns:
-            Deserialized component.
-        """
-        deserialize_secrets_inplace(data["init_parameters"], keys=["token"])
-        return default_from_dict(cls, data)
-
-    def _get_telemetry_data(self) -> Dict[str, Any]:
-        """
-        Data that is sent to Posthog for usage analytics.
-        """
-        # Don't send URL as it is sensitive information
-        return {"model": self.model}
-
-    @component.output_types(embedding=List[float])
-    def run(self, text: str):
-        """
-        Embed a single string.
-
-        :param text:
-            Text to embed.
-
-        :returns:
-            A dictionary with the following keys:
-            - `embedding`: The embedding of the input text.
-        """
-        if not isinstance(text, str):
-            raise TypeError(
-                "HuggingFaceTEITextEmbedder expects a string as an input."
-                "In case you want to embed a list of Documents, please use the HuggingFaceTEIDocumentEmbedder."
-            )
-
-        text_to_embed = self.prefix + text + self.suffix
-
-        response = self.client.post(
-            json={"inputs": [text_to_embed], "truncate": self.truncate, "normalize": self.normalize},
-            task="feature-extraction",
-        )
-        embedding = json.loads(response.decode())[0]
-
-        return {"embedding": embedding}
diff --git a/haystack/components/evaluators/sas_evaluator.py b/haystack/components/evaluators/sas_evaluator.py
index 7625b837..0fc5e80d 100644
--- a/haystack/components/evaluators/sas_evaluator.py
+++ b/haystack/components/evaluators/sas_evaluator.py
@@ -157,6 +157,9 @@ class SASEvaluator:
         if len(ground_truth_answers) != len(predicted_answers):
             raise ValueError("The number of predictions and labels must be the same.")
 
+        if any(answer is None for answer in predicted_answers):
+            raise ValueError("Predicted answers must not contain None values.")
+
         if len(predicted_answers) == 0:
             return {"score": 0.0, "individual_scores": [0.0]}
 
diff --git a/haystack/components/generators/__init__.py b/haystack/components/generators/__init__.py
index db685521..b93270d8 100644
--- a/haystack/components/generators/__init__.py
+++ b/haystack/components/generators/__init__.py
@@ -7,13 +7,6 @@ from haystack.components.generators.openai import (  # noqa: I001 (otherwise we
 )
 from haystack.components.generators.azure import AzureOpenAIGenerator
 from haystack.components.generators.hugging_face_local import HuggingFaceLocalGenerator
-from haystack.components.generators.hugging_face_tgi import HuggingFaceTGIGenerator
 from haystack.components.generators.hugging_face_api import HuggingFaceAPIGenerator
 
-__all__ = [
-    "HuggingFaceLocalGenerator",
-    "HuggingFaceTGIGenerator",
-    "HuggingFaceAPIGenerator",
-    "OpenAIGenerator",
-    "AzureOpenAIGenerator",
-]
+__all__ = ["HuggingFaceLocalGenerator", "HuggingFaceAPIGenerator", "OpenAIGenerator", "AzureOpenAIGenerator"]
diff --git a/haystack/components/generators/chat/__init__.py b/haystack/components/generators/chat/__init__.py
index c42b6934..1c7ecbad 100644
--- a/haystack/components/generators/chat/__init__.py
+++ b/haystack/components/generators/chat/__init__.py
@@ -7,12 +7,10 @@ from haystack.components.generators.chat.openai import (  # noqa: I001 (otherwis
 )
 from haystack.components.generators.chat.azure import AzureOpenAIChatGenerator
 from haystack.components.generators.chat.hugging_face_local import HuggingFaceLocalChatGenerator
-from haystack.components.generators.chat.hugging_face_tgi import HuggingFaceTGIChatGenerator
 from haystack.components.generators.chat.hugging_face_api import HuggingFaceAPIChatGenerator
 
 __all__ = [
     "HuggingFaceLocalChatGenerator",
-    "HuggingFaceTGIChatGenerator",
     "HuggingFaceAPIChatGenerator",
     "OpenAIChatGenerator",
     "AzureOpenAIChatGenerator",
diff --git a/haystack/components/generators/chat/hugging_face_tgi.py b/haystack/components/generators/chat/hugging_face_tgi.py
deleted file mode 100644
index fe51e0da..00000000
--- a/haystack/components/generators/chat/hugging_face_tgi.py
+++ /dev/null
@@ -1,332 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import warnings
-from dataclasses import asdict
-from typing import Any, Callable, Dict, Iterable, List, Optional
-from urllib.parse import urlparse
-
-from haystack import component, default_from_dict, default_to_dict, logging
-from haystack.dataclasses import ChatMessage, StreamingChunk
-from haystack.lazy_imports import LazyImport
-from haystack.utils import Secret, deserialize_callable, deserialize_secrets_inplace, serialize_callable
-from haystack.utils.hf import HFModelType, check_generation_params, check_valid_model, list_inference_deployed_models
-
-with LazyImport(message="Run 'pip install \"huggingface_hub>=0.23.0\" transformers'") as transformers_import:
-    from huggingface_hub import (
-        InferenceClient,
-        TextGenerationOutput,
-        TextGenerationOutputToken,
-        TextGenerationStreamOutput,
-    )
-    from transformers import AutoTokenizer
-
-logger = logging.getLogger(__name__)
-
-
-@component
-class HuggingFaceTGIChatGenerator:
-    """
-    A Chat-based text generation component using Hugging Face's Text Generation Inference (TGI) framework.
-
-    Enables text generation using HuggingFace Hub hosted chat-based LLMs. This component is designed to seamlessly
-    inference chat-based models deployed on the Text Generation Inference (TGI) backend.
-
-    You can use this component for chat LLMs hosted on Hugging Face inference endpoints, the rate-limited
-    Inference API tier.
-
-    Key Features and Compatibility:
-    - Primary Compatibility: designed to work seamlessly with any chat-based model deployed using the TGI
-       framework. For more information on TGI, visit [text-generation-inference](https://github.com/huggingface/text-generation-inference)
-
-    - Hugging Face Inference Endpoints: Supports inference of TGI chat LLMs deployed on Hugging Face
-       inference endpoints. For more details, refer to [inference-endpoints](https://huggingface.co/inference-endpoints)
-
-    - Inference API Support: supports inference of TGI chat LLMs hosted on the rate-limited Inference
-      API tier. Learn more about the Inference API at [inference-api](https://huggingface.co/inference-api).
-      Discover available chat models using the following command:
-      `wget -qO- https://api-inference.huggingface.co/framework/text-generation-inference | grep chat`
-      and simply use the model ID as the model parameter for this component. You'll also need to provide a valid
-      Hugging Face API token as the token parameter.
-
-    - Custom TGI Endpoints: supports inference of TGI chat LLMs deployed on custom TGI endpoints. Anyone can
-      deploy their own TGI endpoint using the TGI framework. For more details, refer to [inference-endpoints](https://huggingface.co/inference-endpoints)
-
-    Input and Output Format:
-      - ChatMessage Format: This component uses the ChatMessage format to structure both input and output,
-        ensuring coherent and contextually relevant responses in chat-based text generation scenarios. Details on the
-        ChatMessage format can be found [here](https://docs.haystack.deepset.ai/v2.0/docs/data-classes#chatmessage).
-
-
-    ```python
-    from haystack.components.generators.chat import HuggingFaceTGIChatGenerator
-    from haystack.dataclasses import ChatMessage
-    from haystack.utils import Secret
-
-    messages = [ChatMessage.from_system("\\nYou are a helpful, respectful and honest assistant"),
-                ChatMessage.from_user("What's Natural Language Processing?")]
-
-
-    client = HuggingFaceTGIChatGenerator(model="HuggingFaceH4/zephyr-7b-beta", token=Secret.from_token("<api-key>"))
-    client.warm_up()
-    response = client.run(messages, generation_kwargs={"max_new_tokens": 120})
-    print(response)
-    ```
-
-    For chat LLMs hosted on paid https://huggingface.co/inference-endpoints endpoint and/or your own custom TGI
-    endpoint, you'll need to provide the URL of the endpoint as well as a valid token:
-
-    ```python
-    from haystack.components.generators.chat import HuggingFaceTGIChatGenerator
-    from haystack.dataclasses import ChatMessage
-
-    messages = [ChatMessage.from_system("\\nYou are a helpful, respectful and honest assistant"),
-                ChatMessage.from_user("What's Natural Language Processing?")]
-
-    client = HuggingFaceTGIChatGenerator(model="HuggingFaceH4/zephyr-7b-beta",
-                                         url="<your-tgi-endpoint-url>",
-                                         token=Secret.from_token("<your-api-key>"))
-    client.warm_up()
-    response = client.run(messages, generation_kwargs={"max_new_tokens": 120})
-    print(response)
-    ```
-    """
-
-    def __init__(
-        self,
-        model: str = "HuggingFaceH4/zephyr-7b-beta",
-        url: Optional[str] = None,
-        token: Optional[Secret] = Secret.from_env_var("HF_API_TOKEN", strict=False),
-        chat_template: Optional[str] = None,
-        generation_kwargs: Optional[Dict[str, Any]] = None,
-        stop_words: Optional[List[str]] = None,
-        streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
-    ):
-        """
-        Initialize the HuggingFaceTGIChatGenerator instance.
-
-        :param model: A string representing the model path or URL. Default is "HuggingFaceH4/zephyr-7b-beta".
-        :param url: An optional string representing the URL of the TGI endpoint.
-        :param chat_template: This optional parameter allows you to specify a Jinja template for formatting chat
-            messages. While high-quality and well-supported chat models typically include their own chat templates
-            accessible through their tokenizer, there are models that do not offer this feature. For such scenarios,
-            or if you wish to use a custom template instead of the model's default, you can use this parameter to
-            set your preferred chat template.
-        :param token: The Hugging Face token for HTTP bearer authorization.
-            You can find your HF token at https://huggingface.co/settings/tokens.
-        :param generation_kwargs: A dictionary containing keyword arguments to customize text generation.
-            Some examples: `max_new_tokens`, `temperature`, `top_k`, `top_p`,...
-            See Hugging Face's [documentation](https://huggingface.co/docs/huggingface_hub/v0.18.0.rc0/en/package_reference/inference_client#huggingface_hub.inference._text_generation.TextGenerationParameters)
-            for more information.
-        :param stop_words: An optional list of strings representing the stop words.
-        :param streaming_callback: An optional callable for handling streaming responses.
-        """
-        warnings.warn(
-            "`HuggingFaceTGIChatGenerator` is deprecated and will be removed in Haystack 2.3.0."
-            "Use `HuggingFaceAPIChatGenerator` instead.",
-            DeprecationWarning,
-        )
-        transformers_import.check()
-
-        if url:
-            r = urlparse(url)
-            is_valid_url = all([r.scheme in ["http", "https"], r.netloc])
-            if not is_valid_url:
-                raise ValueError(f"Invalid TGI endpoint URL provided: {url}")
-
-        check_valid_model(model, HFModelType.GENERATION, token)
-
-        # handle generation kwargs setup
-        generation_kwargs = generation_kwargs.copy() if generation_kwargs else {}
-        check_generation_params(generation_kwargs, ["n"])
-        generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", [])
-        generation_kwargs["stop_sequences"].extend(stop_words or [])
-        generation_kwargs.setdefault("max_new_tokens", 512)
-
-        self.model = model
-        self.url = url
-        self.chat_template = chat_template
-        self.token = token
-        self.generation_kwargs = generation_kwargs
-        self.client = InferenceClient(url or model, token=token.resolve_value() if token else None)
-        self.streaming_callback = streaming_callback
-        self.tokenizer = None
-        self._warmed_up: bool = False
-
-    def warm_up(self) -> None:
-        """
-        Warm up the tokenizer by loading it from the model.
-
-        If the url is not provided, check if the model is deployed on the free tier of the HF inference API.
-        Load the tokenizer
-        """
-        if self._warmed_up:
-            return
-
-        # is this user using HF free tier inference API?
-        if self.model and not self.url:
-            deployed_models = list_inference_deployed_models()
-            # Determine if the specified model is deployed in the free tier.
-            if self.model not in deployed_models:
-                raise ValueError(
-                    f"The model {self.model} is not deployed on the free tier of the HF inference API. "
-                    "To use free tier models provide the model ID and the token. Valid models are: "
-                    f"{deployed_models}"
-                )
-
-        self.tokenizer = AutoTokenizer.from_pretrained(
-            self.model, token=self.token.resolve_value() if self.token else None
-        )
-
-        # mypy can't infer that chat_template attribute exists on the object returned by AutoTokenizer.from_pretrained
-        chat_template = getattr(self.tokenizer, "chat_template", None)
-        if not chat_template and not self.chat_template:
-            logger.warning(
-                "The model '{model}' doesn't have a default chat_template, and no chat_template was supplied during "
-                "this component's initialization. It’s possible that the model doesn't support ChatML inference "
-                "format, potentially leading to unexpected behavior.",
-                model=self.model,
-            )
-
-        self._warmed_up = True
-
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Serialize this component to a dictionary.
-
-        :return: A dictionary containing the serialized component.
-        """
-        callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
-        return default_to_dict(
-            self,
-            model=self.model,
-            url=self.url,
-            chat_template=self.chat_template,
-            token=self.token.to_dict() if self.token else None,
-            generation_kwargs=self.generation_kwargs,
-            streaming_callback=callback_name,
-        )
-
-    @classmethod
-    def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTGIChatGenerator":
-        """
-        Deserialize this component from a dictionary.
-        """
-        deserialize_secrets_inplace(data["init_parameters"], keys=["token"])
-        init_params = data.get("init_parameters", {})
-        serialized_callback_handler = init_params.get("streaming_callback")
-        if serialized_callback_handler:
-            data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
-        return default_from_dict(cls, data)
-
-    def _get_telemetry_data(self) -> Dict[str, Any]:
-        """
-        Data that is sent to Posthog for usage analytics.
-        """
-        # Don't send URL as it is sensitive information
-        return {"model": self.model}
-
-    @component.output_types(replies=List[ChatMessage])
-    def run(self, messages: List[ChatMessage], generation_kwargs: Optional[Dict[str, Any]] = None):
-        """
-        Invoke the text generation inference based on the provided messages and generation parameters.
-
-        :param messages: A list of ChatMessage instances representing the input messages.
-        :param generation_kwargs: Additional keyword arguments for text generation.
-        :return: A list containing the generated responses as ChatMessage instances.
-        """
-        if not self._warmed_up:
-            raise RuntimeError(
-                "The component HuggingFaceTGIChatGenerator was not warmed up. Please call warm_up() before running."
-            )
-
-        # check generation kwargs given as parameters to override the default ones
-        additional_params = ["n", "stop_words"]
-        check_generation_params(generation_kwargs, additional_params)
-
-        # update generation kwargs by merging with the default ones
-        generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
-        num_responses = generation_kwargs.pop("n", 1)
-
-        # merge stop_words and stop_sequences into a single list
-        generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", [])
-        generation_kwargs["stop_sequences"].extend(generation_kwargs.pop("stop_words", []))
-
-        if self.tokenizer is None:
-            raise RuntimeError("Please call warm_up() before running LLM inference.")
-
-        # apply either model's chat template or the user-provided one
-        formatted_messages = [message.to_openai_format() for message in messages]
-        prepared_prompt: str = self.tokenizer.apply_chat_template(
-            conversation=formatted_messages, chat_template=self.chat_template, tokenize=False
-        )
-        prompt_token_count: int = len(self.tokenizer.encode(prepared_prompt, add_special_tokens=False))
-
-        if self.streaming_callback:
-            if num_responses > 1:
-                raise ValueError("Cannot stream multiple responses, please set n=1.")
-
-            return self._run_streaming(prepared_prompt, prompt_token_count, generation_kwargs)
-
-        return self._run_non_streaming(prepared_prompt, prompt_token_count, num_responses, generation_kwargs)
-
-    def _run_streaming(
-        self, prepared_prompt: str, prompt_token_count: int, generation_kwargs: Dict[str, Any]
-    ) -> Dict[str, List[ChatMessage]]:
-        res: Iterable[TextGenerationStreamOutput] = self.client.text_generation(
-            prepared_prompt, stream=True, details=True, **generation_kwargs
-        )
-        chunk = None
-        # pylint: disable=not-an-iterable
-        for chunk in res:
-            token: TextGenerationOutputToken = chunk.token
-            if token.special:
-                continue
-            chunk_metadata = {**asdict(token), **(asdict(chunk.details) if chunk.details else {})}
-            stream_chunk = StreamingChunk(token.text, chunk_metadata)
-            self.streaming_callback(stream_chunk)  # type: ignore # streaming_callback is not None (verified in the run method)
-
-        message = ChatMessage.from_assistant(chunk.generated_text)
-        message.meta.update(
-            {
-                "finish_reason": chunk.details.finish_reason if chunk.details else None,
-                "index": 0,
-                "model": self.client.model,
-                "usage": {
-                    "completion_tokens": chunk.details.generated_tokens if chunk.details else 0,
-                    "prompt_tokens": prompt_token_count,
-                    "total_tokens": prompt_token_count + chunk.details.generated_tokens if chunk.details else 0,
-                },
-            }
-        )
-        return {"replies": [message]}
-
-    def _run_non_streaming(
-        self, prepared_prompt: str, prompt_token_count: int, num_responses: int, generation_kwargs: Dict[str, Any]
-    ) -> Dict[str, List[ChatMessage]]:
-        chat_messages: List[ChatMessage] = []
-        for _i in range(num_responses):
-            tgr: TextGenerationOutput = self.client.text_generation(prepared_prompt, details=True, **generation_kwargs)
-            message = ChatMessage.from_assistant(tgr.generated_text)
-            if tgr.details:
-                completion_tokens = len(tgr.details.tokens)
-                prompt_token_count = prompt_token_count + completion_tokens
-                finish_reason = tgr.details.finish_reason
-            else:
-                finish_reason = None
-                completion_tokens = 0
-            message.meta.update(
-                {
-                    "finish_reason": finish_reason,
-                    "index": _i,
-                    "model": self.client.model,
-                    "usage": {
-                        "completion_tokens": completion_tokens,
-                        "prompt_tokens": prompt_token_count,
-                        "total_tokens": prompt_token_count + completion_tokens,
-                    },
-                }
-            )
-            chat_messages.append(message)
-        return {"replies": chat_messages}
diff --git a/haystack/components/generators/hugging_face_tgi.py b/haystack/components/generators/hugging_face_tgi.py
deleted file mode 100644
index 50dbc278..00000000
--- a/haystack/components/generators/hugging_face_tgi.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-
-import warnings
-from dataclasses import asdict
-from typing import Any, Callable, Dict, Iterable, List, Optional
-from urllib.parse import urlparse
-
-from haystack import component, default_from_dict, default_to_dict, logging
-from haystack.dataclasses import StreamingChunk
-from haystack.lazy_imports import LazyImport
-from haystack.utils import Secret, deserialize_callable, deserialize_secrets_inplace, serialize_callable
-from haystack.utils.hf import HFModelType, check_generation_params, check_valid_model, list_inference_deployed_models
-
-with LazyImport(message="Run 'pip install \"huggingface_hub>=0.23.0\" transformers'") as transformers_import:
-    from huggingface_hub import (
-        InferenceClient,
-        TextGenerationOutput,
-        TextGenerationOutputToken,
-        TextGenerationStreamOutput,
-    )
-    from transformers import AutoTokenizer
-
-
-logger = logging.getLogger(__name__)
-
-
-@component
-class HuggingFaceTGIGenerator:
-    """
-    Enables text generation using HuggingFace Hub hosted non-chat LLMs.
-
-    This component is designed to seamlessly inference models deployed on the Text Generation Inference (TGI) backend.
-    You can use this component for LLMs hosted on Hugging Face inference endpoints, the rate-limited
-    Inference API tier.
-
-    Key Features and Compatibility:
-     - Primary Compatibility: designed to work seamlessly with any non-based model deployed using the TGI
-       framework. For more information on TGI, visit
-       [text-generation-inference](https://github.com/huggingface/text-generation-inference)
-
-    - Hugging Face Inference Endpoints: Supports inference of TGI chat LLMs deployed on Hugging Face
-       inference endpoints. For more details, refer to
-       [inference-endpoints](https://huggingface.co/inference-endpoints)
-
-    - Inference API Support: supports inference of TGI LLMs hosted on the rate-limited Inference
-      API tier. Learn more about the Inference API at [inference-api](https://huggingface.co/inference-api).
-      Discover available chat models using the following command:
-      `wget -qO- https://api-inference.huggingface.co/framework/text-generation-inference | grep chat`
-      and simply use the model ID as the model parameter for this component. You'll also need to provide a valid
-      Hugging Face API token as the token parameter.
-
-    - Custom TGI Endpoints: supports inference of TGI chat LLMs deployed on custom TGI endpoints. Anyone can
-      deploy their own TGI endpoint using the TGI framework. For more details, refer to
-      [inference-endpoints](https://huggingface.co/inference-endpoints)
-
-     Input and Output Format:
-      - String Format: This component uses the str format for structuring both input and output,
-        ensuring coherent and contextually relevant responses in text generation scenarios.
-
-    ```python
-    from haystack.components.generators import HuggingFaceTGIGenerator
-    from haystack.utils import Secret
-
-    client = HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1", token=Secret.from_token("<your-api-key>"))
-    client.warm_up()
-    response = client.run("What's Natural Language Processing?", generation_kwargs={"max_new_tokens": 120})
-    print(response)
-    ```
-
-    Or for LLMs hosted on paid https://huggingface.co/inference-endpoints endpoint, and/or your own custom TGI endpoint.
-    In these two cases, you'll need to provide the URL of the endpoint as well as a valid token:
-
-    ```python
-    from haystack.components.generators import HuggingFaceTGIGenerator
-    client = HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1",
-                                     url="<your-tgi-endpoint-url>",
-                                     token=Secret.from_token("<your-api-key>"))
-    client.warm_up()
-    response = client.run("What's Natural Language Processing?")
-    print(response)
-    ```
-    """
-
-    def __init__(
-        self,
-        model: str = "mistralai/Mistral-7B-v0.1",
-        url: Optional[str] = None,
-        token: Optional[Secret] = Secret.from_env_var("HF_API_TOKEN", strict=False),
-        generation_kwargs: Optional[Dict[str, Any]] = None,
-        stop_words: Optional[List[str]] = None,
-        streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
-    ):
-        """
-        Initialize the HuggingFaceTGIGenerator instance.
-
-        :param model:
-            A string representing the model id on HF Hub. Default is "mistralai/Mistral-7B-v0.1".
-        :param url:
-            An optional string representing the URL of the TGI endpoint. If the url is not provided, check if the model
-            is deployed on the free tier of the HF inference API.
-        :param token: The HuggingFace token to use as HTTP bearer authorization
-            You can find your HF token in your [account settings](https://huggingface.co/settings/tokens)
-        :param generation_kwargs:
-            A dictionary containing keyword arguments to customize text generation.
-                Some examples: `max_new_tokens`, `temperature`, `top_k`, `top_p`,...
-                See Hugging Face's documentation for more information at:
-                [TextGenerationParameters](https://huggingface.co/docs/huggingface_hub/v0.18.0.rc0/en/package_reference/inference_client#huggingface_hub.inference._text_generation.TextGenerationParameters
-        :param stop_words: An optional list of strings representing the stop words.
-        :param streaming_callback: An optional callable for handling streaming responses.
-        """
-        warnings.warn(
-            "`HuggingFaceTGIGenerator` is deprecated and will be removed in Haystack 2.3.0."
-            "Use `HuggingFaceAPIGenerator` instead.",
-            DeprecationWarning,
-        )
-
-        transformers_import.check()
-
-        if url:
-            r = urlparse(url)
-            is_valid_url = all([r.scheme in ["http", "https"], r.netloc])
-            if not is_valid_url:
-                raise ValueError(f"Invalid TGI endpoint URL provided: {url}")
-
-        check_valid_model(model, HFModelType.GENERATION, token)
-
-        # handle generation kwargs setup
-        generation_kwargs = generation_kwargs.copy() if generation_kwargs else {}
-        check_generation_params(generation_kwargs, ["n"])
-        generation_kwargs["stop_sequences"] = generation_kwargs.get("stop_sequences", [])
-        generation_kwargs["stop_sequences"].extend(stop_words or [])
-        generation_kwargs.setdefault("max_new_tokens", 512)
-
-        self.model = model
-        self.url = url
-        self.token = token
-        self.generation_kwargs = generation_kwargs
-        self.client = InferenceClient(url or model, token=token.resolve_value() if token else None)
-        self.streaming_callback = streaming_callback
-        self.tokenizer = None
-
-    def warm_up(self) -> None:
-        """
-        Initializes the component.
-        """
-        if self.tokenizer is not None:
-            return
-
-        # is this user using HF free tier inference API?
-        if self.model and not self.url:
-            deployed_models = list_inference_deployed_models()
-            # Determine if the specified model is deployed in the free tier.
-            if self.model not in deployed_models:
-                raise ValueError(
-                    f"The model {self.model} is not deployed on the free tier of the HF inference API. "
-                    "To use free tier models provide the model ID and the token. Valid models are: "
-                    f"{deployed_models}"
-                )
-
-        self.tokenizer = AutoTokenizer.from_pretrained(
-            self.model, token=self.token.resolve_value() if self.token else None
-        )
-
-    def to_dict(self) -> Dict[str, Any]:
-        """
-        Serialize this component to a dictionary.
-
-        :returns:
-            A dictionary containing the serialized component.
-        """
-        callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
-        return default_to_dict(
-            self,
-            model=self.model,
-            url=self.url,
-            token=self.token.to_dict() if self.token else None,
-            generation_kwargs=self.generation_kwargs,
-            streaming_callback=callback_name,
-        )
-
-    @classmethod
-    def from_dict(cls, data: Dict[str, Any]) -> "HuggingFaceTGIGenerator":
-        """
-        Deserialize this component from a dictionary.
-        """
-        deserialize_secrets_inplace(data["init_parameters"], keys=["token"])
-        init_params = data.get("init_parameters", {})
-        serialized_callback_handler = init_params.get("streaming_callback")
-        if serialized_callback_handler:
-            data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
-        return default_from_dict(cls, data)
-
-    def _get_telemetry_data(self) -> Dict[str, Any]:
-        """
-        Data that is sent to Posthog for usage analytics.
-        """
-        # Don't send URL as it is sensitive information
-        return {"model": self.model}
-
-    @component.output_types(replies=List[str], meta=List[Dict[str, Any]])
-    def run(self, prompt: str, generation_kwargs: Optional[Dict[str, Any]] = None):
-        """
-        Invoke the text generation inference for the given prompt and generation parameters.
-
-        :param prompt:
-            A string representing the prompt.
-        :param generation_kwargs:
-            Additional keyword arguments for text generation.
-        :returns:
-            A dictionary containing the generated replies and metadata. Both are lists of length n.
-            - replies: A list of strings representing the generated replies.
-        """
-        if not self.tokenizer:
-            msg = (
-                "The component HuggingFaceTGIGenerator was not warmed up. Please call warm_up() before "
-                "running LLM inference."
-            )
-            raise RuntimeError(msg)
-
-        # check generation kwargs given as parameters to override the default ones
-        additional_params = ["n", "stop_words"]
-        check_generation_params(generation_kwargs, additional_params)
-
-        # update generation kwargs by merging with the default ones
-        generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
-        num_responses = generation_kwargs.pop("n", 1)
-        generation_kwargs.setdefault("stop_sequences", []).extend(generation_kwargs.pop("stop_words", []))
-
-        prompt_token_count = len(self.tokenizer.encode(prompt, add_special_tokens=False))
-
-        if self.streaming_callback:
-            if num_responses > 1:
-                raise ValueError("Cannot stream multiple responses, please set n=1.")
-
-            return self._run_streaming(prompt, prompt_token_count, generation_kwargs)
-
-        return self._run_non_streaming(prompt, prompt_token_count, num_responses, generation_kwargs)
-
-    def _run_streaming(self, prompt: str, prompt_token_count: int, generation_kwargs: Dict[str, Any]):
-        res_chunk: Iterable[TextGenerationStreamOutput] = self.client.text_generation(
-            prompt, details=True, stream=True, **generation_kwargs
-        )
-        chunks: List[StreamingChunk] = []
-        # pylint: disable=not-an-iterable
-        for chunk in res_chunk:
-            token: TextGenerationOutputToken = chunk.token
-            if token.special:
-                continue
-            chunk_metadata = {**asdict(token), **(asdict(chunk.details) if chunk.details else {})}
-            stream_chunk = StreamingChunk(token.text, chunk_metadata)
-            chunks.append(stream_chunk)
-            self.streaming_callback(stream_chunk)  # type: ignore # streaming_callback is not None (verified in the run method)
-        metadata = {
-            "finish_reason": chunks[-1].meta.get("finish_reason", None),
-            "model": self.client.model,
-            "usage": {
-                "completion_tokens": chunks[-1].meta.get("generated_tokens", 0),
-                "prompt_tokens": prompt_token_count,
-                "total_tokens": prompt_token_count + chunks[-1].meta.get("generated_tokens", 0),
-            },
-        }
-        return {"replies": ["".join([chunk.content for chunk in chunks])], "meta": [metadata]}
-
-    def _run_non_streaming(
-        self, prompt: str, prompt_token_count: int, num_responses: int, generation_kwargs: Dict[str, Any]
-    ):
-        responses: List[str] = []
-        all_metadata: List[Dict[str, Any]] = []
-        for _i in range(num_responses):
-            tgr: TextGenerationOutput = self.client.text_generation(prompt, details=True, **generation_kwargs)
-            if tgr.details:
-                completion_tokens = len(tgr.details.tokens)
-                prompt_token_count = prompt_token_count + completion_tokens
-                finish_reason = tgr.details.finish_reason
-            else:
-                finish_reason = None
-                completion_tokens = 0
-            all_metadata.append(
-                {
-                    "model": self.client.model,
-                    "index": _i,
-                    "finish_reason": finish_reason,
-                    "usage": {
-                        "completion_tokens": completion_tokens,
-                        "prompt_tokens": prompt_token_count,
-                        "total_tokens": prompt_token_count + completion_tokens,
-                    },
-                }
-            )
-            responses.append(tgr.generated_text)
-        return {"replies": responses, "meta": all_metadata}
diff --git a/haystack/components/validators/json_schema.py b/haystack/components/validators/json_schema.py
index 8f1f8e4d..fb2c11b4 100644
--- a/haystack/components/validators/json_schema.py
+++ b/haystack/components/validators/json_schema.py
@@ -13,6 +13,20 @@ with LazyImport(message="Run 'pip install jsonschema'") as jsonschema_import:
     from jsonschema import ValidationError, validate
 
 
+def is_valid_json(s: str) -> bool:
+    """
+    Check if the provided string is a valid JSON.
+
+    :param s: The string to be checked.
+    :returns: `True` if the string is a valid JSON; otherwise, `False`.
+    """
+    try:
+        json.loads(s)
+    except ValueError:
+        return False
+    return True
+
+
 @component
 class JsonSchemaValidator:
     """
@@ -77,13 +91,15 @@ class JsonSchemaValidator:
 
     # Default error description template
     default_error_template = (
-        "The JSON content in the next message does not conform to the provided schema.\n"
+        "The following generated JSON does not conform to the provided schema.\n"
+        "Generated JSON: {failing_json}\n"
         "Error details:\n- Message: {error_message}\n"
         "- Error Path in JSON: {error_path}\n"
         "- Schema Path: {error_schema_path}\n"
         "Please match the following schema:\n"
         "{json_schema}\n"
-        "and provide the corrected JSON content ONLY."
+        "and provide the corrected JSON content ONLY. Please do not output anything else than the raw corrected "
+        "JSON string, this is the most important part of the task. Don't use any markdown and don't add any comment."
     )
 
     def __init__(self, json_schema: Optional[Dict[str, Any]] = None, error_template: Optional[str] = None):
@@ -125,14 +141,23 @@ class JsonSchemaValidator:
             dictionaries.
         """
         last_message = messages[-1]
-        last_message_content = json.loads(last_message.content)
+        if not is_valid_json(last_message.content):
+            return {
+                "validation_error": [
+                    ChatMessage.from_user(
+                        f"The message '{last_message.content}' is not a valid JSON object. "
+                        f"Please provide only a valid JSON object in string format."
+                        f"Don't use any markdown and don't add any comment."
+                    )
+                ]
+            }
 
+        last_message_content = json.loads(last_message.content)
         json_schema = json_schema or self.json_schema
         error_template = error_template or self.error_template or self.default_error_template
 
         if not json_schema:
             raise ValueError("Provide a JSON schema for validation either in the run method or in the component init.")
-
         # fc payload is json object but subtree `parameters` is string - we need to convert to json object
         # we need complete json to validate it against schema
         last_message_json = self._recursive_json_to_object(last_message_content)
@@ -149,7 +174,7 @@ class JsonSchemaValidator:
                 else:
                     validate(instance=content, schema=validation_schema)
 
-            return {"validated": messages}
+            return {"validated": [last_message]}
         except ValidationError as e:
             error_path = " -> ".join(map(str, e.absolute_path)) if e.absolute_path else "N/A"
             error_schema_path = " -> ".join(map(str, e.absolute_schema_path)) if e.absolute_schema_path else "N/A"
@@ -157,10 +182,14 @@ class JsonSchemaValidator:
             error_template = error_template or self.default_error_template
 
             recovery_prompt = self._construct_error_recovery_message(
-                error_template, str(e), error_path, error_schema_path, validation_schema
+                error_template,
+                str(e),
+                error_path,
+                error_schema_path,
+                validation_schema,
+                failing_json=last_message.content,
             )
-            complete_message_list = [ChatMessage.from_user(recovery_prompt)] + messages
-            return {"validation_error": complete_message_list}
+            return {"validation_error": [ChatMessage.from_user(recovery_prompt)]}
 
     def _construct_error_recovery_message(
         self,
@@ -169,6 +198,7 @@ class JsonSchemaValidator:
         error_path: str,
         error_schema_path: str,
         json_schema: Dict[str, Any],
+        failing_json: str,
     ) -> str:
         """
         Constructs an error recovery message using a specified template or the default one if none is provided.
@@ -178,6 +208,7 @@ class JsonSchemaValidator:
         :param error_path: The path in the JSON content where the error occurred.
         :param error_schema_path: The path in the JSON schema where the error occurred.
         :param json_schema: The JSON schema against which the content is validated.
+        :param failing_json: The generated invalid JSON string.
         """
         error_template = error_template or self.default_error_template
 
@@ -186,6 +217,7 @@ class JsonSchemaValidator:
             error_path=error_path,
             error_schema_path=error_schema_path,
             json_schema=json_schema,
+            failing_json=failing_json,
         )
 
     def _is_openai_function_calling_schema(self, json_schema: Dict[str, Any]) -> bool:
@@ -215,11 +247,10 @@ class JsonSchemaValidator:
                 if isinstance(value, str):
                     try:
                         json_value = json.loads(value)
-                        new_dict[key] = (
-                            self._recursive_json_to_object(json_value)
-                            if isinstance(json_value, (dict, list))
-                            else json_value
-                        )
+                        if isinstance(json_value, (dict, list)):
+                            new_dict[key] = self._recursive_json_to_object(json_value)
+                        else:
+                            new_dict[key] = value  # Preserve the original string value
                     except json.JSONDecodeError:
                         new_dict[key] = value
                 elif isinstance(value, dict):
diff --git a/haystack/core/component/component.py b/haystack/core/component/component.py
index ba966288..12e3643c 100644
--- a/haystack/core/component/component.py
+++ b/haystack/core/component/component.py
@@ -75,6 +75,7 @@ from collections.abc import Callable
 from contextlib import contextmanager
 from contextvars import ContextVar
 from copy import deepcopy
+from dataclasses import dataclass
 from types import new_class
 from typing import Any, Dict, Optional, Protocol, runtime_checkable
 
@@ -87,8 +88,24 @@ from .types import InputSocket, OutputSocket, _empty
 logger = logging.getLogger(__name__)
 
 
-# Callback inputs: component class (Type) and init parameters (as keyword arguments) (Dict[str, Any]).
-_COMPONENT_PRE_INIT_CALLBACK: ContextVar[Optional[Callable]] = ContextVar("component_pre_init_callback", default=None)
+@dataclass
+class PreInitHookPayload:
+    """
+    Payload for the hook called before a component instance is initialized.
+
+    :param callback:
+        Receives the following inputs: component class and init parameter keyword args.
+    :param in_progress:
+        Flag to indicate if the hook is currently being executed.
+        Used to prevent it from being called recursively (if the component's constructor
+        instantiates another component).
+    """
+
+    callback: Callable
+    in_progress: bool = False
+
+
+_COMPONENT_PRE_INIT_HOOK: ContextVar[Optional[PreInitHookPayload]] = ContextVar("component_pre_init_hook", default=None)
 
 
 @contextmanager
@@ -102,11 +119,11 @@ def _hook_component_init(callback: Callable):
     :param callback:
         Callback function to invoke.
     """
-    token = _COMPONENT_PRE_INIT_CALLBACK.set(callback)
+    token = _COMPONENT_PRE_INIT_HOOK.set(PreInitHookPayload(callback))
     try:
         yield
     finally:
-        _COMPONENT_PRE_INIT_CALLBACK.reset(token)
+        _COMPONENT_PRE_INIT_HOOK.reset(token)
 
 
 @runtime_checkable
@@ -172,17 +189,21 @@ class ComponentMeta(type):
         This method is called when clients instantiate a Component and runs before __new__ and __init__.
         """
         # This will call __new__ then __init__, giving us back the Component instance
-        pre_init_hook = _COMPONENT_PRE_INIT_CALLBACK.get()
-        if pre_init_hook is None:
+        pre_init_hook = _COMPONENT_PRE_INIT_HOOK.get()
+        if pre_init_hook is None or pre_init_hook.in_progress:
             instance = super().__call__(*args, **kwargs)
         else:
-            named_positional_args = ComponentMeta.positional_to_kwargs(cls, args)
-            assert (
-                set(named_positional_args.keys()).intersection(kwargs.keys()) == set()
-            ), "positional and keyword arguments overlap"
-            kwargs.update(named_positional_args)
-            pre_init_hook(cls, kwargs)
-            instance = super().__call__(**kwargs)
+            try:
+                pre_init_hook.in_progress = True
+                named_positional_args = ComponentMeta.positional_to_kwargs(cls, args)
+                assert (
+                    set(named_positional_args.keys()).intersection(kwargs.keys()) == set()
+                ), "positional and keyword arguments overlap"
+                kwargs.update(named_positional_args)
+                pre_init_hook.callback(cls, kwargs)
+                instance = super().__call__(**kwargs)
+            finally:
+                pre_init_hook.in_progress = False
 
         # Before returning, we have the chance to modify the newly created
         # Component instance, so we take the chance and set up the I/O sockets
diff --git a/haystack/utils/hf.py b/haystack/utils/hf.py
index 76139b81..f527b243 100644
--- a/haystack/utils/hf.py
+++ b/haystack/utils/hf.py
@@ -7,8 +7,6 @@ import inspect
 from enum import Enum
 from typing import Any, Callable, Dict, List, Optional, Union
 
-import requests
-
 from haystack import logging
 from haystack.dataclasses import StreamingChunk
 from haystack.lazy_imports import LazyImport
@@ -212,27 +210,6 @@ def resolve_hf_pipeline_kwargs(
     return huggingface_pipeline_kwargs
 
 
-def list_inference_deployed_models(headers: Optional[Dict] = None) -> List[str]:
-    """
-    List all currently deployed models on HF TGI free tier
-
-    :param headers: Optional dictionary of headers to include in the request
-    :return: list of all currently deployed models
-    :raises Exception: If the request to the TGI API fails
-
-    """
-    resp = requests.get(
-        "https://api-inference.huggingface.co/framework/text-generation-inference", headers=headers, timeout=10
-    )
-
-    payload = resp.json()
-    if resp.status_code != 200:
-        message = payload.get("error", "Unknown TGI error")
-        error_type = payload.get("error_type", "Unknown TGI error type")
-        raise Exception(f"Failed to fetch TGI deployed models: {message}. Error type: {error_type}")
-    return [model["model_id"] for model in payload]
-
-
 def check_valid_model(model_id: str, model_type: HFModelType, token: Optional[Secret]) -> None:
     """
     Check if the provided model ID corresponds to a valid model on HuggingFace Hub.
diff --git a/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml b/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml
new file mode 100644
index 00000000..d12adfde
--- /dev/null
+++ b/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    `SASEvaluator` now raises a `ValueError` if a `None` value is contained in the `predicted_answers` input.
diff --git a/releasenotes/notes/fix-caching-filters-syntax-d4c165ce0b0f258f.yaml b/releasenotes/notes/fix-caching-filters-syntax-d4c165ce0b0f258f.yaml
new file mode 100644
index 00000000..7c7cf8df
--- /dev/null
+++ b/releasenotes/notes/fix-caching-filters-syntax-d4c165ce0b0f258f.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    Use new filter syntax in the CacheChecker component instead of legacy one.
diff --git a/releasenotes/notes/fix-recursive-json-schema-validator-cdb7684de3c75e4e.yaml b/releasenotes/notes/fix-recursive-json-schema-validator-cdb7684de3c75e4e.yaml
new file mode 100644
index 00000000..c24e6870
--- /dev/null
+++ b/releasenotes/notes/fix-recursive-json-schema-validator-cdb7684de3c75e4e.yaml
@@ -0,0 +1,8 @@
+---
+enhancements:
+  - |
+    Made JSON schema validator compatible with all LLM by switching error template handling to a single user message.
+    Also reduce cost by only including last error instead of full message history.
+fixes:
+  - |
+    Fix recursive JSON type conversion in the schema validator to be less aggressive (no infinite recursion).
diff --git a/releasenotes/notes/rm-deprecated-tei-embedders-cfcd660c906ba69e.yaml b/releasenotes/notes/rm-deprecated-tei-embedders-cfcd660c906ba69e.yaml
new file mode 100644
index 00000000..7cdc8579
--- /dev/null
+++ b/releasenotes/notes/rm-deprecated-tei-embedders-cfcd660c906ba69e.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Deprecated `HuggingFaceTEITextEmbedder` and `HuggingFaceTEIDocumentEmbedder` have been removed.
+    Use `HuggingFaceAPITextEmbedder` and `HuggingFaceAPIDocumentEmbedder` instead.
diff --git a/releasenotes/notes/rm-deprecated-tgi-generators-6086bbb36417c0d8.yaml b/releasenotes/notes/rm-deprecated-tgi-generators-6086bbb36417c0d8.yaml
new file mode 100644
index 00000000..a635630e
--- /dev/null
+++ b/releasenotes/notes/rm-deprecated-tgi-generators-6086bbb36417c0d8.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Deprecated `HuggingFaceTGIGenerator` and `HuggingFaceTGIChatGenerator` have been removed.
+    Use `HuggingFaceAPIGenerator` and `HuggingFaceAPIChatGenerator` instead.


Test patch:
diff --git a/test/components/caching/test_url_cache_checker.py b/test/components/caching/test_url_cache_checker.py
index 3e92fef1..9bf1e12c 100644
--- a/test/components/caching/test_url_cache_checker.py
+++ b/test/components/caching/test_url_cache_checker.py
@@ -7,6 +7,7 @@ from haystack import Document, DeserializationError
 from haystack.testing.factory import document_store_class
 from haystack.document_stores.in_memory import InMemoryDocumentStore
 from haystack.components.caching.cache_checker import CacheChecker
+from unittest.mock import MagicMock
 
 
 class TestCacheChecker:
@@ -82,3 +83,11 @@ class TestCacheChecker:
         checker = CacheChecker(docstore, cache_field="url")
         results = checker.run(items=["https://example.com/1", "https://example.com/5"])
         assert results == {"hits": [documents[0], documents[2]], "misses": ["https://example.com/5"]}
+
+    def test_filters_syntax(self):
+        mocked_docstore_class = document_store_class("MockedDocumentStore")
+        mocked_docstore_class.filter_documents = MagicMock()
+        checker = CacheChecker(document_store=mocked_docstore_class(), cache_field="url")
+        checker.run(items=["https://example.com/1"])
+        valid_filters_syntax = {"field": "url", "operator": "==", "value": "https://example.com/1"}
+        mocked_docstore_class.filter_documents.assert_any_call(filters=valid_filters_syntax)
diff --git a/test/components/embedders/test_hugging_face_tei_document_embedder.py b/test/components/embedders/test_hugging_face_tei_document_embedder.py
deleted file mode 100644
index 8ac279dc..00000000
--- a/test/components/embedders/test_hugging_face_tei_document_embedder.py
+++ /dev/null
@@ -1,398 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-import os
-from unittest.mock import MagicMock, patch
-
-import numpy as np
-import pytest
-from huggingface_hub.utils import RepositoryNotFoundError
-
-from haystack.components.embedders.hugging_face_tei_document_embedder import HuggingFaceTEIDocumentEmbedder
-from haystack.dataclasses import Document
-from haystack.utils.auth import Secret
-
-
-@pytest.fixture
-def mock_check_valid_model():
-    with patch(
-        "haystack.components.embedders.hugging_face_tei_document_embedder.check_valid_model",
-        MagicMock(return_value=None),
-    ) as mock:
-        yield mock
-
-
-def mock_embedding_generation(json, **kwargs):
-    response = str(np.array([np.random.rand(384) for i in range(len(json["inputs"]))]).tolist()).encode()
-    return response
-
-
-class TestHuggingFaceTEIDocumentEmbedder:
-    def test_init_default(self, monkeypatch, mock_check_valid_model):
-        monkeypatch.setenv("HF_API_TOKEN", "fake-api-token")
-        embedder = HuggingFaceTEIDocumentEmbedder()
-
-        assert embedder.model == "BAAI/bge-small-en-v1.5"
-        assert embedder.url is None
-        assert embedder.token == Secret.from_env_var("HF_API_TOKEN", strict=False)
-        assert embedder.prefix == ""
-        assert embedder.suffix == ""
-        assert embedder.truncate is True
-        assert embedder.normalize is False
-        assert embedder.batch_size == 32
-        assert embedder.progress_bar is True
-        assert embedder.meta_fields_to_embed == []
-        assert embedder.embedding_separator == "\n"
-
-    def test_init_with_parameters(self, mock_check_valid_model):
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-            prefix="prefix",
-            suffix="suffix",
-            truncate=False,
-            normalize=True,
-            batch_size=64,
-            progress_bar=False,
-            meta_fields_to_embed=["test_field"],
-            embedding_separator=" | ",
-        )
-
-        assert embedder.model == "sentence-transformers/all-mpnet-base-v2"
-        assert embedder.url == "https://some_embedding_model.com"
-        assert embedder.token == Secret.from_token("fake-api-token")
-        assert embedder.prefix == "prefix"
-        assert embedder.suffix == "suffix"
-        assert embedder.truncate is False
-        assert embedder.normalize is True
-        assert embedder.batch_size == 64
-        assert embedder.progress_bar is False
-        assert embedder.meta_fields_to_embed == ["test_field"]
-        assert embedder.embedding_separator == " | "
-
-    def test_initialize_with_invalid_url(self, mock_check_valid_model):
-        with pytest.raises(ValueError):
-            HuggingFaceTEIDocumentEmbedder(model="sentence-transformers/all-mpnet-base-v2", url="invalid_url")
-
-    def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model):
-        # When custom TEI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id
-        mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id")
-        with pytest.raises(RepositoryNotFoundError):
-            HuggingFaceTEIDocumentEmbedder(model="invalid_model_id", url="https://some_embedding_model.com")
-
-    def test_to_dict(self, mock_check_valid_model):
-        component = HuggingFaceTEIDocumentEmbedder()
-        data = component.to_dict()
-
-        assert data == {
-            "type": "haystack.components.embedders.hugging_face_tei_document_embedder.HuggingFaceTEIDocumentEmbedder",
-            "init_parameters": {
-                "model": "BAAI/bge-small-en-v1.5",
-                "token": {"env_vars": ["HF_API_TOKEN"], "strict": False, "type": "env_var"},
-                "url": None,
-                "prefix": "",
-                "suffix": "",
-                "truncate": True,
-                "normalize": False,
-                "batch_size": 32,
-                "progress_bar": True,
-                "meta_fields_to_embed": [],
-                "embedding_separator": "\n",
-            },
-        }
-
-    def test_from_dict(self, mock_check_valid_model):
-        data = {
-            "type": "haystack.components.embedders.hugging_face_tei_document_embedder.HuggingFaceTEIDocumentEmbedder",
-            "init_parameters": {
-                "model": "BAAI/bge-small-en-v1.5",
-                "token": {"env_vars": ["HF_API_TOKEN"], "strict": False, "type": "env_var"},
-                "url": None,
-                "prefix": "",
-                "suffix": "",
-                "truncate": True,
-                "normalize": False,
-                "batch_size": 32,
-                "progress_bar": True,
-                "meta_fields_to_embed": [],
-                "embedding_separator": "\n",
-            },
-        }
-
-        embedder = HuggingFaceTEIDocumentEmbedder.from_dict(data)
-
-        assert embedder.model == "BAAI/bge-small-en-v1.5"
-        assert embedder.url is None
-        assert embedder.token == Secret.from_env_var("HF_API_TOKEN", strict=False)
-        assert embedder.prefix == ""
-        assert embedder.suffix == ""
-        assert embedder.truncate is True
-        assert embedder.normalize is False
-        assert embedder.batch_size == 32
-        assert embedder.progress_bar is True
-        assert embedder.meta_fields_to_embed == []
-        assert embedder.embedding_separator == "\n"
-
-    def test_to_dict_with_custom_init_parameters(self, mock_check_valid_model):
-        component = HuggingFaceTEIDocumentEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_env_var("ENV_VAR", strict=False),
-            prefix="prefix",
-            suffix="suffix",
-            truncate=False,
-            normalize=True,
-            batch_size=64,
-            progress_bar=False,
-            meta_fields_to_embed=["test_field"],
-            embedding_separator=" | ",
-        )
-
-        data = component.to_dict()
-
-        assert data == {
-            "type": "haystack.components.embedders.hugging_face_tei_document_embedder.HuggingFaceTEIDocumentEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"},
-                "model": "sentence-transformers/all-mpnet-base-v2",
-                "url": "https://some_embedding_model.com",
-                "prefix": "prefix",
-                "suffix": "suffix",
-                "truncate": False,
-                "normalize": True,
-                "batch_size": 64,
-                "progress_bar": False,
-                "meta_fields_to_embed": ["test_field"],
-                "embedding_separator": " | ",
-            },
-        }
-
-    def test_from_dict_with_custom_init_parameters(self, mock_check_valid_model):
-        data = {
-            "type": "haystack.components.embedders.hugging_face_tei_document_embedder.HuggingFaceTEIDocumentEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"},
-                "model": "sentence-transformers/all-mpnet-base-v2",
-                "url": "https://some_embedding_model.com",
-                "prefix": "prefix",
-                "suffix": "suffix",
-                "truncate": False,
-                "normalize": True,
-                "batch_size": 64,
-                "progress_bar": False,
-                "meta_fields_to_embed": ["test_field"],
-                "embedding_separator": " | ",
-            },
-        }
-
-        embedder = HuggingFaceTEIDocumentEmbedder.from_dict(data)
-
-        assert embedder.model == "sentence-transformers/all-mpnet-base-v2"
-        assert embedder.url == "https://some_embedding_model.com"
-        assert embedder.token == Secret.from_env_var("ENV_VAR", strict=False)
-        assert embedder.prefix == "prefix"
-        assert embedder.suffix == "suffix"
-        assert embedder.truncate is False
-        assert embedder.normalize is True
-        assert embedder.batch_size == 64
-        assert embedder.progress_bar is False
-        assert embedder.meta_fields_to_embed == ["test_field"]
-        assert embedder.embedding_separator == " | "
-
-    def test_prepare_texts_to_embed_w_metadata(self, mock_check_valid_model):
-        documents = [
-            Document(content=f"document number {i}: content", meta={"meta_field": f"meta_value {i}"}) for i in range(5)
-        ]
-
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-            meta_fields_to_embed=["meta_field"],
-            embedding_separator=" | ",
-        )
-
-        prepared_texts = embedder._prepare_texts_to_embed(documents)
-
-        assert prepared_texts == [
-            "meta_value 0 | document number 0: content",
-            "meta_value 1 | document number 1: content",
-            "meta_value 2 | document number 2: content",
-            "meta_value 3 | document number 3: content",
-            "meta_value 4 | document number 4: content",
-        ]
-
-    def test_prepare_texts_to_embed_w_suffix(self, mock_check_valid_model):
-        documents = [Document(content=f"document number {i}") for i in range(5)]
-
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-            prefix="my_prefix ",
-            suffix=" my_suffix",
-        )
-
-        prepared_texts = embedder._prepare_texts_to_embed(documents)
-
-        assert prepared_texts == [
-            "my_prefix document number 0 my_suffix",
-            "my_prefix document number 1 my_suffix",
-            "my_prefix document number 2 my_suffix",
-            "my_prefix document number 3 my_suffix",
-            "my_prefix document number 4 my_suffix",
-        ]
-
-    def test_embed_batch(self, mock_check_valid_model):
-        texts = ["text 1", "text 2", "text 3", "text 4", "text 5"]
-
-        with patch("huggingface_hub.InferenceClient.post") as mock_embedding_patch:
-            mock_embedding_patch.side_effect = mock_embedding_generation
-
-            embedder = HuggingFaceTEIDocumentEmbedder(
-                model="BAAI/bge-small-en-v1.5",
-                url="https://some_embedding_model.com",
-                token=Secret.from_token("fake-api-token"),
-            )
-            embeddings = embedder._embed_batch(texts_to_embed=texts, batch_size=2)
-
-            assert mock_embedding_patch.call_count == 3
-
-        assert isinstance(embeddings, list)
-        assert len(embeddings) == len(texts)
-        for embedding in embeddings:
-            assert isinstance(embedding, list)
-            assert len(embedding) == 384
-            assert all(isinstance(x, float) for x in embedding)
-
-    def test_run(self, mock_check_valid_model):
-        docs = [
-            Document(content="I love cheese", meta={"topic": "Cuisine"}),
-            Document(content="A transformer is a deep learning architecture", meta={"topic": "ML"}),
-        ]
-
-        with patch("huggingface_hub.InferenceClient.post") as mock_embedding_patch:
-            mock_embedding_patch.side_effect = mock_embedding_generation
-
-            embedder = HuggingFaceTEIDocumentEmbedder(
-                model="BAAI/bge-small-en-v1.5",
-                token=Secret.from_token("fake-api-token"),
-                prefix="prefix ",
-                suffix=" suffix",
-                meta_fields_to_embed=["topic"],
-                embedding_separator=" | ",
-            )
-
-            result = embedder.run(documents=docs)
-
-            mock_embedding_patch.assert_called_once_with(
-                json={
-                    "inputs": [
-                        "prefix Cuisine | I love cheese suffix",
-                        "prefix ML | A transformer is a deep learning architecture suffix",
-                    ],
-                    "truncate": True,
-                    "normalize": False,
-                },
-                task="feature-extraction",
-            )
-        documents_with_embeddings = result["documents"]
-
-        assert isinstance(documents_with_embeddings, list)
-        assert len(documents_with_embeddings) == len(docs)
-        for doc in documents_with_embeddings:
-            assert isinstance(doc, Document)
-            assert isinstance(doc.embedding, list)
-            assert len(doc.embedding) == 384
-            assert all(isinstance(x, float) for x in doc.embedding)
-
-    @pytest.mark.flaky(reruns=5, reruns_delay=5)
-    @pytest.mark.integration
-    @pytest.mark.skipif(
-        not os.environ.get("HF_API_TOKEN", None),
-        reason="Export an env var called HF_API_TOKEN containing the Hugging Face token to run this test.",
-    )
-    def test_run_inference_api_endpoint(self):
-        docs = [
-            Document(content="I love cheese", meta={"topic": "Cuisine"}),
-            Document(content="A transformer is a deep learning architecture", meta={"topic": "ML"}),
-        ]
-
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="sentence-transformers/all-MiniLM-L6-v2", meta_fields_to_embed=["topic"], embedding_separator=" | "
-        )
-
-        result = embedder.run(documents=docs)
-        documents_with_embeddings = result["documents"]
-
-        assert isinstance(documents_with_embeddings, list)
-        assert len(documents_with_embeddings) == len(docs)
-        for doc in documents_with_embeddings:
-            assert isinstance(doc, Document)
-            assert isinstance(doc.embedding, list)
-            assert len(doc.embedding) == 384
-            assert all(isinstance(x, float) for x in doc.embedding)
-
-    def test_run_custom_batch_size(self, mock_check_valid_model):
-        docs = [
-            Document(content="I love cheese", meta={"topic": "Cuisine"}),
-            Document(content="A transformer is a deep learning architecture", meta={"topic": "ML"}),
-        ]
-
-        with patch("huggingface_hub.InferenceClient.post") as mock_embedding_patch:
-            mock_embedding_patch.side_effect = mock_embedding_generation
-
-            embedder = HuggingFaceTEIDocumentEmbedder(
-                model="BAAI/bge-small-en-v1.5",
-                token=Secret.from_token("fake-api-token"),
-                prefix="prefix ",
-                suffix=" suffix",
-                meta_fields_to_embed=["topic"],
-                embedding_separator=" | ",
-                batch_size=1,
-            )
-
-            result = embedder.run(documents=docs)
-
-            assert mock_embedding_patch.call_count == 2
-
-        documents_with_embeddings = result["documents"]
-
-        assert isinstance(documents_with_embeddings, list)
-        assert len(documents_with_embeddings) == len(docs)
-        for doc in documents_with_embeddings:
-            assert isinstance(doc, Document)
-            assert isinstance(doc.embedding, list)
-            assert len(doc.embedding) == 384
-            assert all(isinstance(x, float) for x in doc.embedding)
-
-    def test_run_wrong_input_format(self, mock_check_valid_model):
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="BAAI/bge-small-en-v1.5",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-        )
-
-        # wrong formats
-        string_input = "text"
-        list_integers_input = [1, 2, 3]
-
-        with pytest.raises(TypeError, match="HuggingFaceTEIDocumentEmbedder expects a list of Documents as input"):
-            embedder.run(documents=string_input)
-
-        with pytest.raises(TypeError, match="HuggingFaceTEIDocumentEmbedder expects a list of Documents as input"):
-            embedder.run(documents=list_integers_input)
-
-    def test_run_on_empty_list(self, mock_check_valid_model):
-        embedder = HuggingFaceTEIDocumentEmbedder(
-            model="BAAI/bge-small-en-v1.5",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-        )
-
-        empty_list_input = []
-        result = embedder.run(documents=empty_list_input)
-
-        assert result["documents"] is not None
-        assert not result["documents"]  # empty list
diff --git a/test/components/embedders/test_hugging_face_tei_text_embedder.py b/test/components/embedders/test_hugging_face_tei_text_embedder.py
deleted file mode 100644
index 8cc3d445..00000000
--- a/test/components/embedders/test_hugging_face_tei_text_embedder.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-import os
-from unittest.mock import MagicMock, patch
-
-import numpy as np
-import pytest
-from huggingface_hub.utils import RepositoryNotFoundError
-
-from haystack.components.embedders.hugging_face_tei_text_embedder import HuggingFaceTEITextEmbedder
-from haystack.utils.auth import Secret
-
-
-@pytest.fixture
-def mock_check_valid_model():
-    with patch(
-        "haystack.components.embedders.hugging_face_tei_text_embedder.check_valid_model", MagicMock(return_value=None)
-    ) as mock:
-        yield mock
-
-
-def mock_embedding_generation(json, **kwargs):
-    response = str(np.array([np.random.rand(384) for i in range(len(json["inputs"]))]).tolist()).encode()
-    return response
-
-
-class TestHuggingFaceTEITextEmbedder:
-    def test_init_default(self, monkeypatch, mock_check_valid_model):
-        monkeypatch.setenv("HF_API_TOKEN", "fake-api-token")
-        embedder = HuggingFaceTEITextEmbedder()
-
-        assert embedder.model == "BAAI/bge-small-en-v1.5"
-        assert embedder.url is None
-        assert embedder.token == Secret.from_env_var("HF_API_TOKEN", strict=False)
-        assert embedder.prefix == ""
-        assert embedder.suffix == ""
-        assert embedder.truncate is True
-        assert embedder.normalize is False
-
-    def test_init_with_parameters(self, mock_check_valid_model):
-        embedder = HuggingFaceTEITextEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-            prefix="prefix",
-            suffix="suffix",
-            truncate=False,
-            normalize=True,
-        )
-
-        assert embedder.model == "sentence-transformers/all-mpnet-base-v2"
-        assert embedder.url == "https://some_embedding_model.com"
-        assert embedder.token == Secret.from_token("fake-api-token")
-        assert embedder.prefix == "prefix"
-        assert embedder.suffix == "suffix"
-        assert embedder.truncate is False
-        assert embedder.normalize is True
-
-    def test_initialize_with_invalid_url(self, mock_check_valid_model):
-        with pytest.raises(ValueError):
-            HuggingFaceTEITextEmbedder(model="sentence-transformers/all-mpnet-base-v2", url="invalid_url")
-
-    def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model):
-        # When custom TEI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id
-        mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id")
-        with pytest.raises(RepositoryNotFoundError):
-            HuggingFaceTEITextEmbedder(model="invalid_model_id", url="https://some_embedding_model.com")
-
-    def test_to_dict(self, mock_check_valid_model):
-        component = HuggingFaceTEITextEmbedder()
-        data = component.to_dict()
-
-        assert data == {
-            "type": "haystack.components.embedders.hugging_face_tei_text_embedder.HuggingFaceTEITextEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["HF_API_TOKEN"], "strict": False, "type": "env_var"},
-                "model": "BAAI/bge-small-en-v1.5",
-                "url": None,
-                "prefix": "",
-                "suffix": "",
-                "truncate": True,
-                "normalize": False,
-            },
-        }
-
-    def test_from_dict(self, mock_check_valid_model):
-        data = {
-            "type": "haystack.components.embedders.hugging_face_tei_text_embedder.HuggingFaceTEITextEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["HF_API_TOKEN"], "strict": False, "type": "env_var"},
-                "model": "BAAI/bge-small-en-v1.5",
-                "url": None,
-                "prefix": "",
-                "suffix": "",
-                "truncate": True,
-                "normalize": False,
-            },
-        }
-
-        embedder = HuggingFaceTEITextEmbedder.from_dict(data)
-
-        assert embedder.model == "BAAI/bge-small-en-v1.5"
-        assert embedder.url is None
-        assert embedder.token == Secret.from_env_var("HF_API_TOKEN", strict=False)
-        assert embedder.prefix == ""
-        assert embedder.suffix == ""
-        assert embedder.truncate is True
-        assert embedder.normalize is False
-
-    def test_to_dict_with_custom_init_parameters(self, mock_check_valid_model):
-        component = HuggingFaceTEITextEmbedder(
-            model="sentence-transformers/all-mpnet-base-v2",
-            url="https://some_embedding_model.com",
-            token=Secret.from_env_var("ENV_VAR", strict=False),
-            prefix="prefix",
-            suffix="suffix",
-            truncate=False,
-            normalize=True,
-        )
-
-        data = component.to_dict()
-
-        assert data == {
-            "type": "haystack.components.embedders.hugging_face_tei_text_embedder.HuggingFaceTEITextEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"},
-                "model": "sentence-transformers/all-mpnet-base-v2",
-                "url": "https://some_embedding_model.com",
-                "prefix": "prefix",
-                "suffix": "suffix",
-                "truncate": False,
-                "normalize": True,
-            },
-        }
-
-    def test_from_dict_with_custom_init_parameters(self, mock_check_valid_model):
-        data = {
-            "type": "haystack.components.embedders.hugging_face_tei_text_embedder.HuggingFaceTEITextEmbedder",
-            "init_parameters": {
-                "token": {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"},
-                "model": "sentence-transformers/all-mpnet-base-v2",
-                "url": "https://some_embedding_model.com",
-                "prefix": "prefix",
-                "suffix": "suffix",
-                "truncate": False,
-                "normalize": True,
-            },
-        }
-
-        embedder = HuggingFaceTEITextEmbedder.from_dict(data)
-
-        assert embedder.model == "sentence-transformers/all-mpnet-base-v2"
-        assert embedder.url == "https://some_embedding_model.com"
-        assert embedder.token == Secret.from_env_var("ENV_VAR", strict=False)
-        assert embedder.prefix == "prefix"
-        assert embedder.suffix == "suffix"
-        assert embedder.truncate is False
-        assert embedder.normalize is True
-
-    def test_run(self, mock_check_valid_model):
-        with patch("huggingface_hub.InferenceClient.post") as mock_embedding_patch:
-            mock_embedding_patch.side_effect = mock_embedding_generation
-
-            embedder = HuggingFaceTEITextEmbedder(
-                model="BAAI/bge-small-en-v1.5",
-                token=Secret.from_token("fake-api-token"),
-                prefix="prefix ",
-                suffix=" suffix",
-            )
-
-            result = embedder.run(text="The food was delicious")
-
-            mock_embedding_patch.assert_called_once_with(
-                json={"inputs": ["prefix The food was delicious suffix"], "truncate": True, "normalize": False},
-                task="feature-extraction",
-            )
-
-        assert len(result["embedding"]) == 384
-        assert all(isinstance(x, float) for x in result["embedding"])
-
-    @pytest.mark.flaky(reruns=5, reruns_delay=5)
-    @pytest.mark.integration
-    @pytest.mark.skipif(
-        not os.environ.get("HF_API_TOKEN", None),
-        reason="Export an env var called HF_API_TOKEN containing the Hugging Face token to run this test.",
-    )
-    def test_run_inference_api_endpoint(self):
-        embedder = HuggingFaceTEITextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")
-        result = embedder.run(text="The food was delicious")
-
-        assert len(result["embedding"]) == 384
-        assert all(isinstance(x, float) for x in result["embedding"])
-
-    def test_run_wrong_input_format(self, mock_check_valid_model):
-        embedder = HuggingFaceTEITextEmbedder(
-            model="BAAI/bge-small-en-v1.5",
-            url="https://some_embedding_model.com",
-            token=Secret.from_token("fake-api-token"),
-        )
-
-        list_integers_input = [1, 2, 3]
-
-        with pytest.raises(TypeError, match="HuggingFaceTEITextEmbedder expects a string as an input"):
-            embedder.run(text=list_integers_input)
diff --git a/test/components/evaluators/test_sas_evaluator.py b/test/components/evaluators/test_sas_evaluator.py
index 4a9ee402..d0e2e9b8 100644
--- a/test/components/evaluators/test_sas_evaluator.py
+++ b/test/components/evaluators/test_sas_evaluator.py
@@ -73,6 +73,21 @@ class TestSASEvaluator:
         with pytest.raises(ValueError):
             evaluator.run(ground_truth_answers=ground_truths, predicted_answers=predictions)
 
+    def test_run_with_none_in_predictions(self):
+        evaluator = SASEvaluator()
+        ground_truths = [
+            "A construction budget of US $2.3 billion",
+            "The Eiffel Tower, completed in 1889, symbolizes Paris's cultural magnificence.",
+            "The Meiji Restoration in 1868 transformed Japan into a modernized world power.",
+        ]
+        predictions = [
+            "A construction budget of US $2.3 billion",
+            None,
+            "The Meiji Restoration in 1868 transformed Japan into a modernized world power.",
+        ]
+        with pytest.raises(ValueError):
+            evaluator.run(ground_truth_answers=ground_truths, predicted_answers=predictions)
+
     def test_run_not_warmed_up(self):
         evaluator = SASEvaluator()
         ground_truths = [
diff --git a/test/components/generators/chat/test_hugging_face_tgi.py b/test/components/generators/chat/test_hugging_face_tgi.py
deleted file mode 100644
index 55ddaf1f..00000000
--- a/test/components/generators/chat/test_hugging_face_tgi.py
+++ /dev/null
@@ -1,367 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-from unittest.mock import MagicMock, Mock, patch
-
-import pytest
-from huggingface_hub import (
-    TextGenerationOutputToken,
-    TextGenerationStreamOutput,
-    TextGenerationStreamOutputStreamDetails,
-)
-from huggingface_hub.utils import RepositoryNotFoundError
-
-from haystack.components.generators.chat import HuggingFaceTGIChatGenerator
-from haystack.dataclasses import ChatMessage, StreamingChunk
-from haystack.utils.auth import Secret
-
-
-@pytest.fixture
-def mock_list_inference_deployed_models():
-    with patch(
-        "haystack.components.generators.chat.hugging_face_tgi.list_inference_deployed_models",
-        MagicMock(
-            return_value=[
-                "HuggingFaceH4/zephyr-7b-alpha",
-                "HuggingFaceH4/zephyr-7b-beta",
-                "mistralai/Mistral-7B-v0.1",
-                "meta-llama/Llama-2-13b-chat-hf",
-            ]
-        ),
-    ) as mock:
-        yield mock
-
-
-@pytest.fixture
-def mock_check_valid_model():
-    with patch(
-        "haystack.components.generators.chat.hugging_face_tgi.check_valid_model", MagicMock(return_value=None)
-    ) as mock:
-        yield mock
-
-
-@pytest.fixture
-def mock_text_generation():
-    with patch("huggingface_hub.InferenceClient.text_generation", autospec=True) as mock_text_generation:
-        mock_response = Mock()
-        mock_response.generated_text = "I'm fine, thanks."
-        details = Mock()
-        details.finish_reason = MagicMock(field1="value")
-        details.tokens = [1, 2, 3]
-        mock_response.details = details
-        mock_text_generation.return_value = mock_response
-        yield mock_text_generation
-
-
-# used to test serialization of streaming_callback
-def streaming_callback_handler(x):
-    return x
-
-
-class TestHuggingFaceTGIChatGenerator:
-    def test_initialize_with_valid_model_and_generation_parameters(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_list_inference_deployed_models
-    ):
-        model = "HuggingFaceH4/zephyr-7b-alpha"
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIChatGenerator(
-            model=model,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-        generator.warm_up()
-
-        assert generator.generation_kwargs == {
-            **generation_kwargs,
-            **{"stop_sequences": ["stop"]},
-            **{"max_new_tokens": 512},
-        }
-        assert generator.tokenizer is not None
-        assert generator.client is not None
-        assert generator.streaming_callback == streaming_callback
-
-    def test_to_dict(self, mock_check_valid_model):
-        # Initialize the HuggingFaceTGIChatGenerator object with valid parameters
-        generator = HuggingFaceTGIChatGenerator(
-            model="NousResearch/Llama-2-7b-chat-hf",
-            token=Secret.from_env_var("ENV_VAR", strict=False),
-            generation_kwargs={"n": 5},
-            stop_words=["stop", "words"],
-            streaming_callback=lambda x: x,
-        )
-
-        # Call the to_dict method
-        result = generator.to_dict()
-        init_params = result["init_parameters"]
-
-        # Assert that the init_params dictionary contains the expected keys and values
-        assert init_params["model"] == "NousResearch/Llama-2-7b-chat-hf"
-        assert init_params["token"] == {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"}
-        assert init_params["generation_kwargs"] == {"n": 5, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-
-    def test_from_dict(self, mock_check_valid_model):
-        generator = HuggingFaceTGIChatGenerator(
-            model="NousResearch/Llama-2-7b-chat-hf",
-            generation_kwargs={"n": 5},
-            stop_words=["stop", "words"],
-            streaming_callback=streaming_callback_handler,
-        )
-        # Call the to_dict method
-        result = generator.to_dict()
-
-        generator_2 = HuggingFaceTGIChatGenerator.from_dict(result)
-        assert generator_2.model == "NousResearch/Llama-2-7b-chat-hf"
-        assert generator_2.generation_kwargs == {"n": 5, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-        assert generator_2.streaming_callback is streaming_callback_handler
-
-    def test_warm_up(self, mock_check_valid_model, mock_auto_tokenizer, mock_list_inference_deployed_models):
-        generator = HuggingFaceTGIChatGenerator()
-        generator.warm_up()
-
-        # Assert that the tokenizer is now initialized
-        assert generator.tokenizer is not None
-
-    def test_warm_up_no_chat_template(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_list_inference_deployed_models, caplog
-    ):
-        generator = HuggingFaceTGIChatGenerator(model="meta-llama/Llama-2-13b-chat-hf")
-
-        # Set chat_template to None for this specific test
-        mock_auto_tokenizer.chat_template = None
-        generator.warm_up()
-
-        # warning message should be logged
-        assert "The model 'meta-llama/Llama-2-13b-chat-hf' doesn't have a default chat_template" in caplog.text
-
-    def test_custom_chat_template(
-        self,
-        chat_messages,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        mock_list_inference_deployed_models,
-    ):
-        custom_chat_template = "Here goes some Jinja template"
-
-        # mocked method to check if we called apply_chat_template with the custom template
-        mock_auto_tokenizer.apply_chat_template = MagicMock(return_value="some_value")
-
-        generator = HuggingFaceTGIChatGenerator(chat_template=custom_chat_template)
-        generator.warm_up()
-
-        assert generator.chat_template == custom_chat_template
-
-        generator.run(messages=chat_messages)
-        assert mock_auto_tokenizer.apply_chat_template.call_count == 1
-
-        # and we indeed called apply_chat_template with the custom template
-        _, kwargs = mock_auto_tokenizer.apply_chat_template.call_args
-        assert kwargs["chat_template"] == custom_chat_template
-
-    def test_initialize_with_invalid_model_path_or_url(self, mock_check_valid_model):
-        model = "invalid_model"
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        mock_check_valid_model.side_effect = ValueError("Invalid model path or url")
-
-        with pytest.raises(ValueError):
-            HuggingFaceTGIChatGenerator(
-                model=model,
-                generation_kwargs=generation_kwargs,
-                stop_words=stop_words,
-                streaming_callback=streaming_callback,
-            )
-
-    def test_initialize_with_invalid_url(self, mock_check_valid_model):
-        with pytest.raises(ValueError):
-            HuggingFaceTGIChatGenerator(model="NousResearch/Llama-2-7b-chat-hf", url="invalid_url")
-
-    def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model):
-        # When custom TGI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id
-        mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id")
-        with pytest.raises(RepositoryNotFoundError):
-            HuggingFaceTGIChatGenerator(model="invalid_model_id", url="https://some_chat_model.com")
-
-    def test_generate_text_response_with_valid_prompt_and_generation_parameters(
-        self,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        chat_messages,
-        mock_list_inference_deployed_models,
-    ):
-        model = "meta-llama/Llama-2-13b-chat-hf"
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIChatGenerator(
-            model=model,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-        generator.warm_up()
-
-        response = generator.run(messages=chat_messages)
-
-        # check kwargs passed to text_generation
-        # note how n because it is not text generation parameter was not passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop"], "max_new_tokens": 512}
-
-        assert isinstance(response, dict)
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) == 1
-        assert [isinstance(reply, ChatMessage) for reply in response["replies"]]
-
-    def test_generate_multiple_text_responses_with_valid_prompt_and_generation_parameters(
-        self,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        chat_messages,
-        mock_list_inference_deployed_models,
-    ):
-        model = "meta-llama/Llama-2-13b-chat-hf"
-        token = None
-        generation_kwargs = {"n": 3}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIChatGenerator(
-            model=model,
-            token=token,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-        generator.warm_up()
-
-        response = generator.run(chat_messages)
-
-        # check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop"], "max_new_tokens": 512}
-
-        # note how n caused n replies to be generated
-        assert isinstance(response, dict)
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) == 3
-        assert [isinstance(reply, ChatMessage) for reply in response["replies"]]
-
-    def test_generate_text_with_stop_words(
-        self,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        chat_messages,
-        mock_list_inference_deployed_models,
-    ):
-        generator = HuggingFaceTGIChatGenerator()
-        generator.warm_up()
-
-        stop_words = ["stop", "words"]
-
-        # Generate text response with stop words
-        response = generator.run(chat_messages, generation_kwargs={"stop_words": stop_words})
-
-        # check kwargs passed to text_generation
-        # we translate stop_words to stop_sequences
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-
-        # Assert that the response contains the generated replies
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, ChatMessage) for reply in response["replies"]]
-
-    def test_generate_text_with_custom_generation_parameters(
-        self,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        chat_messages,
-        mock_list_inference_deployed_models,
-    ):
-        # Create an instance of HuggingFaceRemoteGenerator with no generation parameters
-        generator = HuggingFaceTGIChatGenerator()
-        generator.warm_up()
-
-        # but then we pass them in run
-        generation_kwargs = {"temperature": 0.8, "max_new_tokens": 100}
-        response = generator.run(chat_messages, generation_kwargs=generation_kwargs)
-
-        # again check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "max_new_tokens": 100, "stop_sequences": [], "temperature": 0.8}
-
-        # Assert that the response contains the generated replies and the right response
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, ChatMessage) for reply in response["replies"]]
-        assert response["replies"][0].content == "I'm fine, thanks."
-
-    def test_generate_text_with_streaming_callback(
-        self,
-        mock_check_valid_model,
-        mock_auto_tokenizer,
-        mock_text_generation,
-        chat_messages,
-        mock_list_inference_deployed_models,
-    ):
-        streaming_call_count = 0
-
-        # Define the streaming callback function
-        def streaming_callback_fn(chunk: StreamingChunk):
-            nonlocal streaming_call_count
-            streaming_call_count += 1
-            assert isinstance(chunk, StreamingChunk)
-
-        # Create an instance of HuggingFaceRemoteGenerator
-        generator = HuggingFaceTGIChatGenerator(streaming_callback=streaming_callback_fn)
-        generator.warm_up()
-
-        # Create a fake streamed response
-        # self needed here, don't remove
-        def mock_iter(self):
-            yield TextGenerationStreamOutput(
-                index=0,
-                generated_text=None,
-                token=TextGenerationOutputToken(id=1, text="I'm fine, thanks.", logprob=0.0, special=False),
-            )
-            yield TextGenerationStreamOutput(
-                index=0,
-                generated_text=None,
-                token=TextGenerationOutputToken(id=1, text="Ok bye", logprob=0.0, special=False),
-                details=TextGenerationStreamOutputStreamDetails(finish_reason="length", generated_tokens=5, seed=None),
-            )
-
-        mock_response = Mock(**{"__iter__": mock_iter})
-        mock_text_generation.return_value = mock_response
-
-        # Generate text response with streaming callback
-        response = generator.run(chat_messages)
-
-        # check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": [], "stream": True, "max_new_tokens": 512}
-
-        # Assert that the streaming callback was called twice
-        assert streaming_call_count == 2
-
-        # Assert that the response contains the generated replies
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, ChatMessage) for reply in response["replies"]]
diff --git a/test/components/generators/test_hugging_face_tgi.py b/test/components/generators/test_hugging_face_tgi.py
deleted file mode 100644
index dea561d7..00000000
--- a/test/components/generators/test_hugging_face_tgi.py
+++ /dev/null
@@ -1,315 +0,0 @@
-# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
-#
-# SPDX-License-Identifier: Apache-2.0
-from unittest.mock import MagicMock, Mock, patch
-
-import pytest
-from huggingface_hub import (
-    TextGenerationOutputToken,
-    TextGenerationStreamOutput,
-    TextGenerationStreamOutputStreamDetails,
-)
-from huggingface_hub.utils import RepositoryNotFoundError
-
-from haystack.components.generators import HuggingFaceTGIGenerator
-from haystack.dataclasses import StreamingChunk
-from haystack.utils.auth import Secret
-
-
-@pytest.fixture
-def mock_list_inference_deployed_models():
-    with patch(
-        "haystack.components.generators.hugging_face_tgi.list_inference_deployed_models",
-        MagicMock(
-            return_value=["HuggingFaceH4/zephyr-7b-alpha", "HuggingFaceH4/zephyr-7b-alpha", "mistralai/Mistral-7B-v0.1"]
-        ),
-    ) as mock:
-        yield mock
-
-
-@pytest.fixture
-def mock_check_valid_model():
-    with patch(
-        "haystack.components.generators.hugging_face_tgi.check_valid_model", MagicMock(return_value=None)
-    ) as mock:
-        yield mock
-
-
-@pytest.fixture
-def mock_text_generation():
-    with patch("huggingface_hub.InferenceClient.text_generation", autospec=True) as mock_text_generation:
-        mock_response = Mock()
-        mock_response.generated_text = "I'm fine, thanks."
-        details = Mock()
-        details.finish_reason = MagicMock(field1="value")
-        details.tokens = [1, 2, 3]
-        mock_response.details = details
-        mock_text_generation.return_value = mock_response
-        yield mock_text_generation
-
-
-# used to test serialization of streaming_callback
-def streaming_callback_handler(x):
-    return x
-
-
-class TestHuggingFaceTGIGenerator:
-    def test_initialize_with_valid_model_and_generation_parameters(self, mock_check_valid_model):
-        model = "HuggingFaceH4/zephyr-7b-alpha"
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIGenerator(
-            model=model,
-            url=None,
-            token=None,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-
-        assert generator.model == model
-        assert generator.generation_kwargs == {
-            **generation_kwargs,
-            **{"stop_sequences": ["stop"]},
-            **{"max_new_tokens": 512},
-        }
-        assert generator.tokenizer is None
-        assert generator.client is not None
-        assert generator.streaming_callback == streaming_callback
-
-    def test_to_dict(self, mock_check_valid_model):
-        # Initialize the HuggingFaceRemoteGenerator object with valid parameters
-        generator = HuggingFaceTGIGenerator(
-            token=Secret.from_env_var("ENV_VAR", strict=False),
-            generation_kwargs={"n": 5},
-            stop_words=["stop", "words"],
-            streaming_callback=lambda x: x,
-        )
-
-        # Call the to_dict method
-        result = generator.to_dict()
-        init_params = result["init_parameters"]
-
-        # Assert that the init_params dictionary contains the expected keys and values
-        assert init_params["model"] == "mistralai/Mistral-7B-v0.1"
-        assert init_params["token"] == {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"}
-        assert init_params["generation_kwargs"] == {"n": 5, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-
-    def test_from_dict(self, mock_check_valid_model):
-        generator = HuggingFaceTGIGenerator(
-            model="mistralai/Mistral-7B-v0.1",
-            generation_kwargs={"n": 5},
-            stop_words=["stop", "words"],
-            streaming_callback=streaming_callback_handler,
-        )
-        # Call the to_dict method
-        result = generator.to_dict()
-
-        # now deserialize, call from_dict
-        generator_2 = HuggingFaceTGIGenerator.from_dict(result)
-        assert generator_2.model == "mistralai/Mistral-7B-v0.1"
-        assert generator_2.generation_kwargs == {"n": 5, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-        assert generator_2.streaming_callback is streaming_callback_handler
-
-    def test_initialize_with_invalid_url(self, mock_check_valid_model):
-        with pytest.raises(ValueError):
-            HuggingFaceTGIGenerator(model="mistralai/Mistral-7B-v0.1", url="invalid_url")
-
-    def test_initialize_with_url_but_invalid_model(self, mock_check_valid_model):
-        # When custom TGI endpoint is used via URL, model must be provided and valid HuggingFace Hub model id
-        mock_check_valid_model.side_effect = RepositoryNotFoundError("Invalid model id")
-        with pytest.raises(RepositoryNotFoundError):
-            HuggingFaceTGIGenerator(model="invalid_model_id", url="https://some_chat_model.com")
-
-    def test_generate_text_response_with_valid_prompt_and_generation_parameters(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, mock_list_inference_deployed_models
-    ):
-        model = "mistralai/Mistral-7B-v0.1"
-
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIGenerator(
-            model=model,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-        generator.warm_up()
-
-        prompt = "Hello, how are you?"
-        response = generator.run(prompt)
-
-        # check kwargs passed to text_generation
-        # note how n was not passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop"], "max_new_tokens": 512}
-
-        assert isinstance(response, dict)
-        assert "replies" in response
-        assert "meta" in response
-        assert isinstance(response["replies"], list)
-        assert isinstance(response["meta"], list)
-        assert len(response["replies"]) == 1
-        assert len(response["meta"]) == 1
-        assert [isinstance(reply, str) for reply in response["replies"]]
-
-    def test_generate_multiple_text_responses_with_valid_prompt_and_generation_parameters(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, mock_list_inference_deployed_models
-    ):
-        model = "mistralai/Mistral-7B-v0.1"
-        generation_kwargs = {"n": 3}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        generator = HuggingFaceTGIGenerator(
-            model=model,
-            generation_kwargs=generation_kwargs,
-            stop_words=stop_words,
-            streaming_callback=streaming_callback,
-        )
-        generator.warm_up()
-
-        prompt = "Hello, how are you?"
-        response = generator.run(prompt)
-
-        # check kwargs passed to text_generation
-        # note how n was not passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop"], "max_new_tokens": 512}
-
-        assert isinstance(response, dict)
-        assert "replies" in response
-        assert "meta" in response
-        assert isinstance(response["replies"], list)
-        assert [isinstance(reply, str) for reply in response["replies"]]
-
-        assert isinstance(response["meta"], list)
-        assert len(response["replies"]) == 3
-        assert len(response["meta"]) == 3
-        assert [isinstance(reply, dict) for reply in response["meta"]]
-
-    def test_initialize_with_invalid_model(self, mock_check_valid_model):
-        model = "invalid_model"
-        generation_kwargs = {"n": 1}
-        stop_words = ["stop"]
-        streaming_callback = None
-
-        mock_check_valid_model.side_effect = ValueError("Invalid model path or url")
-
-        with pytest.raises(ValueError):
-            HuggingFaceTGIGenerator(
-                model=model,
-                generation_kwargs=generation_kwargs,
-                stop_words=stop_words,
-                streaming_callback=streaming_callback,
-            )
-
-    def test_generate_text_with_stop_words(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, mock_list_inference_deployed_models
-    ):
-        generator = HuggingFaceTGIGenerator()
-        generator.warm_up()
-
-        # Generate text response with stop words
-        response = generator.run("How are you?", generation_kwargs={"stop_words": ["stop", "words"]})
-
-        # check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": ["stop", "words"], "max_new_tokens": 512}
-
-        # Assert that the response contains the generated replies
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, str) for reply in response["replies"]]
-
-        # Assert that the response contains the metadata
-        assert "meta" in response
-        assert isinstance(response["meta"], list)
-        assert len(response["meta"]) > 0
-        assert [isinstance(reply, dict) for reply in response["replies"]]
-
-    def test_generate_text_with_custom_generation_parameters(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, mock_list_inference_deployed_models
-    ):
-        generator = HuggingFaceTGIGenerator()
-        generator.warm_up()
-
-        generation_kwargs = {"temperature": 0.8, "max_new_tokens": 100}
-        response = generator.run("How are you?", generation_kwargs=generation_kwargs)
-
-        # check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "max_new_tokens": 100, "stop_sequences": [], "temperature": 0.8}
-
-        # Assert that the response contains the generated replies and the right response
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, str) for reply in response["replies"]]
-        assert response["replies"][0] == "I'm fine, thanks."
-
-        # Assert that the response contains the metadata
-        assert "meta" in response
-        assert isinstance(response["meta"], list)
-        assert len(response["meta"]) > 0
-        assert [isinstance(reply, str) for reply in response["replies"]]
-
-    def test_generate_text_with_streaming_callback(
-        self, mock_check_valid_model, mock_auto_tokenizer, mock_text_generation, mock_list_inference_deployed_models
-    ):
-        streaming_call_count = 0
-
-        # Define the streaming callback function
-        def streaming_callback_fn(chunk: StreamingChunk):
-            nonlocal streaming_call_count
-            streaming_call_count += 1
-            assert isinstance(chunk, StreamingChunk)
-
-        # Create an instance of HuggingFaceRemoteGenerator
-        generator = HuggingFaceTGIGenerator(streaming_callback=streaming_callback_fn)
-        generator.warm_up()
-
-        # Create a fake streamed response
-        # Don't remove self
-        def mock_iter(self):
-            yield TextGenerationStreamOutput(
-                index=0,
-                generated_text=None,
-                token=TextGenerationOutputToken(id=1, text="I'm fine, thanks.", logprob=0.0, special=False),
-            )
-            yield TextGenerationStreamOutput(
-                index=1,
-                generated_text=None,
-                token=TextGenerationOutputToken(id=1, text="Ok bye", logprob=0.0, special=False),
-                details=TextGenerationStreamOutputStreamDetails(finish_reason="length", generated_tokens=5, seed=None),
-            )
-
-        mock_response = Mock(**{"__iter__": mock_iter})
-        mock_text_generation.return_value = mock_response
-
-        # Generate text response with streaming callback
-        response = generator.run("prompt")
-
-        # check kwargs passed to text_generation
-        _, kwargs = mock_text_generation.call_args
-        assert kwargs == {"details": True, "stop_sequences": [], "stream": True, "max_new_tokens": 512}
-
-        # Assert that the streaming callback was called twice
-        assert streaming_call_count == 2
-
-        # Assert that the response contains the generated replies
-        assert "replies" in response
-        assert isinstance(response["replies"], list)
-        assert len(response["replies"]) > 0
-        assert [isinstance(reply, str) for reply in response["replies"]]
-
-        # Assert that the response contains the metadata
-        assert "meta" in response
-        assert isinstance(response["meta"], list)
-        assert len(response["meta"]) > 0
-        assert [isinstance(reply, dict) for reply in response["replies"]]
diff --git a/test/components/validators/test_json_schema.py b/test/components/validators/test_json_schema.py
index 034bf6b9..39e9c785 100644
--- a/test/components/validators/test_json_schema.py
+++ b/test/components/validators/test_json_schema.py
@@ -4,11 +4,10 @@
 import json
 from typing import List
 
-from haystack import component, Pipeline
-from haystack.components.validators import JsonSchemaValidator
-
 import pytest
 
+from haystack import Pipeline, component
+from haystack.components.validators import JsonSchemaValidator
 from haystack.dataclasses import ChatMessage
 
 
@@ -110,10 +109,9 @@ class TestJsonSchemaValidator:
         ]
 
         result = validator.run(messages, json_schema_github_compare)
-
         assert "validated" in result
-        assert len(result["validated"]) == 2
-        assert result["validated"] == messages
+        assert len(result["validated"]) == 1
+        assert result["validated"][0] == messages[1]
 
     #  Validates a message against an OpenAI function calling schema successfully.
     def test_validates_message_against_openai_function_calling_schema(
@@ -142,8 +140,8 @@ class TestJsonSchemaValidator:
         result = validator.run(messages, json_schema_github_compare_openai)
 
         assert "validated" in result
-        assert len(result["validated"]) == 2
-        assert result["validated"] == messages
+        assert len(result["validated"]) == 1
+        assert result["validated"][0] == messages[1]
 
     #  Constructs a custom error recovery message when validation fails.
     def test_construct_custom_error_recovery_message(self):
@@ -155,10 +153,11 @@ class TestJsonSchemaValidator:
             "- Schema Path: {error_schema_path}\n"
             "Please match the following schema:\n"
             "{json_schema}\n"
+            "Failing Json: {failing_json}\n"
         )
 
         recovery_message = validator._construct_error_recovery_message(
-            new_error_template, "Error message", "Error path", "Error schema path", {"type": "object"}
+            new_error_template, "Error message", "Error path", "Error schema path", {"type": "object"}, "Failing Json"
         )
 
         expected_recovery_message = (
@@ -167,6 +166,7 @@ class TestJsonSchemaValidator:
             "- Schema Path: Error schema path\n"
             "Please match the following schema:\n"
             "{'type': 'object'}\n"
+            "Failing Json: Failing Json\n"
         )
         assert recovery_message == expected_recovery_message
 
@@ -201,5 +201,5 @@ class TestJsonSchemaValidator:
         pipe.connect("message_producer", "schema_validator")
         result = pipe.run(data={"schema_validator": {"json_schema": json_schema_github_compare}})
         assert "validation_error" in result["schema_validator"]
-        assert len(result["schema_validator"]["validation_error"]) > 1
+        assert len(result["schema_validator"]["validation_error"]) == 1
         assert "Error details" in result["schema_validator"]["validation_error"][0].content
diff --git a/test/core/pipeline/test_pipeline.py b/test/core/pipeline/test_pipeline.py
index c35fa9d6..221c3523 100644
--- a/test/core/pipeline/test_pipeline.py
+++ b/test/core/pipeline/test_pipeline.py
@@ -31,6 +31,17 @@ class FakeComponent:
         return {"value": input_}
 
 
+@component
+class FakeComponentSquared:
+    def __init__(self, an_init_param: Optional[str] = None):
+        self.an_init_param = an_init_param
+        self.inner = FakeComponent()
+
+    @component.output_types(value=str)
+    def run(self, input_: str):
+        return {"value": input_}
+
+
 class TestPipeline:
     """
     This class contains only unit tests for the Pipeline class.
@@ -414,6 +425,19 @@ class TestPipeline:
         assert greet.message == "modified test"
         assert greet.log_level == "DEBUG"
 
+        # Test with a component that internally instantiates another component
+        def component_pre_init_callback_check_class(name, component_cls, init_params):
+            assert name == "fake_component_squared"
+            assert component_cls == FakeComponentSquared
+
+        pipe = Pipeline()
+        pipe.add_component("fake_component_squared", FakeComponentSquared())
+        pipe = Pipeline.from_dict(
+            pipe.to_dict(),
+            callbacks=DeserializationCallbacks(component_pre_init=component_pre_init_callback_check_class),
+        )
+        assert type(pipe.graph.nodes["fake_component_squared"]["instance"].inner) == FakeComponent
+
     # UNIT
     def test_from_dict_with_empty_dict(self):
         assert Pipeline() == Pipeline.from_dict({})