Class: Parse::Embeddings::Voyage
- Defined in:
- lib/parse/embeddings/voyage.rb
Overview
Voyage AI embeddings provider. Wraps POST /v1/embeddings for
text-only models, POST /v1/multimodalembeddings for the
multimodal text+image models (text via #embed_text, images via
#embed_image), and POST /v1/contextualizedembeddings for the
contextualized chunk models (single texts via #embed_text,
whole chunked documents via #embed_chunks).
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-4,voyage-code-3, andvoyage-code-2(the only 1536-dim one). - 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. - contextualized chunk:
voyage-context-4andvoyage-context-3. Each chunk's vector also encodes the document it came from, so chunks are embedded a document at a time through #embed_chunks. #embed_text sends every string as a one-chunk document. That is the right shape for queries, and it is also what theembedmacro and BatchEmbedder send for stored fields, so those vectors carry no surrounding-document context; call #embed_chunks directly for chunk-level context.
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
- RESPONSE_BYTES_PER_VALUE =
Upper bounds on the JSON size of one returned vector, used to plan contextualized requests: a float such as
-1.2345678901234567e-05,is under 32 bytes, and the{"object":"embedding","embedding":[], "index":N}wrapper is under 256. 32- RESPONSE_BYTES_PER_VECTOR_ENVELOPE =
256- 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-4" => 1024, "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, "voyage-context-4" => 1024, "voyage-context-3" => 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-4" => [256, 512, 1024, 2048], "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], "voyage-context-4" => [256, 512, 1024, 2048], "voyage-context-3" => [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-4" => 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, # Per document (one inner list of chunks). The request as a # whole is capped at 120k tokens across every document. "voyage-context-4" => 32_000, "voyage-context-3" => 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
- CONTEXTUALIZED_MODELS =
Models that route to
/v1/contextualizedembeddingswith the{ inputs: [[chunk, ...], ...] }envelope: one inner list per document, each chunk embedded with the rest of its document as context. The endpoint has notruncationfield, so it is never sent for these models. %w[voyage-context-4 voyage-context-3].freeze
- MAX_CONTEXT_DOCUMENTS =
Voyage's per-request limits for the contextualized endpoint: at most this many documents, and this many chunks summed across them. The endpoint also caps a request at 120k tokens summed across every document, which the SDK cannot check without a tokenizer.
1_000- MAX_CONTEXT_CHUNKS =
16_000- CONTEXT_DEFAULT_BATCH_SIZE =
Default
embed_batch_sizefor CONTEXTUALIZED_MODELS. Each string is a whole document there, so 128 paragraph-sized inputs would overrun the 120k-token request cap; 32 leaves room for documents averaging under about 3,700 tokens. 32- 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
- MEDIA_MODALITIES =
Everything modality-specific about a non-text input, in one table.
modelsnames the constant gating which models accept the modality;url/base64are the wire content-type keys.Adding a modality (audio, when Voyage ships it) is a row here plus a MediaFile constructor and a one-line
embed_audiodelegating to #embed_media — the streaming body, row builder, batching, URL validation, and instrumentation are all modality-agnostic already. { image: { url: "image_url", base64: "image_base64", models: :MULTIMODAL_MODELS, noun: "image" }, video: { url: "video_url", base64: "video_base64", models: :VIDEO_MODELS, noun: "video" }, }.freeze
Constants inherited from Provider
Provider::AS_NOTIFICATION_NAME
Instance Method Summary collapse
-
#apply_output_dimension!(body) ⇒ Object
protected
Forward
output_dimensiononly when the configured width differs from the model's native default. -
#atlas? ⇒ Boolean
True when routed through ATLAS_BASE_URL.
- #backoff_seconds(attempt) ⇒ Object protected
- #build_connection ⇒ Object protected
-
#build_contextualized_body(documents, wire_input_type) ⇒ Object
protected
Build the wire body for
/v1/contextualizedembeddings. -
#build_media_rows(sources, kind:, allow_insecure:, caller_name:) ⇒ Array<Hash>
protected
Build
inputs[].content[]row descriptors for a batch of media sources, accepting URL Strings, in-memory ImageFetch::FetchedImage wrappers, and file-backed MediaFile wrappers. -
#build_multimodal_body(strings, wire_input_type) ⇒ Object
protected
Build the wire body for
/v1/multimodalembeddingsfor TEXT inputs: each string wraps as a single{type: "text", text:}content row. -
#build_request_body(rows, wire_input_type) ⇒ Object
protected
Assemble the request body for a set of row descriptors.
-
#build_text_body(strings, wire_input_type) ⇒ Object
protected
Build the wire body for the standard
/v1/embeddingsendpoint (text-only models). - #dimensions ⇒ Object
-
#dispatch_media(rows, input_type, wire_input_type, kind) ⇒ Object
protected
Issue one multimodal request for a set of same-representation rows and return the vectors in row order.
- #embed_batch_size ⇒ Object
-
#embed_chunks(documents, input_type: :search_document) ⇒ Array<Array<Array<Float>>>
Embed chunked documents through Voyage's
/v1/contextualizedembeddingsendpoint. -
#embed_contextualized(documents, input_type, wire_input_type) ⇒ Object
protected
Embed documents through the contextualized endpoint and return one Array of chunk vectors per document, in input order.
-
#embed_contextualized_request(documents, input_type, wire_input_type) ⇒ Object
protected
Issue one contextualized request and return one Array of chunk vectors per document.
-
#embed_image(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>
Embed a batch of images through Voyage's
/v1/multimodalembeddingsendpoint. -
#embed_media(sources, kind:, input_type:, allow_insecure:) ⇒ Array<Array<Float>>
protected
Shared implementation behind #embed_image and #embed_video.
-
#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. -
#enforce_media_size!(bytes, index, caller_name, path = nil) ⇒ Object
protected
Refuse a payload Voyage will reject anyway.
-
#extract_vectors!(payload, input_count, value_key: "embedding") ⇒ Object
protected
Voyage's response shape mirrors OpenAI:.
-
#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: nil, 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
-
#json_string_prefix(str) ⇒ Object
protected
A JSON string literal with its opening quote and escaped contents but no closing quote, so a streamed payload can be appended before the string is closed.
- #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
- #parse_json_body!(body, max_bytes = MAX_RESPONSE_BYTES) ⇒ Object protected
- #post_embeddings(body, path: "embeddings", max_response_bytes: MAX_RESPONSE_BYTES) ⇒ Object protected
-
#response_bytes_per_vector ⇒ Integer
protected
The planning estimate for one vector's JSON size.
- #retry_after_seconds(response) ⇒ Object protected
-
#row_representation(row) ⇒ Object
protected
Which wire representation a row descriptor uses.
- #supports_input_type? ⇒ Boolean
Methods inherited from Provider
#cache_identity, #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: nil, dimensions: nil, truncation: true, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Voyage
Returns a new instance of Voyage.
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/parse/embeddings/voyage.rb', line 339 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: nil, 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) ||= CONTEXTUALIZED_MODELS.include?(model) ? CONTEXT_DEFAULT_BATCH_SIZE : DEFAULT_BATCH_SIZE 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
#apply_output_dimension!(body) ⇒ Object (protected)
Forward output_dimension only when the configured width
differs from the model's native default. Sending it to a model
with a single supported width is a 400, and sending the native
width needlessly is redundant — so both are omitted.
The constructor has already rejected any width outside MODEL_SUPPORTED_DIMENSIONS, so no re-validation is needed.
764 765 766 767 768 |
# File 'lib/parse/embeddings/voyage.rb', line 764 def apply_output_dimension!(body) return body if @dimensions == MODEL_DEFAULT_DIMENSIONS.fetch(@model) body[:output_dimension] = @dimensions body end |
#atlas? ⇒ Boolean
Returns true when routed through ATLAS_BASE_URL.
405 406 407 |
# File 'lib/parse/embeddings/voyage.rb', line 405 def atlas? @endpoint == :atlas end |
#backoff_seconds(attempt) ⇒ Object (protected)
1107 1108 1109 |
# File 'lib/parse/embeddings/voyage.rb', line 1107 def backoff_seconds(attempt) [0.5 * (2 ** (attempt - 1)), 30.0].min end |
#build_connection ⇒ Object (protected)
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 |
# File 'lib/parse/embeddings/voyage.rb', line 640 def build_connection headers = { "Authorization" => "Bearer #{@api_key}", "Content-Type" => "application/json", "Accept" => "application/json", "User-Agent" => "parse-stack-embeddings/#{user_agent_version}", } faraday_opts = { url: @base_url, headers: headers } faraday_opts[:proxy] = nil unless @allow_faraday_proxy conn = Faraday.new(**faraday_opts) do |f| f..timeout = @timeout f..open_timeout = @open_timeout f.adapter Faraday.default_adapter end conn.proxy = nil if !@allow_faraday_proxy && conn.respond_to?(:proxy=) conn end |
#build_contextualized_body(documents, wire_input_type) ⇒ Object (protected)
Build the wire body for /v1/contextualizedembeddings. The
endpoint documents no truncation field, so none is sent.
697 698 699 700 701 702 |
# File 'lib/parse/embeddings/voyage.rb', line 697 def build_contextualized_body(documents, wire_input_type) body = { inputs: documents, model: @model } body[:input_type] = wire_input_type if wire_input_type apply_output_dimension!(body) body end |
#build_media_rows(sources, kind:, allow_insecure:, caller_name:) ⇒ Array<Hash> (protected)
Build inputs[].content[] row descriptors for a batch of media
sources, accepting URL Strings, in-memory
ImageFetch::FetchedImage wrappers, and file-backed
MediaFile wrappers.
Every URL is validated up-front so a malformed entry in slot N cannot get past validation while slots 0..N-1 are already in the wire body — no partial forwarding.
Returns descriptors rather than finished Hashes so
#build_request_body can assemble the JSON structurally. A
streamed row is { stream: MediaFile, key: String }; every
other row is a ready-to-serialize Hash.
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 |
# File 'lib/parse/embeddings/voyage.rb', line 878 def build_media_rows(sources, kind:, allow_insecure:, caller_name:) spec = MEDIA_MODALITIES.fetch(kind) url_key = spec[:url] b64_key = spec[:base64] sources.each_with_index.map do |src, i| case src when Parse::Embeddings::MediaFile unless src.kind == kind raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name} sources[#{i}] is a " \ "#{src.kind} MediaFile; expected #{kind}." end enforce_media_size!(src.byte_size, i, caller_name, src.path) { stream: src, key: b64_key } when Parse::Embeddings::ImageFetch::FetchedImage # The only in-memory wrapper the SDK ships is for images. unless kind == :image raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name} sources[#{i}] is a FetchedImage; " \ "wrap #{spec[:noun]} sources with Parse::Embeddings::MediaFile.#{kind}." end enforce_media_size!(src.bytes.bytesize, i, caller_name) { content: [{ type: b64_key, b64_key => src.to_data_uri }] } when String canonical = Parse::Embeddings.validate_image_url!(src, allow_insecure: allow_insecure) { content: [{ type: url_key, url_key => canonical }] } else raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name} sources[#{i}] must be a URL String " \ "or a Parse::Embeddings::MediaFile (got #{src.class})." end end end |
#build_multimodal_body(strings, wire_input_type) ⇒ Object (protected)
Build the wire body for /v1/multimodalembeddings for TEXT
inputs: each string wraps as a single {type: "text", text:}
content row. Image inputs build their content rows inline in
#embed_image (image_url / image_base64) and do not pass
through here.
681 682 683 684 685 686 687 688 689 690 691 692 693 |
# File 'lib/parse/embeddings/voyage.rb', line 681 def build_multimodal_body(strings, wire_input_type) body = { inputs: strings.map { |s| { content: [{ type: "text", text: s }] } }, model: @model, } body[:input_type] = wire_input_type if wire_input_type # `truncation` is documented for the multimodal endpoint too — # forward it for parity with the text path so callers get the # same fail-on-overlength behavior across models. body[:truncation] = @truncation apply_output_dimension!(body) body end |
#build_request_body(rows, wire_input_type) ⇒ Object (protected)
Assemble the request body for a set of row descriptors.
With no streamed rows this returns a plain Hash, serialized later by #post_embeddings. With them it returns a StreamingBody whose JSON is built structurally — each fragment is serialized independently and concatenated in order, so the file payloads are spliced by position rather than by searching the serialized document.
Building it structurally is a correctness requirement, not a
style preference: an earlier version emitted a sentinel token
and located it with String#split, which let a caller-supplied
URL containing that token capture a local file's bytes and ship
them to the provider as a URL to fetch.
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 |
# File 'lib/parse/embeddings/voyage.rb', line 949 def build_request_body(rows, wire_input_type) trailer = { model: @model, truncation: @truncation } trailer[:input_type] = wire_input_type if wire_input_type apply_output_dimension!(trailer) return { inputs: rows }.merge(trailer) unless rows.any? { |r| r[:stream] } segments = [+'{"inputs":['] rows.each_with_index do |row, i| segments << "," if i.positive? if (media = row[:stream]) key = row[:key] # Open the JSON string, emit the data: prefix, stream the # payload, then close it. Base64 needs no escaping, and the # prefix is escaped by #to_json before its closing quote is # trimmed. segments << %({"content":[{"type":#{key.to_json},#{key.to_json}:) segments << json_string_prefix(media.data_uri_prefix) segments << media.stream_segment segments << %("}]}) else segments << row.to_json end end segments << "]" trailer.each { |k, v| segments << ",#{k.to_s.to_json}:#{v.to_json}" } segments << "}" Parse::Embeddings::StreamingBody.new(segments) end |
#build_text_body(strings, wire_input_type) ⇒ Object (protected)
Build the wire body for the standard /v1/embeddings endpoint
(text-only models).
662 663 664 665 666 667 668 669 670 671 672 673 674 |
# File 'lib/parse/embeddings/voyage.rb', line 662 def build_text_body(strings, wire_input_type) body = { input: strings, model: @model, truncation: @truncation, } # Only forward input_type when it has a wire value. Voyage # treats absent and `null` identically (unconditioned head), # but absent is the spec-correct form for non-retrieval intent. body[:input_type] = wire_input_type if wire_input_type apply_output_dimension!(body) body end |
#dimensions ⇒ Object
389 390 391 |
# File 'lib/parse/embeddings/voyage.rb', line 389 def dimensions @dimensions end |
#dispatch_media(rows, input_type, wire_input_type, kind) ⇒ Object (protected)
Issue one multimodal request for a set of same-representation rows and return the vectors in row order.
849 850 851 852 853 854 855 856 857 858 859 860 861 |
# File 'lib/parse/embeddings/voyage.rb', line 849 def dispatch_media(rows, input_type, wire_input_type, kind) body = build_request_body(rows, wire_input_type) (rows.length, input_type, modality: kind) do |emit_payload| payload = (body, path: "multimodalembeddings") 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, rows.length) validate_response!(rows.length, vectors) end end |
#embed_batch_size ⇒ Object
409 410 411 |
# File 'lib/parse/embeddings/voyage.rb', line 409 def @embed_batch_size end |
#embed_chunks(documents, input_type: :search_document) ⇒ Array<Array<Array<Float>>>
Embed chunked documents through Voyage's
/v1/contextualizedembeddings endpoint. Every chunk's vector
encodes the surrounding document as well as the chunk itself,
so pass the chunks of one document together, in order, rather
than one call per chunk.
Contextualized model required. Only CONTEXTUALIZED_MODELS accept this shape; any other model raises BadRequestError before any network call.
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 |
# File 'lib/parse/embeddings/voyage.rb', line 586 def (documents, input_type: :search_document) unless CONTEXTUALIZED_MODELS.include?(@model) raise BadRequestError, "Parse::Embeddings::Voyage#embed_chunks: model #{@model.inspect} does not " \ "accept chunked documents. Configure the provider with a contextualized model " \ "(supported: #{CONTEXTUALIZED_MODELS.inspect})." end unless documents.is_a?(Array) raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks expects Array<Array<String>> " \ "(got #{documents.class})." end return [] if documents.empty? documents.each_with_index do |chunks, i| unless chunks.is_a?(Array) && !chunks.empty? raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks documents[#{i}] must be a non-empty " \ "Array of chunk Strings." end chunks.each_with_index do |c, j| unless c.is_a?(String) && !c.empty? raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks documents[#{i}][#{j}] must be a " \ "non-empty String." end end end if documents.length > MAX_CONTEXT_DOCUMENTS raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks: #{documents.length} documents exceeds " \ "Voyage's per-request cap (#{MAX_CONTEXT_DOCUMENTS}). Split the input." end chunk_total = documents.sum(&:length) if chunk_total > MAX_CONTEXT_CHUNKS raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks: #{chunk_total} chunks exceeds " \ "Voyage's per-request cap (#{MAX_CONTEXT_CHUNKS}). Split the input." end unless INPUT_TYPE_WIRE_VALUES.key?(input_type) raise ArgumentError, "Parse::Embeddings::Voyage#embed_chunks input_type #{input_type.inspect} not in " \ "#{INPUT_TYPE_WIRE_VALUES.keys.inspect}." end (documents, input_type, INPUT_TYPE_WIRE_VALUES[input_type]) end |
#embed_contextualized(documents, input_type, wire_input_type) ⇒ Object (protected)
Embed documents through the contextualized endpoint and return one Array of chunk vectors per document, in input order.
Voyage accepts up to MAX_CONTEXT_CHUNKS chunks per request, but that many vectors serialize to far more than MAX_RESPONSE_BYTES. Whole documents are therefore grouped so each response's estimated size stays within the cap, and the groups are sent in turn. A single document too large for the cap on its own is sent alone with a response allowance sized to its chunk count.
713 714 715 716 717 718 719 720 721 722 723 724 725 726 |
# File 'lib/parse/embeddings/voyage.rb', line 713 def (documents, input_type, wire_input_type) per_vector = response_bytes_per_vector chunks_per_request = [MAX_RESPONSE_BYTES / per_vector, 1].max groups = [] documents.each do |doc| last = groups.last if last && last.sum(&:length) + doc.length <= chunks_per_request last << doc else groups << [doc] end end groups.flat_map { |group| (group, input_type, wire_input_type) } end |
#embed_contextualized_request(documents, input_type, wire_input_type) ⇒ Object (protected)
Issue one contextualized request and return one Array of chunk vectors per document.
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 |
# File 'lib/parse/embeddings/voyage.rb', line 735 def (documents, input_type, wire_input_type) body = build_contextualized_body(documents, wire_input_type) chunk_count = documents.sum(&:length) allowance = [MAX_RESPONSE_BYTES, (chunk_count * response_bytes_per_vector) + 65_536].max (chunk_count, input_type) do |emit_payload| payload = (body, path: "contextualizedembeddings", max_response_bytes: allowance) 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 # The response nests the standard envelope: the outer `data` # holds one `{ data: [...], index: }` list per document, and # each inner list is shaped like a `/v1/embeddings` response. per_document = extract_vectors!(payload, documents.length, value_key: "data") per_document.each_with_index.map do |entry, i| vectors = extract_vectors!({ "data" => entry }, documents[i].length) validate_response!(documents[i].length, vectors) end end 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.
533 534 535 536 |
# File 'lib/parse/embeddings/voyage.rb', line 533 def (sources, input_type: :search_document, allow_insecure: false) (sources, kind: :image, input_type: input_type, allow_insecure: allow_insecure) end |
#embed_media(sources, kind:, input_type:, allow_insecure:) ⇒ Array<Array<Float>> (protected)
Shared implementation behind #embed_image and #embed_video.
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 |
# File 'lib/parse/embeddings/voyage.rb', line 790 def (sources, kind:, input_type:, allow_insecure:) spec = MEDIA_MODALITIES.fetch(kind) capable = self.class.const_get(spec[:models]) caller_name = "embed_#{kind}" unless capable.include?(@model) raise BadRequestError, "Parse::Embeddings::Voyage##{caller_name}: model #{@model.inspect} does not " \ "accept #{spec[:noun]} inputs. Configure the provider with a capable model " \ "(supported: #{capable.inspect})." end unless sources.is_a?(Array) raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name} expects Array of #{spec[:noun]} " \ "URLs (got #{sources.class})." end return [] if sources.empty? unless INPUT_TYPE_WIRE_VALUES.key?(input_type) raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name} input_type #{input_type.inspect} " \ "not in #{INPUT_TYPE_WIRE_VALUES.keys.inspect}." end # Voyage caps multimodal requests at the same per-request size # as the text endpoint. The text path chunks automatically; the # media path has no chunker (every directive is a single # source), so guard the direct-API caller against a silent 400. if sources.length > @embed_batch_size raise ArgumentError, "Parse::Embeddings::Voyage##{caller_name}: batch size #{sources.length} " \ "exceeds the configured cap #{@embed_batch_size} (Voyage per-request max: " \ "128). Split the input and call #{caller_name} once per chunk." end rows = build_media_rows( sources, kind: kind, allow_insecure: allow_insecure, caller_name: caller_name, ) wire_input_type = INPUT_TYPE_WIRE_VALUES[input_type] # Voyage requires a single representation per request: "each # request should use either image_base64/video_base64 or # image_url/video_url exclusively, not both." A mixed batch is # therefore split into one request per representation and # reassembled in the caller's original order, so the 1:1 # alignment this method promises still holds. groups = rows.each_with_index.group_by { |row, _i| row_representation(row) } return dispatch_media(rows, input_type, wire_input_type, kind) if groups.size == 1 results = Array.new(rows.length) groups.each_value do |pairs| subset = pairs.map(&:first) vectors = dispatch_media(subset, input_type, wire_input_type, kind) pairs.each_with_index { |(_row, original_index), n| results[original_index] = vectors[n] } end results end |
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with strings.
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/parse/embeddings/voyage.rb', line 429 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] if CONTEXTUALIZED_MODELS.include?(@model) if strings.length > MAX_CONTEXT_DOCUMENTS raise ArgumentError, "Parse::Embeddings::Voyage#embed_text: #{strings.length} inputs exceeds Voyage's " \ "per-request cap for #{@model} (#{MAX_CONTEXT_DOCUMENTS}). Split the input." end # Each string is its own one-chunk document, so the response # carries exactly one vector per document. return (strings.map { |s| [s] }, input_type, wire_input_type).map(&:first) end # 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.
563 564 565 566 |
# File 'lib/parse/embeddings/voyage.rb', line 563 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.
400 401 402 |
# File 'lib/parse/embeddings/voyage.rb', line 400 def endpoint @endpoint end |
#enforce_media_size!(bytes, index, caller_name, path = nil) ⇒ Object (protected)
Refuse a payload Voyage will reject anyway. Enforced here rather than relying on Parse::Embeddings.max_media_bytes, which callers may legitimately raise for other providers.
916 917 918 919 920 921 922 923 924 |
# File 'lib/parse/embeddings/voyage.rb', line 916 def enforce_media_size!(bytes, index, caller_name, path = nil) return if bytes <= MAX_MEDIA_BYTES where = path ? " (#{path})" : "" raise BadRequestError, "Parse::Embeddings::Voyage##{caller_name} sources[#{index}]#{where} is " \ "#{bytes} bytes, over Voyage's #{MAX_MEDIA_BYTES}-byte per-file limit. " \ "Downscale or re-encode before embedding." end |
#extract_vectors!(payload, input_count, value_key: "embedding") ⇒ Object (protected)
Voyage's response shape mirrors OpenAI:
{ "object": "list", "data": [ { "object": "embedding", "embedding": [...], "index": 0 }, ... ], "model": "voyage-3", "usage": { "total_tokens": N } }
value_key names the field read from each entry: "embedding"
for the flat envelope, "data" for the outer list of the
contextualized envelope.
1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 |
# File 'lib/parse/embeddings/voyage.rb', line 1073 def extract_vectors!(payload, input_count, value_key: "embedding") unless payload.is_a?(Hash) raise InvalidResponseError, "Parse::Embeddings::Voyage: response body is not a JSON object." end data = payload["data"] unless data.is_a?(Array) raise InvalidResponseError, "Parse::Embeddings::Voyage: response.data is not an Array." end if data.length != input_count raise InvalidResponseError, "Parse::Embeddings::Voyage: response.data.length #{data.length} != input count #{input_count}." end sorted = data.each_with_index.map do |entry, i| unless entry.is_a?(Hash) raise InvalidResponseError, "Parse::Embeddings::Voyage: response.data[#{i}] is not a JSON object." end idx = entry["index"] unless idx.is_a?(Integer) && idx >= 0 && idx < input_count raise InvalidResponseError, "Parse::Embeddings::Voyage: response.data[#{i}].index #{idx.inspect} out of range." end [idx, entry[value_key]] end indices = sorted.map(&:first) if indices.uniq.length != indices.length raise InvalidResponseError, "Parse::Embeddings::Voyage: duplicate index in response.data." end sorted.sort_by(&:first).map(&:last) end |
#inspect_attrs ⇒ Object
634 635 636 |
# File 'lib/parse/embeddings/voyage.rb', line 634 def inspect_attrs super.merge(base: safe_base_host, endpoint: @endpoint, retries: @max_retries) end |
#json_string_prefix(str) ⇒ Object (protected)
A JSON string literal with its opening quote and escaped contents but no closing quote, so a streamed payload can be appended before the string is closed.
983 984 985 986 |
# File 'lib/parse/embeddings/voyage.rb', line 983 def json_string_prefix(str) encoded = str.to_json encoded[0...-1] end |
#max_input_tokens ⇒ Object
413 414 415 |
# File 'lib/parse/embeddings/voyage.rb', line 413 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.
492 493 494 495 |
# File 'lib/parse/embeddings/voyage.rb', line 492 def modalities return [:text] unless MULTIMODAL_MODELS.include?(@model) VIDEO_MODELS.include?(@model) ? %i[text image video] : %i[text image] end |
#model_name ⇒ Object
393 394 395 |
# File 'lib/parse/embeddings/voyage.rb', line 393 def model_name @model end |
#normalize? ⇒ Boolean
417 418 419 420 |
# File 'lib/parse/embeddings/voyage.rb', line 417 def normalize? # Voyage's v3 embeddings are documented unit-normalized. true end |
#parse_json_body!(body, max_bytes = MAX_RESPONSE_BYTES) ⇒ Object (protected)
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 |
# File 'lib/parse/embeddings/voyage.rb', line 1045 def parse_json_body!(body, max_bytes = MAX_RESPONSE_BYTES) s = body.to_s if s.bytesize > max_bytes raise InvalidResponseError, "Parse::Embeddings::Voyage: response body exceeds #{max_bytes} bytes " \ "(#{s.bytesize}). Refusing to parse." end JSON.parse(s, max_nesting: 32) rescue JSON::ParserError => e raise InvalidResponseError, "Parse::Embeddings::Voyage: response is not valid JSON (#{e.})." end |
#post_embeddings(body, path: "embeddings", max_response_bytes: MAX_RESPONSE_BYTES) ⇒ Object (protected)
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 |
# File 'lib/parse/embeddings/voyage.rb', line 990 def (body, path: "embeddings", max_response_bytes: MAX_RESPONSE_BYTES) attempts = 0 loop do attempts += 1 begin response = @connection.post(path) do |req| if body.is_a?(Parse::Embeddings::StreamingBody) # Faraday's net_http adapter routes an IO-shaped body # to Net::HTTP#body_stream. Rewind so a retry replays # from the start, and set Content-Length explicitly — # without it Net::HTTP falls back to chunked transfer # encoding, which some API gateways reject. body.rewind req.headers["Content-Length"] = body.size.to_s req.body = body else req.body = body.to_json end end rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e if attempts > @max_retries raise TransientError, "Parse::Embeddings::Voyage: #{e.class} after #{attempts} attempt(s)." end sleep(backoff_seconds(attempts)) next end status = response.status return parse_json_body!(response.body, max_response_bytes) if status >= 200 && status < 300 if status == 401 raise AuthenticationError, "Parse::Embeddings::Voyage: 401 Unauthorized — check api_key." end if status == 429 if attempts > @max_retries raise RateLimitError, "Parse::Embeddings::Voyage: 429 rate limited after #{attempts} attempt(s)." end sleep(retry_after_seconds(response) || backoff_seconds(attempts)) next end if status >= 500 if attempts > @max_retries raise TransientError, "Parse::Embeddings::Voyage: #{status} after #{attempts} attempt(s)." end sleep(backoff_seconds(attempts)) next end raise BadRequestError, "Parse::Embeddings::Voyage: #{status} from POST /#{path}." end end |
#response_bytes_per_vector ⇒ Integer (protected)
Returns the planning estimate for one vector's JSON size.
729 730 731 |
# File 'lib/parse/embeddings/voyage.rb', line 729 def response_bytes_per_vector (@dimensions * RESPONSE_BYTES_PER_VALUE) + RESPONSE_BYTES_PER_VECTOR_ENVELOPE end |
#retry_after_seconds(response) ⇒ Object (protected)
1111 1112 1113 1114 1115 1116 |
# File 'lib/parse/embeddings/voyage.rb', line 1111 def retry_after_seconds(response) ra = response.respond_to?(:headers) ? response.headers["retry-after"] || response.headers["Retry-After"] : nil return nil unless ra v = ra.to_f v.positive? ? [v, 60.0].min : nil end |
#row_representation(row) ⇒ Object (protected)
Which wire representation a row descriptor uses. Voyage requires a single representation per request, so this is what #embed_media partitions on.
929 930 931 932 933 |
# File 'lib/parse/embeddings/voyage.rb', line 929 def row_representation(row) return :base64 if row[:stream] type = row.dig(:content, 0, :type).to_s type.end_with?("_base64") ? :base64 : :url end |
#supports_input_type? ⇒ Boolean
422 423 424 |
# File 'lib/parse/embeddings/voyage.rb', line 422 def supports_input_type? true end |