Deterministic, scripted adapter for text-embedding testing. Implements
ALLM.EmbeddingAdapter.
Layer B — runtime. FakeEmbeddings is the canonical testing embedding
adapter; it ships in lib/ (not test/support/) because users need it for
their own application tests, mirroring the ALLM.Providers.Fake and
ALLM.Providers.FakeImages precedent.
What FakeEmbeddings is (and isn't)
FakeEmbeddings mostly ignores the %ALLM.EmbeddingRequest{} passed to
embed/2. It does inspect :input to enforce the empty-input and
batch-size gates that the ALLM.EmbeddingAdapter contract requires of
every implementation, and reads :model / :metadata to round-trip onto
the response; otherwise the scripted response is produced irrespective of
the request. In particular the scripted vectors are not derived from
the input strings, and the script — not length(input) — decides how many
embeddings come back.
Script shapes
opts[:adapter_opts][:embedding_script] accepts a list of script entries.
See script/1 for the full grammar.
adapter_opts: [
embedding_script: [
{:ok, [%ALLM.Embedding{...}]},
{:ok, [%ALLM.Embedding{...}], usage: %ALLM.Usage{...}},
{:error, %ALLM.Error.EmbeddingAdapterError{reason: :rate_limited}},
{:retry_until_call, 3}
]
]{:retry_until_call, n} returns a synthetic
%ALLM.Error.EmbeddingAdapterError{reason: :rate_limited, retry_after_ms: 0}
for the first n - 1 calls against this entry, then advances the cursor to
the next entry on call n. Vehicle for testing ALLM.Retry.run/3
integration in ALLM.embed/3. Consecutive {:retry_until_call, _} entries
chain: the call that exhausts one entry's budget lands on the next entry
and opens its budget, which is how a layered retry budget is scripted.
Multi-call scripting uses the same list — each call advances a process-local
cursor (or an explicit Agent cursor passed via
adapter_opts[:script_cursor] from start_script_cursor/0).
Cursor behaviour
Multi-call scripts (:embedding_script) advance a per-process cursor on
every call. The cursor lives in the process dictionary at
{:allm_fake_embeddings_cursor, key_id}, isolated per ExUnit test process
(async: true), GC'd on pid-down, zero-setup for the common case. The
key_id is chosen by this precedence:
adapter_opts[:script_cursor]— an explicit Agent pid (handled separately; seestart_script_cursor/0).adapter_opts[:cursor_key]— the engine's stable:id, injected by the façade dispatch chokepoint viaALLM.Engine.put_cursor_key/2.:erlang.phash2(script)— the content-hash fallback for direct adapter calls with no engine.
At the façade the cursor keys on engine identity, so two engines built with
content-equal :embedding_script values each read index 0 on their first
call, even in the same process. The content-hash footgun remains only for
DIRECT adapter calls — ALLM.Providers.FakeEmbeddings.embed(req, opts)
invoked without an engine receives no :cursor_key. Workaround for that
path: pass distinct adapter_opts[:script_cursor] Agent pids from
start_script_cursor/0.
Test-only capture seam
Pass adapter_opts[:capture_pid] with a pid to receive a side-channel
message every time embed/2 is invoked, BEFORE any gate runs and before
the script is consulted. The message has the form:
{ALLM.Providers.FakeEmbeddings, :call, %{request: request, opts: opts}}This is purely a side-channel — it does NOT affect the response. It exists
so test files can assert on what the adapter received without the
Process.register/2 + named-pid pattern (which forces async: false).
With :capture_pid in scope, tests stay async: true and use
assert_receive {ALLM.Providers.FakeEmbeddings, :call, _} to pattern-match
on the captured payload.
Examples
iex> e = ALLM.Embedding.new(vector: [0.1, 0.2])
iex> req = ALLM.EmbeddingRequest.new(input: ["a kestrel"])
iex> opts = [adapter_opts: [embedding_script: [{:ok, [e]}]]]
iex> {:ok, resp} = ALLM.Providers.FakeEmbeddings.embed(req, opts)
iex> ALLM.EmbeddingResponse.vectors(resp)
[[0.1, 0.2]]
Summary
Types
One scripted embedding result.
Functions
Read the current cursor index for an Agent-backed cursor. Used in tests to assert how many calls have been consumed.
Execute a scripted embedding request.
Return the maximum number of inputs FakeEmbeddings accepts per call.
Document and validate the script grammar for
adapter_opts[:embedding_script].
Start an Agent-backed script cursor for cross-process multi-call scripting and for disambiguating content-equal scripts in the same process.
Types
@type script_entry() :: {:ok, [ALLM.Embedding.t()]} | {:ok, [ALLM.Embedding.t()], keyword()} | {:error, ALLM.Error.EmbeddingAdapterError.t()} | {:retry_until_call, pos_integer()}
One scripted embedding result.
Functions
@spec cursor_index(pid()) :: non_neg_integer()
Read the current cursor index for an Agent-backed cursor. Used in tests to assert how many calls have been consumed.
Examples
iex> pid = ALLM.Providers.FakeEmbeddings.start_script_cursor
iex> e = ALLM.Embedding.new(vector: [0.0])
iex> req = ALLM.EmbeddingRequest.new(input: ["x"])
iex> opts = [adapter_opts: [embedding_script: [{:ok, [e]}], script_cursor: pid]]
iex> {:ok, _} = ALLM.Providers.FakeEmbeddings.embed(req, opts)
iex> ALLM.Providers.FakeEmbeddings.cursor_index(pid)
1
@spec embed( ALLM.EmbeddingRequest.t(), keyword() ) :: {:ok, ALLM.EmbeddingResponse.t()} | {:error, ALLM.Error.EmbeddingAdapterError.t()}
Execute a scripted embedding request.
Gate order, all before the script is consulted:
adapter_opts[:capture_pid]side-channel (fires even for rejected calls).input: []→{:error, %EmbeddingAdapterError{reason: :invalid_request}}.length(input) > max_batch_size()→{:error, %EmbeddingAdapterError{reason: :batch_too_large, metadata: %{count: n, max: max}}}.
Otherwise reads the script from opts[:adapter_opts][:embedding_script],
advances the process-local cursor, and returns the entry verbatim. An empty
or exhausted script returns
{:error, %EmbeddingAdapterError{reason: :unknown, metadata: %{cause: :no_scripted_embedding}}}.
Propagates opts[:request_id] onto response.request_id, request.model
onto response.model, and round-trips request.metadata onto
response.metadata.
Examples
iex> e = ALLM.Embedding.new(vector: [1.0, 0.0])
iex> req = ALLM.EmbeddingRequest.new(input: ["x"], metadata: %{trace: "t1"})
iex> opts = [adapter_opts: [embedding_script: [{:ok, [e]}]], request_id: "rid-1"]
iex> {:ok, resp} = ALLM.Providers.FakeEmbeddings.embed(req, opts)
iex> {resp.request_id, resp.metadata}
{"rid-1", %{trace: "t1"}}
iex> req = ALLM.EmbeddingRequest.new(input: [])
iex> {:error, err} = ALLM.Providers.FakeEmbeddings.embed(req, [])
iex> err.reason
:invalid_request
@spec max_batch_size() :: pos_integer()
Return the maximum number of inputs FakeEmbeddings accepts per call.
Matches the largest cap among the bundled real providers so a test written against the fake is not accidentally narrower than production. Tests that need to observe chunking supply their own stub with a smaller cap.
Examples
iex> ALLM.Providers.FakeEmbeddings.max_batch_size
2048
@spec script([script_entry()]) :: :ok
Document and validate the script grammar for
adapter_opts[:embedding_script].
Each entry is one of:
{:ok, [%ALLM.Embedding{}, ...]}— return the listed embeddings plus a default%ALLM.Usage{}.{:ok, [%ALLM.Embedding{}, ...], usage: %ALLM.Usage{}}— return the listed embeddings plus the supplied usage.{:error, %ALLM.Error.EmbeddingAdapterError{}}— return the struct verbatim.{:retry_until_call, n}— synthetic:rate_limitedfor the firstn - 1calls against this entry. Consecutive entries of this shape chain into a layered budget (the call that exhausts one entry opens the next entry's budget).
Returns :ok when the script is well-formed; raises ArgumentError on the
first invalid entry. (Validation is opt-in — the runtime embed/2 path
tolerates a mix of legal entries and surfaces a cursor-exhausted shape on
nil lookups.)
Examples
iex> e = ALLM.Embedding.new(vector: [0.0])
iex> ALLM.Providers.FakeEmbeddings.script([{:ok, [e]}])
:ok
@spec start_script_cursor() :: pid()
Start an Agent-backed script cursor for cross-process multi-call scripting and for disambiguating content-equal scripts in the same process.
Pass the returned pid as adapter_opts[:script_cursor]; subsequent calls
increment the cursor on the Agent rather than on the process dictionary.
Examples
iex> pid = ALLM.Providers.FakeEmbeddings.start_script_cursor
iex> ALLM.Providers.FakeEmbeddings.cursor_index(pid)
0