fix(rerank): harden web_search rerank fallback and correct stale docs

list()-materialize the reranker's output inside the guarded block so a
None / non-iterable / lazily-raising reranker falls back to native order
instead of raising out of web_search, and reject bool indices (an int
subclass) the same way _parse_hits already does.

Drop leftover web_fetch references from the rerank settings and Reranker
role help (reranking is wired into web_search only), and document both
endpoint paths (tools.reranker_alias and tools.rerank_url).
This commit is contained in:
Patrick Buckley
2026-06-01 10:52:32 -07:00
parent 6a0bc852d9
commit c6e0293185
5 changed files with 38 additions and 10 deletions
+19
View File
@@ -359,6 +359,25 @@ class TestWebSearchReranking:
out = _format_searxng(_results("A", "B"), "q", reranker=boom)
assert out.index("[A]") < out.index("[B]") # native order preserved
def test_none_order_falls_back_to_native_order(self):
# A reranker that returns None (or any non-iterable) must fall back, not
# raise — list() materializes it inside the guarded block.
out = _format_searxng(_results("A", "B"), "q", reranker=lambda q, d: None)
assert out.index("[A]") < out.index("[B]") # native order preserved
def test_non_int_indices_ignored(self):
# bool is an int subclass; True/False must not be honored as indices 1/0.
# Junk entries are skipped while valid ints still apply: only 2 and 0 here.
out = _format_searxng(
_results("A", "B", "C"),
"q",
max_results=3,
reranker=lambda q, d: [True, "1", 2, 0],
)
# 2 -> C, 0 -> A; B (no valid index) is kept after. If True were honored
# as index 1, B would jump to the front — this pins it last.
assert out.index("[C]") < out.index("[A]") < out.index("[B]")
def test_skipped_for_single_result(self):
called = {"n": 0}
+1 -1
View File
@@ -5205,7 +5205,7 @@ const MODEL_ROLES = [
{
label: "Reranker",
description:
"Reranks web_search results and selects the most relevant chunks of large web_fetch pages. Point at a model whose base_url is a Cohere/Jina-compatible /rerank endpoint and whose capabilities include supports_rerank. Empty falls back to the tools.rerank_url setting (if set).",
"Reranks web_search results. Point at a model whose base_url is a Cohere/Jina-compatible /rerank endpoint and whose capabilities include supports_rerank. Empty falls back to the tools.rerank_url setting (if set).",
aliasKey: "tools.reranker_alias",
fallbackKind: "disabled",
mediaCapability: "supports_rerank",
+1 -1
View File
@@ -1359,7 +1359,7 @@ class ChatSession:
)
def _rerank_enabled_for(self, tool: str) -> bool:
"""Whether reranking is enabled for ``tool`` ('web_search' | 'web_fetch').
"""Whether reranking is enabled for ``tool`` (currently: 'web_search').
The per-tool toggles default on; the operative gate is whether an
endpoint is configured (``_resolve_rerank_client`` returns None when
+7 -6
View File
@@ -259,9 +259,9 @@ def _build_registry() -> dict[str, SettingDef]:
help="Full URL of a Cohere/Jina-compatible rerank endpoint, including the path "
"(e.g. 'http://vllm:8000/rerank', 'http://tei:8080/rerank', or "
"'https://api.cohere.com/v2/rerank'). Turnstone runs no rerank model itself: "
"web_search results and large web_fetch pages are reranked by POSTing to this "
"endpoint. Empty disables reranking entirely. Overrides config.toml [tools] "
"rerank_url and $TURNSTONE_RERANK_URL.",
"web_search results are reranked by POSTing to this endpoint. Empty disables "
"reranking entirely. Overrides config.toml [tools] rerank_url and "
"$TURNSTONE_RERANK_URL.",
),
SettingDef(
"tools.rerank_model",
@@ -291,9 +291,10 @@ def _build_registry() -> dict[str, SettingDef]:
True,
"Rerank web_search results when a rerank endpoint is configured",
"tools",
help="When a rerank endpoint is set (tools.rerank_url), re-order web_search "
"results by query relevance before returning the top hits. No effect when no "
"endpoint is configured. Disable to keep the search backend's native ranking.",
help="When a rerank endpoint is configured (via tools.reranker_alias or "
"tools.rerank_url), re-order web_search results by query relevance before "
"returning the top hits. No effect when no endpoint is configured. Disable to "
"keep the search backend's native ranking.",
),
SettingDef(
"tools.reranker_alias",
+10 -2
View File
@@ -173,14 +173,22 @@ def _rerank_results(
tail = results[_RERANK_POOL:]
try:
docs = [f"{r.get('title', '')}\n{r.get('content') or ''}".strip() for r in pool]
order = reranker(query, docs)
# Materialize inside the try so a None / non-iterable / lazily-raising
# reranker falls back here instead of exploding the splice loop below.
order = list(reranker(query, docs))
except Exception as e:
log.warning("rerank failed; using native result order: %s", e)
return results
seen: set[int] = set()
reordered: list[dict[str, Any]] = []
for idx in order:
if isinstance(idx, int) and 0 <= idx < len(pool) and idx not in seen:
# bool is an int subclass — reject a stray True/False posing as index 1/0.
if (
isinstance(idx, int)
and not isinstance(idx, bool)
and 0 <= idx < len(pool)
and idx not in seen
):
seen.add(idx)
reordered.append(pool[idx])
if not reordered: