From 0e11abb15dc7ec7ed8cd282ff2b77190efba4598 Mon Sep 17 00:00:00 2001 From: fzowl Date: Tue, 8 Sep 2026 18:45:36 +0200 Subject: [PATCH] feat(voyage): rebrand to VoyageAI by MongoDB, refresh model list, support both contextual input shapes --- README.md | 2 +- .../embedding/transformation_contextual.py | 18 ++++- ...odel_prices_and_context_window_backup.json | 20 ++++++ .../provider_endpoints_support_backup.json | 2 +- .../provider_create_fields.json | 2 +- model_prices_and_context_window.json | 20 ++++++ provider_endpoints_support.json | 2 +- tests/llm_translation/test_voyage_ai.py | 66 +++++++++++++++++++ .../src/components/provider_info_helpers.tsx | 2 +- 9 files changed, 128 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 92757fcbbc1..2879802981d 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | [Vercel AI Gateway (`vercel_ai_gateway`)](https://docs.litellm.ai/docs/providers/vercel_ai_gateway) | ✅ | ✅ | ✅ | | | | | | | | | [VLLM (`vllm`)](https://docs.litellm.ai/docs/providers/vllm) | ✅ | ✅ | ✅ | | | | | | | | | [Volcengine (`volcengine`)](https://docs.litellm.ai/docs/providers/volcano) | ✅ | ✅ | ✅ | | | | | | | | -| [Voyage AI (`voyage`)](https://docs.litellm.ai/docs/providers/voyage) | | | | ✅ | | | | | | | +| [VoyageAI by MongoDB (`voyage`)](https://docs.litellm.ai/docs/providers/voyage) | | | | ✅ | | | | | | | | [WandB Inference (`wandb`)](https://docs.litellm.ai/docs/providers/wandb_inference) | ✅ | ✅ | ✅ | | | | | | | | | [Watsonx Text (`watsonx_text`)](https://docs.litellm.ai/docs/providers/watsonx) | ✅ | ✅ | ✅ | | | | | | | | | [xAI (`xai`)](https://docs.litellm.ai/docs/providers/xai) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index ec77ce91d33..260806fc164 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -105,11 +105,27 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig): headers: dict, ) -> dict: return { - "inputs": input, + "inputs": self._normalize_contextual_inputs(input), "model": model, **optional_params, } + @staticmethod + def _normalize_contextual_inputs( + input: AllEmbeddingInputValues | list[list[str]], + ) -> AllEmbeddingInputValues | list[list[str]]: + """ + Voyage's contextualized embeddings API accepts ``inputs`` as either a + flat ``list[str]`` (one document's chunks) or a nested ``list[list[str]]`` + (multiple documents). Both are sent through unchanged; a bare ``str`` is + wrapped into a single-element list so the payload always matches the spec. + + Reference: https://docs.voyageai.com/docs/contextualized-chunk-embeddings + """ + if isinstance(input, str): + return [input] + return input + def transform_embedding_response( self, model: str, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 4273ec54472..8177b6ce142 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -56413,6 +56413,26 @@ "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", diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index dbeaccdda2d..1f512ab927e 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -2283,7 +2283,7 @@ } }, "voyage": { - "display_name": "Voyage AI (`voyage`)", + "display_name": "VoyageAI by MongoDB (`voyage`)", "url": "https://docs.litellm.ai/docs/providers/voyage", "endpoints": { "chat_completions": false, diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 66f8c2ea36f..ae24eb78bfe 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3228,7 +3228,7 @@ }, { "provider": "Voyage", - "provider_display_name": "Voyage AI", + "provider_display_name": "VoyageAI by MongoDB", "litellm_provider": "voyage", "credential_fields": [ { diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 4273ec54472..8177b6ce142 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -56413,6 +56413,26 @@ "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", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index c71f4a82a4a..3af67954bed 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2594,7 +2594,7 @@ } }, "voyage": { - "display_name": "Voyage AI (`voyage`)", + "display_name": "VoyageAI by MongoDB (`voyage`)", "url": "https://docs.litellm.ai/docs/providers/voyage", "endpoints": { "chat_completions": false, diff --git a/tests/llm_translation/test_voyage_ai.py b/tests/llm_translation/test_voyage_ai.py index 208e01110da..33705bf7b34 100644 --- a/tests/llm_translation/test_voyage_ai.py +++ b/tests/llm_translation/test_voyage_ai.py @@ -195,6 +195,51 @@ class TestVoyageContextualEmbeddings: assert transformed["model"] == "voyage-context-3" assert transformed["encoding_format"] == "float" + def test_contextual_embedding_flat_list_input(self): + """A flat list[str] is passed through unchanged as inputs (spec allows list[str])""" + from litellm.llms.voyage.embedding.transformation_contextual import ( + VoyageContextualEmbeddingConfig, + ) + + config = VoyageContextualEmbeddingConfig() + flat_input = ["chunk one", "chunk two"] + + transformed = config.transform_embedding_request( + "voyage-context-4", flat_input, {}, {} + ) + + assert transformed["inputs"] == flat_input + assert transformed["model"] == "voyage-context-4" + + def test_contextual_embedding_nested_list_input(self): + """A nested list[list[str]] is passed through unchanged as inputs""" + from litellm.llms.voyage.embedding.transformation_contextual import ( + VoyageContextualEmbeddingConfig, + ) + + config = VoyageContextualEmbeddingConfig() + nested_input = [["doc a chunk 1", "doc a chunk 2"], ["doc b chunk 1"]] + + transformed = config.transform_embedding_request( + "voyage-context-4", nested_input, {}, {} + ) + + assert transformed["inputs"] == nested_input + + def test_contextual_embedding_str_input_wrapped(self): + """A bare str is wrapped into a single-element list so inputs is always a list""" + from litellm.llms.voyage.embedding.transformation_contextual import ( + VoyageContextualEmbeddingConfig, + ) + + config = VoyageContextualEmbeddingConfig() + + transformed = config.transform_embedding_request( + "voyage-context-4", "just one chunk", {}, {} + ) + + assert transformed["inputs"] == ["just one chunk"] + def test_contextual_embedding_response_transformation(self): """Test response transformation for contextual embeddings""" from litellm.llms.voyage.embedding.transformation_contextual import ( @@ -428,3 +473,24 @@ 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" diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index d01a6a34cbe..3dbc3d7df26 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -171,7 +171,7 @@ export enum Providers { VERTEX_AI_BETA = "Vertex Ai Beta", VLLM = "Local vLLM", VolcEngine = "VolcEngine", - Voyage = "Voyage AI", + Voyage = "VoyageAI by MongoDB", WANDB = "Wandb", WATSONX = "Watsonx", WATSONX_TEXT = "Watsonx Text",