# `ALLM.Providers.FakeEmbeddings`
[🔗](https://github.com/cykod/ALLM/blob/v0.6.1/lib/allm/providers/fake_embeddings.ex#L1)

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 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]]

# `script_entry`

```elixir
@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.

# `cursor_index`

```elixir
@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`

```elixir
@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:

  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`

```elixir
@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`

```elixir
@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`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
