mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
feat(voyage): add voyage-4 family + voyage-context-4, fix contextual list[str] input
- Add voyage-4, voyage-4-large, voyage-4-lite, voyage-4-nano (/bin/bash) and voyage-context-4 to the cost/context map (both root and backup json). - Contextual embeddings: normalize flat list[str] input. Voyage rejects a flat list[str] unless input_type='query', so wrap each string as its own single-chunk document otherwise; single str -> [[str]]; list[list[str]] passes through. Keeps list[str] when input_type='query'. - Add tests for input normalization and voyage-4 family pricing.
This commit is contained in:
parent
5bb7394c93
commit
f88dd65590
4 changed files with 197 additions and 1 deletions
|
|
@ -106,11 +106,42 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
headers: dict,
|
||||
) -> dict:
|
||||
return {
|
||||
"inputs": input,
|
||||
"inputs": self._transform_contextual_inputs(input, optional_params),
|
||||
"model": model,
|
||||
**optional_params,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _transform_contextual_inputs(
|
||||
input: Union[AllEmbeddingInputValues, List[List[str]]],
|
||||
optional_params: dict,
|
||||
) -> List[List[str]]:
|
||||
"""
|
||||
Voyage's contextualized embeddings API expects ``inputs`` to be a
|
||||
``list[list[str]]`` (each inner list is a document made of chunks that
|
||||
share context).
|
||||
|
||||
It also accepts a flat ``list[str]`` - but only when
|
||||
``input_type == "query"``. In every other case a flat list must be
|
||||
wrapped so each string becomes its own single-chunk document, otherwise
|
||||
the API rejects the request with a 400.
|
||||
|
||||
Reference: https://docs.voyageai.com/reference/contextualized-embeddings-api
|
||||
"""
|
||||
# Single string -> one document with a single chunk.
|
||||
if isinstance(input, str):
|
||||
return [[input]]
|
||||
|
||||
# Flat list[str]: keep as list[str] when the API allows it
|
||||
# (input_type="query"), otherwise wrap each string as its own document.
|
||||
if isinstance(input, list) and all(isinstance(i, str) for i in input):
|
||||
if optional_params.get("input_type") == "query":
|
||||
return input # type: ignore[return-value]
|
||||
return [[i] for i in input]
|
||||
|
||||
# Already list[list[str]] (or another shape) -> pass through unchanged.
|
||||
return input # type: ignore[return-value]
|
||||
|
||||
def transform_embedding_response(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,26 @@ class TestVoyageAI(BaseLLMEmbeddingTest):
|
|||
assert response.usage.total_tokens > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected_input_cost",
|
||||
[
|
||||
("voyage/voyage-4", 6e-08),
|
||||
("voyage/voyage-4-large", 1.2e-07),
|
||||
("voyage/voyage-4-lite", 2e-08),
|
||||
("voyage/voyage-4-nano", 0.0),
|
||||
("voyage/voyage-context-4", 1.2e-07),
|
||||
],
|
||||
)
|
||||
def test_voyage_4_family_pricing_registered(model, expected_input_cost):
|
||||
"""The voyage-4 family and voyage-context-4 must be in the cost map."""
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
info = litellm.get_model_info(model=model)
|
||||
assert info["litellm_provider"] == "voyage"
|
||||
assert info["mode"] == "embedding"
|
||||
assert info["input_cost_per_token"] == expected_input_cost
|
||||
|
||||
|
||||
def test_voyage_ai_embedding_extra_params():
|
||||
"""Test Voyage AI embedding with extra parameters"""
|
||||
try:
|
||||
|
|
@ -142,6 +162,7 @@ class TestVoyageContextualEmbeddings:
|
|||
|
||||
# Test contextual model detection
|
||||
assert config.is_contextualized_embeddings("voyage-context-3") is True
|
||||
assert config.is_contextualized_embeddings("voyage-context-4") is True
|
||||
assert config.is_contextualized_embeddings("voyage-context-2") is True
|
||||
assert config.is_contextualized_embeddings("context-model") is True
|
||||
|
||||
|
|
@ -198,6 +219,70 @@ class TestVoyageContextualEmbeddings:
|
|||
assert transformed["model"] == "voyage-context-3"
|
||||
assert transformed["encoding_format"] == "float"
|
||||
|
||||
def test_contextual_flat_list_str_is_wrapped(self):
|
||||
"""Flat list[str] without input_type must be wrapped to list[list[str]]."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", ["Hello", "world"], {}, {}
|
||||
)
|
||||
|
||||
# Voyage rejects a flat list[str] unless input_type="query", so each
|
||||
# string becomes its own single-chunk document.
|
||||
assert transformed["inputs"] == [["Hello"], ["world"]]
|
||||
assert transformed["model"] == "voyage-context-4"
|
||||
|
||||
def test_contextual_flat_list_str_query_stays_flat(self):
|
||||
"""Flat list[str] with input_type='query' is sent as-is (list[str])."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4",
|
||||
["Hello", "world"],
|
||||
{"input_type": "query"},
|
||||
{},
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["Hello", "world"]
|
||||
assert transformed["input_type"] == "query"
|
||||
|
||||
def test_contextual_single_string_is_wrapped(self):
|
||||
"""A single string is wrapped to a single document with a single chunk."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", "Hello", {}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == [["Hello"]]
|
||||
|
||||
def test_contextual_nested_input_passthrough(self):
|
||||
"""Already-nested list[list[str]] input is passed through unchanged."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
nested = [["Hello", "world"], ["Test"]]
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", nested, {}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == nested
|
||||
|
||||
def test_contextual_embedding_response_transformation(self):
|
||||
"""Test response transformation for contextual embeddings"""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue