mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(voyage): let caller params win for contextual auto-chunking and drop duplicate cost map entries
A flat list[str] sent to voyage-context-4 is treated as independent inputs and forwarded flat with enable_auto_chunking=True, chunk_size=32000, and input_type=document unless the caller already set input_type=query. Caller-supplied params now override the defaults instead of being clobbered. The voyage-4 family and voyage-context-4 cost map entries already exist on litellm_internal_staging, and voyage-4-nano is not served by the Voyage API, so those additions and their pricing test are dropped.
This commit is contained in:
parent
8c8cd890ad
commit
d3a0b0d45b
5 changed files with 59 additions and 272 deletions
|
|
@ -3,6 +3,7 @@ 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 typing import Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -100,7 +101,7 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
|
||||
AUTO_CHUNK_SIZE = 32000
|
||||
AUTO_CHUNK_SIZE: Final = 32000
|
||||
|
||||
def transform_embedding_request(
|
||||
self,
|
||||
|
|
@ -109,71 +110,27 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig):
|
|||
optional_params: dict,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
inputs, extra_params = self._transform_contextual_inputs(input, optional_params)
|
||||
return {
|
||||
"inputs": inputs,
|
||||
"inputs": [input] if isinstance(input, str) else input,
|
||||
"model": model,
|
||||
**self._auto_chunk_params(input, optional_params),
|
||||
**optional_params,
|
||||
**extra_params,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _transform_contextual_inputs(
|
||||
def _auto_chunk_params(
|
||||
cls,
|
||||
input: AllEmbeddingInputValues | list[list[str]], # mutable-ok: union with AllEmbeddingInputValues
|
||||
optional_params: dict, # mutable-ok: matches public API
|
||||
) -> tuple[list[str] | list[list[str]], dict]: # mutable-ok: returned to caller who owns it
|
||||
"""
|
||||
Normalize ``input`` for Voyage's contextualized embeddings API and
|
||||
return ``(inputs, extra_params)`` where ``extra_params`` carries any
|
||||
request fields (e.g. auto-chunking) needed for the chosen shape.
|
||||
|
||||
The API contract (verified against the live endpoint) is:
|
||||
|
||||
- A flat ``list[str]`` is only accepted with ``input_type="query"`` or
|
||||
with ``enable_auto_chunking=True`` (which itself requires
|
||||
``input_type="document"``).
|
||||
- A ``list[list[str]]`` (each inner list = one document's chunks) is
|
||||
always accepted.
|
||||
|
||||
So we prefer to send a flat ``list[str]`` and let the API auto-chunk,
|
||||
instead of pre-wrapping into ``list[list[str]]``:
|
||||
|
||||
- ``str`` -> ``[str]`` + ``enable_auto_chunking`` (input_type=document)
|
||||
- flat ``list[str]`` + ``input_type="query"`` -> kept flat, as-is
|
||||
- flat ``list[str]`` otherwise -> kept flat + ``enable_auto_chunking``
|
||||
(input_type=document)
|
||||
- ``list[list[str]]`` -> passed through unchanged
|
||||
|
||||
Reference: https://docs.voyageai.com/reference/contextualized-embeddings-api
|
||||
"""
|
||||
if isinstance(input, str):
|
||||
if optional_params.get("input_type") == "query":
|
||||
return [input], {} # mutable-ok: fresh list returned to caller
|
||||
return [input], cls._auto_chunk_params(optional_params) # mutable-ok: fresh list returned to caller
|
||||
|
||||
if all(isinstance(i, str) for i in input):
|
||||
if optional_params.get("input_type") == "query":
|
||||
return input, {} # pyright: ignore[reportReturnType] # narrowed to list[str] by all(isinstance) check
|
||||
return input, cls._auto_chunk_params(optional_params) # pyright: ignore[reportReturnType] # narrowed to list[str]
|
||||
|
||||
return input, {} # pyright: ignore[reportReturnType] # list[list[str]] branch
|
||||
|
||||
@classmethod
|
||||
def _auto_chunk_params(cls, optional_params: dict) -> dict: # mutable-ok: matches public API
|
||||
"""
|
||||
Params required to send a flat ``list[str]`` to the contextualized API.
|
||||
|
||||
``enable_auto_chunking=True`` requires ``input_type="document"``, so set
|
||||
it unless the caller already provided an ``input_type``.
|
||||
"""
|
||||
params: dict[str, object] = { # mutable-ok: building return value
|
||||
input: AllEmbeddingInputValues | list[list[str]],
|
||||
optional_params: Mapping[str, object],
|
||||
) -> Mapping[str, object]:
|
||||
is_flat: Final = isinstance(input, str) or all(isinstance(item, str) for item in input)
|
||||
if not is_flat or optional_params.get("input_type") == "query":
|
||||
return {}
|
||||
return {
|
||||
"enable_auto_chunking": True,
|
||||
"chunk_size": cls.AUTO_CHUNK_SIZE,
|
||||
"input_type": "document",
|
||||
}
|
||||
if not optional_params.get("input_type"):
|
||||
params["input_type"] = "document"
|
||||
return params
|
||||
|
||||
def transform_embedding_response(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -39676,38 +39676,6 @@
|
|||
"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",
|
||||
|
|
@ -39732,14 +39700,6 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -39676,38 +39676,6 @@
|
|||
"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",
|
||||
|
|
@ -39732,14 +39700,6 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -72,26 +72,6 @@ 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:
|
||||
|
|
@ -220,117 +200,6 @@ class TestVoyageContextualEmbeddings:
|
|||
assert transformed["model"] == "voyage-context-3"
|
||||
assert transformed["encoding_format"] == "float"
|
||||
|
||||
def test_contextual_flat_list_str_is_auto_chunked(self):
|
||||
"""Flat list[str] without input_type stays flat and is auto-chunked.
|
||||
|
||||
The API accepts a flat list[str] only with input_type="query" or with
|
||||
enable_auto_chunking=True (which requires input_type="document"), so we
|
||||
keep the list flat and let the API auto-chunk it.
|
||||
"""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", ["Hello", "world"], {}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["Hello", "world"]
|
||||
assert transformed["model"] == "voyage-context-4"
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
assert transformed["chunk_size"] == 32000
|
||||
assert transformed["input_type"] == "document"
|
||||
|
||||
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"
|
||||
# A query list is accepted as-is, no auto-chunking needed.
|
||||
assert "enable_auto_chunking" not in transformed
|
||||
|
||||
def test_contextual_flat_list_str_document_input_type_preserved(self):
|
||||
"""Explicit input_type='document' is preserved while auto-chunking."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4",
|
||||
["Hello", "world"],
|
||||
{"input_type": "document"},
|
||||
{},
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["Hello", "world"]
|
||||
assert transformed["input_type"] == "document"
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
assert transformed["chunk_size"] == 32000
|
||||
|
||||
def test_contextual_single_string_is_auto_chunked(self):
|
||||
"""A single string becomes a one-element flat list and is auto-chunked."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", "Hello", {}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["Hello"]
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
assert transformed["chunk_size"] == 32000
|
||||
assert transformed["input_type"] == "document"
|
||||
|
||||
def test_contextual_single_string_query_no_auto_chunk(self):
|
||||
"""A single string with input_type='query' is not auto-chunked."""
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", "Hello", {"input_type": "query"}, {}
|
||||
)
|
||||
|
||||
assert transformed["inputs"] == ["Hello"]
|
||||
assert transformed["input_type"] == "query"
|
||||
assert "enable_auto_chunking" not in transformed
|
||||
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -74,15 +74,11 @@ class TestVoyageContextualEmbeddings:
|
|||
assert headers == {"Authorization": "Bearer test-key"}
|
||||
|
||||
def test_validate_environment_secret_fallback(self, monkeypatch):
|
||||
import litellm.llms.voyage.embedding.transformation_contextual as module
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
def fake_get_secret(name):
|
||||
return "secret-key" if name == "VOYAGE_API_KEY" else None
|
||||
|
||||
monkeypatch.setattr(module, "get_secret_str", fake_get_secret)
|
||||
monkeypatch.setenv("VOYAGE_API_KEY", "secret-key")
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
headers = config.validate_environment(
|
||||
{}, "voyage-context-4", [], {}, {}, api_key=None
|
||||
|
|
@ -142,6 +138,51 @@ class TestVoyageContextualEmbeddings:
|
|||
assert transformed["input_type"] == "document"
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
|
||||
def test_flat_list_str_caller_chunk_params_win(self):
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4",
|
||||
["Hello", "world"],
|
||||
{"input_type": "document", "chunk_size": 512, "chunk_overlap": 32},
|
||||
{},
|
||||
)
|
||||
assert transformed["enable_auto_chunking"] is True
|
||||
assert transformed["chunk_size"] == 512
|
||||
assert transformed["chunk_overlap"] == 32
|
||||
assert transformed["input_type"] == "document"
|
||||
|
||||
def test_flat_list_str_caller_can_disable_auto_chunking(self):
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", ["Hello"], {"enable_auto_chunking": False}, {}
|
||||
)
|
||||
assert transformed["enable_auto_chunking"] is False
|
||||
assert transformed["input_type"] == "document"
|
||||
|
||||
def test_nested_list_keeps_caller_params(self):
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
)
|
||||
|
||||
config = VoyageContextualEmbeddingConfig()
|
||||
transformed = config.transform_embedding_request(
|
||||
"voyage-context-4", [["Hello", "world"]], {"input_type": "document", "output_dimension": 512}, {}
|
||||
)
|
||||
assert transformed == {
|
||||
"inputs": [["Hello", "world"]],
|
||||
"model": "voyage-context-4",
|
||||
"input_type": "document",
|
||||
"output_dimension": 512,
|
||||
}
|
||||
|
||||
def test_single_string_auto_chunked(self):
|
||||
from litellm.llms.voyage.embedding.transformation_contextual import (
|
||||
VoyageContextualEmbeddingConfig,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue