The seam through which the package calls a host's LLM engine.
The package declares no host dependency (see this repo's CLAUDE.md §1),
so ALLM.Pipeline.LLMStep cannot name a host's engine module (e.g.
MyApp.LLMEngine). The engine is reached at RUNTIME
through impl/0 instead, exactly as the repo is reached through
ALLM.Pipeline.Config.repo/0 and the persistence adapter through
ALLM.Pipeline.Store.impl/0.
The success shape is the host's, unchanged
generate_structured/4 returns the {:ok, %{parsed: _, tokens: _}} envelope
the host's engine already returns, and the error term stays opaque to the
package. This seam relocates the call; it does not redefine it. A host
adapter is therefore a delegation, not a translation — e.g. a host's
MyApp.Pipelines.LLM.
Engine names are the host's vocabulary
resolve_engine/1 takes an atom naming a call-site intent (:nano,
:summarize, …) and returns whatever engine value the host's
generate_structured/4 accepts. The package never inspects it and never
validates the name — the vocabulary belongs to the host, and a step declaring
engine: :nano is asserting that its host knows that name. An unknown name is
the host adapter's error to raise.
There is no package default, and impl/0 raises
Unlike Store, Artifacts and Lock — each of which ships an adapter the
package can fall back to — there is nothing here for the package to default
to: an LLM adapter is a provider integration with credentials, retry policy
and logging, all of which live in the host. A nil-returning or silently
no-op default would be the documented fail-open shape (root CLAUDE.md: "a
test-env default that does live I/O fails OPEN"), and its inverse — a default
that quietly does nothing — is no better, because a step would then report
success having called no model.
So impl/0 raises, naming the llm: registry key. That is the same shape
ALLM.Pipeline.Config.repo/0 uses, and for the same reason: the alternative fails far from
the cause.
Configuration
defmodule MyApp.Pipelines do
use ALLM.Pipeline.Registry,
repo: MyApp.Repo,
store: …, artifacts: …, lock: …,
llm: MyApp.Pipelines.LLM
endllm: is optional on the registry: a host that runs no LLM steps should
not have to name an engine. Declaring it writes
config :allm_pipeline, ALLM.Pipeline.LLM, impl: MyApp.Pipelines.LLM, with
put_new semantics, so an env-specific config-file override still wins (see
ALLM.Pipeline.Registry, "Precedence").
Summary
Types
A host engine handle, opaque to the package.
A prompt string, or an explicit message list the host's engine understands.
The host's structured-output envelope, unchanged.
Callbacks
Dispatch a strict-mode structured-output request and return the host's envelope.
Resolve a call-site intent name (:nano, :summarize, …) to a host engine.
Types
@type engine() :: term()
A host engine handle, opaque to the package.
Whatever resolve_engine/1 returns is passed straight back into
generate_structured/4; nothing here inspects it.
A prompt string, or an explicit message list the host's engine understands.
@type result() :: {:ok, %{parsed: map(), tokens: non_neg_integer()}} | {:error, term()}
The host's structured-output envelope, unchanged.
parsed is the decoded JSON object with string keys —
ALLM.Pipeline.LLMStep's coerce/2 reads it by wire property name.
Callbacks
@callback generate_structured( prompt :: prompt(), schema :: map(), schema_name :: String.t(), engine :: engine() ) :: result()
Dispatch a strict-mode structured-output request and return the host's envelope.
schema is the derived strict-mode JSON schema
(__allm_schema__(:json_schema)); schema_name is the name the provider
records the response format under.
Resolve a call-site intent name (:nano, :summarize, …) to a host engine.
Raising on an unknown name is the adapter's responsibility: a typo'd
engine: on a step must not silently fall back to a default engine.
Functions
@spec impl() :: module()
The host's LLM adapter.
Resolved at RUNTIME, like every config read in this package. Unlike the three
adapter seams there is no package default — see the moduledoc — so this
raises when the host declared no llm: rather than returning nil and
failing inside a generated call_llm/1 with a BadFunctionError that names
neither this key nor this package.