diff --git a/src/everos/component/rerank/dashscope_provider.py b/src/everos/component/rerank/dashscope_provider.py index 498f26d77..23d0ad7fa 100644 --- a/src/everos/component/rerank/dashscope_provider.py +++ b/src/everos/component/rerank/dashscope_provider.py @@ -34,9 +34,10 @@ returned list is sorted score-descending defensively regardless of server ordering. ``api_key`` is required — DashScope has no anonymous tier. -Only ``gte-rerank-v2`` is supported by this provider for now. -``qwen3-rerank`` uses a different endpoint / request shape and should be -added as a separate branch if EverOS decides to support it later. +``gte-rerank-v2`` and ``qwen3-rerank`` are supported. Both use the same native +endpoint and body; ``qwen3-rerank`` additionally takes the task instruction as +``parameters.instruct`` (local patch 2026-10-02: the intl region has no +``gte-rerank-v2``). """ from __future__ import annotations @@ -69,7 +70,8 @@ class DashScopeRerankProvider: """ _SERVICE_PATH = "/api/v1/services/rerank/text-rerank/text-rerank" - _SUPPORTED_MODELS = frozenset({"gte-rerank-v2"}) + _SUPPORTED_MODELS = frozenset({"gte-rerank-v2", "qwen3-rerank"}) + _INSTRUCT_MODELS = frozenset({"qwen3-rerank"}) def __init__( self, @@ -84,7 +86,8 @@ def __init__( ) -> None: if model not in self._SUPPORTED_MODELS: raise ValueError( - f"DashScope rerank currently supports gte-rerank-v2 only; got {model!r}" + f"DashScope rerank supports {sorted(self._SUPPORTED_MODELS)}; " + f"got {model!r}" ) self._model = model self._api_key = api_key @@ -103,9 +106,8 @@ async def rerank( ) -> list[RerankResult]: """Score every document against ``query``; return sorted desc. - ``instruction`` is accepted for protocol parity but not transmitted: - DashScope applies the reranker template server-side, so there is no - client-side prompt to fill. + ``instruction`` is sent as ``parameters.instruct`` for ``qwen3-rerank``; + ``gte-rerank-v2`` takes no instruction, so it is dropped there. """ if not documents: return [] @@ -115,7 +117,7 @@ async def rerank( for offset in range(0, len(documents), self._batch_size) ] chunk_results = await asyncio.gather( - *(self._score_chunk(query, docs) for _, docs in chunks) + *(self._score_chunk(query, docs, instruction) for _, docs in chunks) ) scored: list[RerankResult] = [] for (offset, _), partial in zip(chunks, chunk_results, strict=True): @@ -126,12 +128,18 @@ async def rerank( return scored async def _score_chunk( - self, query: str, documents: list[str] + self, query: str, documents: list[str], instruction: str | None = None ) -> list[RerankResult]: + parameters: dict[str, Any] = { + "return_documents": False, + "top_n": len(documents), + } + if instruction and self._model in self._INSTRUCT_MODELS: + parameters["instruct"] = instruction payload: dict[str, Any] = { "model": self._model, "input": {"query": query, "documents": documents}, - "parameters": {"return_documents": False, "top_n": len(documents)}, + "parameters": parameters, } headers = { "Authorization": f"Bearer {self._api_key}", diff --git a/tests/unit/test_component/test_rerank/test_dashscope_provider.py b/tests/unit/test_component/test_rerank/test_dashscope_provider.py index 3fce9afad..17fbb076f 100644 --- a/tests/unit/test_component/test_rerank/test_dashscope_provider.py +++ b/tests/unit/test_component/test_rerank/test_dashscope_provider.py @@ -31,15 +31,54 @@ def _ok_response(items: list[dict[str, float | int]]) -> httpx.Response: return httpx.Response(200, json={"output": {"results": items}}) -def test_only_gte_rerank_v2_is_supported() -> None: - with pytest.raises(ValueError, match="gte-rerank-v2 only"): +def test_unsupported_model_rejected() -> None: + with pytest.raises(ValueError, match="supports"): DashScopeRerankProvider( - model="qwen3-rerank", + model="gte-rerank", api_key="k", base_url="https://dashscope.aliyuncs.com", ) +async def test_qwen3_rerank_forwards_instruction( + monkeypatch: pytest.MonkeyPatch, +) -> None: + seen_bodies: list[dict[str, object]] = [] + + def handler(req: httpx.Request) -> httpx.Response: + seen_bodies.append(json.loads(req.content)) + return _ok_response([{"index": 0, "relevance_score": 0.5}]) + + _patch_httpx(monkeypatch, handler) + p = DashScopeRerankProvider( + model="qwen3-rerank", + api_key="k", + base_url="https://dashscope-intl.aliyuncs.com", + ) + await p.rerank("q", ["a"], instruction="judge it") + await p.rerank("q", ["a"]) + assert seen_bodies[0]["model"] == "qwen3-rerank" + assert seen_bodies[0]["parameters"]["instruct"] == "judge it" + assert "instruct" not in seen_bodies[1]["parameters"] + + +async def test_gte_rerank_v2_never_sends_instruction( + monkeypatch: pytest.MonkeyPatch, +) -> None: + seen_bodies: list[dict[str, object]] = [] + + def handler(req: httpx.Request) -> httpx.Response: + seen_bodies.append(json.loads(req.content)) + return _ok_response([{"index": 0, "relevance_score": 0.5}]) + + _patch_httpx(monkeypatch, handler) + p = DashScopeRerankProvider( + model="gte-rerank-v2", api_key="k", base_url="https://dashscope.aliyuncs.com" + ) + await p.rerank("q", ["a"], instruction="judge it") + assert "instruct" not in seen_bodies[0]["parameters"] + + async def test_empty_documents_short_circuits(monkeypatch: pytest.MonkeyPatch) -> None: calls = 0