One-call host integration

View Source

ReqLLM owns one model interaction. A host such as Jido owns everything that happens between interactions: policy, approvals, tool execution, persistence, loop termination, retries of completed steps, and the decision to call a model again.

This guide describes the stable V1 boundary for that integration. It uses the same public API as an application calling ReqLLM directly; there is no parallel host facade or agent runtime.

The boundary

PhaseReqLLM providesThe host owns
ResolveReqLLM.model/1 and an %LLMDB.Model{}Model selection policy
InvokeOne ReqLLM.generate_text/3 or ReqLLM.stream_text/3 operationWhen and whether to invoke it
ConsumeReqLLM.Response, ReqLLM.StreamResponse, and ReqLLM.StreamEventUI updates and application state
InspectNormalized output, tool calls, usage, warnings, finish reasons, and errorsApproval and execution policy
ContinueCanonical tool-result messages and ReqLLM.Context.append_tool_exchange/3Tool results and any later invocation
ObserveA ReqLLM request_id and caller-provided conversation_idWorkflow, tenant, trace, and step correlation

Keep control flow on canonical values and projections:

response.provider_meta and the provider_metadata returned by ReqLLM.Response.call_metadata/1 are provider extensions. They are useful for observability and provider-specific capabilities, but a portable host should not depend on their keys for core control flow.

One buffered interaction

A host adapter can keep one call small and explicit:

defmodule MyApp.LLMHost do
  def call_once(model_spec, context, tools, conversation_id) do
    with {:ok, model} <- ReqLLM.model(model_spec),
         {:ok, response} <-
           ReqLLM.generate_text(model, context,
             tools: tools,
             telemetry: [conversation_id: conversation_id]
           ) do
      {:ok,
       %{
         response: response,
         outcome: ReqLLM.Response.classify(response),
         output_items: ReqLLM.Response.output_items(response),
         usage: ReqLLM.Response.usage(response),
         call: ReqLLM.Response.call_metadata(response)
       }}
    end
  end
end

The non-bang APIs preserve {:ok, value} and {:error, reason} so the host can handle failures without converting them to exceptions. Bang variants remain available when exception-based control flow is intentional.

The call performs no follow-up work. If outcome.type is :tool_calls, the host decides whether each call is allowed, rejected, deferred, or executed.

Transcription keeps its established compact result by default. A host that needs call-level observability can opt into the detailed facade without making a second provider request or changing the nested result:

{:ok, detailed} =
  ReqLLM.transcribe_detailed(model, audio,
    telemetry: [conversation_id: conversation_id]
  )

transcript = detailed.result
call = detailed.call_metadata

call always identifies the resolved model and provider. Usage and cost, request or response identifiers, warnings, timings, and provider metadata are present only when observed. The top-level request_id is ReqLLM's telemetry correlation ID; a provider request ID remains under provider_metadata.

Inspect, execute, and continue explicitly

The host can inspect calls before any callback runs:

resolutions =
  Enum.map(ReqLLM.Response.tool_calls(response), fn call ->
    ReqLLM.ToolCall.resolve(call, tools)
  end)

An application call is executable only when its resolution state is :valid. Unknown or invalid calls, provider-executed builtins, and provider-native calls remain visible without being treated as application callbacks. The host owns approval and may call ReqLLM.ToolCall.execute/3, invoke its own tool runtime, or produce an application-defined failure result. Provider-executed builtins need no local result; a provider-native call still needs an explicit matched result before its exchange can be appended for continuation.

After execution, build canonical result messages and append the complete exchange:

results = [
  ReqLLM.Context.tool_result("call_1", "lookup", "Documentation found")
]

{:ok, continued_context} =
  ReqLLM.Context.append_tool_exchange(input_context, response, results)

Results are matched by tool-call ID and appended in assistant call order. The helper accepts either the original input context or response.context; it does not duplicate the assistant message when that message is already last.

Tool definitions can contain callback functions and are application runtime state. Exclude them from durable data and restore the host's current tool registry before a later call:

checkpoint = Jason.encode!(%{continued_context | tools: []})

JSON decoding returns ordinary maps, so the host also owns reconstructing and versioning %ReqLLM.Context{}, message, and content-part values at its persistence boundary. ReqLLM does not provide a checkpoint store or resume a workflow. A later model interaction is another explicit host action:

MyApp.LLMHost.call_once(model, restored_context, tools, conversation_id)

One streaming interaction

ReqLLM.StreamResponse has one consumable stream. Choose exactly one view for each response:

Consuming one view consumes the underlying stream. Do not enumerate events and then try to materialize the same StreamResponse.

For a host that consumes events directly:

with {:ok, stream_response} <-
       ReqLLM.stream_text(model, context,
         tools: tools,
         telemetry: [conversation_id: conversation_id]
       ) do
  try do
    stream_response
    |> ReqLLM.StreamResponse.events()
    |> Enum.each(&MyApp.Events.handle/1)
  after
    ReqLLM.StreamResponse.close(stream_response)
  end
end

A fully consumed event stream begins with :start, preserves output delta order, emits assembled tool calls, usage, and warnings when available, and ends with one :finish, :cancelled, or :error event. Always call ReqLLM.StreamResponse.close/1 after consuming the raw stream or using tokens/1, events/1, text/1, tool_calls/1, extract_tool_calls/1, or classify/1. An after block also guarantees cancellation and cleanup when enumeration stops early or raises. Only process_stream/2 and to_response/1 close their metadata handles before returning.

Buffered responses and fully materialized streams expose the same canonical information for capabilities supported by both paths. Streaming deltas retain arrival order and may expose information incrementally before the final values are known.

Correlation and errors

ReqLLM assigns one request_id to all telemetry events for a model interaction. Pass telemetry: [conversation_id: value] to add the host's workflow, session, or step correlation. The host should use conversation_id across interactions and treat each ReqLLM request_id as one call within that larger operation.

Provider and validation failures retain ReqLLM's public error tuples and error structures. Event consumers receive a terminal :error event for failures encountered by the event projection; direct legacy chunk consumption keeps its existing exception behavior. Cross-call retry policy remains a host decision.

What this contract does not include

ReqLLM does not schedule another interaction, implement an agent loop, approve tools, persist memory, checkpoint workflows, delegate tasks, or manage a sandbox. A concrete Jido adapter belongs in Jido or a separate integration package and should depend on the public values in this guide rather than provider modules or wire payloads.

See the compatibility policy for the complete ReqLLM/Jido ownership boundary, the provider-native integration guide for MCP and provider-owned capability placement, and the telemetry guide for event details.