Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions docs/my-website/docs/providers/voyage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)` |
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
26 changes: 25 additions & 1 deletion litellm/llms/voyage/embedding/transformation_contextual.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -27417,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,
Expand Down
48 changes: 48 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -27417,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,
Expand Down
25 changes: 25 additions & 0 deletions tests/llm_translation/test_voyage_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,28 @@ 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),
("voyage/voyage-multimodal-3.5", 1.2e-07, 32000),
],
)
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
Empty file.
Original file line number Diff line number Diff line change
@@ -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"]]
Loading