PtcRunner.LLM behaviour (PtcRunner v0.9.0)

Copy Markdown View Source

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.LLMAdapter

Or 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

Callbacks

Make an LLM call.

Stream an LLM response.

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

chunk()

@type chunk() :: %{delta: String.t()} | %{done: true, tokens: tokens()}

message()

@type message() :: %{role: :system | :user | :assistant | :tool, content: String.t()}

response()

@type response() :: %{content: String.t(), tokens: tokens()}

tokens()

@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()
}

tool_call_response()

@type tool_call_response() :: %{
  tool_calls: [map()],
  content: String.t() | nil,
  tokens: tokens()
}

Callbacks

call(model, request)

@callback call(model :: String.t(), request :: map()) :: {:ok, map()} | {:error, term()}

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}.

stream(model, request)

(optional)
@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

adapter!()

@spec adapter!() :: module()

Returns the configured LLM adapter module.

Resolution order:

  1. config :ptc_runner, :llm_adapter, MyAdapter
  2. PtcRunner.LLM.ReqLLMAdapter if req_llm is available
  3. Raises if no adapter found

call(model, request)

@spec call(String.t(), map()) :: {:ok, map()} | {:error, term()}

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"}]
})

callback(model, opts \\ [])

@spec callback(
  String.t(),
  keyword()
) :: (map() -> {:ok, map()} | {:error, term()})

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)

stream(model, request)

@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()