Behaviour and convenience API for LLM adapters.
Provides a standard interface for LLM providers, with auto-discovery of the
built-in ReqLLMAdapter when req_llm is available.
Configuration
Set a custom adapter in config:
config :ptc_runner, :llm_adapter, MyApp.LLMAdapterOr the built-in adapter is used automatically when {:req_llm, "~> 1.2"} is
added to your dependencies.
Usage
# Create a SubAgent-compatible callback
llm = PtcRunner.LLM.callback("bedrock:haiku", cache: true)
{:ok, step} = PtcRunner.SubAgent.run(agent, llm: llm)
# Stream responses through SubAgent for real-time chat UX
on_chunk = fn %{delta: text} -> send(self(), {:chunk, text}) end
{:ok, step} = PtcRunner.SubAgent.run(agent, llm: llm, on_chunk: on_chunk)
# Stream responses directly (without SubAgent)
{:ok, stream} = PtcRunner.LLM.stream("bedrock:haiku", %{system: "...", messages: [...]})
stream |> Stream.each(fn
%{delta: text} -> send_chunk(text)
%{done: true, tokens: t} -> track_usage(t)
end) |> Stream.run()Custom Adapters
Implement the PtcRunner.LLM behaviour:
defmodule MyApp.LLMAdapter do
@behaviour PtcRunner.LLM
@impl true
def call(model, request) do
# Your implementation
end
@impl true
def stream(model, request) do
# Optional streaming support
end
end
Summary
Functions
Returns the configured LLM adapter module.
Make a direct LLM call using the configured adapter.
Create a SubAgent-compatible callback function for a model.
Stream an LLM response using the configured adapter.
Types
@type message() :: %{role: :system | :user | :assistant | :tool, content: String.t()}
@type tokens() :: %{ optional(:input) => non_neg_integer(), optional(:output) => non_neg_integer(), optional(:cache_creation) => non_neg_integer(), optional(:cache_read) => non_neg_integer(), optional(:total_cost) => float() }
Callbacks
Make an LLM call.
The request map contains:
:system- System prompt string:messages- List of message maps:schema- JSON Schema map (triggers structured output):tools- Tool definitions (triggers tool calling):cache- Boolean for prompt caching
Returns {:ok, response} or {:error, reason}.
@callback stream(model :: String.t(), request :: map()) :: {:ok, Enumerable.t()} | {:error, term()}
Stream an LLM response.
Returns {:ok, stream} where stream is an Enumerable of chunk maps:
%{delta: "text"}for content chunks%{done: true, tokens: %{...}}for the final chunk
Functions
@spec adapter!() :: module()
Returns the configured LLM adapter module.
Resolution order:
config :ptc_runner, :llm_adapter, MyAdapterPtcRunner.LLM.ReqLLMAdapterifreq_llmis available- Raises if no adapter found
Make a direct LLM call using the configured adapter.
Examples
{:ok, response} = PtcRunner.LLM.call("bedrock:haiku", %{
system: "You are helpful.",
messages: [%{role: :user, content: "Hello"}]
})
Create a SubAgent-compatible callback function for a model.
When the request map contains a :stream key with a callback function,
the callback will use adapter.stream/2 (if available) and pipe chunks
through the stream function. The return value remains {:ok, %{content, tokens}}
so downstream code is unaffected.
Options
:cache- Enable prompt caching (default: false)
Examples
llm = PtcRunner.LLM.callback("bedrock:haiku", cache: true)
{:ok, step} = PtcRunner.SubAgent.run(agent, llm: llm)
@spec stream(String.t(), map()) :: {:ok, Enumerable.t()} | {:error, term()}
Stream an LLM response using the configured adapter.
Returns {:ok, stream} where stream emits %{delta: text} and %{done: true, tokens: map()}.
Examples
{:ok, stream} = PtcRunner.LLM.stream("bedrock:haiku", %{
system: "You are helpful.",
messages: [%{role: :user, content: "Tell me a story"}]
})
stream |> Stream.each(fn
%{delta: text} -> IO.write(text)
%{done: true} -> IO.puts("")
end) |> Stream.run()