ALLM.Providers.FakeImages (allm v0.3.0)

Copy Markdown View Source

Deterministic, scripted adapter for image-generation testing. Implements ALLM.ImageAdapter. See spec §35.8.

Layer B — runtime. FakeImages is the canonical testing image adapter; it ships in lib/ (not test/support/) because users need it for their own application tests, mirroring the v0.2 ALLM.Providers.Fake precedent.

What FakeImages is (and isn't)

FakeImages mostly ignores the %ALLM.ImageRequest{} passed to generate/2. It does inspect :operation to enforce the supported_operations/0 gate, and reads :metadata to round-trip onto the response per the ImageAdapter contract; otherwise the scripted response is produced irrespective of the request.

Script shapes

opts[:adapter_opts][:image_script] accepts a list of script entries. See script/1 for the full grammar.

adapter_opts: [
  image_script: [
    {:ok, [%ALLM.Image{...}], usage: %ALLM.ImageUsage{...}},
    {:error, %ALLM.Error.ImageAdapterError{reason: :rate_limited}},
    {:retry_until_call, 3}
  ]
]

{:retry_until_call, n} (added in 14.3) returns a synthetic %ALLM.Error.ImageAdapterError{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.generate_image/3.

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

By default the cursor lives in the process dictionary at {:allm_fake_images_cursor, :erlang.phash2(scripts)} — isolated per ExUnit test process (async: true), GC'd on pid-down. Pass adapter_opts[:script_cursor] with the pid returned from start_script_cursor/0 to share / isolate cursors explicitly across processes.

Test-only capture seam

Pass adapter_opts[:capture_pid] with a pid to receive a side-channel message every time generate/2 is invoked, BEFORE the script is consulted. The message has the form:

{ALLM.Providers.FakeImages, :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.FakeImages, :call, _} to pattern-match on the captured payload.

Examples

iex> img = ALLM.Image.from_binary(<<137, 80, 78, 71>>, "image/png")
iex> req = ALLM.ImageRequest.new(prompt: "a kestrel")
iex> opts = [adapter_opts: [image_script: [{:ok, [img]}]]]
iex> {:ok, resp} = ALLM.Providers.FakeImages.generate(req, opts)
iex> length(resp.images)
1

Summary

Types

One scripted image-generation 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 image-generation request. See spec §35.3.

Document and validate the script grammar for adapter_opts[:image_script].

Start an Agent-backed script cursor for cross-process multi-call scripting and for disambiguating content-equal scripts in the same process.

Return the list of operations FakeImages can perform.

Types

script_entry()

@type script_entry() ::
  {:ok, [ALLM.Image.t()]}
  | {:ok, [ALLM.Image.t()], keyword()}
  | {:error, ALLM.Error.ImageAdapterError.t()}
  | {:retry_until_call, pos_integer()}

One scripted image-generation 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.

generate(request, opts)

@spec generate(
  ALLM.ImageRequest.t(),
  keyword()
) :: {:ok, ALLM.ImageResponse.t()} | {:error, ALLM.Error.ImageAdapterError.t()}

Execute a scripted image-generation request. See spec §35.3.

Returns {:error, %ImageAdapterError{reason: :unsupported_operation, metadata: %{operation: op}}} BEFORE consulting the script when request.operation not in supported_operations() (entry-point gate per the conformance contract).

Otherwise reads the script from opts[:adapter_opts][:image_script], advances the process-local cursor, and returns the entry verbatim. Empty / exhausted script returns {:error, %ImageAdapterError{reason: :unknown, metadata: %{cause: :no_scripted_image}}}.

Propagates opts[:request_id] onto response.request_id when the script does not already populate it. Round-trips request.metadata onto response.metadata when the script does not populate it.

When opts[:adapter_opts][:capture_pid] is a pid, sends {ALLM.Providers.FakeImages, :call, %{request: request, opts: opts}} to that pid BEFORE the operation gate fires (so even rejected calls are captured). Side-channel only — does not affect the response.

Examples

iex> img = ALLM.Image.from_url("https://example.com/x.png")
iex> req = ALLM.ImageRequest.new(prompt: "a kestrel")
iex> opts = [adapter_opts: [image_script: [{:ok, [img]}]]]
iex> {:ok, resp} = ALLM.Providers.FakeImages.generate(req, opts)
iex> resp.usage.images
1

iex> img = ALLM.Image.from_binary(<<1>>, "image/png")
iex> req = ALLM.ImageRequest.new(prompt: "a kestrel")
iex> opts = [adapter_opts: [image_script: [{:ok, [img]}], capture_pid: self()]]
iex> {:ok, _} = ALLM.Providers.FakeImages.generate(req, opts)
iex> receive do
...>   {ALLM.Providers.FakeImages, :call, %{request: ^req}} -> :ok
...> after
...>   0 -> :no_message
...> end
:ok

script(entries)

@spec script([script_entry()]) :: :ok

Document and validate the script grammar for adapter_opts[:image_script].

Each entry is one of:

  • {:ok, [%Image{}, ...]} — return the listed images plus a default %ImageUsage{images: length(images)}.
  • {:ok, [%Image{}, ...], usage: %ImageUsage{}} — return the listed images plus the supplied usage.
  • {:error, %ImageAdapterError{}} — return the struct verbatim.

Returns :ok when the script is well-formed; raises ArgumentError on the first invalid entry. (Validation is opt-in — the runtime generate/2 path tolerates a mix of legal entries and surfaces a cursor-exhausted shape on nil lookups.)

Examples

iex> img = ALLM.Image.from_binary(<<1>>, "image/png")
iex> ALLM.Providers.FakeImages.script([{:ok, [img]}])
: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.FakeImages.start_script_cursor()
iex> ALLM.Providers.FakeImages.cursor_index(pid)
0

supported_operations()

@spec supported_operations() :: [ALLM.ImageRequest.operation()]

Return the list of operations FakeImages can perform.

Defaults to all three image operations; tests asserting per-adapter rejection paths typically construct a custom @behaviour ALLM.ImageAdapter module rather than narrowing this list.

Examples

iex> ALLM.Providers.FakeImages.supported_operations()
[:generate, :edit, :variation]