From 9eea7c5628fab445a685e02b221d818f324309c1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 12 Sep 2026 12:23:55 +0800 Subject: [PATCH] fix: propagate Xinference rerank failures instead of returning empty results The Xinference rerank provider swallowed upstream failures and an uninitialized model by returning an empty list, which the retrieval manager read as a legitimate empty rerank and used to overwrite the fused candidates with nothing. Other providers (e.g. TEI) raise, and the manager only preserves the fused results on that path, so a transient Xinference outage silently degraded every knowledge-base query to zero hits. Raise instead, matching the provider contract and letting the manager's degradation keep the unreranked results. Fixes #10000 --- .../sources/xinference_rerank_source.py | 8 ++-- tests/test_xinference_rerank_source.py | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/test_xinference_rerank_source.py diff --git a/astrbot/core/provider/sources/xinference_rerank_source.py b/astrbot/core/provider/sources/xinference_rerank_source.py index 9c3a77c158..d834abfcc3 100644 --- a/astrbot/core/provider/sources/xinference_rerank_source.py +++ b/astrbot/core/provider/sources/xinference_rerank_source.py @@ -90,8 +90,7 @@ async def rerank( top_n: int | None = None, ) -> list[RerankResult]: if not self.model: - logger.error("Xinference rerank model is not initialized.") - return [] + raise RuntimeError("Xinference rerank model is not initialized") try: response = await self.model.rerank(documents, query, top_n) results = response.get("results", []) @@ -110,9 +109,12 @@ async def rerank( for result in results ] except Exception as e: + # An empty list reads as a legitimate empty rerank to the caller, + # which would overwrite the fused candidates with nothing (#10000). + # Propagate so the retrieval manager keeps the unreranked results. logger.error(f"Xinference rerank failed: {e}") logger.debug(f"Xinference rerank failed with exception: {e}", exc_info=True) - return [] + raise async def terminate(self) -> None: """关闭客户端会话""" diff --git a/tests/test_xinference_rerank_source.py b/tests/test_xinference_rerank_source.py new file mode 100644 index 0000000000..82f8393297 --- /dev/null +++ b/tests/test_xinference_rerank_source.py @@ -0,0 +1,38 @@ +"""Regression tests for the Xinference rerank provider failure contract (#10000). + +The provider used to swallow upstream failures and return an empty list, +which the retrieval manager read as a legitimate empty rerank result and +used to overwrite the fused candidates. Failures now propagate so the +manager can fall back to the unreranked results. +""" + +from unittest.mock import AsyncMock, Mock + +import pytest + +from astrbot.core.provider.sources.xinference_rerank_source import ( + XinferenceRerankProvider, +) + + +@pytest.fixture +def provider() -> XinferenceRerankProvider: + instance = XinferenceRerankProvider.__new__(XinferenceRerankProvider) + instance.model = Mock() + return instance + + +@pytest.mark.asyncio +async def test_rerank_failure_propagates_instead_of_empty_list(provider): + provider.model.rerank = AsyncMock(side_effect=RuntimeError("upstream down")) + + with pytest.raises(RuntimeError, match="upstream down"): + await provider.rerank(query="q", documents=["a", "b"]) + + +@pytest.mark.asyncio +async def test_uninitialized_model_raises_instead_of_empty_list(provider): + provider.model = None + + with pytest.raises(RuntimeError, match="not initialized"): + await provider.rerank(query="q", documents=["a"])