ALLM.Providers.FakeEmbeddings (allm v0.5.0)

Copy Markdown View Source

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:

  1. adapter_opts[:script_cursor] — an explicit Agent pid (handled separately; see start_script_cursor/0).
  2. adapter_opts[:cursor_key] — the engine's stable :id, injected by the façade dispatch chokepoint via ALLM.Engine.put_cursor_key/2.
  3. :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 callsALLM.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

script_entry()

@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

cursor_index(pid)

@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

embed(request, opts)

Execute a scripted embedding request.

Gate order, all before the script is consulted:

  1. adapter_opts[:capture_pid] side-channel (fires even for rejected calls).
  2. input: []{:error, %EmbeddingAdapterError{reason: :invalid_request}}.
  3. 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

max_batch_size()

@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

script(entries)

@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_limited for the first n - 1 calls 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

start_script_cursor()

@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