Class: Parse::Embeddings::Cohere

Inherits:
Provider
  • Object
show all
Defined in:
lib/parse/embeddings/cohere.rb

Overview

Cohere embeddings provider. Wraps POST /v1/embed.

Supported models:

  • v4embed-v4.0 (1536 native, Matryoshka 512, 1024, 1536, 128k-token context). Unified text + image model at the network boundary. The text path uses Cohere's /v1/embed endpoint; the image path (#embed_image, v5.1+) uses the /v2/embed multimodal endpoint with OpenAI-style { type: "image_url", image_url: { url: ... } } content rows. Text vectors stored today share the vector space with the eventual image vectors (no re-embed required when adding image-side data).
  • v3embed-english-v3.0, embed-multilingual-v3.0 (both 1024-dim), embed-english-light-v3.0, embed-multilingual-light-v3.0 (both 384-dim). Text-only.

Asymmetric input types

Cohere is one of the providers that DOES distinguish queries from documents at the wire level via the input_type request field. Sending input_type: "search_query" for a query and "search_document" for a corpus item is required for good recall on Cohere's v3 models — using the same type for both halves of a retrieval pair degrades nDCG by a noticeable margin (Cohere's own benchmarks). Provider#supports_input_type? returns true here so callers / cache-keying middleware can branch on this.

The accepted Symbol values map to the Cohere wire strings:

  • :search_query"search_query"
  • :search_document"search_document"
  • :classification"classification"
  • :clustering"clustering"

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 — same model as Parse::Client and OpenAI.
  • #inspect (inherited from Provider) never surfaces @api_key.
  • Authorization and Cohere-Api-Key are in Middleware::BodyBuilder::REDACTED_HEADERS.

Examples:

registration

Parse::Embeddings.register(:cohere,
  Parse::Embeddings::Cohere.new(
    api_key: ENV.fetch("COHERE_API_KEY"),
    model:   "embed-english-v3.0",
  ))

Defined Under Namespace

Classes: AuthenticationError, BadRequestError, RateLimitError, TransientError

Constant Summary collapse

DEFAULT_BASE_URL =
"https://api.cohere.com/v1"
DEFAULT_MODEL =
"embed-english-v3.0"
DEFAULT_TIMEOUT =
30
DEFAULT_OPEN_TIMEOUT =
5
DEFAULT_MAX_RETRIES =
3
DEFAULT_BATCH_SIZE =

Cohere documents a hard cap of 96 inputs per /embed call.

96
MAX_RESPONSE_BYTES =
16 * 1024 * 1024
MODEL_DEFAULT_DIMENSIONS =
{
  "embed-v4.0"                     => 1536,
  "embed-english-v3.0"             => 1024,
  "embed-multilingual-v3.0"        => 1024,
  "embed-english-light-v3.0"       => 384,
  "embed-multilingual-light-v3.0"  => 384,
}.freeze
MODEL_MAX_INPUT_TOKENS =
{
  "embed-v4.0"                     => 128_000,
  "embed-english-v3.0"             => 512,
  "embed-multilingual-v3.0"        => 512,
  "embed-english-light-v3.0"       => 512,
  "embed-multilingual-light-v3.0"  => 512,
}.freeze
MATRYOSHKA_MODELS =

Models that accept Cohere's output_dimension Matryoshka truncation parameter. v4.0 is the only such row today; v3 models reject the field with a 400.

%w[embed-v4.0].freeze
MULTIMODAL_MODELS =

Models that accept image inputs via the /v2/embed multimodal endpoint. Currently only embed-v4.0 — v3 is text-only.

%w[embed-v4.0].freeze
MATRYOSHKA_WIDTHS =

Allowed Matryoshka widths per model (Cohere quantizes the available truncations rather than accepting any integer ≤ native). Empty allowlist = any integer ≤ native is fine, but for v4.0 Cohere documents exactly these four widths.

{
  "embed-v4.0" => [256, 512, 1024, 1536].freeze,
}.freeze
INPUT_TYPE_WIRE_VALUES =

Map SDK-canonical input_type symbols to Cohere wire strings. Symbols outside this set raise — silently downgrading :unknown_type to "search_document" would mask cache-key bugs in higher layers (the value participates in cache keys).

{
  search_query:    "search_query",
  search_document: "search_document",
  classification:  "classification",
  clustering:      "clustering",
}.freeze

Constants inherited from Provider

Provider::AS_NOTIFICATION_NAME

Instance Method Summary collapse

Methods inherited from Provider

#embed_text_batched, #inspect, #instrument_embed, #validate_response!

Constructor Details

#initialize(api_key:, model: DEFAULT_MODEL, dimensions: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, embed_batch_size: DEFAULT_BATCH_SIZE, allow_faraday_proxy: false, allow_insecure_base_url: false, connection: nil) ⇒ Cohere

Returns a new instance of Cohere.

Parameters:

  • api_key (String)

    required. Sent as Authorization: Bearer ….

  • model (String) (defaults to: DEFAULT_MODEL)
  • base_url (String) (defaults to: DEFAULT_BASE_URL)

    override. Must be HTTPS unless allow_insecure_base_url: true.

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    read timeout, seconds.

  • open_timeout (Integer) (defaults to: DEFAULT_OPEN_TIMEOUT)

    connect timeout, seconds.

  • max_retries (Integer) (defaults to: DEFAULT_MAX_RETRIES)

    retry attempts on 429/5xx/timeouts.

  • embed_batch_size (Integer) (defaults to: DEFAULT_BATCH_SIZE)

    inputs per request (max 96).

  • allow_faraday_proxy (Boolean) (defaults to: false)

    opt in to proxy / env-proxy autodiscovery. Defaults false.

  • allow_insecure_base_url (Boolean) (defaults to: false)

    permit http:// base (local proxies). Defaults false.

  • connection (Faraday::Connection, nil) (defaults to: nil)

    injection seam for tests.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/parse/embeddings/cohere.rb', line 138

def initialize(
  api_key:,
  model: DEFAULT_MODEL,
  dimensions: nil,
  base_url: DEFAULT_BASE_URL,
  timeout: DEFAULT_TIMEOUT,
  open_timeout: DEFAULT_OPEN_TIMEOUT,
  max_retries: DEFAULT_MAX_RETRIES,
  embed_batch_size: DEFAULT_BATCH_SIZE,
  allow_faraday_proxy: false,
  allow_insecure_base_url: false,
  connection: nil
)
  validate_api_key!(api_key)
  validate_model!(model)
  validate_dimensions!(model, dimensions)
  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, embed_batch_size)
  if embed_batch_size > 96
    raise ArgumentError,
          "Parse::Embeddings::Cohere: embed_batch_size #{embed_batch_size} exceeds Cohere's per-request cap (96)."
  end

  @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 = embed_batch_size
  @allow_faraday_proxy = allow_faraday_proxy
  @connection = connection || build_connection
end

Instance Method Details

#dimensionsObject



176
177
178
# File 'lib/parse/embeddings/cohere.rb', line 176

def dimensions
  @dimensions
end

#embed_batch_sizeObject



184
185
186
# File 'lib/parse/embeddings/cohere.rb', line 184

def embed_batch_size
  @embed_batch_size
end

#embed_image(sources, input_type: :search_document, allow_insecure: false) ⇒ Array<Array<Float>>

Embed a batch of images through Cohere's /v2/embed multimodal 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! (sentinel-gated egress opt-in, CIDR / port / host allowlist) and forwards the canonicalized URL string in the { type: "image_url", image_url: { 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 base64 data URI in the same image_url content row (Cohere v2 accepts data URIs). No URL validation runs and the trust_provider_url_fetch sentinel is NOT required.

Multimodal model required. Cohere's v3 models do not accept image inputs; calling embed_image on a v3-configured provider raises BadRequestError before any network call.

Wire shape differs from Voyage#embed_image. Voyage uses { type: "image_url", image_url: "<url>" } (flat String); Cohere v2 uses { type: "image_url", image_url: { url: "<url>" } } (nested object), matching the OpenAI chat-completions content convention. The high-level SDK contract is identical — callers pass an Array<String> of URLs.

Parameters:

  • sources (Array<String>)

    image URLs. Each must satisfy Parse::Embeddings.validate_image_url!; failing entries abort the whole batch (no partial forwarding).

  • input_type (Symbol) (defaults to: :search_document)

    one of INPUT_TYPE_WIRE_VALUES's keys; mapped to Cohere's input_type field. Defaults to :search_document.

  • allow_insecure (Boolean) (defaults to: false)

    forwarded to the URL validator; permit http:// for local-dev CDN proxies.

Returns:

  • (Array<Array<Float>>)

    vectors aligned 1:1 with sources.



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
335
336
337
338
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
# File 'lib/parse/embeddings/cohere.rb', line 301

def embed_image(sources, input_type: :search_document, allow_insecure: false)
  unless MULTIMODAL_MODELS.include?(@model)
    raise BadRequestError,
          "Parse::Embeddings::Cohere#embed_image: model #{@model.inspect} does not " \
          "accept image inputs. Configure the provider with a multimodal model " \
          "(supported: #{MULTIMODAL_MODELS.inspect})."
  end
  unless sources.is_a?(Array)
    raise ArgumentError,
          "Parse::Embeddings::Cohere#embed_image expects Array of image URLs " \
          "(got #{sources.class})."
  end
  return [] if sources.empty?

  wire_input_type = INPUT_TYPE_WIRE_VALUES[input_type]
  unless wire_input_type
    raise ArgumentError,
          "Parse::Embeddings::Cohere#embed_image input_type #{input_type.inspect} not in " \
          "#{INPUT_TYPE_WIRE_VALUES.keys.inspect}."
  end
  # Cohere caps `/v2/embed` at the same 96-input per-call limit
  # as `/v1/embed`. Guard direct-API callers against a silent
  # 400 — the DSL passes a single URL per directive.
  if sources.length > @embed_batch_size
    raise ArgumentError,
          "Parse::Embeddings::Cohere#embed_image: batch size #{sources.length} exceeds " \
          "the configured cap #{@embed_batch_size} (Cohere per-request max: 96). " \
          "Split the input and call embed_image once per chunk."
  end

  # Validate every URL up-front so a malformed entry in slot N
  # does not slip through after slots 0..N-1 are already in the
  # wire body. URL entries forward the validator's canonicalized
  # URL — not the caller's raw input; fetched-bytes entries skip
  # URL validation (already downloaded + verified by ImageFetch)
  # and forward as a base64 data URI.
  content_rows = sources.each_with_index.map do |src, i|
    if src.is_a?(Parse::Embeddings::ImageFetch::FetchedImage)
      { content: [{ type: "image_url", image_url: { url: src.to_data_uri } }] }
    elsif src.is_a?(String)
      canonical = Parse::Embeddings.validate_image_url!(src, allow_insecure: allow_insecure)
      { content: [{ type: "image_url", image_url: { url: canonical } }] }
    else
      raise ArgumentError,
            "Parse::Embeddings::Cohere#embed_image sources[#{i}] must be a URL String " \
            "or Parse::Embeddings::ImageFetch::FetchedImage (got #{src.class})."
    end
  end

  body = {
    model: @model,
    input_type: wire_input_type,
    embedding_types: ["float"],
    inputs: content_rows,
  }

  instrument_embed(sources.length, input_type, modality: :image) do |emit_payload|
    payload = post_embeddings(body, path: v2_embed_path)
    if payload.is_a?(Hash) && payload["meta"].is_a?(Hash) &&
       payload["meta"]["billed_units"].is_a?(Hash)
      tt = payload["meta"]["billed_units"]["input_tokens"]
      emit_payload[:total_tokens] = tt if tt.is_a?(Integer) && tt >= 0
    end
    vectors = extract_vectors!(payload, sources.length)
    validate_response!(sources.length, vectors)
  end
end

#embed_text(strings, input_type: :search_document) ⇒ Array<Array<Float>>

Returns vectors aligned 1:1 with strings.

Parameters:

Returns:

  • (Array<Array<Float>>)

    vectors aligned 1:1 with strings.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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
# File 'lib/parse/embeddings/cohere.rb', line 204

def embed_text(strings, input_type: :search_document)
  unless strings.is_a?(Array)
    raise ArgumentError,
          "Parse::Embeddings::Cohere#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::Cohere#embed_text strings[#{i}] is not a String (#{s.class})."
    end
    if s.empty?
      raise ArgumentError,
            "Parse::Embeddings::Cohere#embed_text strings[#{i}] is empty; Cohere rejects empty inputs."
    end
  end
  wire_input_type = INPUT_TYPE_WIRE_VALUES[input_type]
  unless wire_input_type
    raise ArgumentError,
          "Parse::Embeddings::Cohere#embed_text input_type #{input_type.inspect} not in " \
          "#{INPUT_TYPE_WIRE_VALUES.keys.inspect}."
  end

  body = {
    texts: strings,
    model: @model,
    input_type: wire_input_type,
    embedding_types: ["float"],
  }
  # Forward `output_dimension` only for Matryoshka-capable models
  # whose active width differs from native. Sending it to a v3
  # row would yield a 400 from Cohere.
  if MATRYOSHKA_MODELS.include?(@model) &&
     @dimensions != MODEL_DEFAULT_DIMENSIONS.fetch(@model)
    body[:output_dimension] = @dimensions
  end

  instrument_embed(strings.length, input_type) do |emit_payload|
    payload = post_embeddings(body)
    # Cohere's response carries `meta.billed_units.input_tokens`
    # (and `output_tokens`, though for embeddings it's 0). Forward
    # input_tokens as the operator-facing cost number on the AS::N
    # payload so cost subscribers can budget across providers.
    if payload.is_a?(Hash) && payload["meta"].is_a?(Hash) &&
       payload["meta"]["billed_units"].is_a?(Hash)
      tt = payload["meta"]["billed_units"]["input_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_attrsObject



369
370
371
# File 'lib/parse/embeddings/cohere.rb', line 369

def inspect_attrs
  super.merge(base: safe_base_host, retries: @max_retries)
end

#max_input_tokensObject



188
189
190
# File 'lib/parse/embeddings/cohere.rb', line 188

def max_input_tokens
  MODEL_MAX_INPUT_TOKENS[@model]
end

#modalitiesArray<Symbol>

Returns [:text, :image] for embed-v4.0, [:text] for v3 models.

Returns:

  • (Array<Symbol>)

    [:text, :image] for embed-v4.0, [:text] for v3 models.



259
260
261
# File 'lib/parse/embeddings/cohere.rb', line 259

def modalities
  MULTIMODAL_MODELS.include?(@model) ? %i[text image] : [:text]
end

#model_nameObject



180
181
182
# File 'lib/parse/embeddings/cohere.rb', line 180

def model_name
  @model
end

#normalize?Boolean

Returns:

  • (Boolean)


192
193
194
195
# File 'lib/parse/embeddings/cohere.rb', line 192

def normalize?
  # Cohere v3 embeddings are documented unit-normalized.
  true
end

#supports_input_type?Boolean

Returns:

  • (Boolean)


197
198
199
# File 'lib/parse/embeddings/cohere.rb', line 197

def supports_input_type?
  true
end