ExAgent.Model behaviour (ExAgent v2.2.1)

View Source

Behaviour that every provider model implements.

A "model" is any struct whose module @behaviours ExAgent.Model. The struct carries the model's own state (base URL, API key, configured options…). Callers never invoke a provider module directly — they go through the dispatch functions in this module (request/4, request_stream/4, …), which resolve the implementer from the struct's __struct__.

This uses Elixir behaviours plus struct dispatch so providers can stay small and explicit.

Summary

Callbacks

Optional static configuration binding for persisted continuation, without IO.

Declares capabilities (optional; extra capabilities default to false). The core rejects function tools and tool-based structured output when supports_tools is false. Explicit native output requires supports_json_schema_output: true before request admission. Native JSON/thinking flags do not describe tool-output mode and are not used to silently downgrade it. Streaming requires its own callback.

Make a non-streaming request and return a full Message.Response.

Make a lazy streaming request. Events are {:text_delta, binary}, optional {:thinking_delta, binary} (separate from output text), optional {:usage, cumulative_request_usage}, and exactly one terminal {:response, response, final_model} or {:error, reason}. Usage snapshots replace earlier snapshots for this request; they are not additive deltas. When params.tool_call_deltas is true, implementations may also emit {:tool_call_delta, %{index: index, name: name_or_nil, fragment: binary}} as tool-call arguments are generated: the first event for an index carries the tool name, later ones carry raw argument JSON fragments. They are a preview only; the terminal response stays authoritative. Implementations must release resources when enumeration halts. A stream ending without a terminal is a protocol error. Implementations may return {:error, _} directly when streaming is unsupported.

Optional no-IO preflight required by persisted continuation. Validate restored logical history, provider/endpoint binding and codec/settings using the trusted current model and tool inventory. Never dispatch a request from this callback. Ordinary run/request consumers do not require it.

Functions

Returns the bounded, portable static continuation binding; no Model IO.

Resolve a custom Model struct, Test label, or stock ReqLLM model spec.

Types

messages()

@type messages() :: [ExAgent.Message.t()]

model()

@type model() :: struct()

Callbacks

continuation_binding(model)

(optional)
@callback continuation_binding(model()) :: {:ok, term()} | {:error, term()}

Optional static configuration binding for persisted continuation, without IO.

Return {:ok, json_data} (at most 4096 encoded JSON bytes) or {:error, reason}. Exclude credentials, transient state and runtime counters. The normalized value must be deterministic across equivalent configurations. Absent callbacks return {:ok, nil}. Frame restore checks both the trusted template and app-codec result; a changed binding rejects before resuming effects, including a first uncertain Model request with no response history. Callback errors are normalized, not echoed.

model_name(model)

@callback model_name(model()) :: String.t()

profile(model)

(optional)
@callback profile(model()) :: ExAgent.ModelProfile.t()

Declares capabilities (optional; extra capabilities default to false). The core rejects function tools and tool-based structured output when supports_tools is false. Explicit native output requires supports_json_schema_output: true before request admission. Native JSON/thinking flags do not describe tool-output mode and are not used to silently downgrade it. Streaming requires its own callback.

request(model, messages, arg3, t)

@callback request(
  model :: model(),
  messages(),
  ExAgent.ModelSettings.t() | nil,
  ExAgent.ModelRequestParameters.t()
) :: {:ok, ExAgent.Message.Response.t(), model()} | {:error, term()}

Make a non-streaming request and return a full Message.Response.

Implementations return the (possibly updated) model struct alongside the response. Real providers return the model unchanged; stateful models (e.g. the script-driven Test) thread their internal state through the run this way, avoiding globals.

request_stream(model, messages, arg3, t)

(optional)
@callback request_stream(
  model :: model(),
  messages(),
  ExAgent.ModelSettings.t() | nil,
  ExAgent.ModelRequestParameters.t()
) :: Enumerable.t() | {:error, term()}

Make a lazy streaming request. Events are {:text_delta, binary}, optional {:thinking_delta, binary} (separate from output text), optional {:usage, cumulative_request_usage}, and exactly one terminal {:response, response, final_model} or {:error, reason}. Usage snapshots replace earlier snapshots for this request; they are not additive deltas. When params.tool_call_deltas is true, implementations may also emit {:tool_call_delta, %{index: index, name: name_or_nil, fragment: binary}} as tool-call arguments are generated: the first event for an index carries the tool name, later ones carry raw argument JSON fragments. They are a preview only; the terminal response stays authoritative. Implementations must release resources when enumeration halts. A stream ending without a terminal is a protocol error. Implementations may return {:error, _} directly when streaming is unsupported.

system(model)

@callback system(model()) :: String.t()

validate_resume(model, messages, t, t)

(optional)
@callback validate_resume(
  model(),
  messages(),
  ExAgent.ModelSettings.t(),
  ExAgent.ModelRequestParameters.t()
) :: :ok | {:error, term()}

Optional no-IO preflight required by persisted continuation. Validate restored logical history, provider/endpoint binding and codec/settings using the trusted current model and tool inventory. Never dispatch a request from this callback. Ordinary run/request consumers do not require it.

Functions

continuation_binding(model)

Returns the bounded, portable static continuation binding; no Model IO.

model_name(model)

@spec model_name(model()) :: String.t()

profile(model)

@spec profile(model()) :: ExAgent.ModelProfile.t()

request(model, messages, settings, params)

@spec request(
  model(),
  messages(),
  ExAgent.ModelSettings.t() | nil,
  ExAgent.ModelRequestParameters.t()
) ::
  {:ok, ExAgent.Message.Response.t(), model()} | {:error, term()}

request_stream(model, messages, settings, params)

resolve(spec, opts \\ [])

@spec resolve(model() | String.t() | map() | tuple(), keyword()) ::
  {:ok, model()} | {:error, term()}

Resolve a custom Model struct, Test label, or stock ReqLLM model spec.

Custom structs are returned unchanged. Catalogue strings, explicit maps, stock tuples and LLMDB.Model specs resolve through public ReqLLM.model/1 and use ExAgent.Models.ReqLLM. Catalogue membership does not enable tools or streaming. Pass instance credentials and an explicitly qualified profile with resolve/2, or construct ExAgent.Models.ReqLLM directly. No credentials are read from the environment by this resolver. opencode: and zai: shortcuts require explicit spec/endpoint/auth migration; they never silently choose another provider.

system(model)

@spec system(model()) :: String.t()