From ecc60c0c5646e7c1c75f6dd1dc46654e51242836 Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 27 Jul 2026 18:07:48 +0200 Subject: [PATCH 1/3] feat(voyage): add voyage-context-4 and voyage-4 family models - Add voyage-context-4 (contextual, 120K max tokens, $0.12/1M) - Add voyage-4, voyage-4-large, voyage-4-lite, and free voyage-4-nano - Normalize contextual embedding inputs so a flat list[str] is sent to the API as list[list[str]] (each document a list[str] of chunks) - Update Voyage provider docs and add contextual transformation tests --- docs/my-website/docs/providers/voyage.md | 17 +++++- .../embedding/transformation_contextual.py | 26 +++++++- ...odel_prices_and_context_window_backup.json | 40 +++++++++++++ model_prices_and_context_window.json | 40 +++++++++++++ .../llms/voyage/embedding/__init__.py | 0 .../test_voyage_contextual_transformation.py | 60 +++++++++++++++++++ 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 tests/test_litellm/llms/voyage/embedding/__init__.py create mode 100644 tests/test_litellm/llms/voyage/embedding/test_voyage_contextual_transformation.py diff --git a/docs/my-website/docs/providers/voyage.md b/docs/my-website/docs/providers/voyage.md index 43369cd6ab78..4fab5384d3ec 100644 --- a/docs/my-website/docs/providers/voyage.md +++ b/docs/my-website/docs/providers/voyage.md @@ -54,6 +54,10 @@ All models listed here https://docs.voyageai.com/embeddings/#models-and-specific | Model Name | Function Call | |-------------------------|------------------------------------------------------------| +| voyage-4-large | `embedding(model="voyage/voyage-4-large", input)` | +| voyage-4 | `embedding(model="voyage/voyage-4", input)` | +| voyage-4-lite | `embedding(model="voyage/voyage-4-lite", input)` | +| voyage-4-nano | `embedding(model="voyage/voyage-4-nano", input)` | | voyage-3.5 | `embedding(model="voyage/voyage-3.5", input)` | | voyage-3.5-lite | `embedding(model="voyage/voyage-3.5-lite", input)` | | voyage-3-large | `embedding(model="voyage/voyage-3-large", input)` | @@ -72,9 +76,11 @@ All models listed here https://docs.voyageai.com/embeddings/#models-and-specific | voyage-lite-01 | `embedding(model="voyage/voyage-lite-01", input)` | | voyage-lite-01-instruct | `embedding(model="voyage/voyage-lite-01-instruct", input)` | -## Contextual Embeddings (voyage-context-3) +## Contextual Embeddings (voyage-context-4, voyage-context-3) -VoyageAI's `voyage-context-3` model provides contextualized chunk embeddings, where each chunk is embedded with awareness of its surrounding document context. This significantly improves retrieval quality compared to standard context-agnostic embeddings. +VoyageAI's contextualized embedding models (`voyage-context-4` and the previous-generation `voyage-context-3`) provide contextualized chunk embeddings, where each chunk is embedded with awareness of its surrounding document context. This significantly improves retrieval quality compared to standard context-agnostic embeddings. + +`voyage-context-4` is the current model; `voyage-context-3` remains available as a legacy option. Swap the model name in the examples below to switch between them. ### Key Benefits - Chunks understand their position and role within the full document @@ -149,7 +155,12 @@ print(f"Processed {len(response.data)} documents") | voyage-code-3 | Code retrieval and search | 32K | $0.18 | | voyage-finance-2 | Financial documents | 32K | $0.12 | | voyage-law-2 | Legal documents | 16K | $0.12 | -| voyage-context-3 | Contextual document embeddings | 32K | $0.18 | +| voyage-4-large | Best overall quality | 32K | $0.12 | +| voyage-4 | General-purpose | 32K | $0.06 | +| voyage-4-lite | Latency-sensitive applications | 32K | $0.02 | +| voyage-4-nano | Open-weight, free | 32K | $0.00 | +| voyage-context-4 | Contextual document embeddings (current) | 32K/doc | $0.12 | +| voyage-context-3 | Contextual document embeddings (legacy) | 32K/doc | $0.18 | ## Rerank diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index 4df2fa4ba312..336030d0b187 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -106,11 +106,35 @@ def transform_embedding_request( headers: dict, ) -> dict: return { - "inputs": input, + "inputs": self._normalize_inputs(input), "model": model, **optional_params, } + @staticmethod + def _normalize_inputs( + input: Union[AllEmbeddingInputValues, List[List[str]]] + ) -> List[List[str]]: + """ + The Voyage contextualized embeddings API expects ``inputs`` to be a + list of documents, where each document is a ``list[str]`` of chunks. + + Callers commonly pass a flat ``list[str]`` (the standard OpenAI + embedding input). Wrap it into a single document so the API always + receives ``list[list[str]]`` with ``list[str]`` elements. + """ + # A single string -> one document with one chunk. + if isinstance(input, str): + return [[input]] + if isinstance(input, list): + # Already list[list[str]] -> pass through unchanged. + if input and all(isinstance(item, list) for item in input): + return input # type: ignore[return-value] + # Flat list[str] -> wrap as a single document of chunks. + if all(isinstance(item, str) for item in input): + return [list(input)] # type: ignore[arg-type] + return input # type: ignore[return-value] + 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 c584deb683a3..fc710497592b 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -27345,6 +27345,38 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-4": { + "input_cost_per_token": 6e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-4-large": { + "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 + }, + "voyage/voyage-4-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "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 + }, "voyage/voyage-code-2": { "input_cost_per_token": 1.2e-07, "litellm_provider": "voyage", @@ -27369,6 +27401,14 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-context-4": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 120000, + "max_tokens": 120000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, "voyage/voyage-finance-2": { "input_cost_per_token": 1.2e-07, "litellm_provider": "voyage", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index c584deb683a3..fc710497592b 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -27345,6 +27345,38 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-4": { + "input_cost_per_token": 6e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-4-large": { + "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 + }, + "voyage/voyage-4-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "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 + }, "voyage/voyage-code-2": { "input_cost_per_token": 1.2e-07, "litellm_provider": "voyage", @@ -27369,6 +27401,14 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-context-4": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 120000, + "max_tokens": 120000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, "voyage/voyage-finance-2": { "input_cost_per_token": 1.2e-07, "litellm_provider": "voyage", diff --git a/tests/test_litellm/llms/voyage/embedding/__init__.py b/tests/test_litellm/llms/voyage/embedding/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/test_litellm/llms/voyage/embedding/test_voyage_contextual_transformation.py b/tests/test_litellm/llms/voyage/embedding/test_voyage_contextual_transformation.py new file mode 100644 index 000000000000..b4d97d0084e2 --- /dev/null +++ b/tests/test_litellm/llms/voyage/embedding/test_voyage_contextual_transformation.py @@ -0,0 +1,60 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../../..")) + +from litellm.llms.voyage.embedding.transformation_contextual import ( + VoyageContextualEmbeddingConfig, +) + + +class TestVoyageContextualDetection: + def test_voyage_context_4_detected(self): + config = VoyageContextualEmbeddingConfig() + assert config.is_contextualized_embeddings("voyage-context-4") is True + assert config.is_contextualized_embeddings("voyage/voyage-context-4") is True + + def test_non_contextual_models_not_detected(self): + config = VoyageContextualEmbeddingConfig() + assert config.is_contextualized_embeddings("voyage-4") is False + assert config.is_contextualized_embeddings("voyage-4-nano") is False + assert config.is_contextualized_embeddings("voyage-3.5") is False + + +class TestVoyageContextualInputNormalization: + """ + The contextualized embeddings API expects ``inputs`` to be + ``list[list[str]]`` (each document is a ``list[str]`` of chunks). + """ + + def test_flat_list_of_str_is_wrapped(self): + config = VoyageContextualEmbeddingConfig() + transformed = config.transform_embedding_request( + "voyage-context-4", ["hello", "world"], {}, {} + ) + assert transformed["inputs"] == [["hello", "world"]] + assert all(isinstance(doc, list) for doc in transformed["inputs"]) + + def test_single_string_is_wrapped(self): + config = VoyageContextualEmbeddingConfig() + transformed = config.transform_embedding_request( + "voyage-context-4", "hello", {}, {} + ) + assert transformed["inputs"] == [["hello"]] + + def test_nested_list_passed_through(self): + config = VoyageContextualEmbeddingConfig() + nested = [["a", "b"], ["c"]] + transformed = config.transform_embedding_request( + "voyage-context-4", nested, {}, {} + ) + assert transformed["inputs"] == nested + + def test_model_and_optional_params_preserved(self): + config = VoyageContextualEmbeddingConfig() + transformed = config.transform_embedding_request( + "voyage-context-4", ["x"], {"output_dimension": 512}, {} + ) + assert transformed["model"] == "voyage-context-4" + assert transformed["output_dimension"] == 512 + assert transformed["inputs"] == [["x"]] From c8e9de1b0e880f56a054cc84dac2bf8e06525ac2 Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 27 Jul 2026 19:23:37 +0200 Subject: [PATCH 2/3] test(voyage): validate registered pricing for voyage-4 family and voyage-context-4 --- tests/llm_translation/test_voyage_ai.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/llm_translation/test_voyage_ai.py b/tests/llm_translation/test_voyage_ai.py index a0b9ee0a44bb..ada725b0ace5 100644 --- a/tests/llm_translation/test_voyage_ai.py +++ b/tests/llm_translation/test_voyage_ai.py @@ -431,3 +431,27 @@ def test_contextual_embedding_multiple_inputs(self): except Exception as e: pytest.fail(f"Error occurred: {e}") + + +class TestVoyageModelPrices: + """Ensure newly added Voyage models are registered with correct pricing""" + + @pytest.mark.parametrize( + "model, input_cost, max_tokens", + [ + ("voyage/voyage-4", 6e-08, 32000), + ("voyage/voyage-4-large", 1.2e-07, 32000), + ("voyage/voyage-4-lite", 2e-08, 32000), + ("voyage/voyage-4-nano", 0.0, 32000), + ("voyage/voyage-context-4", 1.2e-07, 120000), + ], + ) + def test_voyage_model_registered(self, model, input_cost, max_tokens): + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + info = litellm.get_model_info(model) + assert info["litellm_provider"] == "voyage" + assert info["mode"] == "embedding" + assert info["input_cost_per_token"] == input_cost + assert info["max_input_tokens"] == max_tokens From 5e0e705b97d05c158205647069dbdcdc463be114 Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 27 Jul 2026 19:25:17 +0200 Subject: [PATCH 3/3] feat(voyage): add voyage-multimodal-3.5 and model registration tests - Add voyage-multimodal-3.5 embedding model ($0.12/1M, 32k context) - Add pricing/registration tests for voyage-4 family, voyage-context-4, voyage-4-nano, and voyage-multimodal-3.5 --- litellm/model_prices_and_context_window_backup.json | 8 ++++++++ model_prices_and_context_window.json | 8 ++++++++ tests/llm_translation/test_voyage_ai.py | 1 + 3 files changed, 17 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index fc710497592b..0dcd949c542d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -27457,6 +27457,14 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-multimodal-3.5": { + "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 + }, "wandb/openai/gpt-oss-120b": { "max_tokens": 131072, "max_input_tokens": 131072, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index fc710497592b..0dcd949c542d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -27457,6 +27457,14 @@ "mode": "embedding", "output_cost_per_token": 0.0 }, + "voyage/voyage-multimodal-3.5": { + "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 + }, "wandb/openai/gpt-oss-120b": { "max_tokens": 131072, "max_input_tokens": 131072, diff --git a/tests/llm_translation/test_voyage_ai.py b/tests/llm_translation/test_voyage_ai.py index ada725b0ace5..9e8520f23746 100644 --- a/tests/llm_translation/test_voyage_ai.py +++ b/tests/llm_translation/test_voyage_ai.py @@ -444,6 +444,7 @@ class TestVoyageModelPrices: ("voyage/voyage-4-lite", 2e-08, 32000), ("voyage/voyage-4-nano", 0.0, 32000), ("voyage/voyage-context-4", 1.2e-07, 120000), + ("voyage/voyage-multimodal-3.5", 1.2e-07, 32000), ], ) def test_voyage_model_registered(self, model, input_cost, max_tokens):