From fcd9a894d969b68278228d9b79285d46536eed31 Mon Sep 17 00:00:00 2001 From: fzowl Date: Fri, 18 Sep 2026 19:21:43 +0200 Subject: [PATCH] fix(voyage): route rerank on the key it authenticates with get_complete_url picked the rerank host from the environment while the auth header carried the request key, so an explicit MongoDB-issued key was posted to api.voyageai.com. validate_environment now hands the resolved key to the config instance and get_complete_url reads it back, so the host and the credential always come from one key. The config is built per request, so nothing carries over between them. --- litellm/llms/voyage/rerank/transformation.py | 13 ++++- .../llms/voyage/test_common_utils.py | 58 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/litellm/llms/voyage/rerank/transformation.py b/litellm/llms/voyage/rerank/transformation.py index 7a9ec94feb7..c302509ea35 100644 --- a/litellm/llms/voyage/rerank/transformation.py +++ b/litellm/llms/voyage/rerank/transformation.py @@ -25,6 +25,16 @@ from ..embedding.transformation import VoyageError class VoyageRerankConfig(BaseRerankConfig): + """ + ``validate_environment`` stores the credential it authenticates with so ``get_complete_url`` + can select the host that issued it. ``ProviderConfigManager.get_provider_rerank_config`` + builds this config per request, so that key never reaches another one. + """ + + def __init__(self) -> None: + super().__init__() + self._api_key: str | None = None + def get_supported_cohere_rerank_params(self, model: str) -> list: return ["query", "documents", "top_n", "return_documents"] @@ -60,7 +70,7 @@ class VoyageRerankConfig(BaseRerankConfig): optional_params: dict | None = None, ) -> str: if api_base is None: - return f"{get_default_base_url()}/rerank" + return f"{get_default_base_url(self._api_key)}/rerank" api_base = api_base.rstrip("/") if not api_base.endswith("/v1/rerank"): if api_base.endswith("/v1"): @@ -144,6 +154,7 @@ class VoyageRerankConfig(BaseRerankConfig): resolved_api_key: Final = get_voyage_api_key(api_key) if resolved_api_key is None: raise ValueError("Voyage AI API key is required. Set via `api_key` parameter or `VOYAGE_API_KEY` env var.") + self._api_key = resolved_api_key return { "Authorization": f"Bearer {resolved_api_key}", "content-type": "application/json", diff --git a/tests/test_litellm/llms/voyage/test_common_utils.py b/tests/test_litellm/llms/voyage/test_common_utils.py index c475bedeaba..511391093f3 100644 --- a/tests/test_litellm/llms/voyage/test_common_utils.py +++ b/tests/test_litellm/llms/voyage/test_common_utils.py @@ -72,10 +72,61 @@ def test_explicit_api_base_overrides_key_routing(config, endpoint): assert url == f"https://gateway.internal/v1/{endpoint}" +@pytest.mark.parametrize("api_key, expected_host", [("al-key", MONGODB_API_BASE), ("pa-key", VOYAGE_API_BASE)]) +def test_rerank_routes_by_request_key_prefix(api_key, expected_host): + config = VoyageRerankConfig() + config.validate_environment({}, "rerank-2.5", api_key=api_key) + + assert config.get_complete_url(None, "rerank-2.5") == f"{expected_host}/rerank" + + @pytest.mark.parametrize("api_key, expected_host", [("al-key", MONGODB_API_BASE), ("pa-key", VOYAGE_API_BASE)]) def test_rerank_routes_by_env_key_prefix(monkeypatch, api_key, expected_host): monkeypatch.setenv("VOYAGE_API_KEY", api_key) - assert VoyageRerankConfig().get_complete_url(None, "rerank-2.5") == f"{expected_host}/rerank" + config = VoyageRerankConfig() + config.validate_environment({}, "rerank-2.5") + + assert config.get_complete_url(None, "rerank-2.5") == f"{expected_host}/rerank" + + +def test_rerank_request_key_beats_env_key_for_routing(monkeypatch): + """A MongoDB key on the request must not be posted to the Voyage host the env key names""" + monkeypatch.setenv("VOYAGE_API_KEY", "pa-from-env") + config = VoyageRerankConfig() + + headers = config.validate_environment({}, "rerank-2.5", api_key="al-on-request") + + assert headers["Authorization"] == "Bearer al-on-request" + assert config.get_complete_url(None, "rerank-2.5") == f"{MONGODB_API_BASE}/rerank" + + +def test_rerank_config_is_built_per_request_so_keys_cannot_leak(monkeypatch): + """get_complete_url reads a key off the instance, so each request must get its own instance""" + import litellm + from litellm.types.utils import LlmProviders + from litellm.utils import ProviderConfigManager + + monkeypatch.delenv("VOYAGE_API_KEY", raising=False) + first = ProviderConfigManager.get_provider_rerank_config( + model="rerank-2.5", provider=LlmProviders.VOYAGE, api_base=None, present_version_params=[] + ) + second = ProviderConfigManager.get_provider_rerank_config( + model="rerank-2.5", provider=LlmProviders.VOYAGE, api_base=None, present_version_params=[] + ) + assert isinstance(first, litellm.VoyageRerankConfig) and first is not second + + first.validate_environment({}, "rerank-2.5", api_key="al-first-request") + second.validate_environment({}, "rerank-2.5", api_key="pa-second-request") + + assert first.get_complete_url(None, "rerank-2.5") == f"{MONGODB_API_BASE}/rerank" + assert second.get_complete_url(None, "rerank-2.5") == f"{VOYAGE_API_BASE}/rerank" + + +def test_rerank_falls_back_to_env_when_validate_environment_did_not_run(monkeypatch): + """A caller that skips validate_environment keeps the pre-existing env-only behaviour""" + monkeypatch.setenv("VOYAGE_API_KEY", "al-from-env") + + assert VoyageRerankConfig().get_complete_url(None, "rerank-2.5") == f"{MONGODB_API_BASE}/rerank" @pytest.mark.parametrize( @@ -95,11 +146,12 @@ def test_auth_header_uses_the_key_the_url_was_routed_on(monkeypatch, config): def test_rerank_auth_header_uses_the_key_the_url_was_routed_on(monkeypatch): monkeypatch.setenv("VOYAGE_AI_TOKEN", "al-from-env") + config = VoyageRerankConfig() - headers = VoyageRerankConfig().validate_environment({}, "rerank-2.5") + headers = config.validate_environment({}, "rerank-2.5") assert headers["Authorization"] == "Bearer al-from-env" - assert VoyageRerankConfig().get_complete_url(None, "rerank-2.5").startswith(MONGODB_API_BASE) + assert config.get_complete_url(None, "rerank-2.5").startswith(MONGODB_API_BASE) def test_get_voyage_api_key_prefers_env_vars_in_documented_order(monkeypatch):