mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(dashscope): honor rerank base overrides and model metadata
This commit is contained in:
parent
cf8f548792
commit
331db73127
10 changed files with 198 additions and 75 deletions
|
|
@ -1,44 +0,0 @@
|
|||
# Qwen3.7 text reranking
|
||||
|
||||
Use `dashscope/qwen3.7-text-rerank` with LiteLLM's rerank interface and a Beijing DashScope API key
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
response = litellm.rerank(
|
||||
model="dashscope/qwen3.7-text-rerank",
|
||||
query="How can I reset my password?",
|
||||
documents=[
|
||||
"The weather is sunny today.",
|
||||
"Open account settings and select Reset password.",
|
||||
"How do I change my password?",
|
||||
],
|
||||
top_n=2,
|
||||
return_documents=True,
|
||||
instruction="Retrieve semantically similar text.",
|
||||
)
|
||||
```
|
||||
|
||||
Set `DASHSCOPE_API_KEY` in the environment or pass `api_key` explicitly. The asynchronous equivalent is `await litellm.arerank(...)`
|
||||
|
||||
For the proxy, add a model entry and send the same query, documents and options to `/v1/rerank` using its configured model alias
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: qwen37-rerank
|
||||
litellm_params:
|
||||
model: dashscope/qwen3.7-text-rerank
|
||||
api_key: os.environ/DASHSCOPE_API_KEY
|
||||
model_info:
|
||||
mode: rerank
|
||||
```
|
||||
|
||||
LiteLLM sends the native DashScope request to `https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank`. An explicit `api_base` or `DASHSCOPE_API_BASE_RERANK` can select a different host, an `/api/v1` base, or the complete native endpoint. Chat-compatible DashScope paths are converted to the native path for this model
|
||||
|
||||
`instruction` maps to DashScope's `parameters.instruct`. When omitted, the provider chooses its default relevance criterion. `top_n` and `return_documents` map to the corresponding native parameters. Live calls on 2026-09-08 confirmed that this model returns `document.text` when `return_documents=True`, despite the official parameter table omitting it from the supported-model list
|
||||
|
||||
The response contains the provider request ID, original document indices, relevance scores and requested document text. `meta.tokens.input_tokens` comes from `usage.prompt_tokens`; `meta.billed_units.total_tokens` comes from `usage.total_tokens`. These counters do not add a model price or dollar-cost calculation
|
||||
|
||||
The existing `dashscope/qwen3-rerank` model retains its compatible protocol. This change does not add multimodal reranking or reinterpret structured candidate documents
|
||||
|
||||
Protocol reference: [DashScope text rerank API](https://help.aliyun.com/zh/model-studio/text-rerank-api)
|
||||
|
|
@ -2,10 +2,15 @@
|
|||
Common utilities for the DashScope LLM provider.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from collections.abc import Mapping
|
||||
from functools import lru_cache
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final, Literal
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
|
@ -34,15 +39,59 @@ def get_dashscope_family_embedding_config(custom_llm_provider: str) -> "BaseEmbe
|
|||
return DashScopeEmbeddingConfig()
|
||||
|
||||
|
||||
def get_dashscope_family_rerank_config(custom_llm_provider: str, model: str) -> "BaseRerankConfig":
|
||||
def get_dashscope_family_rerank_config(
|
||||
custom_llm_provider: str, model: str, api_base: str | None = None
|
||||
) -> "BaseRerankConfig":
|
||||
provider_config: Final = _get_dashscope_family_rerank_provider_config(custom_llm_provider)
|
||||
if model == "qwen3.7-text-rerank":
|
||||
model_cost: Final[Mapping[str, object]] = litellm.model_cost
|
||||
runtime_api: Final = next(
|
||||
(
|
||||
declared_api
|
||||
for key in (f"{custom_llm_provider}/{model}", f"dashscope/{model}", model)
|
||||
if (declared_api := _rerank_api_from_model_info(model_cost.get(key))) is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
rerank_api: Final = runtime_api or _bundled_dashscope_rerank_apis().get(f"dashscope/{model}")
|
||||
if rerank_api == "native":
|
||||
from litellm.llms.dashscope.rerank.native_transformation import DashScopeNativeRerankConfig
|
||||
|
||||
return DashScopeNativeRerankConfig(provider_config)
|
||||
return DashScopeNativeRerankConfig(
|
||||
provider_config, api_base=api_base or get_secret_str(f"{custom_llm_provider.upper()}_API_BASE_RERANK")
|
||||
)
|
||||
return provider_config
|
||||
|
||||
|
||||
def _rerank_api_from_model_info(raw_model_info: object) -> str | None:
|
||||
if raw_model_info is None:
|
||||
return None
|
||||
model_info: Final = TypeAdapter(Mapping[str, object]).validate_python(raw_model_info)
|
||||
provider_info: Final = model_info.get("provider_specific_entry")
|
||||
if provider_info is None:
|
||||
return None
|
||||
metadata: Final = TypeAdapter(Mapping[str, object]).validate_python(provider_info)
|
||||
rerank_api: Final[str | None] = TypeAdapter(Literal["native", "compatible"] | None).validate_python(
|
||||
metadata.get("rerank_api")
|
||||
)
|
||||
return rerank_api
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _bundled_dashscope_rerank_apis() -> Mapping[str, str]:
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
|
||||
model_infos: Final = TypeAdapter(Mapping[str, object]).validate_json(
|
||||
GetModelCostMap.read_local_model_cost_map_text()
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
key: rerank_api
|
||||
for key, model_info in model_infos.items()
|
||||
if key.startswith("dashscope/") and (rerank_api := _rerank_api_from_model_info(model_info)) is not None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_dashscope_family_rerank_provider_config(custom_llm_provider: str) -> "DashScopeRerankConfig":
|
||||
if custom_llm_provider == "qwencloud":
|
||||
from litellm.llms.dashscope.qwencloud import QwenCloudRerankConfig
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import ClassVar, Final
|
||||
from typing import Final
|
||||
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
|
@ -47,13 +47,11 @@ class QwenAIPlatformEmbeddingConfig(DashScopeEmbeddingConfig):
|
|||
|
||||
|
||||
class QwenAIPlatformRerankConfig(DashScopeRerankConfig):
|
||||
DEFAULT_RERANK_API_BASE: ClassVar[str] = QWEN_AI_PLATFORM_RERANK_API_BASE
|
||||
|
||||
def _resolve_api_key(self, api_key: str | None) -> str:
|
||||
return _require_qwen_ai_platform_api_key(api_key)
|
||||
|
||||
def _resolve_rerank_api_base(self, api_base: str | None) -> str:
|
||||
return api_base or get_secret_str("QWEN_AI_PLATFORM_API_BASE_RERANK") or self.DEFAULT_RERANK_API_BASE
|
||||
return api_base or get_secret_str("QWEN_AI_PLATFORM_API_BASE_RERANK") or QWEN_AI_PLATFORM_RERANK_API_BASE
|
||||
|
||||
|
||||
class QwenAIPlatformImageGenerationConfig(DashScopeImageGenerationConfig):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import ClassVar, Final
|
||||
from typing import Final
|
||||
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
|
|
@ -47,13 +47,11 @@ class QwenCloudEmbeddingConfig(DashScopeEmbeddingConfig):
|
|||
|
||||
|
||||
class QwenCloudRerankConfig(DashScopeRerankConfig):
|
||||
DEFAULT_RERANK_API_BASE: ClassVar[str] = QWENCLOUD_RERANK_API_BASE
|
||||
|
||||
def _resolve_api_key(self, api_key: str | None) -> str:
|
||||
return _require_qwencloud_api_key(api_key)
|
||||
|
||||
def _resolve_rerank_api_base(self, api_base: str | None) -> str:
|
||||
return api_base or get_secret_str("QWENCLOUD_API_BASE_RERANK") or self.DEFAULT_RERANK_API_BASE
|
||||
return api_base or get_secret_str("QWENCLOUD_API_BASE_RERANK") or QWENCLOUD_RERANK_API_BASE
|
||||
|
||||
|
||||
class QwenCloudImageGenerationConfig(DashScopeImageGenerationConfig):
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ from .transformation import DashScopeRerankConfig, DashScopeRerankUsage
|
|||
|
||||
|
||||
class DashScopeNativeRerankConfig(DashScopeRerankConfig):
|
||||
def __init__(self, provider_config: DashScopeRerankConfig) -> None:
|
||||
# Reuse brand-specific credentials and hosts without duplicating alias subclasses.
|
||||
def __init__(self, provider_config: DashScopeRerankConfig, api_base: str | None = None) -> None:
|
||||
self._provider_config: Final = provider_config
|
||||
self._api_base: Final = api_base
|
||||
|
||||
def _resolve_api_key(self, api_key: str | None) -> str:
|
||||
return self._provider_config._resolve_api_key(api_key)
|
||||
|
|
@ -32,12 +32,7 @@ class DashScopeNativeRerankConfig(DashScopeRerankConfig):
|
|||
model: str,
|
||||
optional_params: Mapping[str, object] | None = None,
|
||||
) -> str:
|
||||
# Provider discovery supplies the brand's chat default even when no api_base was passed.
|
||||
# Resolve the rerank-specific environment override before constructing the native path.
|
||||
default_chat_base: Final = self._provider_config.DEFAULT_RERANK_API_BASE.replace(
|
||||
"/compatible-api/v1/reranks", "/compatible-mode/v1"
|
||||
)
|
||||
native_base: Final = self._resolve_rerank_api_base(None if api_base == default_chat_base else api_base)
|
||||
native_base: Final = self._resolve_rerank_api_base(self._api_base or api_base)
|
||||
parsed: Final = urlsplit(native_base.rstrip("/"))
|
||||
if parsed.path.endswith("/services/rerank/text-rerank/text-rerank"):
|
||||
return urlunsplit(parsed)
|
||||
|
|
@ -79,5 +74,4 @@ class DashScopeNativeRerankConfig(DashScopeRerankConfig):
|
|||
output: Final = TypeAdapter(Mapping[str, object]).validate_python(
|
||||
response_json.get("output", MappingProxyType({}))
|
||||
)
|
||||
# Native usage separates prompt_tokens from total_tokens; keep both provider counters.
|
||||
return output.get("results"), response_json.get("request_id"), usage.get("prompt_tokens")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Docs - https://help.aliyun.com/zh/model-studio/text-rerank-api
|
|||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import ClassVar, Final
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
|
@ -61,12 +61,8 @@ class DashScopeRerankConfig(BaseRerankConfig):
|
|||
documents, top_n, return_documents. Response: results[].index,
|
||||
results[].relevance_score, optionally results[].document.text (when
|
||||
return_documents=true), plus a top-level usage.total_tokens counter.
|
||||
|
||||
Brand aliases supply their own API key and base resolvers.
|
||||
"""
|
||||
|
||||
DEFAULT_RERANK_API_BASE: ClassVar[str] = DEFAULT_RERANK_URL
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
|
@ -81,7 +77,7 @@ class DashScopeRerankConfig(BaseRerankConfig):
|
|||
def _resolve_rerank_api_base(self, api_base: str | None) -> str:
|
||||
if api_base is not None:
|
||||
return api_base
|
||||
return get_secret_str("DASHSCOPE_API_BASE_RERANK") or self.DEFAULT_RERANK_API_BASE
|
||||
return get_secret_str("DASHSCOPE_API_BASE_RERANK") or DEFAULT_RERANK_URL
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
|
|
@ -136,7 +132,6 @@ class DashScopeRerankConfig(BaseRerankConfig):
|
|||
max_tokens_per_doc: int | None = None,
|
||||
instruction: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
# rank_fields and max_*_per_doc have no supported mapping and are omitted.
|
||||
params: Final = MappingProxyType(
|
||||
{
|
||||
"query": query,
|
||||
|
|
@ -255,7 +250,6 @@ class DashScopeRerankConfig(BaseRerankConfig):
|
|||
def _get_response_fields(
|
||||
self, response_json: Mapping[str, object], usage: DashScopeRerankUsage
|
||||
) -> tuple[object, object, int | None]:
|
||||
# The compatible API reports its input count only as total_tokens.
|
||||
return response_json.get("results"), response_json.get("id"), usage.get("total_tokens")
|
||||
|
||||
def get_error_class(
|
||||
|
|
|
|||
|
|
@ -14753,6 +14753,14 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"dashscope/qwen3.7-text-rerank": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "rerank",
|
||||
"provider_specific_entry": {
|
||||
"rerank_api": "native"
|
||||
},
|
||||
"source": "https://help.aliyun.com/zh/model-studio/text-rerank-api"
|
||||
},
|
||||
"dashscope/qwen-turbo": {
|
||||
"input_cost_per_token": 5e-08,
|
||||
"litellm_provider": "dashscope",
|
||||
|
|
|
|||
|
|
@ -8439,7 +8439,7 @@ class ProviderConfigManager:
|
|||
get_dashscope_family_rerank_config,
|
||||
)
|
||||
|
||||
return get_dashscope_family_rerank_config(provider.value, model)
|
||||
return get_dashscope_family_rerank_config(provider.value, model, api_base)
|
||||
return litellm.CohereRerankConfig()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -14753,6 +14753,14 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"dashscope/qwen3.7-text-rerank": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "rerank",
|
||||
"provider_specific_entry": {
|
||||
"rerank_api": "native"
|
||||
},
|
||||
"source": "https://help.aliyun.com/zh/model-studio/text-rerank-api"
|
||||
},
|
||||
"dashscope/qwen-turbo": {
|
||||
"input_cost_per_token": 5e-08,
|
||||
"litellm_provider": "dashscope",
|
||||
|
|
|
|||
|
|
@ -414,7 +414,11 @@ def test_qwen37_rerank_explicit_region(provider, host):
|
|||
def test_qwen37_rerank_response_logging():
|
||||
config = get_dashscope_family_rerank_config("dashscope", "qwen3.7-text-rerank")
|
||||
logging = MagicMock()
|
||||
request = {"model": "qwen3.7-text-rerank", "input": {"query": "question", "documents": ["answer"]}, "parameters": {}}
|
||||
request = {
|
||||
"model": "qwen3.7-text-rerank",
|
||||
"input": {"query": "question", "documents": ["answer"]},
|
||||
"parameters": {},
|
||||
}
|
||||
payload = {"request_id": "request-id", "output": {"results": [{"index": 0, "relevance_score": 0.88}]}}
|
||||
|
||||
response = config.transform_rerank_response(
|
||||
|
|
@ -490,3 +494,117 @@ async def test_qwen37_rerank_preserves_provider_error(is_async, respx_mock: resp
|
|||
|
||||
assert error.value.status_code == 400
|
||||
assert "DashscopeException" in str(error.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("is_async", [False, True])
|
||||
@pytest.mark.parametrize("provider", ["dashscope", "qwencloud", "qwen_ai_platform"])
|
||||
@pytest.mark.parametrize("base_case", ["rerank_override", "explicit", "explicit_default", "general_only"])
|
||||
async def test_native_rerank_base_precedence(provider, is_async, base_case, respx_mock, monkeypatch):
|
||||
import litellm
|
||||
|
||||
prefix = provider.upper()
|
||||
default_host = "dashscope-intl.aliyuncs.com" if provider == "qwencloud" else "dashscope.aliyuncs.com"
|
||||
monkeypatch.setenv(f"{prefix}_API_BASE", "https://chat.example/compatible-mode/v1")
|
||||
monkeypatch.delenv(f"{prefix}_API_BASE_RERANK", raising=False)
|
||||
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
|
||||
if base_case != "general_only":
|
||||
monkeypatch.setenv(f"{prefix}_API_BASE_RERANK", "https://rerank.example/api/v1")
|
||||
explicit_base = {
|
||||
"rerank_override": None,
|
||||
"explicit": "https://explicit.example/api/v1",
|
||||
"explicit_default": f"https://{default_host}/compatible-mode/v1",
|
||||
"general_only": None,
|
||||
}[base_case]
|
||||
expected_host = {
|
||||
"rerank_override": "rerank.example",
|
||||
"explicit": "explicit.example",
|
||||
"explicit_default": default_host,
|
||||
"general_only": "chat.example",
|
||||
}[base_case]
|
||||
route = respx_mock.post(f"https://{expected_host}/api/v1/services/rerank/text-rerank/text-rerank")
|
||||
route.respond(
|
||||
200, json={"request_id": "base-precedence", "output": {"results": [{"index": 0, "relevance_score": 0.9}]}}
|
||||
)
|
||||
kwargs = {
|
||||
"model": f"{provider}/qwen3.7-text-rerank",
|
||||
"query": "question",
|
||||
"documents": ["answer"],
|
||||
"api_key": "fake-review-key",
|
||||
"api_base": explicit_base,
|
||||
}
|
||||
|
||||
response = await litellm.arerank(**kwargs) if is_async else litellm.rerank(**kwargs)
|
||||
|
||||
assert json.loads(route.calls[0].request.content)["input"] == {"query": "question", "documents": ["answer"]}
|
||||
assert response.id == "base-precedence"
|
||||
assert response.results == [{"index": 0, "relevance_score": 0.9}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["dashscope", "qwencloud", "qwen_ai_platform"])
|
||||
@pytest.mark.parametrize("rerank_api", ["native", "compatible"])
|
||||
def test_rerank_protocol_uses_runtime_model_metadata(provider, rerank_api, respx_mock, monkeypatch):
|
||||
import litellm
|
||||
|
||||
model = "custom-rerank" if rerank_api == "native" else "qwen3.7-text-rerank"
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
model if provider == "dashscope" and rerank_api == "native" else f"{provider}/{model}",
|
||||
{
|
||||
"litellm_provider": provider,
|
||||
"mode": "rerank",
|
||||
"provider_specific_entry": {"rerank_api": rerank_api},
|
||||
},
|
||||
)
|
||||
results = [{"index": 0, "relevance_score": 0.9}]
|
||||
url = (
|
||||
"https://proxy.example/api/v1/services/rerank/text-rerank/text-rerank"
|
||||
if rerank_api == "native"
|
||||
else "https://proxy.example/api/v1/reranks"
|
||||
)
|
||||
route = respx_mock.post(url)
|
||||
route.respond(
|
||||
200,
|
||||
json={"request_id": "metadata", "output": {"results": results}}
|
||||
if rerank_api == "native"
|
||||
else {"id": "metadata", "results": results},
|
||||
)
|
||||
|
||||
response = litellm.rerank(
|
||||
model=f"{provider}/{model}",
|
||||
query="question",
|
||||
documents=["answer"],
|
||||
api_key="fake-key",
|
||||
api_base="https://proxy.example/api/v1",
|
||||
return_documents=None,
|
||||
)
|
||||
|
||||
assert json.loads(route.calls[0].request.content) == (
|
||||
{"model": model, "input": {"query": "question", "documents": ["answer"]}, "parameters": {}}
|
||||
if rerank_api == "native"
|
||||
else {"model": model, "query": "question", "documents": ["answer"]}
|
||||
)
|
||||
assert response.id == "metadata"
|
||||
assert response.results == results
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["dashscope", "qwencloud", "qwen_ai_platform"])
|
||||
def test_rerank_uses_bundled_metadata_when_remote_map_lacks_model(provider, respx_mock, monkeypatch):
|
||||
import litellm
|
||||
|
||||
for prefix in ("dashscope", "qwencloud", "qwen_ai_platform"):
|
||||
monkeypatch.delitem(litellm.model_cost, f"{prefix}/qwen3.7-text-rerank", raising=False)
|
||||
route = respx_mock.post("https://proxy.example/api/v1/services/rerank/text-rerank/text-rerank")
|
||||
route.respond(200, json={"request_id": "bundled", "output": {"results": [{"index": 0, "relevance_score": 0.9}]}})
|
||||
|
||||
response = litellm.rerank(
|
||||
model=f"{provider}/qwen3.7-text-rerank",
|
||||
query="question",
|
||||
documents=["answer"],
|
||||
api_key="fake-key",
|
||||
api_base="https://proxy.example/api/v1",
|
||||
)
|
||||
|
||||
assert json.loads(route.calls[0].request.content)["input"] == {"query": "question", "documents": ["answer"]}
|
||||
assert response.id == "bundled"
|
||||
assert response.results == [{"index": 0, "relevance_score": 0.9}]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue