mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
* test: migrate selected llm translation contracts * test: migrate anthropic completion translation tests * test: migrate bedrock and cohere translation tests * test: tighten bedrock translation migration * test: migrate azure and openai model translation tests * test: migrate openai and small-provider translation tests * test: migrate gemini and vertex translation tests * test: migrate optional params and rerank translation tests * test: migrate huggingface, json and openrouter translation tests * test: fix openrouter integration expectations * test: migrate databricks, xai, cometapi and fireworks translation tests * test: migrate ovhcloud, prompt and small provider translation tests * test: split reasoning effort grid into unit and e2e * test: migrate router and realtime guardrail translation tests * test: migrate google interactions translation tests * test: migrate a2a, evals, skills and hosted vllm translation tests * test: remove remaining llm translation suite * test: tighten migrated llm translation unit tests and land missing rows Restores rows the first pass dropped or weakened (bedrock agentcore and openwebui thinking stream, anthropic prefix, citations and tool_choice, azure agents handler, huggingface bare api_base, optional params num_retries, alternating roles, rerank response type), replaces truthiness and call-count checks with whole-value assertions, and moves tests to the mirror file of the code they cover * test: move cohere, databricks, fireworks and gemini rows to mirror files over mocked HTTP Restores three TestCohereV2Transform methods the first pass nested out of collection, replaces direct transform calls with respx-backed completion calls, and drops the databricks and fireworks files that sat outside their mirror paths * test: close lost-intent gaps found in review of the llm translation migration Points the hosted-mock guard at the triton test's new integration path, restores exact rerank result assertions for infinity, drops the reasoning e2e fallback that passed for any multi-line answer, and adds the mixtral invoke system prompt variants, eval deletion, and async pdf url download that the legacy suite exercised * test: assert provider acceptance in the reasoning effort e2e The live half of the split grid proves each route accepts reasoning_effort. claude-fable-5-1 on the direct route answers without surfacing reasoning content, so a reasoning-presence check either fails on head or needs a fallback that passes for any answer. Wire shape stays covered by the unit grid * test: cover async and stream-end paths the legacy translation suite exercised Adds async variants for bedrock converse file and thinking content, bedrock embedding timeout and region resolution, interaction stream termination and success logging, xai reasoning token folding, and bytes chunks with a prefix before data:. Also scopes the vertex safety settings global in test_vertex_safety_settings with monkeypatch so it stops leaking into gemini request bodies on the same worker * test: type the new llm translation e2e calls and mark moved fixture dirs as packages * test: name the reasoning effort e2e models as module constants * test: assert reasoning content, register the new gemini coverage ids and give the guardrail config e2e a unique name * test: block the prompt the shared guardrail denies and drop the reasoning presence check * test: drop the duplicate litellm import left by the main sync --------- Co-authored-by: yuneng <yuneng@berri.ai>
586 lines
21 KiB
Python
586 lines
21 KiB
Python
import asyncio
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Final
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
|
|
|
|
import litellm
|
|
from litellm.caching.caching import Cache
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
|
from litellm.types.rerank import RerankResponse
|
|
|
|
MARKER_QUERY = "MARKER_QUERY_do_not_log_at_info"
|
|
MARKER_DOC = "MARKER_DOC_sensitive_customer_text"
|
|
COHERE_RERANK_RESPONSE: Final = {
|
|
"id": "rerank-1",
|
|
"results": [
|
|
{"index": 0, "relevance_score": 0.95, "document": {"text": "hello"}},
|
|
{"index": 1, "relevance_score": 0.1, "document": {"text": "world"}},
|
|
],
|
|
"meta": {"api_version": {"version": "2"}, "billed_units": {"search_units": 1}},
|
|
}
|
|
|
|
|
|
class RespxAsyncTransport(httpx.AsyncBaseTransport):
|
|
def __init__(self, router: respx.MockRouter) -> None:
|
|
self.router = router
|
|
|
|
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
return await self.router.async_handler(request)
|
|
|
|
|
|
async def _cohere_rerank(sync_mode: bool, router: respx.MockRouter, top_n: int | None) -> RerankResponse:
|
|
if sync_mode:
|
|
return litellm.rerank(
|
|
model="cohere/rerank-v4.0-pro",
|
|
query="hello",
|
|
documents=["hello", "world"],
|
|
top_n=top_n,
|
|
api_key="test-api-key",
|
|
)
|
|
client: Final = AsyncHTTPHandler(transport=RespxAsyncTransport(router))
|
|
response: Final = await litellm.arerank(
|
|
model="cohere/rerank-v4.0-pro",
|
|
query="hello",
|
|
documents=["hello", "world"],
|
|
top_n=top_n,
|
|
api_key="test-api-key",
|
|
client=client,
|
|
)
|
|
await client.client.aclose()
|
|
return response
|
|
|
|
|
|
def _mock_cohere_response() -> MagicMock:
|
|
mock_response = MagicMock()
|
|
|
|
def return_val():
|
|
return {
|
|
"id": "cmpl-mockid",
|
|
"results": [{"index": 0, "relevance_score": 0.95}],
|
|
"meta": {
|
|
"api_version": {"version": "1.0"},
|
|
"billed_units": {"search_units": 1},
|
|
},
|
|
}
|
|
|
|
mock_response.json = return_val
|
|
mock_response.headers = {"key": "value"}
|
|
mock_response.status_code = 200
|
|
return mock_response
|
|
|
|
|
|
def test_rerank_does_not_log_request_content_at_info(caplog):
|
|
"""Regression for #32525: rerank must not emit query/documents to logs at INFO.
|
|
|
|
The mapped ``optional_rerank_params`` (which always contains ``query`` and
|
|
``documents``) bypasses ``turn_off_message_logging`` / ``redact_messages``,
|
|
so logging it at INFO leaks raw request content into stdout and any log sink.
|
|
"""
|
|
litellm.cohere_key = "test_api_key"
|
|
caplog.set_level(logging.DEBUG, logger="LiteLLM")
|
|
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post",
|
|
return_value=_mock_cohere_response(),
|
|
):
|
|
litellm.rerank(
|
|
model="cohere/rerank-english-v3.0",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC, "unrelated"],
|
|
top_n=2,
|
|
)
|
|
|
|
litellm_records = [r for r in caplog.records if r.name == "LiteLLM"]
|
|
|
|
info_or_above = [
|
|
r.getMessage()
|
|
for r in litellm_records
|
|
if r.levelno >= logging.INFO and (MARKER_QUERY in r.getMessage() or MARKER_DOC in r.getMessage())
|
|
]
|
|
assert not info_or_above, f"rerank leaked request content at INFO+: {info_or_above}"
|
|
|
|
optional_params_logs = [r for r in litellm_records if "optional_rerank_params" in r.getMessage()]
|
|
assert optional_params_logs, "expected the optional_rerank_params line to be logged"
|
|
assert all(r.levelno == logging.DEBUG for r in optional_params_logs), (
|
|
"optional_rerank_params must be logged at DEBUG, not INFO"
|
|
)
|
|
|
|
|
|
TOGETHER_RERANK_BODY = {
|
|
"id": "rerank-mock-id",
|
|
"results": [{"index": 0, "relevance_score": 0.95}],
|
|
"usage": {"prompt_tokens": 10, "total_tokens": 10},
|
|
}
|
|
|
|
|
|
def test_together_rerank_defaults_to_together_ai_host(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression for the Together host migration: rerank used to hardcode
|
|
https://api.together.xyz/v1/rerank. The default must now be api.together.ai."""
|
|
monkeypatch.delenv("TOGETHER_AI_API_BASE", raising=False)
|
|
|
|
mock_route = respx_mock.post("https://api.together.ai/v1/rerank")
|
|
mock_route.return_value = httpx.Response(200, json=TOGETHER_RERANK_BODY)
|
|
|
|
response = litellm.rerank(
|
|
model="together_ai/mixedbread-ai/mxbai-rerank-large-v2",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-together-key",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert response.results[0]["relevance_score"] == 0.95
|
|
|
|
|
|
def test_together_rerank_honors_api_base(respx_mock: respx.MockRouter):
|
|
"""Regression: a custom api_base was silently ignored by the Together rerank handler."""
|
|
mock_route = respx_mock.post("https://custom-together.example/v1/rerank")
|
|
mock_route.return_value = httpx.Response(200, json=TOGETHER_RERANK_BODY)
|
|
|
|
litellm.rerank(
|
|
model="together_ai/mixedbread-ai/mxbai-rerank-large-v2",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-together-key",
|
|
api_base="https://custom-together.example/v1",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert mock_route.calls[0].request.headers["authorization"] == "Bearer fake-together-key"
|
|
|
|
|
|
DASHSCOPE_RERANK_BODY = {
|
|
"object": "list",
|
|
"results": [{"index": 0, "relevance_score": 0.95}],
|
|
"model": "qwen3-rerank",
|
|
"id": "rerank-mock-id",
|
|
"usage": {"total_tokens": 10},
|
|
}
|
|
|
|
|
|
def test_dashscope_rerank_defaults_to_live_rerank_route(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression for the dead default endpoint: get_llm_provider always returns the
|
|
chat base for dashscope, which used to hijack rerank onto the dead
|
|
/compatible-mode/v1/reranks route."""
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
|
|
|
|
mock_route = respx_mock.post("https://dashscope.aliyuncs.com/compatible-api/v1/reranks")
|
|
mock_route.return_value = httpx.Response(200, json=DASHSCOPE_RERANK_BODY)
|
|
|
|
response = litellm.rerank(
|
|
model="dashscope/qwen3-rerank",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-dashscope-key",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert response.results[0]["relevance_score"] == 0.95
|
|
|
|
|
|
def test_dashscope_rerank_chat_env_base_keeps_host_and_rerank_route(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression: a chat-style DASHSCOPE_API_BASE must not hijack rerank onto the
|
|
chat path, while its host (the region) is preserved."""
|
|
monkeypatch.setenv("DASHSCOPE_API_BASE", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1")
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
|
|
|
|
mock_route = respx_mock.post("https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks")
|
|
mock_route.return_value = httpx.Response(200, json=DASHSCOPE_RERANK_BODY)
|
|
|
|
litellm.rerank(
|
|
model="dashscope/qwen3-rerank",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-dashscope-key",
|
|
)
|
|
|
|
assert mock_route.called
|
|
|
|
|
|
def test_dashscope_rerank_explicit_api_base_wins(respx_mock: respx.MockRouter, monkeypatch):
|
|
monkeypatch.setenv("DASHSCOPE_API_BASE", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1")
|
|
|
|
mock_route = respx_mock.post("https://custom-rerank.example/v1/reranks")
|
|
mock_route.return_value = httpx.Response(200, json=DASHSCOPE_RERANK_BODY)
|
|
|
|
litellm.rerank(
|
|
model="dashscope/qwen3-rerank",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-dashscope-key",
|
|
api_base="https://custom-rerank.example/v1",
|
|
)
|
|
|
|
assert mock_route.called
|
|
|
|
|
|
DASHSCOPE_404_BODY = {
|
|
"error": {
|
|
"message": "The model `does-not-exist` does not exist or you do not have access to it.",
|
|
"type": "invalid_request_error",
|
|
"param": None,
|
|
"code": "model_not_found",
|
|
},
|
|
"request_id": "mock-request-id",
|
|
}
|
|
|
|
|
|
def test_rerank_error_names_provider_and_keeps_body(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression for the rerank error path mapping with the unresolved provider param:
|
|
a provider 404 surfaced as 'None - ' instead of naming the provider and its error body."""
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
|
|
|
|
mock_route = respx_mock.post("https://dashscope.example/v1/reranks")
|
|
mock_route.return_value = httpx.Response(404, json=DASHSCOPE_404_BODY)
|
|
|
|
with pytest.raises(litellm.NotFoundError) as exc_info:
|
|
litellm.rerank(
|
|
model="dashscope/does-not-exist",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-dashscope-key",
|
|
api_base="https://dashscope.example/v1",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert "DashscopeException" in str(exc_info.value)
|
|
assert "does not exist or you do not have access to it" in str(exc_info.value)
|
|
assert "None - " not in str(exc_info.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_arerank_error_is_mapped_to_litellm_exception(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression for arerank's bare re-raise: provider errors escaped as raw
|
|
provider exception classes instead of the mapped litellm exception contract."""
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
|
|
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
|
|
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
|
|
|
|
mock_route = respx_mock.post("https://dashscope.example/v1/reranks")
|
|
mock_route.return_value = httpx.Response(404, json=DASHSCOPE_404_BODY)
|
|
|
|
with pytest.raises(litellm.NotFoundError) as exc_info:
|
|
await litellm.arerank(
|
|
model="dashscope/does-not-exist",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-dashscope-key",
|
|
api_base="https://dashscope.example/v1",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert "DashscopeException" in str(exc_info.value)
|
|
assert "does not exist or you do not have access to it" in str(exc_info.value)
|
|
assert "None - " not in str(exc_info.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_arerank_declared_authenticating_provider_skips_resolution(monkeypatch):
|
|
"""Regression for the event-loop hazard in arerank's provider pre-resolution:
|
|
get_llm_provider runs the blocking OAuth device flow for github_copilot/chatgpt,
|
|
so arerank must adopt the declared provider instead of resolving it, while the
|
|
except path still maps with that declared provider."""
|
|
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|
|
|
resolution_calls = []
|
|
|
|
def record_resolution(*args, **kwargs):
|
|
resolution_calls.append((args, kwargs))
|
|
return "gpt-4o", "github_copilot", None, None
|
|
|
|
def rerank_raises_provider_error(*args, **kwargs):
|
|
raise BaseLLMException(status_code=401, message='{"error":"bad key"}')
|
|
|
|
monkeypatch.setattr(litellm, "get_llm_provider", record_resolution)
|
|
monkeypatch.setattr(
|
|
"litellm.litellm_core_utils.llm_response_utils.get_api_base.get_llm_provider", record_resolution
|
|
)
|
|
monkeypatch.setattr("litellm.rerank_api.main.rerank", rerank_raises_provider_error)
|
|
|
|
with pytest.raises(litellm.AuthenticationError) as exc_info:
|
|
await litellm.arerank(
|
|
model="github_copilot/gpt-4o",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
)
|
|
|
|
assert resolution_calls == []
|
|
assert "Github_copilotException" in str(exc_info.value)
|
|
assert "None - " not in str(exc_info.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_together_rerank_async_honors_env_api_base(respx_mock: respx.MockRouter, monkeypatch):
|
|
"""Regression: TOGETHER_AI_API_BASE was honored by chat but ignored by rerank."""
|
|
monkeypatch.setenv("TOGETHER_AI_API_BASE", "https://env-together.example/v1")
|
|
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
|
|
|
|
mock_route = respx_mock.post("https://env-together.example/v1/rerank")
|
|
mock_route.return_value = httpx.Response(200, json=TOGETHER_RERANK_BODY)
|
|
|
|
response = await litellm.arerank(
|
|
model="together_ai/mixedbread-ai/mxbai-rerank-large-v2",
|
|
query=MARKER_QUERY,
|
|
documents=[MARKER_DOC],
|
|
api_key="fake-together-key",
|
|
)
|
|
|
|
assert mock_route.called
|
|
assert response.results[0]["relevance_score"] == 0.95
|
|
|
|
|
|
def test_cohere_rerank_v2_client():
|
|
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
|
|
|
client = HTTPHandler()
|
|
litellm.api_base = "http://localhost:4000"
|
|
litellm.set_verbose = True
|
|
|
|
text = "Hello there!"
|
|
list_texts = ["Hello there!", "How are you?", "How do you do?"]
|
|
|
|
rerank_model = "rerank-multilingual-v3.0"
|
|
|
|
with patch.object(client, "post") as mock_post:
|
|
mock_response = MagicMock()
|
|
mock_response.text = json.dumps(
|
|
{
|
|
"id": "cmpl-mockid",
|
|
"results": [
|
|
{"index": 0, "relevance_score": 0.95},
|
|
{"index": 1, "relevance_score": 0.75},
|
|
{"index": 2, "relevance_score": 0.65},
|
|
],
|
|
"usage": {"prompt_tokens": 100, "total_tokens": 150},
|
|
}
|
|
)
|
|
mock_response.status_code = 200
|
|
mock_response.headers = {"Content-Type": "application/json"}
|
|
mock_response.json = lambda: json.loads(mock_response.text)
|
|
|
|
mock_post.return_value = mock_response
|
|
|
|
response = litellm.rerank(
|
|
model=rerank_model,
|
|
query=text,
|
|
documents=list_texts,
|
|
custom_llm_provider="cohere",
|
|
max_tokens_per_doc=3,
|
|
top_n=2,
|
|
api_key="fake-api-key",
|
|
client=client,
|
|
)
|
|
|
|
# Ensure Cohere API is called with the expected params
|
|
mock_post.assert_called_once()
|
|
assert mock_post.call_args.kwargs["url"] == "http://localhost:4000/v2/rerank"
|
|
|
|
request_data = json.loads(mock_post.call_args.kwargs["data"])
|
|
assert request_data["model"] == rerank_model
|
|
assert request_data["query"] == text
|
|
assert request_data["documents"] == list_texts
|
|
assert request_data["max_tokens_per_doc"] == 3
|
|
assert request_data["top_n"] == 2
|
|
|
|
# Ensure litellm response is what we expect
|
|
assert response["results"] == mock_response.json()["results"]
|
|
|
|
|
|
@pytest.mark.usefixtures("fake_provider_credentials")
|
|
def test_rerank_infer_region_from_model_arn(monkeypatch):
|
|
|
|
mock_response = MagicMock()
|
|
|
|
monkeypatch.setenv("AWS_REGION_NAME", "us-east-1")
|
|
args = {
|
|
"model": "bedrock/arn:aws:bedrock:us-west-2::foundation-model/amazon.rerank-v1:0",
|
|
"query": "hello",
|
|
"documents": ["hello", "world"],
|
|
}
|
|
|
|
def return_val():
|
|
return {
|
|
"results": [
|
|
{"index": 0, "relevanceScore": 0.6716859340667725},
|
|
{"index": 1, "relevanceScore": 0.0004994205664843321},
|
|
]
|
|
}
|
|
|
|
mock_response.json = return_val
|
|
mock_response.headers = {"key": "value"}
|
|
mock_response.status_code = 200
|
|
|
|
client = HTTPHandler()
|
|
|
|
with patch.object(client, "post", return_value=mock_response) as mock_post:
|
|
litellm.rerank(
|
|
model=args["model"],
|
|
query=args["query"],
|
|
documents=args["documents"],
|
|
client=client,
|
|
)
|
|
|
|
mock_post.assert_called_once()
|
|
print(f"mock_post.call_args: {mock_post.call_args.kwargs}")
|
|
assert "us-west-2" in mock_post.call_args.kwargs["url"]
|
|
assert "us-east-1" not in mock_post.call_args.kwargs["url"]
|
|
|
|
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("sync_mode", [True, False])
|
|
async def test_cohere_rerank_parses_results_from_provider_response(
|
|
sync_mode: bool, respx_mock: respx.MockRouter
|
|
) -> None:
|
|
route: Final = respx_mock.post("https://api.cohere.com/v2/rerank")
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
response: Final = await _cohere_rerank(sync_mode, respx_mock, top_n=2)
|
|
|
|
assert response.model_dump() == COHERE_RERANK_RESPONSE
|
|
|
|
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("second_top_n", "provider_calls", "second_is_cache_hit"),
|
|
[(2, 1, True), (None, 2, False)],
|
|
)
|
|
async def test_cohere_rerank_cache_hit_requires_identical_top_n(
|
|
second_top_n: int | None,
|
|
provider_calls: int,
|
|
second_is_cache_hit: bool,
|
|
respx_mock: respx.MockRouter,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(litellm, "cache", Cache(type="local"))
|
|
route: Final = respx_mock.post("https://api.cohere.com/v2/rerank")
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
first_response: Final = await _cohere_rerank(True, respx_mock, top_n=2)
|
|
second_response: Final = await _cohere_rerank(True, respx_mock, top_n=second_top_n)
|
|
|
|
assert len(route.calls) == provider_calls
|
|
assert ("cache_key" in second_response._hidden_params) is second_is_cache_hit
|
|
assert second_response.results == first_response.results
|
|
|
|
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
def test_cohere_rerank_completes_global_root_api_base_to_v2_route(
|
|
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(litellm, "api_base", "http://localhost:4000")
|
|
route: Final = respx_mock.post("http://localhost:4000/v2/rerank")
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
litellm.rerank(
|
|
model="cohere/rerank-v4.0-pro",
|
|
query="hello",
|
|
documents=["hello", "world"],
|
|
custom_llm_provider="cohere",
|
|
api_key="test-api-key",
|
|
)
|
|
|
|
assert route.calls[0].request.url == "http://localhost:4000/v2/rerank"
|
|
|
|
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
def test_cohere_rerank_sends_return_documents_and_preserves_text(
|
|
respx_mock: respx.MockRouter,
|
|
) -> None:
|
|
route: Final = respx_mock.post("https://api.cohere.com/v2/rerank")
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
response: Final = litellm.rerank(
|
|
model="cohere/rerank-v4.0-pro",
|
|
query="hello",
|
|
documents=["hello", "world"],
|
|
return_documents=True,
|
|
top_n=2,
|
|
api_key="test-api-key",
|
|
)
|
|
|
|
request_body: Final = json.loads(route.calls[0].request.content)
|
|
assert request_body == {
|
|
"model": "rerank-v4.0-pro",
|
|
"query": "hello",
|
|
"documents": ["hello", "world"],
|
|
"top_n": 2,
|
|
"return_documents": True,
|
|
}
|
|
assert [result["document"]["text"] for result in response.results] == ["hello", "world"]
|
|
|
|
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
@pytest.mark.parametrize(
|
|
("api_base", "endpoint"),
|
|
[
|
|
("https://cohere.example/v1/rerank", "https://cohere.example/v1/rerank"),
|
|
("https://cohere.example", "https://cohere.example/v2/rerank"),
|
|
],
|
|
)
|
|
def test_cohere_rerank_custom_api_base_selects_versioned_route(
|
|
api_base: str, endpoint: str, respx_mock: respx.MockRouter
|
|
) -> None:
|
|
route: Final = respx_mock.post(endpoint)
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
litellm.rerank(
|
|
model="cohere/rerank-v4.0-pro",
|
|
query="hello",
|
|
documents=["hello", "world"],
|
|
top_n=2,
|
|
api_base=api_base,
|
|
api_key="test-api-key",
|
|
)
|
|
|
|
assert json.loads(route.calls[0].request.content) == {
|
|
"model": "rerank-v4.0-pro",
|
|
"query": "hello",
|
|
"documents": ["hello", "world"],
|
|
"top_n": 2,
|
|
"return_documents": True,
|
|
}
|
|
|
|
|
|
class _SuccessCostRecorder(CustomLogger):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.logged = asyncio.Event()
|
|
self.response_cost: object = None
|
|
|
|
async def async_log_success_event(
|
|
self, kwargs: dict[str, object], response_obj: object, start_time: datetime, end_time: datetime
|
|
) -> None:
|
|
self.response_cost = kwargs["response_cost"]
|
|
self.logged.set()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.respx(assert_all_called=True)
|
|
async def test_cohere_rerank_success_callback_receives_search_unit_cost(
|
|
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
recorder: Final = _SuccessCostRecorder()
|
|
monkeypatch.setattr(litellm, "callbacks", [recorder])
|
|
route: Final = respx_mock.post("https://api.cohere.com/v2/rerank")
|
|
route.return_value = httpx.Response(200, json=COHERE_RERANK_RESPONSE)
|
|
|
|
response: Final = await _cohere_rerank(False, respx_mock, top_n=2)
|
|
await asyncio.wait_for(recorder.logged.wait(), timeout=10)
|
|
|
|
search_units: Final = COHERE_RERANK_RESPONSE["meta"]["billed_units"]["search_units"]
|
|
expected_cost: Final = litellm.model_cost["rerank-v4.0-pro"]["input_cost_per_query"] * search_units
|
|
assert expected_cost > 0
|
|
assert recorder.response_cost == expected_cost
|
|
assert response._hidden_params["response_cost"] == expected_cost
|