Text-embedding provider adapter contract.
Layer B — runtime. Implementations take an ALLM.EmbeddingRequest plus a
keyword opts list (resolved at the call site by ALLM.embed/3) and return
either {:ok, %ALLM.EmbeddingResponse{}} or
{:error, %ALLM.Error.EmbeddingAdapterError{}}.
Minimum impl skeleton
defmodule MyEmbeddingProvider do
@behaviour ALLM.EmbeddingAdapter
@impl true
def embed(%ALLM.EmbeddingRequest{input: input}, _opts) do
cond do
input == [] ->
{:error, %ALLM.Error.EmbeddingAdapterError{reason: :invalid_request}}
length(input) > max_batch_size() ->
{:error,
%ALLM.Error.EmbeddingAdapterError{
reason: :batch_too_large,
metadata: %{count: length(input), max: max_batch_size()}
}}
true ->
# Translate request -> HTTP body, fire via Req, translate
# response -> %ALLM.EmbeddingResponse{}.
{:ok, %ALLM.EmbeddingResponse{}}
end
end
@impl true
def max_batch_size, do: 1_000
endBoth gates are mandatory (invariants 4 and 5) and both MUST run before any
HTTP I/O — and, for an adapter that resolves credentials, before
ALLM.Keys.fetch!/2, so that a keyless environment still observes the
rejection rather than a %ALLM.Error.EngineError{reason: :missing_key}.
HTTP transport guidance
Use Req for embedding calls. Embeddings are a request/response shape —
there is no streaming counterpart, so there is no stream_embed/2 and no
ALLM.EmbeddingStreamAdapter.
Batching
max_batch_size/0 is the per-request input cap the provider documents.
ALLM.embed/3 splits an oversized call into chunks of at most that many
inputs and merges the responses, so embed/2 never sees more than
max_batch_size/0 inputs through the façade. Callers driving an adapter
directly are responsible for their own chunking and get :batch_too_large
when they exceed the cap.
Invariants
embed/2is synchronous: it returns only after the HTTP response has been read in full.embed/2never raises for HTTP-shaped failures. Network failures, 4xx, and 5xx all convert to{:error, %ALLM.Error.EmbeddingAdapterError{reason: ..., ...}}. The one documented exception isALLM.Keys.fetch!/2, which raises%ALLM.Error.EngineError{reason: :missing_key}by design; adapters do not rescue it.The return shape is enforced, not merely documented:
ALLM.embed/3raisesArgumentErrornaming the adapter and this invariant whenembed/2returns anything other than those two tuples. The façade does not launder non-conforming shapes into its own error union. Note that the conformance suite's error cases are pre-flight argument gates, so a transport, auth, or rate-limit failure that returns a raw error struct will pass conformance and raise in production — convert every failure shape.embed/2MUST honoropts[:request_timeout]if provided. Exceeding the timeout produces{:error, %ALLM.Error.EmbeddingAdapterError{reason: :timeout}}.embed/2MUST return{:error, %ALLM.Error.EmbeddingAdapterError{reason: :batch_too_large, metadata: %{count: n, max: max_batch_size()}}}BEFORE any HTTP I/O whenlength(request.input) > max_batch_size().embed/2MUST return{:error, %ALLM.Error.EmbeddingAdapterError{reason: :invalid_request}}forinput: []BEFORE any HTTP I/O. The bar holds at the adapter for direct callers even thoughALLM.embed/3also validates.embed/2MUST preserveopts[:request_id]ontoresponse.request_idwhen the response shape allows. Whenopts[:request_id]is absent, the adapter is free to populate it from a provider-supplied id.embed/2MUST round-triprequest.metadataontoresponse.metadataUNCHANGED when the adapter has no use for it. (The library treats request/response metadata as opaque.)embed/2MUST return exactlylength(request.input)embeddings on success, with:indexvalues0..length-1, each vector the same non-zero length.max_batch_size/0is per-module and constant — NOT per-model. Per-model limits are the adapter's internal concern.prepare_request/2(optional) returns an unfiredReq.Requestconfigured exactly asembed/2would fire it, and is defined only forlength(input) <= max_batch_size(). Callers may mutate the returned request before firing.
Cleanup invariant: none. There is no Stream.resource/3 and no Finch
ref in an embeddings call — Req.request/1 owns its connection lifecycle.
Stated explicitly so the absence reads as intent rather than omission.
Summary
Callbacks
Execute an embedding request against the provider synchronously.
Return the maximum number of inputs the provider accepts in a single request.
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize (headers, retries, middleware) before firing.
Callbacks
@callback embed( ALLM.EmbeddingRequest.t(), keyword() ) :: {:ok, ALLM.EmbeddingResponse.t()} | {:error, ALLM.Error.EmbeddingAdapterError.t()}
Execute an embedding request against the provider synchronously.
Returns {:ok, %ALLM.EmbeddingResponse{}} on success, or
{:error, %ALLM.Error.EmbeddingAdapterError{}} on every failure shape.
See ALLM.Error.EmbeddingAdapterError for the closed reason enum and the
per-reason recovery table.
@callback max_batch_size() :: pos_integer()
Return the maximum number of inputs the provider accepts in a single request.
Per-module (one number for the adapter), NOT per-call-with-model-arg. Per-model caps are the adapter's internal concern.
ALLM.embed/3 reads this to size its chunks. Callers who need bounded
retry or wall-clock budgets read it too, and drive embed/2 a chunk at a
time themselves.
@callback prepare_request( ALLM.EmbeddingRequest.t(), keyword() ) :: {:ok, Req.Request.t()} | {:error, ALLM.Error.EmbeddingAdapterError.t()}
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize (headers, retries, middleware) before firing.
Optional. When unimplemented, callers must dispatch to embed/2 directly.