6c9c7fe7f3
CI / integration tests (3.13) (push) Failing after 1s
Commit lint / pull request title (push) Has been skipped
Docs / links (push) Failing after 1s
CI / unit tests (3.13) (push) Failing after 1s
CI / lint (push) Failing after 1s
CI / integration tests (push) Failing after 1s
CI / package build (push) Failing after 1s
Commit lint / commit messages (push) Failing after 1s
CI / unit tests (push) Failing after 1s
210 lines
6.7 KiB
Python
210 lines
6.7 KiB
Python
"""DashScope rerank provider — URL/body shape, results parsing, retries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from everos.component.rerank import DashScopeRerankProvider, RerankError
|
|
|
|
|
|
def _patch_httpx(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
handler: Callable[[httpx.Request], httpx.Response],
|
|
) -> None:
|
|
transport = httpx.MockTransport(handler)
|
|
import everos.component.rerank.dashscope_provider as mod
|
|
|
|
real_cls = httpx.AsyncClient
|
|
|
|
def factory(*args: object, **kwargs: object) -> httpx.AsyncClient:
|
|
kwargs["transport"] = transport
|
|
return real_cls(*args, **kwargs) # type: ignore[arg-type]
|
|
|
|
monkeypatch.setattr(mod.httpx, "AsyncClient", factory)
|
|
|
|
|
|
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"):
|
|
DashScopeRerankProvider(
|
|
model="qwen3-rerank",
|
|
api_key="k",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
)
|
|
|
|
|
|
async def test_empty_documents_short_circuits(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
calls = 0
|
|
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
nonlocal calls
|
|
calls += 1
|
|
return _ok_response([])
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2", api_key="k", base_url="https://dashscope.aliyuncs.com"
|
|
)
|
|
assert await p.rerank("q", []) == []
|
|
assert calls == 0
|
|
|
|
|
|
async def test_url_body_and_sort_desc(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
seen_urls: list[str] = []
|
|
seen_bodies: list[dict[str, object]] = []
|
|
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
seen_urls.append(str(req.url))
|
|
seen_bodies.append(json.loads(req.content))
|
|
return _ok_response(
|
|
[
|
|
{"index": 0, "relevance_score": 0.1},
|
|
{"index": 1, "relevance_score": 0.9},
|
|
{"index": 2, "relevance_score": 0.5},
|
|
]
|
|
)
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="k",
|
|
base_url="https://dashscope.aliyuncs.com/",
|
|
)
|
|
results = await p.rerank("q", ["a", "b", "c"])
|
|
# Trailing slash stripped, native service path appended.
|
|
assert seen_urls == [
|
|
"https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
|
|
]
|
|
# Nested input/parameters body shape.
|
|
body = seen_bodies[0]
|
|
assert body["model"] == "gte-rerank-v2"
|
|
assert body["input"] == {"query": "q", "documents": ["a", "b", "c"]}
|
|
assert body["parameters"]["top_n"] == 3
|
|
assert [r.index for r in results] == [1, 2, 0]
|
|
|
|
|
|
async def test_auth_header_always_present(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
seen_headers: list[dict[str, str]] = []
|
|
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
seen_headers.append(dict(req.headers))
|
|
return _ok_response([{"index": 0, "relevance_score": 0.5}])
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="sk-abc",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
)
|
|
await p.rerank("q", ["a"])
|
|
assert seen_headers[0].get("authorization") == "Bearer sk-abc"
|
|
|
|
|
|
async def test_batching_offsets_indices(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""With batch_size=2 and 3 docs, the second batch's result index 0 becomes 2."""
|
|
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
body = json.loads(req.content)
|
|
docs = body["input"]["documents"]
|
|
return _ok_response(
|
|
[{"index": i, "relevance_score": float(i)} for i in range(len(docs))]
|
|
)
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="k",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
batch_size=2,
|
|
)
|
|
results = await p.rerank("q", ["a", "b", "c"])
|
|
assert sorted(r.index for r in results) == [0, 1, 2]
|
|
|
|
|
|
async def test_4xx_raises_immediately(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
state = {"calls": 0}
|
|
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
state["calls"] += 1
|
|
return httpx.Response(401, text="unauthorized")
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="bad",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
max_retries=3,
|
|
)
|
|
with pytest.raises(RerankError, match="HTTP 401"):
|
|
await p.rerank("q", ["a"])
|
|
assert state["calls"] == 1
|
|
|
|
|
|
async def test_5xx_retries(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
state = {"calls": 0}
|
|
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
state["calls"] += 1
|
|
if state["calls"] < 2:
|
|
return httpx.Response(503, text="unavailable")
|
|
return _ok_response([{"index": 0, "relevance_score": 0.42}])
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="k",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
max_retries=3,
|
|
)
|
|
results = await p.rerank("q", ["a"])
|
|
assert state["calls"] == 2
|
|
assert results[0].score == pytest.approx(0.42)
|
|
|
|
|
|
async def test_transport_error_exhausts(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
raise httpx.ReadTimeout("timeout")
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2",
|
|
api_key="k",
|
|
base_url="https://dashscope.aliyuncs.com",
|
|
max_retries=1,
|
|
)
|
|
with pytest.raises(RerankError, match="transport failure"):
|
|
await p.rerank("q", ["a"])
|
|
|
|
|
|
async def test_malformed_results_missing_output(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
return httpx.Response(200, json={"usage": {}})
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2", api_key="k", base_url="https://dashscope.aliyuncs.com"
|
|
)
|
|
with pytest.raises(RerankError, match="missing results"):
|
|
await p.rerank("q", ["a"])
|
|
|
|
|
|
async def test_malformed_result_entry(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def handler(_req: httpx.Request) -> httpx.Response:
|
|
return httpx.Response(200, json={"output": {"results": [{"index": 0}]}})
|
|
|
|
_patch_httpx(monkeypatch, handler)
|
|
p = DashScopeRerankProvider(
|
|
model="gte-rerank-v2", api_key="k", base_url="https://dashscope.aliyuncs.com"
|
|
)
|
|
with pytest.raises(RerankError, match="malformed rerank result"):
|
|
await p.rerank("q", ["a"])
|