Class: Parse::Embeddings::Voyage
- Defined in:
- lib/parse/embeddings/voyage.rb
Overview
Voyage AI embeddings provider. Wraps POST /v1/embeddings for
text-only models and POST /v1/multimodalembeddings for the
multimodal text+image models (text via #embed_text, images via
#embed_image).
Supported models:
- v4 family —
voyage-4-large,voyage-4,voyage-4-lite,voyage-4-nano(Apache 2.0, open-weight on Hugging Face — also runnable through LocalHTTP when self-hosted on vLLM / Ollama / llama.cpp). - v3 family —
voyage-3-large,voyage-3.5,voyage-3.5-lite,voyage-3,voyage-3-lite. - code models —
voyage-code-3,voyage-code-2(1536-dim). - domain models —
voyage-finance-2,voyage-law-2. - multimodal —
voyage-multimodal-3(text+image) andvoyage-multimodal-3.5(text+image+video). Unified vector space at the network boundary: text routes to/v1/multimodalembeddingswith a{ inputs: [{ content: [{ type: "text", text: … }] }] }envelope, images go through #embed_image, video through #embed_video. All three share the same space, so stored text vectors are comparable against image and video vectors without re-embedding.
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 instead.
Most models expose a Matryoshka ladder
(MODEL_SUPPORTED_DIMENSIONS); note that the whole v4 family
DEFAULTS to 1024 and reaches 2048 or 256 only when dimensions:
asks for it.
Endpoints
The same models are served by Voyage's own API and by MongoDB's
Atlas Embedding and Reranking API. The wire contract is
identical; the credentials are not interchangeable, and Voyage
returns a 403 explaining as much if they are crossed. An Atlas
model API key is recognized by its ATLAS_KEY_PREFIX and routes
to ATLAS_BASE_URL automatically — pass endpoint: to be
explicit. A few older models are absent from Atlas; see
ATLAS_UNAVAILABLE_MODELS.
Memory
Local images and video should be wrapped with MediaFile, which streams the file into the request body StreamingBody::READ_CHUNK bytes at a time. Passing a URL instead keeps the SDK out of the transfer entirely — the provider does the fetch. Only ImageFetch::FetchedImage holds a payload in memory, so prefer it for small images only.
Asymmetric input types
Voyage's input_type field accepts "query" or "document"
(mapped from the SDK-canonical :search_query / :search_document
Symbols). The values are functionally analogous to Cohere's
search_query / search_document — they're encoded by separately
tuned heads, so re-using one type for both sides of a retrieval
pair measurably degrades recall.
Voyage also accepts null (omit the field), which Voyage's docs
recommend for "general purpose" embeddings unrelated to retrieval.
We translate the absent / non-retrieval cases to null rather
than picking a default — Voyage's training depends on the
asymmetry, so guessing on the caller's behalf would be worse than
passing-through.
Security
- The Faraday connection refuses
proxy:unless the caller opts in viaallow_faraday_proxy: true. Env-proxy autodiscovery (HTTPS_PROXYetc.) is suppressed by default. #inspect(inherited from Provider) never surfaces@api_key.AuthorizationandVoyage-Api-Keyare in Middleware::BodyBuilder::REDACTED_HEADERS.
Defined Under Namespace
Classes: AuthenticationError, BadRequestError, RateLimitError, TransientError
Constant Summary collapse
- DEFAULT_BASE_URL =
"https://api.voyageai.com/v1"- ATLAS_BASE_URL =
MongoDB's Atlas Embedding and Reranking API re-exposes the same Voyage models under a MongoDB-operated host. The wire contract (request envelopes, response envelopes, error shapes) is identical — only the host and the credential differ.
"https://ai.mongodb.com/v1"- DEFAULT_MODEL =
Bumped from
voyage-3in 5.6.0: that model is retired from the Atlas endpoint, so an Atlas key used without naming a model failed at construction.voyage-3.5is served by both endpoints and shares the 1024 native width. "voyage-3.5"- DEFAULT_TIMEOUT =
30- DEFAULT_OPEN_TIMEOUT =
5- DEFAULT_MAX_RETRIES =
3- DEFAULT_BATCH_SIZE =
Voyage's documented per-request cap is 128 inputs.
128- MAX_RESPONSE_BYTES =
16 * 1024 * 1024
- MODEL_DEFAULT_DIMENSIONS =
Default (native) vector width per model — the width returned when
output_dimensionis omitted from the request.NOTE: the whole v4 family defaults to 1024, NOT to a per-tier width.
voyage-4-largereaches 2048 andvoyage-4-litereaches 512 only by explicitly requesting them viaoutput_dimension(the constructor'sdimensions:override) — those are Matryoshka options, not native widths. Verified against the live API for every model reachable through ATLAS_BASE_URL; see MODEL_SUPPORTED_DIMENSIONS. { "voyage-4-large" => 1024, "voyage-4" => 1024, "voyage-4-lite" => 1024, "voyage-4-nano" => 1024, "voyage-3-large" => 1024, "voyage-3.5" => 1024, "voyage-3.5-lite" => 1024, "voyage-3" => 1024, "voyage-3-lite" => 512, "voyage-code-3" => 1024, "voyage-code-2" => 1536, "voyage-finance-2" => 1024, "voyage-law-2" => 1024, "voyage-multimodal-3" => 1024, "voyage-multimodal-3.5" => 1024, }.freeze
- MODEL_SUPPORTED_DIMENSIONS =
Every width a model's Matryoshka head will actually return. A model whose list has a single entry accepts no
output_dimensionoverride at all — requesting one is a 400.This replaces the older "Matryoshka-capable models" boolean gate, which was too coarse: the v4 family,
voyage-3-large, the v3.5 family, andvoyage-code-3all accept the full 256/512/1024/2048 ladder, andvoyage-multimodal-3.5accepts it too whilevoyage-multimodal-3does not. { "voyage-4-large" => [256, 512, 1024, 2048], "voyage-4" => [256, 512, 1024, 2048], "voyage-4-lite" => [256, 512, 1024, 2048], "voyage-4-nano" => [256, 512, 1024, 2048], "voyage-3-large" => [256, 512, 1024, 2048], "voyage-3.5" => [256, 512, 1024, 2048], "voyage-3.5-lite" => [256, 512, 1024, 2048], "voyage-3" => [1024], "voyage-3-lite" => [512], "voyage-code-3" => [256, 512, 1024, 2048], "voyage-code-2" => [1536], "voyage-finance-2" => [1024], "voyage-law-2" => [1024], "voyage-multimodal-3" => [1024], "voyage-multimodal-3.5" => [256, 512, 1024, 2048], }.freeze
- MATRYOSHKA_MODELS =
Back-compat alias: the set of models accepting any
output_dimensionother than their native width. Derived from MODEL_SUPPORTED_DIMENSIONS rather than hand-maintained. MODEL_SUPPORTED_DIMENSIONS.select { |_m, dims| dims.length > 1 }.keys.freeze
- MODEL_MAX_INPUT_TOKENS =
{ "voyage-4-large" => 32_000, "voyage-4" => 32_000, "voyage-4-lite" => 32_000, "voyage-4-nano" => 32_000, "voyage-3-large" => 32_000, "voyage-3.5" => 32_000, "voyage-3.5-lite" => 32_000, "voyage-3" => 32_000, "voyage-3-lite" => 32_000, "voyage-code-3" => 32_000, "voyage-code-2" => 16_000, "voyage-finance-2" => 32_000, "voyage-law-2" => 16_000, "voyage-multimodal-3" => 32_000, "voyage-multimodal-3.5" => 32_000, }.freeze
- MULTIMODAL_MODELS =
Models that route to
/v1/multimodalembeddingswith the{ inputs: [{ content: [...] }] }envelope rather than the standard/v1/embeddings{ input: [String] }envelope. Text-only inputs from this provider are wrapped as{ type: "text", text: s }content rows. %w[voyage-multimodal-3 voyage-multimodal-3.5].freeze
- MAX_MEDIA_BYTES =
Voyage's documented hard ceiling for a single image or video. Parse::Embeddings.max_media_bytes is a global convenience knob that may be lowered for any reason — but raising it above this cannot make Voyage accept a larger file, so the adapter enforces its own limit independently.
20 * 1024 * 1024
- VIDEO_MODELS =
Multimodal models that additionally accept video content rows (
video_url/video_base64) via #embed_video.voyage-multimodal-3rejects video with an explicit "does not support video inputs" 400. %w[voyage-multimodal-3.5].freeze
- ATLAS_UNAVAILABLE_MODELS =
Models Voyage's hosted API serves but the Atlas Embedding and Reranking API does not. Verified against both endpoints.
%w[voyage-3 voyage-3-lite].freeze
- SELF_HOSTED_ONLY_MODELS =
Open-weight models that NO hosted endpoint serves — neither Voyage's nor Atlas's.
voyage-4-nanoships under Apache 2.0 on Hugging Face and is meant to be self-hosted (vLLM / Ollama / llama.cpp), reached either through LocalHTTP or through this provider with an explicitbase_url:pointing at the local server. Naming one against a hosted endpoint is always a mistake, so it is refused there rather than failing as an opaque provider 400. %w[voyage-4-nano].freeze
- ATLAS_KEY_PREFIX =
Atlas model API keys carry this prefix and authenticate ONLY against ATLAS_BASE_URL; Voyage's own endpoint rejects them with a 403. Used to infer the endpoint when the caller does not name one explicitly.
"al-"- INPUT_TYPE_WIRE_VALUES =
Map SDK-canonical input_type symbols to Voyage wire strings.
:classification/:clusteringmap tonil(omitted) since Voyage only distinguishes retrieval halves — other intents should receive the unconditioned vector. { search_query: "query", search_document: "document", classification: nil, clustering: nil, }.freeze
Constants inherited from Provider
Provider::AS_NOTIFICATION_NAME
Instance Method Summary collapse
-
#atlas? ⇒ Boolean
True when routed through ATLAS_BASE_URL.
- #dimensions ⇒ Object
- #embed_batch_size ⇒ Object
-
#embed_image(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>
Embed a batch of images through Voyage's
/v1/multimodalembeddingsendpoint. -
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Vectors aligned 1:1 with
strings. -
#embed_video(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>
Embed a batch of videos through
/v1/multimodalembeddings. -
#endpoint ⇒ Symbol
:atlaswhen this provider targets MongoDB's Atlas Embedding and Reranking API,:voyagewhen it targets Voyage's own API,:customfor any other host. -
#initialize(api_key:, model: DEFAULT_MODEL, endpoint: :auto, base_url: nil, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, dimensions: nil, truncation: true, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Voyage
constructor
A new instance of Voyage.
- #inspect_attrs ⇒ Object
- #max_input_tokens ⇒ Object
-
#modalities ⇒ Array<Symbol>
[:text, :image, :video]forvoyage-multimodal-3.5,[:text, :image]forvoyage-multimodal-3, and[:text]for text-only models. - #model_name ⇒ Object
- #normalize? ⇒ Boolean
- #supports_input_type? ⇒ Boolean
Methods inherited from Provider
#embed_text_batched, #inspect, #instrument_embed, #supports_modality?, #validate_response!
Constructor Details
#initialize(api_key:, model: DEFAULT_MODEL, endpoint: :auto, base_url: nil, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, dimensions: nil, truncation: true, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Voyage
Returns a new instance of Voyage.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/parse/embeddings/voyage.rb', line 287 def initialize( api_key:, model: DEFAULT_MODEL, endpoint: :auto, base_url: nil, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, dimensions: nil, truncation: true, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil ) validate_api_key!(api_key) validate_model!(model) resolved_endpoint = resolve_endpoint!(endpoint, api_key, base_url) base_url ||= resolved_endpoint == :atlas ? ATLAS_BASE_URL : DEFAULT_BASE_URL validate_model_for_endpoint!(model, resolved_endpoint) sanitized_base_url = validate_base_url!(base_url, allow_insecure_base_url) validate_positive_integer!(:timeout, timeout) validate_positive_integer!(:open_timeout, open_timeout) validate_non_negative_integer!(:max_retries, max_retries) validate_positive_integer!(:embed_batch_size, ) if > 128 raise ArgumentError, "Parse::Embeddings::Voyage: embed_batch_size #{} exceeds Voyage's per-request cap (128)." end unless [true, false].include?(truncation) raise ArgumentError, "Parse::Embeddings::Voyage: truncation must be true or false (got #{truncation.inspect})." end validate_dimensions!(model, dimensions) @api_key = api_key @model = model @endpoint = resolved_endpoint @dimensions = dimensions || MODEL_DEFAULT_DIMENSIONS.fetch(model) @base_url = sanitized_base_url @timeout = timeout @open_timeout = open_timeout @max_retries = max_retries @embed_batch_size = @truncation = truncation @allow_faraday_proxy = allow_faraday_proxy @connection = connection || build_connection end |
Instance Method Details
#atlas? ⇒ Boolean
Returns true when routed through ATLAS_BASE_URL.
352 353 354 |
# File 'lib/parse/embeddings/voyage.rb', line 352 def atlas? @endpoint == :atlas end |
#dimensions ⇒ Object
336 337 338 |
# File 'lib/parse/embeddings/voyage.rb', line 336 def dimensions @dimensions end |
#embed_batch_size ⇒ Object
356 357 358 |
# File 'lib/parse/embeddings/voyage.rb', line 356 def @embed_batch_size end |
#embed_image(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>
Embed a batch of images through Voyage's
/v1/multimodalembeddings endpoint. Two source forms:
- String URL (v5.1 path) — the provider receives a public
URL and issues its own fetch. The SDK does NOT download the
image; it validates the URL through
Parse::Embeddings.validate_image_url! (CIDR / port / host
allowlist, sentinel-gated egress opt-in) and forwards the
canonicalized URL string in a
{ type: "image_url", image_url: ... }content row. - ImageFetch::FetchedImage (v5.5 bytes
path) — bytes the SDK already downloaded through
File.safe_open_url, magic-byte-verified, and
EXIF-stripped. Forwarded as a
{ type: "image_base64", image_base64: "data:<mime>;base64,..." }content row. No URL validation runs (there is no provider-side fetch) and thetrust_provider_url_fetchsentinel is NOT required.
Multimodal model required. Voyage's text-only models
(voyage-3, voyage-4, etc.) do not accept image inputs;
calling embed_image on a provider configured with one of
those raises BadRequestError before any network call.
469 470 471 472 |
# File 'lib/parse/embeddings/voyage.rb', line 469 def (sources, input_type: :search_document, allow_insecure: false) (sources, kind: :image, input_type: input_type, allow_insecure: allow_insecure) end |
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with strings.
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 |
# File 'lib/parse/embeddings/voyage.rb', line 376 def (strings, input_type: :search_document) unless strings.is_a?(Array) raise ArgumentError, "Parse::Embeddings::Voyage#embed_text expects Array<String> (got #{strings.class})." end return [] if strings.empty? strings.each_with_index do |s, i| unless s.is_a?(String) raise ArgumentError, "Parse::Embeddings::Voyage#embed_text strings[#{i}] is not a String (#{s.class})." end if s.empty? raise ArgumentError, "Parse::Embeddings::Voyage#embed_text strings[#{i}] is empty; Voyage rejects empty inputs." end end unless INPUT_TYPE_WIRE_VALUES.key?(input_type) raise ArgumentError, "Parse::Embeddings::Voyage#embed_text input_type #{input_type.inspect} not in " \ "#{INPUT_TYPE_WIRE_VALUES.keys.inspect}." end wire_input_type = INPUT_TYPE_WIRE_VALUES[input_type] # Multimodal models route to a different endpoint with a # different request envelope. The response envelope shape is # the same (`{ data: [{ embedding, index }], usage: {...} }`) # so `extract_vectors!` is reused as-is. body = if MULTIMODAL_MODELS.include?(@model) build_multimodal_body(strings, wire_input_type) else build_text_body(strings, wire_input_type) end path = MULTIMODAL_MODELS.include?(@model) ? "multimodalembeddings" : "embeddings" (strings.length, input_type) do |emit_payload| payload = (body, path: path) # Voyage's response carries `usage: { total_tokens }`. if payload.is_a?(Hash) && payload["usage"].is_a?(Hash) tt = payload["usage"]["total_tokens"] emit_payload[:total_tokens] = tt if tt.is_a?(Integer) && tt >= 0 end vectors = extract_vectors!(payload, strings.length) validate_response!(strings.length, vectors) end end |
#embed_video(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>
Embed a batch of videos through
/v1/multimodalembeddings. Mirrors #embed_image's source
forms and security posture exactly:
- String URL — forwarded as a
{ type: "video_url", video_url: … }content row after Parse::Embeddings.validate_image_url! canonicalizes and screens it. The provider issues the fetch, so thetrust_provider_url_fetchsentinel IS required and the SDK never downloads the video. - MediaFile — a local file, streamed
into the request body as a
{ type: "video_base64", video_base64: "data:…" }row without ever being held in memory. No URL validation and no sentinel, because nothing is fetched.
Video-capable model required. Only VIDEO_MODELS accept
video; voyage-multimodal-3 rejects it server-side, so this
raises BadRequestError before any network call.
499 500 501 502 |
# File 'lib/parse/embeddings/voyage.rb', line 499 def (sources, input_type: :search_document, allow_insecure: false) (sources, kind: :video, input_type: input_type, allow_insecure: allow_insecure) end |
#endpoint ⇒ Symbol
Returns :atlas when this provider targets MongoDB's
Atlas Embedding and Reranking API, :voyage when it targets
Voyage's own API, :custom for any other host.
347 348 349 |
# File 'lib/parse/embeddings/voyage.rb', line 347 def endpoint @endpoint end |
#inspect_attrs ⇒ Object
504 505 506 |
# File 'lib/parse/embeddings/voyage.rb', line 504 def inspect_attrs super.merge(base: safe_base_host, endpoint: @endpoint, retries: @max_retries) end |
#max_input_tokens ⇒ Object
360 361 362 |
# File 'lib/parse/embeddings/voyage.rb', line 360 def max_input_tokens MODEL_MAX_INPUT_TOKENS[@model] end |
#modalities ⇒ Array<Symbol>
Returns [:text, :image, :video] for
voyage-multimodal-3.5, [:text, :image] for
voyage-multimodal-3, and [:text] for text-only models.
Audio is not offered by any Voyage model.
428 429 430 431 |
# File 'lib/parse/embeddings/voyage.rb', line 428 def modalities return [:text] unless MULTIMODAL_MODELS.include?(@model) VIDEO_MODELS.include?(@model) ? %i[text image video] : %i[text image] end |
#model_name ⇒ Object
340 341 342 |
# File 'lib/parse/embeddings/voyage.rb', line 340 def model_name @model end |
#normalize? ⇒ Boolean
364 365 366 367 |
# File 'lib/parse/embeddings/voyage.rb', line 364 def normalize? # Voyage's v3 embeddings are documented unit-normalized. true end |
#supports_input_type? ⇒ Boolean
369 370 371 |
# File 'lib/parse/embeddings/voyage.rb', line 369 def supports_input_type? true end |