Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/en/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,8 @@ Reranking model for search result refinement. Supports VikingDB (Volcengine), Co
| `model` | str | Model name (for `openai` providers) |
| `timeout` | float | HTTP request timeout in seconds for OpenAI-compatible providers. Increase for slow or cold-starting local rerank servers. Default: `30.0` |
| `threshold` | float | Score threshold between `0.0` and `1.0`; results below this are filtered out. Default: `0.1` |
| `max_chars_per_doc` | int | Truncate each document to at most N characters before reranking; `0` disables truncation (default). Bounds reranker input so one oversized abstract cannot overflow the model and fail the whole batch. Truncates the model input only — stored and returned abstracts are unchanged. Recommended when enabled: `2000` for 512-token rerankers (BGE/MiniLM/VikingDB). Note for CJK text: characters are not tokens, so a char cap can still overflow a token-limited reranker. Default: `0` |
| `max_tokens_per_doc` | int | Forward a per-document **token** truncation limit to providers whose APIs accept it — Cohere v2, LiteLLM, and OpenAI-compatible (Cohere-schema) endpoints receive `max_tokens_per_doc`; `0` omits it (default). Provider-native token truncation closes the CJK gap that `max_chars_per_doc` cannot: N characters can be many more than N tokens. VikingDB/doubao has no truncation field — use `max_chars_per_doc` there. Default: `0` |
| `extra_headers` | object | Custom HTTP headers (for OpenAI-compatible providers, optional) |

**Supported providers:**
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,8 @@ AST 提取支持:Python、JavaScript/TypeScript、Rust、Go、Java、C/C++。
| `model` | str | 模型名称(用于 `openai` 提供方) |
| `timeout` | float | OpenAI 兼容 provider 的 HTTP 请求超时时间,单位为秒。对于较慢或冷启动的本地 rerank 服务可适当增大。默认:`30.0` |
| `threshold` | float | 分数阈值,范围为 `0.0` 到 `1.0`。低于此值的结果会被过滤。默认:`0.1` |
| `max_chars_per_doc` | int | 在精排前将每篇文档截断为至多 N 个字符;`0` 表示不截断(默认)。用于限制精排模型的输入长度,避免单篇超长摘要撑爆模型导致整批精排失败。仅截断送入模型的输入,存储与返回的摘要保持不变。启用时推荐值:512-token 精排模型(BGE/MiniLM/VikingDB)取 `2000`。CJK 文本注意:字符数不等于 token 数,字符上限仍可能撑爆 token 受限的精排模型。默认:`0` |
| `max_tokens_per_doc` | int | 将每篇文档的 **token** 截断上限转发给支持该参数的 provider——Cohere v2、LiteLLM 及 OpenAI 兼容(Cohere schema)接口会收到 `max_tokens_per_doc`;`0` 表示不发送(默认)。provider 原生的 token 截断可弥补 `max_chars_per_doc` 无法覆盖的 CJK 场景:N 个字符可能远多于 N 个 token。VikingDB/doubao 无截断字段,请对该 provider 使用 `max_chars_per_doc`。默认:`0` |
| `extra_headers` | object | 自定义 HTTP 请求头(OpenAI 兼容 provider 可用,可选) |

**支持的提供方:**
Expand Down
25 changes: 15 additions & 10 deletions openviking/models/rerank/cohere_rerank.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ def __init__(
api_key: str,
model: str = "rerank-v3.5",
api_base: str = "https://api.cohere.com",
max_tokens_per_doc: int = 0,
):
super().__init__()
self.api_key = api_key
self.model = model
self.api_base = api_base.rstrip("/")
self.max_tokens_per_doc = max_tokens_per_doc
self.provider = "cohere"
self._client = httpx.Client(
base_url=self.api_base,
Expand All @@ -57,18 +59,20 @@ def rerank_batch(self, query: str, documents: List[str]) -> Optional[List[float]
if not documents:
return []

req_body = {
"model": self.model,
"query": query,
"documents": documents,
"top_n": len(documents),
"return_documents": False,
}
# Cohere v2 truncates each document to this many tokens (provider-native).
if self.max_tokens_per_doc > 0:
req_body["max_tokens_per_doc"] = self.max_tokens_per_doc

try:
started = time.monotonic()
resp = self._client.post(
"/v2/rerank",
json={
"model": self.model,
"query": query,
"documents": documents,
"top_n": len(documents),
"return_documents": False,
},
)
resp = self._client.post("/v2/rerank", json=req_body)
resp.raise_for_status()
data = resp.json()

Expand Down Expand Up @@ -116,4 +120,5 @@ def from_config(cls, config) -> Optional["CohereRerankClient"]:
return cls(
api_key=config.api_key,
model=config.model_name if config.model_name != "doubao-seed-rerank" else "rerank-v3.5",
max_tokens_per_doc=config.max_tokens_per_doc,
)
31 changes: 23 additions & 8 deletions openviking/models/rerank/litellm_rerank.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,28 @@ class LiteLLMRerankClient(RerankBase):
LiteLLM rerank API client.
"""

def __init__(self, api_key: Optional[str], api_base: Optional[str], model_name: str):
def __init__(
self,
api_key: Optional[str],
api_base: Optional[str],
model_name: str,
max_tokens_per_doc: int = 0,
):
"""
Initialize LiteLLM rerank client.

Args:
api_key: API key for LiteLLM providers (optional, can come from env)
api_base: API base for LiteLLM providers (optional, can come from env)
model_name: Model name to use for reranking
max_tokens_per_doc: Per-document token-truncation limit forwarded to litellm.rerank
when > 0; 0 means the parameter is omitted.
"""
super().__init__()
self.api_key = api_key
self.api_base = api_base
self.model_name = model_name
self.max_tokens_per_doc = max_tokens_per_doc
self.provider = "litellm"

def rerank_batch(self, query: str, documents: List[str]) -> Optional[List[float]]:
Expand All @@ -52,14 +61,19 @@ def rerank_batch(self, query: str, documents: List[str]) -> Optional[List[float]
try:
import litellm

rerank_kwargs = {
"model": self.model_name,
"query": query,
"documents": [{"text": d} for d in documents],
"api_key": self.api_key,
"api_base": self.api_base,
}
# litellm forwards this to Cohere-v2-style backends (provider-native truncation).
if self.max_tokens_per_doc > 0:
rerank_kwargs["max_tokens_per_doc"] = self.max_tokens_per_doc

started = time.monotonic()
response = litellm.rerank(
model=self.model_name,
query=query,
documents=[{"text": d} for d in documents],
api_key=self.api_key,
api_base=self.api_base,
)
response = litellm.rerank(**rerank_kwargs)

# Update token usage tracking (estimate from response or input)
response_dict = (
Expand Down Expand Up @@ -121,4 +135,5 @@ def from_config(cls, config) -> Optional["LiteLLMRerankClient"]:
api_key=config.api_key,
api_base=config.api_base,
model_name=config.model,
max_tokens_per_doc=config.max_tokens_per_doc,
)
7 changes: 7 additions & 0 deletions openviking/models/rerank/openai_rerank.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(
model_name: str,
extra_headers: Optional[Dict[str, str]] = None,
timeout: float = 30.0,
max_tokens_per_doc: int = 0,
) -> None:
"""
Initialize OpenAI-compatible rerank client.
Expand All @@ -50,6 +51,7 @@ def __init__(
self.model_name = model_name
self.extra_headers = extra_headers or {}
self.timeout = timeout
self.max_tokens_per_doc = max_tokens_per_doc
self.provider = "openai"

def rerank_batch(self, query: str, documents: List[str]) -> Optional[List[float]]:
Expand All @@ -72,6 +74,10 @@ def rerank_batch(self, query: str, documents: List[str]) -> Optional[List[float]
"query": query,
"documents": documents,
}
# Cohere-schema rerank endpoints (DashScope, vLLM, TEI, ...) accept a per-document
# token-truncation limit. Sent only when configured, so default requests are unchanged.
if self.max_tokens_per_doc > 0:
req_body["max_tokens_per_doc"] = self.max_tokens_per_doc

try:
headers = {
Expand Down Expand Up @@ -144,4 +150,5 @@ def from_config(cls, config) -> Optional["OpenAIRerankClient"]:
model_name=config.model or "qwen3-rerank",
extra_headers=config.extra_headers,
timeout=config.timeout,
max_tokens_per_doc=config.max_tokens_per_doc,
)
9 changes: 8 additions & 1 deletion openviking/retrieve/hierarchical_retriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,15 @@ def _rerank_scores(
if not self._rerank_client or not documents:
return fallback_scores

# Bound reranker input width so one oversized abstract cannot overflow the
# model and fail the whole batch open. 0 = OFF (not "truncate to 0 chars").
# Model input only: scores scatter back onto the full result dict, so stored
# and returned abstracts are untouched. cap=0 is byte-identical parity.
cap = self.rerank_config.max_chars_per_doc if self.rerank_config else 0
model_inputs = [doc[:cap] for doc in documents] if cap > 0 else documents

try:
scores = self._rerank_client.rerank_batch(query, documents)
scores = self._rerank_client.rerank_batch(query, model_inputs)
except Exception as e:
logger.warning(
"[HierarchicalRetriever] Rerank failed, fallback to vector scores: %s", e
Expand Down
46 changes: 46 additions & 0 deletions openviking_cli/utils/config/rerank_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

from pydantic import BaseModel, Field, model_validator

from openviking_cli.utils.logger import get_logger

logger = get_logger(__name__)

# Below this width the reranker query budget dominates and cross-encoder scores
# become unstable; a non-zero cap under it is almost always a misconfiguration.
_MIN_SAFE_CHARS_PER_DOC = 200


class RerankConfig(BaseModel):
"""Configuration for rerank API. Supports VikingDB, Cohere, OpenAI-compatible, and LiteLLM providers."""
Expand Down Expand Up @@ -47,6 +55,32 @@ class RerankConfig(BaseModel):
default=0.1, description="Relevance threshold (score > threshold is relevant)"
)

max_chars_per_doc: int = Field(
default=0,
ge=0,
strict=True,
description=(
"Truncate each document to N characters before reranking; 0 = unbounded. "
"Bounds reranker input so one oversized abstract cannot overflow the model "
"and fail the whole batch. Truncates model input only; stored/returned "
"abstracts are untouched."
),
)

max_tokens_per_doc: int = Field(
default=0,
ge=0,
strict=True,
description=(
"Forward a per-document token-truncation limit to rerank providers whose APIs "
"accept it (Cohere v2, LiteLLM, and OpenAI-compatible Cohere-schema endpoints "
"receive 'max_tokens_per_doc'); 0 = do not send the field (parity). Token "
"truncation is provider-native and closes the CJK gap a character cap cannot: "
"N characters can be many more than N tokens. VikingDB/doubao has no truncation "
"field, so use max_chars_per_doc for that provider."
),
)

model_config = {"extra": "forbid"}

def _effective_provider(self) -> Optional[str]:
Expand Down Expand Up @@ -78,6 +112,18 @@ def validate_provider_fields(self) -> "RerankConfig":
raise ValueError("LiteLLM rerank provider requires 'model'")
return self

@model_validator(mode="after")
def warn_small_char_cap(self) -> "RerankConfig":
"""Soft-warn (never reject) when a non-zero char cap is below the stability floor."""
if 0 < self.max_chars_per_doc < _MIN_SAFE_CHARS_PER_DOC:
logger.warning(
"RerankConfig.max_chars_per_doc=%d is below the ~%d-char cross-encoder "
"stability floor; rerank scores may become unstable. Set 0 to disable truncation.",
self.max_chars_per_doc,
_MIN_SAFE_CHARS_PER_DOC,
)
return self

def is_available(self) -> bool:
"""Check if rerank is configured."""
p = self._effective_provider()
Expand Down
Loading