Skip to content

Responses / OpenAI (embeddings)

OpenAI’s embedding line is the boring, dependable choice — text in, dense float32 vectors out, nothing fancier. Two sizes, Matryoshka truncation, no task semantics. If you want sparse, multivector, or images, see Jina or Gemini.

Install

Terminal window
pnpm add @effect-uai/core @effect-uai/responses effect

Wire it up

import { Config, Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { layer as openaiEmbeddingLayer } from "@effect-uai/responses/OpenAIEmbedding"
const provider = Layer.unwrap(
Effect.gen(function* () {
const apiKey = yield* Config.redacted("OPENAI_API_KEY")
return openaiEmbeddingLayer({ apiKey })
}),
)
const mainLayer = provider.pipe(Layer.provide(FetchHttpClient.layer))

openaiEmbeddingLayer registers two service tags from one underlying implementation:

  • OpenAIEmbedding — the typed tag. Yield this when you want the OpenAI-typed model union and the narrow request shape.
  • EmbeddingModel — the generic tag. Yield this in provider-portable code.

Request shape

interface OpenAIEmbedRequest extends Omit<CommonEmbedRequest, "model" | "task"> {
readonly model: OpenAIEmbeddingModel
readonly dimensions?: number
readonly encoding?: Encoding
}

Note what’s not there: no task. OpenAI’s embedding API has no task-type semantics, so the field is omitted from the typed request — passing it is a compile error. The generic EmbeddingModel tag accepts and silently ignores task so portable code keeps working.

dimensions is Matryoshka truncation: pass any value from 1 to the model’s native dimensionality and the server truncates the vector to that length. Useful when your downstream index has a fixed dim budget.

Calling it

import { OpenAIEmbedding } from "@effect-uai/responses/OpenAIEmbedding"
const program = Effect.gen(function* () {
const oai = yield* OpenAIEmbedding
return oai.embedMany({
model: "text-embedding-3-small",
inputs: documents,
dimensions: 512, // Matryoshka truncation
})
})

Or via the generic tag:

import { embedMany } from "@effect-uai/core/EmbeddingModel"
const result =
yield *
embedMany({
model: "text-embedding-3-small",
inputs: documents,
})

Models

OpenAIEmbeddingModel is a literal union with a (string & {}) tail:

ModelNative dimsMatryoshka
text-embedding-3-small15361..1536
text-embedding-3-large30721..3072
text-embedding-ada-0021536no

Reference: OpenAI embeddings guide.

Encoding support

encodingBehaviour
float32 (default)Native JSON number[] decoding.
int8 / binaryRejected at the OpenAI API.
sparse / multivectorRejected at the OpenAI API.

For storage-cost reductions, do float32 → int8 / binary quantization on your side, or pick a provider that ships quantized output natively (Jina, Cohere, Voyage).

Input shapes

OpenAI’s embedding endpoint takes a single text string per input. The layer accepts the full EmbedInput union for parity, with these behaviours:

  • string — passed through.
  • { text } — passed through.
  • { image } — rejected with AiError.InvalidRequest.
  • { content: [...] } — text parts concatenated with newlines, any image part rejected.

Errors

HTTP failures map to typed AiError variants:

StatusError
429AiError.RateLimited
408/504AiError.Timeout
401AiError.AuthFailed (auth)
403AiError.AuthFailed (permission)
402AiError.AuthFailed (billing)
413AiError.ContextLengthExceeded
>= 500AiError.Unavailable
other 4xxAiError.InvalidRequest

Recover per-tag with Effect.catchTag("RateLimited", handler).

See also