From 4875fc8325a431480ed72e7a4a229b56c523888e Mon Sep 17 00:00:00 2001 From: fzowl Date: Sun, 16 Aug 2026 21:01:58 +0200 Subject: [PATCH 1/7] feat(voyageai): add contextualized embeddings and refresh model support Route voyage-context-* models to VoyageAI's contextualized embeddings API. The vectorizer accepts a flat list[str] of chunks and calls contextualized_embed with enable_auto_chunking=True and chunk_size=32000 so each input document maps to a single chunk, preserving the one-embedding-per-input embed_many contract. Also fix multimodal embed_many, which collapsed multiple requested contents into a single multimodal input; each content is now sent as its own input so the returned embedding count matches the request count. Bump the voyageai floor to >=0.5.0 (contextualized auto-chunking) and refresh docs/examples to current models (rerank-2.5, voyage-context-4). --- docs/user_guide/04_vectorizers.ipynb | 33 +++ docs/user_guide/06_rerankers.ipynb | 2 +- pyproject.toml | 4 +- redisvl/utils/vectorize/voyageai.py | 90 ++++++++- tests/integration/test_vectorizers.py | 116 +++++++++++ uv.lock | 278 ++++++++++++-------------- 6 files changed, 365 insertions(+), 158 deletions(-) diff --git a/docs/user_guide/04_vectorizers.ipynb b/docs/user_guide/04_vectorizers.ipynb index b87c4c571..5de236b81 100644 --- a/docs/user_guide/04_vectorizers.ipynb +++ b/docs/user_guide/04_vectorizers.ipynb @@ -596,6 +596,39 @@ "print(test[:10])" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Contextualized embeddings\n", + "\n", + "VoyageAI's `voyage-context-*` models produce *contextualized* chunk embeddings: each chunk is embedded with awareness of the other chunks in the same request, which improves retrieval quality for chunked documents. The `VoyageAIVectorizer` automatically routes `voyage-context-*` models to the contextualized embeddings API — just pass your list of chunks to `embed_many` and you get one embedding back per chunk." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Contextualized embeddings (voyage-context-* models) embed each chunk with\n", + "# awareness of the surrounding chunks. Pass a list of chunk strings and the\n", + "# vectorizer returns one embedding per chunk. Requires voyageai>=0.5.0.\n", + "context_vo = VoyageAIVectorizer(\n", + " model=\"voyage-context-4\", # See https://docs.voyageai.com/docs/contextualized-chunk-embeddings\n", + " api_config={\"api_key\": api_key},\n", + ")\n", + "\n", + "chunks = [\n", + " \"That is a happy dog\",\n", + " \"That is a happy person\",\n", + " \"Today is a sunny day\",\n", + "]\n", + "context_embeddings = context_vo.embed_many(chunks, input_type=\"document\")\n", + "print(\"Number of embeddings:\", len(context_embeddings))\n", + "print(\"Vector dimensions:\", len(context_embeddings[0]))" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/user_guide/06_rerankers.ipynb b/docs/user_guide/06_rerankers.ipynb index a6f0a7860..982c60c6a 100644 --- a/docs/user_guide/06_rerankers.ipynb +++ b/docs/user_guide/06_rerankers.ipynb @@ -378,7 +378,7 @@ "source": [ "from redisvl.utils.rerank import VoyageAIReranker\n", "\n", - "reranker = VoyageAIReranker(model=\"rerank-lite-1\", limit=3, api_config={\"api_key\": api_key})\n", + "reranker = VoyageAIReranker(model=\"rerank-2.5\", limit=3, api_config={\"api_key\": api_key})\n", "# Please check the available models at https://docs.voyageai.com/docs/reranker" ] }, diff --git a/pyproject.toml b/pyproject.toml index 09ff02c9a..bae39cc2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ mcp = [ mistralai = ["mistralai>=1.0.0,<2"] openai = ["openai>=1.1.0"] cohere = ["cohere>=4.44"] -voyageai = ["voyageai>=0.2.2"] +voyageai = ["voyageai>=0.5.0"] sentence-transformers = ["sentence-transformers>=3.4.0,<4"] langcache = ["langcache>=0.11.0"] # Legacy Google backend for the deprecated VertexAIVectorizer. The <2.0.0 cap is @@ -77,7 +77,7 @@ all = [ "mistralai>=1.0.0,<2", "openai>=1.1.0", "cohere>=4.44", - "voyageai>=0.2.2", + "voyageai>=0.5.0", "sentence-transformers>=3.4.0,<4", "langcache>=0.11.0", "google-cloud-aiplatform>=1.26,<2.0.0", diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index e50fab7c4..662f211f4 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from pydantic import ConfigDict from tenacity import retry, stop_after_attempt, wait_random_exponential @@ -50,6 +50,17 @@ class VoyageAIVectorizer(BaseVectorizer): input_type="document" ) + # Contextualized embeddings (voyage-context-* models) - requires voyageai>=0.5.0 + # Each input document is embedded with awareness of the others in the batch. + context_vectorizer = VoyageAIVectorizer( + model="voyage-context-4", + api_config={"api_key": "your-voyageai-api-key"} + ) + context_embeddings = context_vectorizer.embed_many( + contents=["chunk one", "chunk two", "chunk three"], + input_type="document" + ) + # Multimodal usage - requires Pillow and voyageai>=0.3.6 vectorizer = VoyageAIVectorizer( @@ -131,6 +142,11 @@ def is_multimodal(self) -> bool: """Whether a multimodal model has been configured.""" return "multimodal" in self.model + @property + def is_context(self) -> bool: + """Whether a contextualized-embedding model (voyage-context-*) has been configured.""" + return "context" in self.model + def embed_image(self, image_path: str, **kwargs) -> list[float] | bytes: """Embed an image (from its path on disk) using VoyageAI's multimodal API. Requires pillow to be installed.""" if not self.is_multimodal: @@ -343,10 +359,19 @@ def _embed_many( try: embeddings: list[Any] = [] for batch in self.batchify(contents, batch_size): + if self.is_context: + embeddings.extend( + self._embed_context_batch(batch, input_type, **kwargs) + ) + continue response = self._embed_fn( ( - [batch] if self.is_multimodal else batch - ), # Multimodal requires a list of lists/dicts + # Multimodal wraps each item as its own single-part input + # so one embedding is returned per requested content. + [[item] for item in batch] + if self.is_multimodal + else batch + ), model=self.model, input_type=input_type, truncation=truncation, @@ -359,6 +384,52 @@ def _embed_many( except Exception as e: raise ValueError(f"Embedding texts failed: {e}") + def _embed_context_batch( + self, batch: list[str], input_type: str | None, **kwargs + ) -> list[list[float]]: + """Embed a batch with a contextualized (voyage-context-*) model. + + The batch is passed as a flat ``list[str]`` with auto-chunking enabled + and a large ``chunk_size`` so each input document resolves to a single + chunk, yielding exactly one embedding per requested content. + """ + # contextualized_embed is only present on recent voyageai clients; the + # attr-defined ignore keeps mypy happy without vendored stubs. + response = self._client.contextualized_embed( # type: ignore[attr-defined] + inputs=batch, + model=self.model, + input_type=input_type, + enable_auto_chunking=True, + chunk_size=32000, + **kwargs, + ) + # Take the first chunk per document to keep one embedding per input. + return cast( + "list[list[float]]", + [result.embeddings[0] for result in response.results], + ) + + async def _aembed_context_batch( + self, batch: list[str], input_type: str | None, **kwargs + ) -> list[list[float]]: + """Asynchronously embed a batch with a contextualized model. + + See :meth:`_embed_context_batch` for details on the input format. + """ + response = await self._aclient.contextualized_embed( # type: ignore[attr-defined] + inputs=batch, + model=self.model, + input_type=input_type, + enable_auto_chunking=True, + chunk_size=32000, + **kwargs, + ) + # Take the first chunk per document to keep one embedding per input. + return cast( + "list[list[float]]", + [result.embeddings[0] for result in response.results], + ) + async def _aembed(self, content: Any, **kwargs) -> list[float]: """ Asynchronously generate a vector embedding for a single item using the VoyageAI API. @@ -418,10 +489,19 @@ async def _aembed_many( try: embeddings: list[Any] = [] for batch in self.batchify(contents, batch_size): + if self.is_context: + embeddings.extend( + await self._aembed_context_batch(batch, input_type, **kwargs) + ) + continue response = await self._aembed_fn( ( - [batch] if self.is_multimodal else batch - ), # Multimodal requires a list of lists/dicts + # Multimodal wraps each item as its own single-part input + # so one embedding is returned per requested content. + [[item] for item in batch] + if self.is_multimodal + else batch + ), model=self.model, input_type=input_type, truncation=truncation, diff --git a/tests/integration/test_vectorizers.py b/tests/integration/test_vectorizers.py index 599c13378..7f41302a9 100644 --- a/tests/integration/test_vectorizers.py +++ b/tests/integration/test_vectorizers.py @@ -659,3 +659,119 @@ def test_deprecated_text_parameter_warning(): embeddings = vectorizer.embed_many(texts=TEST_TEXTS) assert isinstance(embeddings, list) assert len(embeddings) == len(TEST_TEXTS) + + +# --- VoyageAI model-routing tests (mocked, no API key required) --- + + +def _fake_voyage_clients(dims=4): + """Build mocked sync/async VoyageAI clients returning fixed-size embeddings. + + Each endpoint returns exactly one embedding per requested input so tests can + assert that batching never collapses multiple inputs into a single request. + """ + from unittest.mock import AsyncMock, MagicMock + + def _ctx_embed(inputs, model, input_type=None, **kwargs): + resp = MagicMock() + # contextualized_embed returns one result per input document, each with + # a list of per-chunk embeddings. + resp.results = [ + MagicMock(index=i, embeddings=[[0.1] * dims]) for i in range(len(inputs)) + ] + return resp + + def _embed(texts, model=None, input_type=None, truncation=True, **kwargs): + resp = MagicMock() + resp.embeddings = [[0.1] * dims for _ in texts] + return resp + + def _mm_embed(inputs, model, input_type=None, truncation=True, **kwargs): + resp = MagicMock() + resp.embeddings = [[0.1] * dims for _ in inputs] + return resp + + client = MagicMock() + client.contextualized_embed.side_effect = _ctx_embed + client.embed.side_effect = _embed + client.multimodal_embed.side_effect = _mm_embed + + aclient = MagicMock() + aclient.contextualized_embed = AsyncMock(side_effect=_ctx_embed) + aclient.embed = AsyncMock(side_effect=_embed) + aclient.multimodal_embed = AsyncMock(side_effect=_mm_embed) + + return client, aclient + + +def _build_voyage_vectorizer(model): + from unittest.mock import patch + + client, aclient = _fake_voyage_clients() + with ( + patch("voyageai.Client", return_value=client), + patch("voyageai.AsyncClient", return_value=aclient), + ): + vectorizer = VoyageAIVectorizer(model=model, api_config={"api_key": "test"}) + return vectorizer, client, aclient + + +def test_voyageai_context_model_detection(): + """voyage-context-* models are detected as contextualized models.""" + ctx_vectorizer, _, _ = _build_voyage_vectorizer("voyage-context-4") + assert ctx_vectorizer.is_context is True + assert ctx_vectorizer.is_multimodal is False + + plain_vectorizer, _, _ = _build_voyage_vectorizer("voyage-3-large") + assert plain_vectorizer.is_context is False + + +def test_voyageai_context_embed_many_uses_contextualized_api(): + """Context models route to contextualized_embed with auto-chunking enabled.""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-context-4") + + embeddings = vectorizer.embed_many( + contents=["chunk one", "chunk two", "chunk three"], input_type="document" + ) + + # One embedding per input, no collapsing. + assert len(embeddings) == 3 + + _, kwargs = client.contextualized_embed.call_args + assert kwargs["inputs"] == ["chunk one", "chunk two", "chunk three"] + assert kwargs["enable_auto_chunking"] is True + assert kwargs["chunk_size"] == 32000 + # contextualized_embed does not accept truncation. + assert "truncation" not in kwargs + + +@pytest.mark.asyncio +async def test_voyageai_context_aembed_many_uses_contextualized_api(): + """Async context models route to contextualized_embed with auto-chunking.""" + vectorizer, _, aclient = _build_voyage_vectorizer("voyage-context-4") + + embeddings = await vectorizer.aembed_many( + contents=["chunk one", "chunk two"], input_type="document" + ) + + assert len(embeddings) == 2 + _, kwargs = aclient.contextualized_embed.call_args + assert kwargs["inputs"] == ["chunk one", "chunk two"] + assert kwargs["enable_auto_chunking"] is True + assert kwargs["chunk_size"] == 32000 + + +def test_voyageai_multimodal_embed_many_does_not_collapse_inputs(): + """Multimodal embed_many sends each content as its own input (no collapsing).""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-multimodal-3.5") + + embeddings = vectorizer.embed_many( + contents=["Ocean waves", "Forest trees"], input_type="document" + ) + + # Two requested contents must yield two embeddings. + assert len(embeddings) == 2 + + args, _ = client.multimodal_embed.call_args + # Each item is wrapped as its own single-part multimodal input. + assert args[0] == [["Ocean waves"], ["Forest trees"]] diff --git a/uv.lock b/uv.lock index 210d06189..02b61e07f 100644 --- a/uv.lock +++ b/uv.lock @@ -29,7 +29,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "caio" }, + { name = "caio", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } wheels = [ @@ -47,7 +47,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "caio" }, + { name = "caio", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } wheels = [ @@ -955,43 +955,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, ] [[package]] @@ -1299,18 +1299,6 @@ server = [ { name = "websockets" }, ] -[[package]] -name = "ffmpeg-python" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "future" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, -] - [[package]] name = "filelock" version = "3.29.0" @@ -1450,15 +1438,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - [[package]] name = "google-api-core" version = "2.25.2" @@ -1467,11 +1446,11 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } wheels = [ @@ -1480,8 +1459,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version >= '3.14'" }, + { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, ] [[package]] @@ -1495,11 +1474,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, + { name = "proto-plus", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } wheels = [ @@ -1508,8 +1487,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version < '3.14'" }, + { name = "grpcio-status", marker = "python_full_version < '3.14'" }, ] [[package]] @@ -1538,18 +1517,18 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "docstring-parser" }, - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-bigquery" }, - { name = "google-cloud-resource-manager" }, - { name = "google-cloud-storage", version = "3.4.1", source = { registry = "https://pypi.org/simple" } }, - { name = "google-genai" }, - { name = "packaging" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "typing-extensions" }, + { name = "docstring-parser", marker = "python_full_version >= '3.14'" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version >= '3.14'" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-bigquery", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-resource-manager", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-storage", version = "3.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-genai", marker = "python_full_version >= '3.14'" }, + { name = "packaging", marker = "python_full_version >= '3.14'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/f3/b2a9417014c93858a2e3266134f931eefd972c2d410b25d7b8782fc6f143/google_cloud_aiplatform-1.148.1.tar.gz", hash = "sha256:75d605fba34e68714bd08e1e482755d0a6e3ae972805f809d088e686c30879e7", size = 10278758, upload-time = "2026-04-17T23:45:26.738Z" } wheels = [ @@ -1567,18 +1546,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "docstring-parser" }, - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-bigquery" }, - { name = "google-cloud-resource-manager" }, - { name = "google-cloud-storage", version = "3.10.1", source = { registry = "https://pypi.org/simple" } }, - { name = "google-genai" }, - { name = "packaging" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "typing-extensions" }, + { name = "docstring-parser", marker = "python_full_version < '3.14'" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version < '3.14'" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-bigquery", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-resource-manager", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-storage", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-genai", marker = "python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "proto-plus", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/a6/4d5bc1a25069a53383cda1b1da4957765be5a36ad1dce8d726fce557fb10/google_cloud_aiplatform-1.154.0.tar.gz", hash = "sha256:3cfb5afb9006ee202eab93ffc19aeeb111d9e62b574ebf80d7ab91aa9f463677", size = 11020790, upload-time = "2026-05-27T19:20:49.723Z" } wheels = [ @@ -1644,12 +1623,12 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version >= '3.14'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/ef/7cefdca67a6c8b3af0ec38612f9e78e5a9f6179dd91352772ae1a9849246/google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268", size = 17238203, upload-time = "2025-10-08T18:43:39.665Z" } wheels = [ @@ -1667,12 +1646,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version < '3.14'" }, + { name = "google-crc32c", marker = "python_full_version < '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } wheels = [ @@ -2103,17 +2082,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "exceptiongroup" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -2131,18 +2110,18 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } wheels = [ @@ -2154,7 +2133,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -4428,14 +4407,14 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, + { name = "accessible-pygments", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "beautifulsoup4", marker = "python_full_version < '3.11'" }, + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "sphinx", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } wheels = [ @@ -4453,13 +4432,13 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, + { name = "accessible-pygments", marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "beautifulsoup4", marker = "python_full_version >= '3.11'" }, + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } wheels = [ @@ -4987,8 +4966,8 @@ requires-dist = [ { name = "tenacity", specifier = ">=8.2.2" }, { name = "urllib3", marker = "extra == 'all'", specifier = "<2.8.0" }, { name = "urllib3", marker = "extra == 'bedrock'", specifier = "<2.8.0" }, - { name = "voyageai", marker = "extra == 'all'", specifier = ">=0.2.2" }, - { name = "voyageai", marker = "extra == 'voyageai'", specifier = ">=0.2.2" }, + { name = "voyageai", marker = "extra == 'all'", specifier = ">=0.5.0" }, + { name = "voyageai", marker = "extra == 'voyageai'", specifier = ">=0.5.0" }, ] provides-extras = ["mcp", "mistralai", "openai", "cohere", "voyageai", "sentence-transformers", "langcache", "vertexai", "google-genai", "bedrock", "pillow", "sql-redis", "all", "ollama"] @@ -5379,10 +5358,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -5429,10 +5408,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -5482,7 +5461,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -5544,7 +5523,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -5724,8 +5703,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" } }, - { name = "sphinx" }, + { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } wheels = [ @@ -5743,8 +5722,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" } }, - { name = "sphinx" }, + { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } wheels = [ @@ -6493,24 +6472,23 @@ wheels = [ [[package]] name = "voyageai" -version = "0.3.7" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "aiolimiter" }, - { name = "ffmpeg-python" }, { name = "langchain-text-splitters" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "pydantic" }, { name = "requests" }, { name = "tenacity" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/16/1b46b3cd401e1717a68197c1fe336d7bb4e0a1833f8105e1738f5b1add05/voyageai-0.3.7.tar.gz", hash = "sha256:826cd97f97223f42b5babc5c459c9c80f3a8215ce5c0e007b0b276550f790d24", size = 26485, upload-time = "2025-12-16T18:43:05.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/d3/b96ecab961692dd55fc5b54fef916c2bc37a942ef33fcc937779d2aaa161/voyageai-0.5.0.tar.gz", hash = "sha256:ed2775fe9faeb96cc2b3931edc35d76185d19f34f1523d1f70535f0451126ae9", size = 37000, upload-time = "2026-07-10T20:13:14.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/64/89f6325666d6836979f94ac88b96fefc7527e02e61abc81359843585e088/voyageai-0.3.7-py3-none-any.whl", hash = "sha256:909f6c033001e5a3b3caf970525bf3614a1bfef9003cf3c3b68207dfdb53e86d", size = 34691, upload-time = "2025-12-16T18:43:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c4/732038d97a0ac02d690f295a9ee28f4d0553894478ca6944411428e544f7/voyageai-0.5.0-py3-none-any.whl", hash = "sha256:5d7e8dc3b74cac4646d4a835a65cf8c9764070c90353e322d84a91a16207e78b", size = 46561, upload-time = "2026-07-10T20:13:13.909Z" }, ] [[package]] From 1540de362f6c1fd7bd31fb215106e98e85fae334 Mon Sep 17 00:00:00 2001 From: fzowl Date: Sun, 16 Aug 2026 22:10:44 +0200 Subject: [PATCH 2/7] feat(voyageai): recognize voyage-4 embedding models in batch sizing Extend _get_batch_size to route the current voyage-4 family into the same token-limit tiers as their voyage-3.5 counterparts (voyage-4-lite -> 30, voyage-4 -> 10); voyage-4-large, voyage-code-4 and voyage-context-* keep the conservative default. Any VoyageAI model id already works since the vectorizer passes model strings through; this just refreshes the throughput heuristic and documents the current catalog on the class docstring. Add a parametrized test covering the batch-size tiers for current models. --- redisvl/utils/vectorize/voyageai.py | 18 +++++++++++++----- tests/integration/test_vectorizers.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index 662f211f4..9ad84f2f4 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -18,8 +18,14 @@ class VoyageAIVectorizer(BaseVectorizer): """The VoyageAIVectorizer class utilizes VoyageAI's API to generate embeddings for text and multimodal (text / image / video) data. - This vectorizer is designed to interact with VoyageAI's /embed and /multimodal_embed APIs, - requiring an API key for authentication. The key can be provided + This vectorizer is designed to interact with VoyageAI's /embed, /multimodal_embed, + and /contextualized_embed APIs. Any model identifier accepted by VoyageAI can be + passed via ``model`` - for example the general-purpose ``voyage-4-large`` / + ``voyage-4`` / ``voyage-4-lite`` family, domain models such as ``voyage-code-4``, + contextualized ``voyage-context-*`` models, and multimodal ``voyage-multimodal-*`` + models. See https://docs.voyageai.com/docs/embeddings for the current catalog. + + It requires an API key for authentication. The key can be provided directly in the `api_config` dictionary or through the `VOYAGE_API_KEY` environment variable. User must obtain an API key from VoyageAI's website (https://dash.voyageai.com/). Additionally, the `voyageai` python @@ -265,12 +271,14 @@ def _get_batch_size(self) -> int: """ if self.model in ["voyage-2", "voyage-02"]: return 72 - elif self.model in ["voyage-3-lite", "voyage-3.5-lite"]: + elif self.model in ["voyage-3-lite", "voyage-3.5-lite", "voyage-4-lite"]: return 30 - elif self.model in ["voyage-3", "voyage-3.5"]: + elif self.model in ["voyage-3", "voyage-3.5", "voyage-4"]: return 10 else: - return 7 # Default for other models + # Default for other models (e.g. voyage-3-large, voyage-4-large, + # voyage-code-*, voyage-finance-2, voyage-law-2, voyage-context-*). + return 7 def _validate_input( self, contents: list[Any], input_type: str | None, truncation: bool | None diff --git a/tests/integration/test_vectorizers.py b/tests/integration/test_vectorizers.py index 7f41302a9..2affaf108 100644 --- a/tests/integration/test_vectorizers.py +++ b/tests/integration/test_vectorizers.py @@ -775,3 +775,22 @@ def test_voyageai_multimodal_embed_many_does_not_collapse_inputs(): args, _ = client.multimodal_embed.call_args # Each item is wrapped as its own single-part multimodal input. assert args[0] == [["Ocean waves"], ["Forest trees"]] + + +@pytest.mark.parametrize( + "model, expected_batch_size", + [ + ("voyage-2", 72), + ("voyage-4-lite", 30), + ("voyage-3.5-lite", 30), + ("voyage-4", 10), + ("voyage-3.5", 10), + ("voyage-4-large", 7), + ("voyage-code-4", 7), + ("voyage-3-large", 7), + ], +) +def test_voyageai_batch_size_for_current_models(model, expected_batch_size): + """Current-generation models fall into batch-size tiers matching their token limits.""" + vectorizer, _, _ = _build_voyage_vectorizer(model) + assert vectorizer._get_batch_size() == expected_batch_size From 40ff9631dfbbe0548846177e178a9f663782d887 Mon Sep 17 00:00:00 2001 From: fzowl Date: Sun, 16 Aug 2026 22:24:18 +0200 Subject: [PATCH 3/7] fix(voyageai): gate contextualized auto-chunking to document inputs VoyageAI's contextualized_embed only accepts enable_auto_chunking=True when input_type="document"; the previous code hardcoded it for every call, so embedding a query against a voyage-context-* model (input_type="query", the standard RAG retrieval path) was rejected by the API. Enable auto-chunking (with chunk_size=32000) only on the document side and omit it for queries, which are embedded as a flat list[str] as-is. Add mocked sync/async tests for the query path, and cover voyage-4-nano and voyage-context-4 in the batch-size tiers to reflect the current catalog. --- redisvl/utils/vectorize/voyageai.py | 61 +++++++++++++++++---------- tests/integration/test_vectorizers.py | 32 ++++++++++++++ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index 9ad84f2f4..dd631c735 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -21,9 +21,10 @@ class VoyageAIVectorizer(BaseVectorizer): This vectorizer is designed to interact with VoyageAI's /embed, /multimodal_embed, and /contextualized_embed APIs. Any model identifier accepted by VoyageAI can be passed via ``model`` - for example the general-purpose ``voyage-4-large`` / - ``voyage-4`` / ``voyage-4-lite`` family, domain models such as ``voyage-code-4``, - contextualized ``voyage-context-*`` models, and multimodal ``voyage-multimodal-*`` - models. See https://docs.voyageai.com/docs/embeddings for the current catalog. + ``voyage-4`` / ``voyage-4-lite`` / ``voyage-4-nano`` family, domain models such + as ``voyage-code-4``, contextualized ``voyage-context-4`` / ``voyage-context-3`` + models, and multimodal ``voyage-multimodal-*`` models. + See https://docs.voyageai.com/docs/embeddings for the current catalog. It requires an API key for authentication. The key can be provided directly in the `api_config` dictionary or through the `VOYAGE_API_KEY` @@ -66,6 +67,12 @@ class VoyageAIVectorizer(BaseVectorizer): contents=["chunk one", "chunk two", "chunk three"], input_type="document" ) + # Retrieval queries use input_type="query"; auto-chunking is a + # document-only feature, so query inputs are embedded as-is. + context_query = context_vectorizer.embed( + content="your query text here", + input_type="query" + ) # Multimodal usage - requires Pillow and voyageai>=0.3.6 @@ -392,26 +399,39 @@ def _embed_many( except Exception as e: raise ValueError(f"Embedding texts failed: {e}") + def _context_embed_kwargs( + self, batch: list[str], input_type: str | None, kwargs: dict[str, Any] + ) -> dict[str, Any]: + """Build the kwargs for a ``contextualized_embed`` call. + + The batch is passed as a flat ``list[str]``. Auto-chunking is only valid + for ``input_type="document"`` (VoyageAI rejects it for queries), so it is + enabled with a large ``chunk_size`` on the document side - making each + input resolve to a single chunk - and left off otherwise. Either way the + first chunk per input is kept, yielding one embedding per requested item. + """ + enable_auto_chunking = input_type == "document" + call_kwargs: dict[str, Any] = { + "inputs": batch, + "model": self.model, + "input_type": input_type, + "enable_auto_chunking": enable_auto_chunking, + **kwargs, + } + if enable_auto_chunking: + call_kwargs["chunk_size"] = 32000 + return call_kwargs + def _embed_context_batch( self, batch: list[str], input_type: str | None, **kwargs ) -> list[list[float]]: - """Embed a batch with a contextualized (voyage-context-*) model. - - The batch is passed as a flat ``list[str]`` with auto-chunking enabled - and a large ``chunk_size`` so each input document resolves to a single - chunk, yielding exactly one embedding per requested content. - """ + """Embed a batch with a contextualized (voyage-context-*) model.""" # contextualized_embed is only present on recent voyageai clients; the # attr-defined ignore keeps mypy happy without vendored stubs. response = self._client.contextualized_embed( # type: ignore[attr-defined] - inputs=batch, - model=self.model, - input_type=input_type, - enable_auto_chunking=True, - chunk_size=32000, - **kwargs, + **self._context_embed_kwargs(batch, input_type, kwargs), ) - # Take the first chunk per document to keep one embedding per input. + # Take the first chunk per input to keep one embedding per input. return cast( "list[list[float]]", [result.embeddings[0] for result in response.results], @@ -425,14 +445,9 @@ async def _aembed_context_batch( See :meth:`_embed_context_batch` for details on the input format. """ response = await self._aclient.contextualized_embed( # type: ignore[attr-defined] - inputs=batch, - model=self.model, - input_type=input_type, - enable_auto_chunking=True, - chunk_size=32000, - **kwargs, + **self._context_embed_kwargs(batch, input_type, kwargs), ) - # Take the first chunk per document to keep one embedding per input. + # Take the first chunk per input to keep one embedding per input. return cast( "list[list[float]]", [result.embeddings[0] for result in response.results], diff --git a/tests/integration/test_vectorizers.py b/tests/integration/test_vectorizers.py index 2affaf108..456f7bc8a 100644 --- a/tests/integration/test_vectorizers.py +++ b/tests/integration/test_vectorizers.py @@ -745,6 +745,21 @@ def test_voyageai_context_embed_many_uses_contextualized_api(): assert "truncation" not in kwargs +def test_voyageai_context_query_disables_auto_chunking(): + """Query inputs must not enable auto-chunking (document-only per VoyageAI).""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-context-4") + + embedding = vectorizer.embed(content="find similar docs", input_type="query") + + assert len(embedding) == 4 + _, kwargs = client.contextualized_embed.call_args + assert kwargs["inputs"] == ["find similar docs"] + assert kwargs["input_type"] == "query" + assert kwargs["enable_auto_chunking"] is False + # chunk_size is only valid alongside auto-chunking, so it must be omitted. + assert "chunk_size" not in kwargs + + @pytest.mark.asyncio async def test_voyageai_context_aembed_many_uses_contextualized_api(): """Async context models route to contextualized_embed with auto-chunking.""" @@ -761,6 +776,21 @@ async def test_voyageai_context_aembed_many_uses_contextualized_api(): assert kwargs["chunk_size"] == 32000 +@pytest.mark.asyncio +async def test_voyageai_context_aquery_disables_auto_chunking(): + """Async query inputs must not enable auto-chunking.""" + vectorizer, _, aclient = _build_voyage_vectorizer("voyage-context-4") + + embedding = await vectorizer.aembed(content="find similar docs", input_type="query") + + assert len(embedding) == 4 + _, kwargs = aclient.contextualized_embed.call_args + assert kwargs["inputs"] == ["find similar docs"] + assert kwargs["input_type"] == "query" + assert kwargs["enable_auto_chunking"] is False + assert "chunk_size" not in kwargs + + def test_voyageai_multimodal_embed_many_does_not_collapse_inputs(): """Multimodal embed_many sends each content as its own input (no collapsing).""" vectorizer, client, _ = _build_voyage_vectorizer("voyage-multimodal-3.5") @@ -786,7 +816,9 @@ def test_voyageai_multimodal_embed_many_does_not_collapse_inputs(): ("voyage-4", 10), ("voyage-3.5", 10), ("voyage-4-large", 7), + ("voyage-4-nano", 7), ("voyage-code-4", 7), + ("voyage-context-4", 7), ("voyage-3-large", 7), ], ) From a2e5942abcd8ad052f74e165c2595006582b1399 Mon Sep 17 00:00:00 2001 From: fzowl Date: Sun, 16 Aug 2026 22:38:06 +0200 Subject: [PATCH 4/7] chore(voyageai): minimize uv.lock diff and note context-model caveats Regenerate uv.lock with the repo-pinned uv (0.12.3) so the lock diff is limited to the voyageai 0.3.7->0.5.0 bump and its dropped transitive deps (ffmpeg-python/future), instead of unrelated resolver churn. Document the contextualized-model caveats in the constructor: long documents auto-chunk but only the first chunk's embedding is kept, and truncation is not forwarded to contextualized_embed. --- redisvl/utils/vectorize/voyageai.py | 5 + uv.lock | 244 ++++++++++++++-------------- 2 files changed, 127 insertions(+), 122 deletions(-) diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index dd631c735..9dd551248 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -144,6 +144,11 @@ def __init__( Notes: - Multimodal models require voyageai>=0.3.6 to be installed for video embeddings, as well as ffmpeg installed on the system. Image embeddings require pillow to be installed. + - Contextualized (``voyage-context-*``) models require voyageai>=0.5.0. To keep the + one-embedding-per-input contract, a document longer than ``chunk_size`` (32000 tokens) + auto-chunks into multiple chunks but only the first chunk's embedding is kept; the rest + are dropped. ``truncation`` is not forwarded to the contextualized API (it does not + accept it), so it is silently ignored for these models. """ super().__init__(model=model, dtype=dtype, cache=cache) diff --git a/uv.lock b/uv.lock index 02b61e07f..f1abe419e 100644 --- a/uv.lock +++ b/uv.lock @@ -29,7 +29,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "caio", marker = "python_full_version < '3.11'" }, + { name = "caio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } wheels = [ @@ -47,7 +47,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "caio", marker = "python_full_version >= '3.11'" }, + { name = "caio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } wheels = [ @@ -955,43 +955,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] curand = [ - { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] [[package]] @@ -1446,11 +1446,11 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-auth", marker = "python_full_version >= '3.14'" }, - { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, - { name = "proto-plus", marker = "python_full_version >= '3.14'" }, - { name = "protobuf", marker = "python_full_version >= '3.14'" }, - { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } wheels = [ @@ -1459,8 +1459,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio", marker = "python_full_version >= '3.14'" }, - { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, + { name = "grpcio" }, + { name = "grpcio-status" }, ] [[package]] @@ -1474,11 +1474,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-auth", marker = "python_full_version < '3.14'" }, - { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, - { name = "proto-plus", marker = "python_full_version < '3.14'" }, - { name = "protobuf", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } wheels = [ @@ -1487,8 +1487,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio", marker = "python_full_version < '3.14'" }, - { name = "grpcio-status", marker = "python_full_version < '3.14'" }, + { name = "grpcio" }, + { name = "grpcio-status" }, ] [[package]] @@ -1517,18 +1517,18 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "docstring-parser", marker = "python_full_version >= '3.14'" }, - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version >= '3.14'" }, - { name = "google-auth", marker = "python_full_version >= '3.14'" }, - { name = "google-cloud-bigquery", marker = "python_full_version >= '3.14'" }, - { name = "google-cloud-resource-manager", marker = "python_full_version >= '3.14'" }, - { name = "google-cloud-storage", version = "3.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "google-genai", marker = "python_full_version >= '3.14'" }, - { name = "packaging", marker = "python_full_version >= '3.14'" }, - { name = "proto-plus", marker = "python_full_version >= '3.14'" }, - { name = "protobuf", marker = "python_full_version >= '3.14'" }, - { name = "pydantic", marker = "python_full_version >= '3.14'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "docstring-parser" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-resource-manager" }, + { name = "google-cloud-storage", version = "3.4.1", source = { registry = "https://pypi.org/simple" } }, + { name = "google-genai" }, + { name = "packaging" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/f3/b2a9417014c93858a2e3266134f931eefd972c2d410b25d7b8782fc6f143/google_cloud_aiplatform-1.148.1.tar.gz", hash = "sha256:75d605fba34e68714bd08e1e482755d0a6e3ae972805f809d088e686c30879e7", size = 10278758, upload-time = "2026-04-17T23:45:26.738Z" } wheels = [ @@ -1546,18 +1546,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "docstring-parser", marker = "python_full_version < '3.14'" }, - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"], marker = "python_full_version < '3.14'" }, - { name = "google-auth", marker = "python_full_version < '3.14'" }, - { name = "google-cloud-bigquery", marker = "python_full_version < '3.14'" }, - { name = "google-cloud-resource-manager", marker = "python_full_version < '3.14'" }, - { name = "google-cloud-storage", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "google-genai", marker = "python_full_version < '3.14'" }, - { name = "packaging", marker = "python_full_version < '3.14'" }, - { name = "proto-plus", marker = "python_full_version < '3.14'" }, - { name = "protobuf", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "docstring-parser" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-resource-manager" }, + { name = "google-cloud-storage", version = "3.10.1", source = { registry = "https://pypi.org/simple" } }, + { name = "google-genai" }, + { name = "packaging" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/a6/4d5bc1a25069a53383cda1b1da4957765be5a36ad1dce8d726fce557fb10/google_cloud_aiplatform-1.154.0.tar.gz", hash = "sha256:3cfb5afb9006ee202eab93ffc19aeeb111d9e62b574ebf80d7ab91aa9f463677", size = 11020790, upload-time = "2026-05-27T19:20:49.723Z" } wheels = [ @@ -1623,12 +1623,12 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "google-auth", marker = "python_full_version >= '3.14'" }, - { name = "google-cloud-core", marker = "python_full_version >= '3.14'" }, - { name = "google-crc32c", marker = "python_full_version >= '3.14'" }, - { name = "google-resumable-media", marker = "python_full_version >= '3.14'" }, - { name = "requests", marker = "python_full_version >= '3.14'" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" } }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/ef/7cefdca67a6c8b3af0ec38612f9e78e5a9f6179dd91352772ae1a9849246/google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268", size = 17238203, upload-time = "2025-10-08T18:43:39.665Z" } wheels = [ @@ -1646,12 +1646,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "google-auth", marker = "python_full_version < '3.14'" }, - { name = "google-cloud-core", marker = "python_full_version < '3.14'" }, - { name = "google-crc32c", marker = "python_full_version < '3.14'" }, - { name = "google-resumable-media", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" } }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } wheels = [ @@ -2082,17 +2082,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -2110,18 +2110,18 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } wheels = [ @@ -2133,7 +2133,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -4407,14 +4407,14 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "accessible-pygments", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "beautifulsoup4", marker = "python_full_version < '3.11'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "sphinx", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } wheels = [ @@ -4432,13 +4432,13 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "accessible-pygments", marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "beautifulsoup4", marker = "python_full_version >= '3.11'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } wheels = [ @@ -5358,10 +5358,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -5408,10 +5408,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -5461,7 +5461,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -5523,7 +5523,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -5703,8 +5703,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", marker = "python_full_version < '3.11'" }, + { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" } }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } wheels = [ @@ -5722,8 +5722,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx", marker = "python_full_version >= '3.11'" }, + { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" } }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } wheels = [ From 809b7bbc09fd2bd5512a2a65e35e581386bc562a Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 17 Aug 2026 00:25:53 +0200 Subject: [PATCH 5/7] docs(voyageai): describe contextualized embeddings as independent per-input The notebook claimed each chunk is embedded 'with awareness of the other chunks in the same request.' That is not what the integration does: every input string is sent as its own auto-chunked document, so inputs are embedded independently. Correct the guide to state this plainly, which matches the one-embedding-per-input contract and cache determinism the vectorizer relies on. --- docs/user_guide/04_vectorizers.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/04_vectorizers.ipynb b/docs/user_guide/04_vectorizers.ipynb index 5de236b81..43c672dac 100644 --- a/docs/user_guide/04_vectorizers.ipynb +++ b/docs/user_guide/04_vectorizers.ipynb @@ -602,7 +602,7 @@ "source": [ "#### Contextualized embeddings\n", "\n", - "VoyageAI's `voyage-context-*` models produce *contextualized* chunk embeddings: each chunk is embedded with awareness of the other chunks in the same request, which improves retrieval quality for chunked documents. The `VoyageAIVectorizer` automatically routes `voyage-context-*` models to the contextualized embeddings API — just pass your list of chunks to `embed_many` and you get one embedding back per chunk." + "VoyageAI's `voyage-context-*` models support *contextualized* chunk embeddings. The `VoyageAIVectorizer` automatically routes `voyage-context-*` models to the contextualized embeddings API: pass your list of chunk strings to `embed_many` and you get one embedding back per chunk. Each input string is sent as its own auto-chunked document, so the inputs are embedded **independently** (a chunk's embedding does not depend on the other strings in the list). This preserves the one-embedding-per-input contract and keeps the embeddings cache deterministic." ] }, { @@ -611,9 +611,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Contextualized embeddings (voyage-context-* models) embed each chunk with\n", - "# awareness of the surrounding chunks. Pass a list of chunk strings and the\n", - "# vectorizer returns one embedding per chunk. Requires voyageai>=0.5.0.\n", + "# Contextualized embeddings (voyage-context-* models). Each input string is sent\n", + "# as its own auto-chunked document and embedded independently, so the vectorizer\n", + "# returns exactly one embedding per input string. Requires voyageai>=0.5.0.\n", "context_vo = VoyageAIVectorizer(\n", " model=\"voyage-context-4\", # See https://docs.voyageai.com/docs/contextualized-chunk-embeddings\n", " api_config={\"api_key\": api_key},\n", From a9d20eee792e5d57fc5414eb4a45b45cb664edf8 Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 17 Aug 2026 00:26:00 +0200 Subject: [PATCH 6/7] feat(voyageai): token-aware batching for the text embedding path The plain text embed()/embed_many() path batched only by a fixed per-model item count, which under-fills small-token batches and can overshoot the per-request token budget for large inputs. Add token-aware batching that grows each batch until it would exceed either the item cap or the model's per-request token limit (VOYAGE_TOKEN_LIMITS, tracking the documented per-model limits), counting tokens with the VoyageAI tokenizer. A single input larger than the token limit is still sent alone rather than dropped. Context/multimodal paths keep item-count batching (auto-chunking / opaque media inputs). Also clarify the docstrings for contextualized models: inputs are embedded independently (no cross-input contextualization). Adds mocked unit tests (run under make test) covering the token boundary split, the oversized-single-input case, the item-count cap, and sync/async parity. --- redisvl/utils/vectorize/voyageai.py | 104 +++++++++++++++++++++++--- tests/integration/test_vectorizers.py | 67 +++++++++++++++++ 2 files changed, 161 insertions(+), 10 deletions(-) diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index 9dd551248..7fcdf50d3 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -13,6 +13,33 @@ # ignore that voyageai isn't imported # mypy: disable-error-code="name-defined" +# Per-request total token limits by model, used for token-aware batching of the +# plain text embedding path. Values track VoyageAI's documented per-request token +# limits (https://docs.voyageai.com/docs/embeddings). Unknown models fall back to +# the conservative default below. +VOYAGE_TOKEN_LIMITS: dict[str, int] = { + "voyage-2": 320_000, + "voyage-02": 320_000, + "voyage-3": 120_000, + "voyage-3-lite": 120_000, + "voyage-3-large": 120_000, + "voyage-3.5": 320_000, + "voyage-3.5-lite": 1_000_000, + "voyage-4": 320_000, + "voyage-4-lite": 1_000_000, + "voyage-4-large": 120_000, + "voyage-4-nano": 120_000, + "voyage-code-2": 120_000, + "voyage-code-3": 120_000, + "voyage-code-4": 120_000, + "voyage-finance-2": 120_000, + "voyage-law-2": 120_000, + "voyage-large-2": 120_000, + "voyage-large-2-instruct": 120_000, + "voyage-multilingual-2": 120_000, +} +DEFAULT_VOYAGE_TOKEN_LIMIT = 120_000 + class VoyageAIVectorizer(BaseVectorizer): """The VoyageAIVectorizer class utilizes VoyageAI's API to generate @@ -58,7 +85,9 @@ class VoyageAIVectorizer(BaseVectorizer): ) # Contextualized embeddings (voyage-context-* models) - requires voyageai>=0.5.0 - # Each input document is embedded with awareness of the others in the batch. + # Each input string is treated as its own document (auto-chunked) and + # embedded independently: inputs do not influence one another, which keeps + # the one-embedding-per-input contract and cache determinism intact. context_vectorizer = VoyageAIVectorizer( model="voyage-context-4", api_config={"api_key": "your-voyageai-api-key"} @@ -144,11 +173,17 @@ def __init__( Notes: - Multimodal models require voyageai>=0.3.6 to be installed for video embeddings, as well as ffmpeg installed on the system. Image embeddings require pillow to be installed. - - Contextualized (``voyage-context-*``) models require voyageai>=0.5.0. To keep the - one-embedding-per-input contract, a document longer than ``chunk_size`` (32000 tokens) - auto-chunks into multiple chunks but only the first chunk's embedding is kept; the rest - are dropped. ``truncation`` is not forwarded to the contextualized API (it does not - accept it), so it is silently ignored for these models. + - Contextualized (``voyage-context-*``) models require voyageai>=0.5.0. Each input + string is sent as its own document with auto-chunking, so inputs are embedded + independently (no cross-input contextualization) and the one-embedding-per-input + contract and cache determinism are preserved. A document longer than ``chunk_size`` + (32000 tokens) auto-chunks into multiple chunks but only the first chunk's embedding + is kept; the rest are dropped. ``truncation`` is not forwarded to the contextualized + API (it does not accept it), so it is silently ignored for these models. + - The plain text embedding path (``embed``/``embed_many`` for non-context, + non-multimodal models) uses token-aware batching: inputs are grouped into requests + bounded by both the per-model item cap and the model's per-request token limit, so + large inputs are packed efficiently without exceeding VoyageAI's token budget. """ super().__init__(model=model, dtype=dtype, cache=cache) @@ -276,10 +311,14 @@ def _set_model_dims(self) -> int: def _get_batch_size(self) -> int: """ - Determine the appropriate batch size based on the model being used. + Determine the per-request item cap for the current model. + + For the plain text path this is combined with the model's per-request + token limit (see :meth:`_batchify_by_tokens`); it is the sole bound for + the context/multimodal paths. Returns: - int: Recommended batch size for the current model + int: Recommended maximum number of items per request """ if self.model in ["voyage-2", "voyage-02"]: return 72 @@ -292,6 +331,37 @@ def _get_batch_size(self) -> int: # voyage-code-*, voyage-finance-2, voyage-law-2, voyage-context-*). return 7 + def _token_limit(self) -> int: + """Per-request token budget for the current model (token-aware batching).""" + return VOYAGE_TOKEN_LIMITS.get(self.model, DEFAULT_VOYAGE_TOKEN_LIMIT) + + def _count_tokens(self, text: str) -> int: + """Count tokens for a single text using VoyageAI's tokenizer.""" + return len(self._client.tokenize([text], model=self.model)[0]) + + def _batchify_by_tokens(self, texts: list[str], batch_size: int): + """Yield batches of texts bounded by both item count and token budget. + + Batches are grown until adding the next text would exceed either the + per-model item cap (``batch_size``) or the model's per-request token limit. + A single text larger than the token limit is still yielded on its own + (never dropped), matching VoyageAI's own client batching behavior. + """ + max_tokens = self._token_limit() + batch: list[str] = [] + batch_tokens = 0 + for text in texts: + n_tokens = self._count_tokens(text) + if batch and ( + len(batch) >= batch_size or batch_tokens + n_tokens > max_tokens + ): + yield batch + batch, batch_tokens = [], 0 + batch.append(text) + batch_tokens += n_tokens + if batch: + yield batch + def _validate_input( self, contents: list[Any], input_type: str | None, truncation: bool | None ): @@ -376,9 +446,16 @@ def _embed_many( if batch_size is None: batch_size = self._get_batch_size() + # The plain text path uses token-aware batching; context/multimodal paths + # stay on fixed item-count batching (auto-chunking / opaque media inputs). + if self.is_context or self.is_multimodal: + batches: Any = self.batchify(contents, batch_size) + else: + batches = self._batchify_by_tokens(contents, batch_size) + try: embeddings: list[Any] = [] - for batch in self.batchify(contents, batch_size): + for batch in batches: if self.is_context: embeddings.extend( self._embed_context_batch(batch, input_type, **kwargs) @@ -514,9 +591,16 @@ async def _aembed_many( if batch_size is None: batch_size = self._get_batch_size() + # The plain text path uses token-aware batching; context/multimodal paths + # stay on fixed item-count batching (auto-chunking / opaque media inputs). + if self.is_context or self.is_multimodal: + batches: Any = self.batchify(contents, batch_size) + else: + batches = self._batchify_by_tokens(contents, batch_size) + try: embeddings: list[Any] = [] - for batch in self.batchify(contents, batch_size): + for batch in batches: if self.is_context: embeddings.extend( await self._aembed_context_batch(batch, input_type, **kwargs) diff --git a/tests/integration/test_vectorizers.py b/tests/integration/test_vectorizers.py index 456f7bc8a..eaf42241e 100644 --- a/tests/integration/test_vectorizers.py +++ b/tests/integration/test_vectorizers.py @@ -691,15 +691,22 @@ def _mm_embed(inputs, model, input_type=None, truncation=True, **kwargs): resp.embeddings = [[0.1] * dims for _ in inputs] return resp + def _tokenize(texts, model=None): + # One token per whitespace-delimited word, so tests can control token + # counts by word count (mirrors voyageai's tokenize return shape). + return [text.split() for text in texts] + client = MagicMock() client.contextualized_embed.side_effect = _ctx_embed client.embed.side_effect = _embed client.multimodal_embed.side_effect = _mm_embed + client.tokenize.side_effect = _tokenize aclient = MagicMock() aclient.contextualized_embed = AsyncMock(side_effect=_ctx_embed) aclient.embed = AsyncMock(side_effect=_embed) aclient.multimodal_embed = AsyncMock(side_effect=_mm_embed) + aclient.tokenize.side_effect = _tokenize return client, aclient @@ -807,6 +814,66 @@ def test_voyageai_multimodal_embed_many_does_not_collapse_inputs(): assert args[0] == [["Ocean waves"], ["Forest trees"]] +# --- VoyageAI token-aware batching tests (mocked, no API key required) --- + + +def test_voyageai_token_aware_batching_splits_on_token_limit(): + """Plain text batches split when the per-request token budget is reached.""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-3-large") + # 3 tokens per text; a 7-token budget fits two texts (6) but not three (9). + vectorizer._token_limit = lambda: 7 + client.embed.reset_mock() + + texts = ["a a a", "b b b", "c c c"] + embeddings = vectorizer.embed_many(contents=texts, input_type="document") + + assert len(embeddings) == 3 + batches = [call.args[0] for call in client.embed.call_args_list] + assert batches == [["a a a", "b b b"], ["c c c"]] + + +def test_voyageai_token_aware_batching_oversized_text_goes_alone(): + """A single text over the token budget is still sent alone, not dropped.""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-3-large") + vectorizer._token_limit = lambda: 5 + client.embed.reset_mock() + + texts = ["a a a a a a a a", "b b"] # 8 tokens (> budget), then 2 tokens + embeddings = vectorizer.embed_many(contents=texts, input_type="document") + + assert len(embeddings) == 2 + batches = [call.args[0] for call in client.embed.call_args_list] + assert batches == [["a a a a a a a a"], ["b b"]] + + +def test_voyageai_token_aware_batching_respects_item_cap(): + """The per-model item cap still bounds a batch when tokens are plentiful.""" + vectorizer, client, _ = _build_voyage_vectorizer("voyage-4") # item cap = 10 + vectorizer._token_limit = lambda: 10_000_000 # effectively unbounded + client.embed.reset_mock() + + texts = ["x"] * 25 # 1 token each + vectorizer.embed_many(contents=texts, input_type="document") + + batches = [call.args[0] for call in client.embed.call_args_list] + assert [len(b) for b in batches] == [10, 10, 5] + + +@pytest.mark.asyncio +async def test_voyageai_token_aware_batching_async_splits_on_token_limit(): + """Async plain text batches split on the token budget too (sync/async parity).""" + vectorizer, _, aclient = _build_voyage_vectorizer("voyage-3-large") + vectorizer._token_limit = lambda: 7 + aclient.embed.reset_mock() + + texts = ["a a a", "b b b", "c c c"] + embeddings = await vectorizer.aembed_many(contents=texts, input_type="document") + + assert len(embeddings) == 3 + batches = [call.args[0] for call in aclient.embed.call_args_list] + assert batches == [["a a a", "b b b"], ["c c c"]] + + @pytest.mark.parametrize( "model, expected_batch_size", [ From 7024c585c485feffdc81520d0953077edb75ed48 Mon Sep 17 00:00:00 2001 From: fzowl Date: Mon, 17 Aug 2026 22:04:49 +0200 Subject: [PATCH 7/7] fix(voyageai): default contextualized input_type to document Contextualized (voyage-context-*) embedding failed on the default path where input_type is omitted: VoyageAI rejects a flat list[str] with no input_type, and callers such as SemanticRouter and SemanticCache never set one. Treat any non-query input (including the omitted default) as a document so auto-chunking stays enabled with chunk_size=32000, and keep skipping auto-chunking only for explicit queries. --- redisvl/utils/vectorize/voyageai.py | 18 ++++++++++----- tests/integration/test_vectorizers.py | 33 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/redisvl/utils/vectorize/voyageai.py b/redisvl/utils/vectorize/voyageai.py index 7fcdf50d3..a681b18e6 100644 --- a/redisvl/utils/vectorize/voyageai.py +++ b/redisvl/utils/vectorize/voyageai.py @@ -487,16 +487,22 @@ def _context_embed_kwargs( """Build the kwargs for a ``contextualized_embed`` call. The batch is passed as a flat ``list[str]``. Auto-chunking is only valid - for ``input_type="document"`` (VoyageAI rejects it for queries), so it is - enabled with a large ``chunk_size`` on the document side - making each - input resolve to a single chunk - and left off otherwise. Either way the - first chunk per input is kept, yielding one embedding per requested item. + for ``input_type="document"`` (VoyageAI rejects it for queries), so every + non-query input - including the default where ``input_type`` is omitted - + is treated as a document and chunked with a large ``chunk_size``, making + each input resolve to a single chunk. Explicit queries skip chunking and + are sent as a flat ``list[str]`` as-is. Either way the first chunk per + input is kept, yielding one embedding per requested item. """ - enable_auto_chunking = input_type == "document" + enable_auto_chunking = input_type != "query" + # Auto-chunking requires input_type="document"; a flat list[str] with no + # type is rejected, so default (None) callers - e.g. SemanticRouter and + # SemanticCache, which never set input_type - resolve to "document". + effective_input_type = "document" if enable_auto_chunking else input_type call_kwargs: dict[str, Any] = { "inputs": batch, "model": self.model, - "input_type": input_type, + "input_type": effective_input_type, "enable_auto_chunking": enable_auto_chunking, **kwargs, } diff --git a/tests/integration/test_vectorizers.py b/tests/integration/test_vectorizers.py index 8fc0402de..3880747d2 100644 --- a/tests/integration/test_vectorizers.py +++ b/tests/integration/test_vectorizers.py @@ -740,6 +740,39 @@ def test_voyageai_context_query_disables_auto_chunking(): assert "chunk_size" not in kwargs +def test_voyageai_context_default_input_type_enables_auto_chunking(): + """Omitted input_type must default to document + auto-chunking. + + SemanticRouter / SemanticCache call embed(_many) without input_type; a flat + list[str] with no type is rejected by VoyageAI, so the default must resolve + to a document (auto-chunking on) rather than passing input_type=None. + """ + vectorizer, client, _ = _build_voyage_vectorizer("voyage-context-4") + + embeddings = vectorizer.embed_many(contents=["chunk one", "chunk two"]) + + assert len(embeddings) == 2 + _, kwargs = client.contextualized_embed.call_args + assert kwargs["inputs"] == ["chunk one", "chunk two"] + assert kwargs["input_type"] == "document" + assert kwargs["enable_auto_chunking"] is True + assert kwargs["chunk_size"] == 32000 + + +@pytest.mark.asyncio +async def test_voyageai_context_adefault_input_type_enables_auto_chunking(): + """Async: omitted input_type must default to document + auto-chunking.""" + vectorizer, _, aclient = _build_voyage_vectorizer("voyage-context-4") + + embeddings = await vectorizer.aembed_many(contents=["chunk one", "chunk two"]) + + assert len(embeddings) == 2 + _, kwargs = aclient.contextualized_embed.call_args + assert kwargs["input_type"] == "document" + assert kwargs["enable_auto_chunking"] is True + assert kwargs["chunk_size"] == 32000 + + @pytest.mark.asyncio async def test_voyageai_context_aembed_many_uses_contextualized_api(): """Async context models route to contextualized_embed with auto-chunking."""