feat(voyageai): add contextualized embeddings and refresh model support - #692
Open
fzowl wants to merge 8 commits into
Open
feat(voyageai): add contextualized embeddings and refresh model support#692fzowl wants to merge 8 commits into
fzowl wants to merge 8 commits into
Conversation
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).
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.
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.
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.
…-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.
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.
vishal-bala
self-requested a review
August 17, 2026 09:28
# Conflicts: # pyproject.toml
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 7024c58. Configure here.
|
|
||
| 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]) |
There was a problem hiding this comment.
Tokenize required for text embeds
Medium Severity
Plain-text embedding now calls Client.tokenize for every input via _batchify_by_tokens, including the single-string dimension probe in _set_model_dims. Voyage loads that tokenizer from Hugging Face for voyageai/{model}, so init and embed_many fail if the Hub is unreachable or no tokenizer exists for the model id, even when the Voyage embed API itself would succeed.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 7024c58. Configure here.
vishal-bala
added a commit
that referenced
this pull request
Aug 19, 2026
… retry errors (#696) ## Why The Azure OpenAI resource CI used is no longer available, with no replacement yet. The six live `AzureOpenAITextVectorizer` tests failed on every run, and because the redis-py matrix and notebook jobs both declare `needs: service-tests`, one unavailable provider blocked all 31 downstream jobs — including a matrix that runs `make test` with no API keys and has zero Azure exposure. `AzureOpenAITextVectorizer` also had no offline tests at all, unlike its `ollama` and `google_genai` siblings. Gating the live tests without adding any would have taken its coverage to zero. ## What changed | | | | --- | --- | | `tests/integration/test_vectorizers.py` | `skipif` gate on the Azure entry in `_vectorizer_params` and `_dtype_params`. `_non_supported_dtype_params` is deliberately **not** gated — `dtype` is validated in `super().__init__` before any client is built, so it passes with no network. | | `tests/unit/test_azure_openai_vectorizer.py` | New. 36 tests + 1 SDK-contract test that skips when `openai` is absent. Fake `openai` module via `types.ModuleType`, following the two existing sibling files. | | `docs/user_guide/04_vectorizers.ipynb` | `# NBVAL_SKIP` on the three Azure cells. Required: without the credentials, `os.environ.get(...) or getpass.getpass(...)` falls through to `getpass`, which has no stdin under nbval. The pre-existing `_azure_configured` guard cannot help — it is computed *after* the prompt. | | `redisvl/utils/vectorize/text/azureopenai.py` | Two fixes, below. | | `.github/workflows/test.yml` | Comments only. No structural change; `needs: service-tests` is untouched. | **Self-healing:** the gate keys on `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY` and `OPENAI_API_VERSION` all being truthy. An unset secret expands to `""`, so the tests skip today and re-enable with no code change once the credentials are available again. The `secrets.AZURE_OPENAI_*` lines stay wired up for exactly that reason. The notebook markers are unconditional and *do* have to be removed by hand. ## Behavior changes Both are in `azureopenai.py` and both were found while writing the tests. Worth a look for release notes: 1. **`reraise=True` on the four `@retry` decorators.** Retry exhaustion now surfaces the underlying `ValueError` instead of `tenacity.RetryError`. All four docstrings already promised `Raises: ValueError`, so this fixes a contract violation. Matches `googlegenai.py` and `ollama.py`; nothing in `redisvl/` catches `RetryError`. 2. **`_initialize_clients` no longer mutates the caller's `api_config`**, and a *partial* `api_config` now falls back to the environment per key as the class docstring promises, instead of raising a bare `KeyError`. Uses the `dict(api_config or {})` pattern already in `googlegenai.py`. ## Coverage: honestly **Lost** — six live IDs stop running in CI: `test_default_dtype[Azure]`, `test_vectorizer_dtype_assignment[Azure]`, and the four `vectorizer`-fixture tests. Realistically that goes to zero with or without this PR, since the resource is unavailable; the gate removes a false signal rather than coverage. What the mocks cannot catch is `openai` SDK drift, deployment-vs-model semantics, real authentication, and true dimensionality. **Mitigations** — the `embeddings.create` → `response.data[i].embedding` shape still has *live* coverage through `OpenAITextVectorizer`, which shares it and is unaffected. And `test_fake_matches_real_openai_sdk_contract` asserts the fake's shape against the installed SDK, so a reshape breaks this file too. **Gained** — several things the live tests never covered: the batching loop (they used 2 texts at default `batch_size=10`, so it never ran), sync/async client symmetry, the three sequential credential guards, `dtype` buffer widths, and that `__init__` fires one billable embed request. ## Verification - Gate fires with the env vars unset **and** empty-string (the actual CI state) → 6 skipped; un-fires when set → tests reach the network, which is correct, since the gate is credential-presence not reachability. - New file: 36 passed + 1 skipped in ~2.7s. Speed is the assertion — a missed `retry.sleep` patch would cost 30s+, and a guard test pins that exactly four methods are retry-decorated. - `black`, `isort`, `mypy`, `codespell` clean. Notebook diff touches only `source` arrays. ## Notes for review - A proposed message-redaction fix was **reverted** after verifying against the installed SDK that the API key is not reachable in provider exception messages — `openai` builds them from `response.status_code` and the response body, and the Azure key travels only as the `api-key` header. It would also have made Azure the only one of ~21 sites not interpolating `{e}`. - Merge-order note: #680 also touches `azureopenai.py`, three lines above the first `reraise=True`. Adjacent, not overlapping. A comment here refers to `tests/unit/test_vectorizer_dim_errors.py` as added by that PR. - #692 touches the same two test/notebook files but appends well clear of these edits. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Behavior changes in AzureOpenAITextVectorizer (api_config copying, per-key env fallback, retry reraise) affect callers relying on mutation or RetryError; CI gating is low risk but live Azure coverage drops until credentials return. > > **Overview** > Unblocks CI after the Azure OpenAI deployment used in tests went away by **skipping live Azure integration tests** when `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, and `OPENAI_API_VERSION` are unset, while **adding a large offline unit suite** with a fake `openai` module. **Notebook validation** no longer hangs on `getpass` because the three Azure cells in `04_vectorizers.ipynb` are marked `# NBVAL_SKIP`. > > **`AzureOpenAITextVectorizer`** now copies `api_config` before popping credentials (no caller mutation), resolves each credential from config **or** env per key, and sets **`reraise=True`** on embed retries so failures surface as `ValueError` instead of `tenacity.RetryError`. Workflow comments document how live tests and notebooks re-enable when secrets return. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 030769c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What
Refreshes the VoyageAI integration against the current VoyageAI catalog (text, contextualized, multimodal, reranker).
voyage-context-*models (e.g.voyage-context-4) route to VoyageAI's contextualized embeddings API.VoyageAIVectorizeraccepts a flatlist[str]of chunks and callscontextualized_embedwithenable_auto_chunking=Trueandchunk_size=32000, so each input document resolves to a single chunk andembed_manykeeps its one-embedding-per-input contract (and cache alignment).embed_manyfix: previously the multimodal branch wrapped the whole batch as[batch], which VoyageAI treats as a single multimodal input with multiple parts — collapsing N requested contents into one embedding. Each content is now sent as its own input ([[item] for item in batch]), so the returned count matches the request count._get_batch_sizenow routes thevoyage-4family into the same token-limit tiers as theirvoyage-3.5counterparts (voyage-4-lite→ 30,voyage-4→ 10);voyage-4-large,voyage-code-4andvoyage-context-*keep the conservative default, matching their 120K-token tier. Every VoyageAI model id already works because the vectorizer passesmodelstrings straight through — this refreshes the throughput heuristic and documents the current families (voyage-4-*,voyage-code-4,voyage-context-*,voyage-multimodal-*) on the class docstring.voyageaifloor to>=0.5.0(latest), where contextualized auto-chunking (enable_auto_chunking,chunk_size) and thevoyage-4series are available.rerank-2.5for the reranker and avoyage-context-4contextualized example for the vectorizer guide.Why
Contextualized (
voyage-context-*) chunk embeddings were not exposed, current models weren't reflected in the docs or batch heuristics, and the multimodal batching bug meantembed_many(["a", "b"])on a multimodal model could return a single embedding.Notes
make test.contextualized_embedcalls carry# type: ignore[attr-defined]so type-checking works without vendored stubs; the mypy config usesignore_missing_importsand does not setwarn_unused_ignores, so the ignores stay valid whether or not the client is typed.requires_api_keys.uv.lockshows marker churn beyond thevoyageaibump (nvidia/grpcio/etc. environment markers) because it was regenerated with a newer uv resolver than the one that produced the base lock. It pinsvoyageai==0.5.0and is self-consistent, and CI installs viauv sync --frozen. Happy to regenerate it against the repo's pinned uv (0.12.3) for a minimal lock diff if preferred.mypy redisvl/utils/vectorize/voyageai.pyis clean,isort --profile black+black --target-version py311report no changes acrossredisvl/andtests/, and the mocked VoyageAI routing/batch-size tests pass againstvoyageai==0.5.0. The fullpytestinvocation pulls in a session-scoped, Docker-backed Redis fixture that cannot start in this environment, so the mocked tests were also executed standalone (bypassing that fixture) and pass end-to-end. GitHub Actions is not enabled on this fork, so upstream CI /make checkshould still be the gate on merge.Note
Medium Risk
Changes core embedding batching and API routing for VoyageAI (including a behavioral fix for multimodal embed_many); dependency floor bump may affect installs, but behavior is covered by new mocked tests.
Overview
VoyageAIVectorizergains first-class support forvoyage-context-*models viacontextualized_embed, with auto-chunking for documents (andinput_typedefaulting to document when omitted), one embedding per input, and sync/async parity. Plain-textembed_manynow batches by per-model token limits as well as item caps;voyage-4*models are included in the updated batch-size tiers.Multimodal
embed_manyno longer wraps the whole batch as a single multi-part input—each item is sent separately so result count matches the request.The optional
voyageaidependency is raised to>=0.5.0(lockfile pins 0.5.0). User guides add a contextualized embedding example and switch the reranker demo torerank-2.5. Mocked tests cover context routing, token batching, multimodal batching, and batch-size tiers.Reviewed by Cursor Bugbot for commit 7024c58. Bugbot is set up for automated code reviews on this repo. Configure here.