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-input path only in v5.0; the image-input path lands with Provider#embed_image in v5.1).
Supported models:
-
**v4 family** — ‘voyage-4-large` (MoE flagship, Matryoshka-capable), `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`, `voyage-3-lite`, `voyage-code-3`.
-
**domain models** — ‘voyage-finance-2`, `voyage-law-2`.
-
multimodal — ‘voyage-multimodal-3` (1024-dim). Unified text+image vector space at the network boundary. This provider exposes the text-input path only: routes to `/v1/multimodalembeddings` with a `{ inputs: [{ content:
- { type: “text”, text: … }
-
}] }‘ envelope. The same model will
accept image inputs in v5.1 when the ‘embed_image` hook ships; text vectors stored today will sit in the same space as the eventual image vectors (no re-embed required).
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 via `allow_faraday_proxy: true`. Env-proxy autodiscovery (`HTTPS_PROXY` etc.) is suppressed by default.
-
‘#inspect` (inherited from Provider) never surfaces `@api_key`.
-
‘Authorization` and `Voyage-Api-Key` are in Middleware::BodyBuilder::REDACTED_HEADERS.
Defined Under Namespace
Classes: AuthenticationError, BadRequestError, RateLimitError, TransientError
Constant Summary collapse
- DEFAULT_BASE_URL =
"https://api.voyageai.com/v1"- DEFAULT_MODEL =
"voyage-3"- 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 =
Native vector widths per model. The v4 family is Voyage’s current flagship line (MoE for ‘voyage-4-large`, open-weight nano under Apache 2.0). `voyage-4-large` supports Matryoshka truncation via the constructor’s ‘dimensions:` override.
{ "voyage-4-large" => 2048, "voyage-4" => 1024, "voyage-4-lite" => 512, "voyage-4-nano" => 256, "voyage-3-large" => 1024, "voyage-3" => 1024, "voyage-3-lite" => 512, "voyage-code-3" => 1024, "voyage-finance-2" => 1024, "voyage-law-2" => 1024, "voyage-multimodal-3" => 1024, }.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" => 32_000, "voyage-3-lite" => 32_000, "voyage-code-3" => 32_000, "voyage-finance-2" => 16_000, "voyage-law-2" => 16_000, "voyage-multimodal-3" => 32_000, }.freeze
- MATRYOSHKA_MODELS =
Models that accept Voyage’s ‘output_dimension` Matryoshka truncation parameter. Sending the field for other models is rejected with a 400 by Voyage, so we gate it explicitly.
%w[voyage-4-large].freeze
- MULTIMODAL_MODELS =
Models that route to ‘/v1/multimodalembeddings` with 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].freeze
- INPUT_TYPE_WIRE_VALUES =
Map SDK-canonical input_type symbols to Voyage wire strings. ‘:classification` / `:clustering` map to `nil` (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
- #dimensions ⇒ Object
- #embed_batch_size ⇒ Object
-
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Vectors aligned 1:1 with ‘strings`.
-
#initialize(api_key:, model: DEFAULT_MODEL, base_url: DEFAULT_BASE_URL, 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
- #model_name ⇒ Object
- #normalize? ⇒ Boolean
- #supports_input_type? ⇒ Boolean
Methods inherited from Provider
#embed_image, #embed_text_batched, #inspect, #instrument_embed, #modalities, #validate_response!
Constructor Details
#initialize(api_key:, model: DEFAULT_MODEL, base_url: DEFAULT_BASE_URL, 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.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/parse/embeddings/voyage.rb', line 155 def initialize( api_key:, model: DEFAULT_MODEL, base_url: DEFAULT_BASE_URL, 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) 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 @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
#dimensions ⇒ Object
199 200 201 |
# File 'lib/parse/embeddings/voyage.rb', line 199 def dimensions @dimensions end |
#embed_batch_size ⇒ Object
207 208 209 |
# File 'lib/parse/embeddings/voyage.rb', line 207 def @embed_batch_size end |
#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>
Returns vectors aligned 1:1 with ‘strings`.
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/parse/embeddings/voyage.rb', line 227 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 |
#inspect_attrs ⇒ Object
275 276 277 |
# File 'lib/parse/embeddings/voyage.rb', line 275 def inspect_attrs super.merge(base: safe_base_host, retries: @max_retries) end |
#max_input_tokens ⇒ Object
211 212 213 |
# File 'lib/parse/embeddings/voyage.rb', line 211 def max_input_tokens MODEL_MAX_INPUT_TOKENS[@model] end |
#model_name ⇒ Object
203 204 205 |
# File 'lib/parse/embeddings/voyage.rb', line 203 def model_name @model end |
#normalize? ⇒ Boolean
215 216 217 218 |
# File 'lib/parse/embeddings/voyage.rb', line 215 def normalize? # Voyage's v3 embeddings are documented unit-normalized. true end |
#supports_input_type? ⇒ Boolean
220 221 222 |
# File 'lib/parse/embeddings/voyage.rb', line 220 def supports_input_type? true end |