mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
feat(voyage): route MongoDB-issued keys to ai.mongodb.com
Mirror voyageai.util.get_default_base_url from the official SDK: a key with the `al-` prefix is issued by MongoDB and is only valid on ai.mongodb.com, every other key on api.voyageai.com. The choice now lives in one shared helper that the embedding, contextual, multimodal, and rerank configs all use for both the default base URL and the Authorization header, so the host and the key always agree. Also drops the voyage-4-nano and voyage-multilingual-2 model map entries: voyage-4-nano is not served on the Voyage API and voyage-multilingual-2 is an older model, so neither belongs in this change.
This commit is contained in:
parent
236d769827
commit
9c89ab96aa
11 changed files with 196 additions and 138 deletions
35
litellm/llms/voyage/common_utils.py
Normal file
35
litellm/llms/voyage/common_utils.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
Shared helpers for the Voyage (VoyageAI by MongoDB) provider.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
VOYAGE_API_BASE: Final = "https://api.voyageai.com/v1"
|
||||
MONGODB_API_BASE: Final = "https://ai.mongodb.com/v1"
|
||||
MONGODB_API_KEY_PREFIX: Final = "al-"
|
||||
|
||||
|
||||
def get_voyage_api_key(api_key: str | None = None) -> str | None:
|
||||
"""Resolve the key a Voyage request will authenticate with, explicit value first."""
|
||||
return (
|
||||
api_key
|
||||
or get_secret_str("VOYAGE_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_TOKEN")
|
||||
)
|
||||
|
||||
|
||||
def get_default_base_url(api_key: str | None = None) -> str:
|
||||
"""
|
||||
Pick the host that issued the key: MongoDB-issued keys (``al-`` prefix) are only
|
||||
valid on ai.mongodb.com, every other key on api.voyageai.com.
|
||||
|
||||
Mirrors ``voyageai.util.get_default_base_url`` in the official SDK:
|
||||
https://github.com/voyage-ai/voyageai-python/blob/main/voyageai/util.py
|
||||
"""
|
||||
resolved: Final = get_voyage_api_key(api_key)
|
||||
if resolved is not None and resolved.startswith(MONGODB_API_KEY_PREFIX):
|
||||
return MONGODB_API_BASE
|
||||
return VOYAGE_API_BASE
|
||||
|
|
@ -5,7 +5,7 @@ import httpx
|
|||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.llms.voyage.common_utils import get_default_base_url, get_voyage_api_key
|
||||
from litellm.types.llms.openai import AllEmbeddingInputValues, AllMessageValues
|
||||
from litellm.types.utils import EmbeddingResponse, Usage
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class VoyageEmbeddingConfig(BaseEmbeddingConfig):
|
|||
if not api_base.endswith("/embeddings"):
|
||||
api_base = f"{api_base}/embeddings"
|
||||
return api_base
|
||||
return "https://api.voyageai.com/v1/embeddings"
|
||||
return f"{get_default_base_url(api_key)}/embeddings"
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
return [
|
||||
|
|
@ -85,14 +85,8 @@ class VoyageEmbeddingConfig(BaseEmbeddingConfig):
|
|||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = (
|
||||
get_secret_str("VOYAGE_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_TOKEN")
|
||||
)
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {get_voyage_api_key(api_key)}",
|
||||
}
|
||||
|
||||
def transform_embedding_request(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ This module is used to transform the request and response for the Voyage context
|
|||
This would be used for all the contextualized embeddings models in Voyage.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -10,10 +12,15 @@ import httpx
|
|||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.llms.voyage.common_utils import get_default_base_url, get_voyage_api_key
|
||||
from litellm.types.llms.openai import AllEmbeddingInputValues, AllMessageValues
|
||||
from litellm.types.utils import EmbeddingResponse, Usage
|
||||
|
||||
NO_CONTEXTUAL_DEFAULTS: Final[Mapping[str, str | bool]] = MappingProxyType({})
|
||||
AUTO_CHUNK_DEFAULTS: Final[Mapping[str, str | bool]] = MappingProxyType(
|
||||
{"input_type": "document", "enable_auto_chunking": True}
|
||||
)
|
||||
|
||||
|
||||
class VoyageError(BaseLLMException):
|
||||
def __init__(
|
||||
|
|
@ -54,7 +61,7 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
if not api_base.endswith("/contextualizedembeddings"):
|
||||
api_base = f"{api_base}/contextualizedembeddings"
|
||||
return api_base
|
||||
return "https://api.voyageai.com/v1/contextualizedembeddings"
|
||||
return f"{get_default_base_url(api_key)}/contextualizedembeddings"
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
return ["encoding_format", "dimensions"]
|
||||
|
|
@ -87,14 +94,8 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = (
|
||||
get_secret_str("VOYAGE_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_TOKEN")
|
||||
)
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {get_voyage_api_key(api_key)}",
|
||||
}
|
||||
|
||||
def transform_embedding_request(
|
||||
|
|
@ -104,41 +105,23 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
optional_params: dict,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
inputs, contextual_params = self._prepare_contextual_inputs(input, optional_params)
|
||||
return {
|
||||
"inputs": inputs,
|
||||
"model": model,
|
||||
**optional_params,
|
||||
**contextual_params,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _prepare_contextual_inputs(
|
||||
input: AllEmbeddingInputValues | list[list[str]],
|
||||
optional_params: dict,
|
||||
) -> tuple[AllEmbeddingInputValues | list[list[str]], dict[str, str | bool]]:
|
||||
"""
|
||||
Shape ``inputs`` and the auto-chunking params to match Voyage's
|
||||
contextualized embeddings contract.
|
||||
|
||||
- ``list[list[str]]`` (pre-chunked documents) is always valid and passes through.
|
||||
- A flat ``list[str]`` or bare ``str`` is only valid as documents when
|
||||
``enable_auto_chunking=True`` with ``input_type="document"``, or as
|
||||
queries with ``input_type="query"``. So a non-query flat input is sent
|
||||
with those two params defaulted (caller-set values win).
|
||||
Shape ``inputs`` and the auto-chunking params to match Voyage's contextualized
|
||||
embeddings contract: ``list[list[str]]`` (pre-chunked documents) is always
|
||||
valid, while a flat ``list[str]`` or bare ``str`` is valid only as queries
|
||||
(``input_type="query"``) or as documents with ``enable_auto_chunking=True``
|
||||
and ``input_type="document"``. Caller-set params win.
|
||||
|
||||
Reference: https://docs.voyageai.com/docs/contextualized-chunk-embeddings
|
||||
"""
|
||||
if isinstance(input, list) and len(input) > 0 and isinstance(input[0], list):
|
||||
return input, {}
|
||||
flat: Final = [input] if isinstance(input, str) else input
|
||||
if optional_params.get("input_type") == "query":
|
||||
return flat, {}
|
||||
contextual_params: Final = {
|
||||
**({"input_type": "document"} if "input_type" not in optional_params else {}),
|
||||
**({"enable_auto_chunking": True} if "enable_auto_chunking" not in optional_params else {}),
|
||||
is_prechunked: Final = isinstance(input, list) and len(input) > 0 and isinstance(input[0], list)
|
||||
is_query: Final = optional_params.get("input_type") == "query"
|
||||
return {
|
||||
"inputs": (input,) if isinstance(input, str) else input,
|
||||
"model": model,
|
||||
**(NO_CONTEXTUAL_DEFAULTS if is_prechunked or is_query else AUTO_CHUNK_DEFAULTS),
|
||||
**optional_params,
|
||||
}
|
||||
return flat, contextual_params
|
||||
|
||||
def transform_embedding_response(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import httpx
|
|||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.llms.voyage.common_utils import get_default_base_url, get_voyage_api_key
|
||||
from litellm.types.llms.openai import AllEmbeddingInputValues, AllMessageValues
|
||||
from litellm.types.utils import EmbeddingResponse, Usage
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ class VoyageMultimodalEmbeddingConfig(BaseEmbeddingConfig):
|
|||
if not api_base.endswith("/multimodalembeddings"):
|
||||
api_base = f"{api_base}/multimodalembeddings"
|
||||
return api_base
|
||||
return "https://api.voyageai.com/v1/multimodalembeddings"
|
||||
return f"{get_default_base_url(api_key)}/multimodalembeddings"
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
return ["dimensions"]
|
||||
|
|
@ -84,19 +84,14 @@ class VoyageMultimodalEmbeddingConfig(BaseEmbeddingConfig):
|
|||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = (
|
||||
get_secret_str("VOYAGE_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
or get_secret_str("VOYAGE_AI_TOKEN")
|
||||
)
|
||||
if not api_key:
|
||||
resolved_api_key: Final = get_voyage_api_key(api_key)
|
||||
if not resolved_api_key:
|
||||
raise ValueError(
|
||||
"Voyage API key is required for multimodal embeddings. "
|
||||
"Set VOYAGE_API_KEY / VOYAGE_AI_API_KEY / VOYAGE_AI_TOKEN "
|
||||
"or pass `api_key` explicitly."
|
||||
)
|
||||
return {"Authorization": f"Bearer {api_key}"}
|
||||
return {"Authorization": f"Bearer {resolved_api_key}"}
|
||||
|
||||
def _normalize_content_item(self, item: dict[str, object]) -> dict[str, object]:
|
||||
item_type: Final = item.get("type")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import httpx
|
|||
|
||||
from litellm.llms.base_llm.chat.transformation import LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.llms.voyage.common_utils import get_default_base_url, get_voyage_api_key
|
||||
from litellm.types.rerank import (
|
||||
RerankBilledUnits,
|
||||
RerankResponse,
|
||||
|
|
@ -59,7 +59,7 @@ class VoyageRerankConfig(BaseRerankConfig):
|
|||
optional_params: dict | None = None,
|
||||
) -> str:
|
||||
if api_base is None:
|
||||
return "https://api.voyageai.com/v1/rerank"
|
||||
return f"{get_default_base_url()}/rerank"
|
||||
api_base = api_base.rstrip("/")
|
||||
if not api_base.endswith("/v1/rerank"):
|
||||
if api_base.endswith("/v1"):
|
||||
|
|
@ -140,12 +140,11 @@ class VoyageRerankConfig(BaseRerankConfig):
|
|||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("VOYAGE_API_KEY") or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
if api_key is None:
|
||||
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.")
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {resolved_api_key}",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56413,26 +56413,6 @@
|
|||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/pricing"
|
||||
},
|
||||
"voyage/voyage-4-nano": {
|
||||
"input_cost_per_token": 0.0,
|
||||
"litellm_provider": "voyage",
|
||||
"max_input_tokens": 32000,
|
||||
"max_tokens": 32000,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/embeddings"
|
||||
},
|
||||
"voyage/voyage-multilingual-2": {
|
||||
"input_cost_per_token": 1.2e-07,
|
||||
"litellm_provider": "voyage",
|
||||
"max_input_tokens": 32000,
|
||||
"max_tokens": 32000,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/pricing"
|
||||
},
|
||||
"voyage/voyage-context-4": {
|
||||
"input_cost_per_token": 1.2e-07,
|
||||
"litellm_provider": "voyage",
|
||||
|
|
|
|||
|
|
@ -56413,26 +56413,6 @@
|
|||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/pricing"
|
||||
},
|
||||
"voyage/voyage-4-nano": {
|
||||
"input_cost_per_token": 0.0,
|
||||
"litellm_provider": "voyage",
|
||||
"max_input_tokens": 32000,
|
||||
"max_tokens": 32000,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/embeddings"
|
||||
},
|
||||
"voyage/voyage-multilingual-2": {
|
||||
"input_cost_per_token": 1.2e-07,
|
||||
"litellm_provider": "voyage",
|
||||
"max_input_tokens": 32000,
|
||||
"max_tokens": 32000,
|
||||
"mode": "embedding",
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"source": "https://docs.voyageai.com/docs/pricing"
|
||||
},
|
||||
"voyage/voyage-context-4": {
|
||||
"input_cost_per_token": 1.2e-07,
|
||||
"litellm_provider": "voyage",
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ class TestVoyageContextualEmbeddings:
|
|||
assert "input_type" not in transformed
|
||||
|
||||
def test_contextual_embedding_str_input_wrapped_with_auto_chunk(self):
|
||||
"""A bare str is wrapped to a one-element list and, as documents, gets auto-chunking + input_type=document"""
|
||||
"""A bare str is wrapped to a one-element sequence and, as documents, gets auto-chunking + input_type=document"""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
|
@ -259,7 +259,7 @@ class TestVoyageContextualEmbeddings:
|
|||
"voyage-context-4", "just one chunk", {}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["just one chunk"]
|
||||
assert list(transformed["inputs"]) == ["just one chunk"]
|
||||
assert transformed["input_type"] == "document"
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
|
||||
|
|
@ -516,24 +516,3 @@ class TestVoyageContextualEmbeddings:
|
|||
|
||||
except Exception as e:
|
||||
pytest.fail(f"Error occurred: {e}")
|
||||
|
||||
|
||||
def test_voyage_current_models_registered():
|
||||
"""The models currently listed on docs.voyageai.com resolve with voyage pricing/context"""
|
||||
from litellm import get_model_info
|
||||
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
|
||||
expected = {
|
||||
"voyage/voyage-4-nano": {"max_input_tokens": 32000, "input_cost_per_token": 0.0},
|
||||
"voyage/voyage-multilingual-2": {"max_input_tokens": 32000, "input_cost_per_token": 1.2e-07},
|
||||
"voyage/voyage-context-4": {"max_input_tokens": 120000, "input_cost_per_token": 1.2e-07},
|
||||
}
|
||||
|
||||
for model, fields in expected.items():
|
||||
info = get_model_info(model)
|
||||
assert info["litellm_provider"] == "voyage", f"{model} wrong provider"
|
||||
assert info["mode"] == "embedding", f"{model} wrong mode"
|
||||
assert info["max_input_tokens"] == fields["max_input_tokens"], f"{model} wrong context"
|
||||
assert info["input_cost_per_token"] == fields["input_cost_per_token"], f"{model} wrong price"
|
||||
|
|
|
|||
|
|
@ -266,11 +266,10 @@ class TestVoyageRerankTransform:
|
|||
assert "top_n" in supported_params
|
||||
assert "return_documents" in supported_params
|
||||
|
||||
@patch("litellm.llms.voyage.rerank.transformation.get_secret_str")
|
||||
def test_validate_environment_missing_api_key(self, mock_get_secret_str):
|
||||
def test_validate_environment_missing_api_key(self, monkeypatch):
|
||||
"""Test that validate_environment raises error when API key is missing."""
|
||||
# Mock get_secret_str to return None for both environment variables
|
||||
mock_get_secret_str.return_value = None
|
||||
for env_var in ("VOYAGE_API_KEY", "VOYAGE_AI_API_KEY", "VOYAGE_AI_TOKEN"):
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
with pytest.raises(ValueError, match="Voyage AI API key is required"):
|
||||
self.config.validate_environment(
|
||||
headers={},
|
||||
|
|
|
|||
117
tests/test_litellm/llms/voyage/test_common_utils.py
Normal file
117
tests/test_litellm/llms/voyage/test_common_utils.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
from litellm.llms.voyage.common_utils import (
|
||||
MONGODB_API_BASE,
|
||||
VOYAGE_API_BASE,
|
||||
get_default_base_url,
|
||||
get_voyage_api_key,
|
||||
)
|
||||
from litellm.llms.voyage.embedding.transformation import VoyageEmbeddingConfig
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
from litellm.llms.voyage.embedding.transformation_multimodal import (
|
||||
VoyageMultimodalEmbeddingConfig,
|
||||
)
|
||||
from litellm.llms.voyage.rerank.transformation import VoyageRerankConfig
|
||||
|
||||
VOYAGE_KEY_ENV_VARS = ("VOYAGE_API_KEY", "VOYAGE_AI_API_KEY", "VOYAGE_AI_TOKEN")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_voyage_env(monkeypatch):
|
||||
for env_var in VOYAGE_KEY_ENV_VARS:
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
|
||||
|
||||
def test_mongodb_key_routes_to_mongodb_host():
|
||||
"""MongoDB-issued keys carry the `al-` prefix and are only valid on ai.mongodb.com"""
|
||||
assert get_default_base_url("al-1234567890") == MONGODB_API_BASE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_key", ["pa-1234567890", "sk-1234567890", "al", "", None])
|
||||
def test_non_mongodb_key_routes_to_voyage_host(api_key):
|
||||
assert get_default_base_url(api_key) == VOYAGE_API_BASE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("env_var", VOYAGE_KEY_ENV_VARS)
|
||||
def test_mongodb_key_from_any_supported_env_var_routes_to_mongodb_host(monkeypatch, env_var):
|
||||
monkeypatch.setenv(env_var, "al-from-env")
|
||||
assert get_default_base_url() == MONGODB_API_BASE
|
||||
|
||||
|
||||
def test_explicit_key_wins_over_env_for_routing(monkeypatch):
|
||||
monkeypatch.setenv("VOYAGE_API_KEY", "al-from-env")
|
||||
assert get_default_base_url("pa-explicit") == VOYAGE_API_BASE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, endpoint",
|
||||
[
|
||||
(VoyageEmbeddingConfig(), "embeddings"),
|
||||
(VoyageContextualEmbeddingConfig(), "contextualizedembeddings"),
|
||||
(VoyageMultimodalEmbeddingConfig(), "multimodalembeddings"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("api_key, expected_host", [("al-key", MONGODB_API_BASE), ("pa-key", VOYAGE_API_BASE)])
|
||||
def test_embedding_configs_route_by_key_prefix(config, endpoint, api_key, expected_host):
|
||||
url = config.get_complete_url(None, api_key, "voyage-3", {}, {})
|
||||
assert url == f"{expected_host}/{endpoint}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, endpoint",
|
||||
[
|
||||
(VoyageEmbeddingConfig(), "embeddings"),
|
||||
(VoyageContextualEmbeddingConfig(), "contextualizedembeddings"),
|
||||
(VoyageMultimodalEmbeddingConfig(), "multimodalembeddings"),
|
||||
],
|
||||
)
|
||||
def test_explicit_api_base_overrides_key_routing(config, endpoint):
|
||||
url = config.get_complete_url("https://gateway.internal/v1", "al-key", "voyage-3", {}, {})
|
||||
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_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"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[VoyageEmbeddingConfig(), VoyageContextualEmbeddingConfig(), VoyageMultimodalEmbeddingConfig()],
|
||||
)
|
||||
def test_auth_header_uses_the_key_the_url_was_routed_on(monkeypatch, config):
|
||||
"""The host is picked from a key, so the Authorization header has to carry that same key"""
|
||||
monkeypatch.setenv("VOYAGE_AI_TOKEN", "al-from-env")
|
||||
|
||||
headers = config.validate_environment({}, "voyage-3", [], {}, {})
|
||||
url = config.get_complete_url(None, None, "voyage-3", {}, {})
|
||||
|
||||
assert headers["Authorization"] == "Bearer al-from-env"
|
||||
assert url.startswith(MONGODB_API_BASE)
|
||||
|
||||
|
||||
def test_rerank_auth_header_uses_the_key_the_url_was_routed_on(monkeypatch):
|
||||
monkeypatch.setenv("VOYAGE_AI_TOKEN", "al-from-env")
|
||||
|
||||
headers = VoyageRerankConfig().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)
|
||||
|
||||
|
||||
def test_get_voyage_api_key_prefers_env_vars_in_documented_order(monkeypatch):
|
||||
monkeypatch.setenv("VOYAGE_AI_API_KEY", "second")
|
||||
monkeypatch.setenv("VOYAGE_AI_TOKEN", "third")
|
||||
assert get_voyage_api_key() == "second"
|
||||
|
||||
monkeypatch.setenv("VOYAGE_API_KEY", "first")
|
||||
assert get_voyage_api_key() == "first"
|
||||
assert get_voyage_api_key("explicit") == "explicit"
|
||||
|
|
@ -172,15 +172,12 @@ class TestVoyageMultimodalEmbeddings:
|
|||
assert headers == {"Authorization": "Bearer test-key"}
|
||||
|
||||
def test_validate_environment_uses_secret_fallback(self, monkeypatch):
|
||||
import litellm.llms.voyage.embedding.transformation_multimodal as module
|
||||
from litellm.llms.voyage.embedding.transformation_multimodal import (
|
||||
VoyageMultimodalEmbeddingConfig,
|
||||
)
|
||||
|
||||
def fake_get_secret(name):
|
||||
return "secret-key" if name == "VOYAGE_AI_API_KEY" else None
|
||||
|
||||
monkeypatch.setattr(module, "get_secret_str", fake_get_secret)
|
||||
monkeypatch.delenv("VOYAGE_API_KEY", raising=False)
|
||||
monkeypatch.setenv("VOYAGE_AI_API_KEY", "secret-key")
|
||||
config = VoyageMultimodalEmbeddingConfig()
|
||||
headers = config.validate_environment(
|
||||
{}, "voyage-multimodal-3.5", [], {}, {}, api_key=None
|
||||
|
|
@ -188,12 +185,12 @@ class TestVoyageMultimodalEmbeddings:
|
|||
assert headers == {"Authorization": "Bearer secret-key"}
|
||||
|
||||
def test_validate_environment_raises_without_api_key(self, monkeypatch):
|
||||
import litellm.llms.voyage.embedding.transformation_multimodal as module
|
||||
from litellm.llms.voyage.embedding.transformation_multimodal import (
|
||||
VoyageMultimodalEmbeddingConfig,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(module, "get_secret_str", lambda name: None)
|
||||
for env_var in ("VOYAGE_API_KEY", "VOYAGE_AI_API_KEY", "VOYAGE_AI_TOKEN"):
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
config = VoyageMultimodalEmbeddingConfig()
|
||||
with pytest.raises(ValueError, match='Voyage API key is required for multimodal embeddings\\. Set') as exc_info:
|
||||
config.validate_environment(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue