v5.6.0 - Atlas Embeddings, Streamed Media, Retrieval Fixes
Feature Release
An embeddings and retrieval release: the Voyage provider now targets MongoDB's Atlas Embedding and Reranking API alongside Voyage's own, adds video embedding and four new models, and streams local media into requests instead of buffering it. Validating that work against the live API surfaced three retrieval defects, all of which are fixed here.
Breaking Changes
- BREAKING: The Voyage provider's default model moves from
voyage-3tovoyage-3.5.voyage-3is retired from the Atlas endpoint, so the previous default caused an Atlas key to fail at construction whenever no model was named. Vectors from the two models are not comparable. Migration: code that relied on the default should pinmodel: "voyage-3"explicitly to keep existing embeddings valid, or re-embed against the new default. A:vectorproperty that declaresmodel:is unaffected, because the new binding audit catches the mismatch before any request rather than letting the two mix silently.
Changes
Voyage models are reachable through MongoDB's Atlas endpoint
- NEW: The same Voyage models are served by Voyage's own API and by MongoDB's Atlas Embedding and Reranking API. The wire contract is identical but the credentials are not interchangeable, and each host rejects the other's key. An Atlas model API key is recognized by its prefix and routed to
https://ai.mongodb.com/v1automatically. Passendpoint: :atlasorendpoint: :voyageto be explicit, or abase_url:to override both. A named endpoint that contradicts an explicitbase_urlis refused rather than silently reconciled, so a credential is never sent to a host the caller did not intend.#endpointand#atlas?report the resolved target. - NEW: Added
voyage-3.5,voyage-3.5-lite,voyage-code-2(1536-dim), andvoyage-multimodal-3.5. - NEW: Models the Atlas endpoint does not serve are refused at construction with a current replacement named. They remain valid against Voyage's own API.
- NEW:
voyage-4-nanois open-weight and served by neither hosted endpoint. It is tracked separately inSELF_HOSTED_ONLY_MODELSand refused against both hosts with a message pointing at a self-hostedbase_url:orParse::Embeddings::LocalHTTP, rather than failing as an opaque provider error.
Video embedding, and local media that never loads into memory
- NEW:
Parse::Embeddings::Voyage#embed_videoembeds video throughvoyage-multimodal-3.5, the only model that accepts it. Text, image, and video vectors share one space, so a stored text vector is comparable against a video vector without re-embedding.#modalitiesreports[:text, :image, :video]for that model and[:text, :image]forvoyage-multimodal-3. - NEW:
Parse::Embeddings::Provider#embed_videojoins#embed_imagein the base protocol with the sameNotImplementedErrordefault, so video is a declared capability rather than one adapter's method.#supports_modality?answers the capability question without rescuing. - NEW:
Parse::Embeddings::MediaFilewraps a local image or video and streams it into the request body. Serializing media withto_jsoncosts roughly 2.4 times the file size in resident memory, counting the raw bytes, the base64 copy, and the serialized document. That is enough to exhaust a small dyno on a single moderate video.MediaFilereads only a 16-byte header at construction, andParse::Embeddings::StreamingBodybase64-encodes the payload into the socket in fixed-size chunks, so peak memory is bounded by the chunk size no matter how large the file is and nothing spills to disk.Content-Lengthis computed exactly, avoiding chunked transfer encoding, and the body replays byte-identically on retry. Passing a URL instead keeps the SDK out of the transfer entirely.
The model dimension table was wrong for the v4 family
- FIXED: The entire v4 family defaults to 1024.
voyage-4-large's 2048 andvoyage-4-lite's 512 were recorded as native widths when they are Matryoshka options reached only by requesting them. Because the provider validates the returned vector width against the declared one, both models raisedParse::Embeddings::InvalidResponseErroron every call.voyage-4-nanois 1024, andvoyage-finance-2carries a 32,000-token context rather than 16,000. - CHANGED: The coarse "Matryoshka-capable models" gate is replaced by per-model
MODEL_SUPPORTED_DIMENSIONS. Any width on a model's ladder is accepted, including one wider than its default, which the old rule rejected as exceeding the native width. A width off the ladder is refused with the supported set named.output_dimensionis sent whenever the configured width differs from the model's default, sovoyage-4-liteat 512 andvoyage-4-largeat 2048 both work. - FIXED: Video validation no longer accepts containers the provider rejects. MP4 is the only format Voyage supports, and WebM and QuickTime payloads are refused by the API, so both are out of the default allowlist. An
ftypbox no longer implies MP4 on its own, because QuickTime and the audio-only profiles share the ISO base media container, so major brands are matched explicitly and an unrecognized brand is refused rather than assumed. Apple's audio-onlyM4Abrand is excluded, closing a type confusion in which an audio file passed as video. - NEW:
Parse::Embeddings.max_media_bytescaps streamed media per file, defaulting to the 20 MB Voyage documents. Streaming already prevents an oversized file from exhausting memory, but the provider still rejects it, so failing locally turns a wasted upload into an immediate error. The Voyage adapter enforces the 20 MB ceiling independently, so raising the global knob for another provider cannot push an oversized payload onto Voyage.
Vector search no longer returns fewer results than requested
- FIXED:
$vectorSearchset itslimittok, but Atlas applies that limit before the SDK's ACL$match,protectedFieldsredaction, pointer-field filtering, and any caller-suppliedfilter. A scoped caller who could read 2 of the top 10 documents asked for 10 and received 2, even when hundreds of readable matches existed further down the ranking. The search now requests a wider internal candidate window, applies every enforcement layer, and only then trims tok. The window is raised only when something can actually drop rows, so a master-key call with no filter keeps its previous one-for-one cost. Acandidate_limit:option onVectorSearch.searchandfind_similartunes the window for principals whose visibility is unusually narrow. HNSW width stays anchored tok, so the wider window does not widen the ANN search. - FIXED: Hybrid search separates the rows each branch retains for fusion from the rows Atlas considers before ACL. Conflating them meant the branch limit was passed as the vector branch's
kand then multiplied a second time by the plain search's own window derivation. A hybridk: 10asked Atlas for 1,000 rows instead of the intended 100, an explicitcandidate_limit: 250became 2,500 on the client path while staying 250 on the native path, and anykabove 100 produced a branchkbeyondVectorSearch::MAX_Kthat failed outright. The fusion depth is now bounded byMAX_Kand the candidate window by Atlas's 10,000 ceiling, with both paths using the same window. Acandidate_limitoutside that range is refused rather than clamped. - FIXED: The native
$rankFusionpipeline trimmed tokin a$limitstage that runs after its ACL$match, reintroducing the underfill the candidate window exists to prevent. It now limits to the candidate window and trims tokonce enforcement has run. - NEW:
parse.vector_search.searchandparse.vector_search.hybridnotifications report the candidate window, attrition, and returned counts, so an underfill is observable rather than silent. The counts are named for where they are measured, because obtaining a true pre-$matchcount would require a$facet.
Vector properties are checked against the provider actually registered
- FIXED: A
:vectorproperty'smodel:was recorded and never enforced. Because models in the same family usually share a width (voyage-3andvoyage-3.5are both 1024), swapping the registered provider's model silently mixed incomparable vectors into one index. There was no error, just degraded recall, repairable only by re-embedding.dimensions:was verified, but only against a vector the provider had already returned and billed for. Both are now checked byParse::Embeddings::BindingAuditbefore any request is issued, on the managed-write path and the query-embedding path alike. The audit runs ahead of the digest short-circuit so an unchanged record still surfaces a drifted binding, and it fails closed: a provider that cannot report#model_nameor#dimensionsis refused rather than skipped, because a declaration that cannot be verified is not a declaration that has been satisfied. - NEW:
Parse::Embeddings::BindingAudit.audit_all!and.audit_all_or_raise!check every declared binding at once, for a boot-time or CI gate rather than waiting for the first save that happens to touch one. - NEW:
:vectorproperties validatesimilarity:against the functions Atlas accepts (euclidean,cosine,dotProduct) at declaration time instead of surfacing a typo as an index error later. - NEW: A
:vectorproperty wider than the Atlas vectorSearch index cap is refused unless it declaressearchable: false.Parse::Vectortolerates up to 16384 dimensions while an Atlas index caps at 8192, so such a property was previously declarable, storable, and permanently unsearchable, with the failure appearing only at query time.searchable: falsenow makes a property genuinely storage-only: excluded fromfind_similarandhybrid_searchfield resolution, refused with an explanation when named directly, and rejected byagent_searchableat class load.
Live provider contract tests
- NEW:
rake test:contractruns live provider contract tests that pin request routing, native dimensions, the Matryoshka ladder, accepted media, size limits, model availability, and response shape. They skip unlessVOYAGE_CONTRACT_KEYis set and are excluded from bothrake testandrake test:unit, so no ordinary run becomes billable because a key happens to be exported. Probes that assert a refusal issue raw requests rather than going through the SDK, since a local guard asserted against itself proves nothing about the contract it encodes. Infrastructure conditions are classified rather than guessed at: a bad credential fails, while rate limiting and 5xx responses skip as "contract not determined".VOYAGE_CONTRACT_RPMpaces the suite per model for low-quota keys.
Behavior Notes
- Audio is not offered by any Voyage model, and neither PDF nor DOCX is accepted as a content type. Render document pages to images and embed those. The SDK does not perform that conversion.
- The candidate-window overfetch is a mitigation, not a completeness guarantee. Atlas caps the candidate window, so a sufficiently selective ACL can still underfill. Deterministic fill would require the authorization predicate to run inside the Atlas prefilter rather than after it. The post-search
$matchremains the enforcement boundary regardless. - Scoped hybrid calls now fetch a wider per-branch window than before. This is correct given the underfill it fixes, but it is a real cost increase worth measuring.
- Mocked tests assert what the SDK believes the API does, so they cannot detect provider drift. Every dimension and media-format correction in this release was invisible to a fully green mocked suite.
Code Example
# Endpoint inferred from the key prefix, so no base_url is needed.
provider = Parse::Embeddings::Voyage.new(
api_key: ENV.fetch("ATLAS_MODEL_API_KEY"),
model: "voyage-multimodal-3.5",
)
provider.endpoint # => :atlas
provider.modalities # => [:text, :image, :video]
# Local media streams into the request; the bytes are never resident.
provider.embed_image([Parse::Embeddings::MediaFile.image("page.png")])
provider.embed_video([Parse::Embeddings::MediaFile.video("demo.mp4")])
# A URL keeps the SDK out of the transfer entirely, since the provider fetches it.
provider.embed_image(["https://cdn.example.com/page.png"])
# Widen the candidate window for a principal that can read only a small
# fraction of the class, so ACL attrition does not return fewer than k.
Post.find_similar(text: "quarterly planning", k: 10, candidate_limit: 500)Commit: 3a925e2
Author: Adrian Curtin
Date: July 26, 2026