HTTP-direct OpenAI backend for GenAgent, built on Req.
Provides GenAgent.Backends.OpenAI, which talks directly to the
OpenAI Responses API
(POST /v1/responses) and translates the response into the
normalized GenAgent.Event values the state machine consumes.
Unlike the CLI-backed backends (gen_agent_claude, gen_agent_codex),
this backend:
- Talks HTTP, not a subprocess
- Has no tool use by default (pure text in/text out)
- Tracks conversation state via the API's server-side
previous_response_id, so multi-turn works without resending the full history each turn - Is the simplest backend to use for HTTP-only workflows or when you do not want a CLI dependency
This backend targets the Responses API (/v1/responses), not
Chat Completions. The Responses API is OpenAI's newer agent-first
primitive and is a much cleaner fit for GenAgent:
- Server-side state via
previous_response_idmeans the session struct only has to track one id across turns, not a messages array. - Reasoning models (o1/o3/o4/gpt-5) surface reasoning items in the
output array; this backend ignores them for text extraction but
surfaces
reasoning_tokensin the:usageevent so patterns can reason about cost. - Built-in tools, streaming, and structured outputs are available in future versions without redesigning the session shape.
If you specifically need Chat Completions, open an issue and we
can add GenAgent.Backends.OpenAI.ChatCompletions alongside.
You need an OpenAI API key. Set OPENAI_API_KEY in your
environment, or pass :api_key as a backend option.
def deps do
[
{:gen_agent, "~> 0.2.0"},
{:gen_agent_openai, "~> 0.1.0"}
]
enddefmodule MyApp.Assistant do
use GenAgent
defmodule State do
defstruct responses: []
end
@impl true
def init_agent(_opts) do
backend_opts = [
instructions: "You are a concise, helpful assistant.",
max_output_tokens: 512
]
{:ok, backend_opts, %State{}}
end
@impl true
def handle_response(_ref, response, state) do
{:noreply, %{state | responses: state.responses ++ [response.text]}}
end
end
{:ok, _pid} = GenAgent.start_agent(MyApp.Assistant,
name: "my-assistant",
backend: GenAgent.Backends.OpenAI
)
{:ok, response} = GenAgent.ask("my-assistant", "Explain OTP gen_statem in one sentence.")
IO.puts(response.text)The Responses API is stateful server-side. Each response is
stored for 30 days and can be referenced via previous_response_id
in the next request. This backend threads one id across turns:
# Turn 1: fresh conversation, no previous_response_id
{:ok, r1} = GenAgent.ask("my-assistant", "Remember the number 42")
# Turn 2: backend sends previous_response_id = r1.response_id
# OpenAI replays turn 1's context on the server side
{:ok, r2} = GenAgent.ask("my-assistant", "What number did I ask you to remember?")
# r2.text =~ "42"The previous_response_id lives on the session struct and is
updated via update_session/2 when each terminal :result event
lands. store: true is sent on every request (the default) so
responses remain referenceable.
OpenAI's docs are explicit: instructions from a prior turn do
not carry over when you chain via previous_response_id. This
backend therefore resends :instructions on every request when
the option is set. The per-turn token cost is tiny, but the
invariant matters -- a future optimization that "only sends
instructions once" would silently break system-prompt behavior on
every turn after the first.
:api_key-- OpenAI API key. Defaults toSystem.get_env("OPENAI_API_KEY").:model-- model name. Defaults to"gpt-5".:instructions-- system prompt (string). Resent every turn.:reasoning_effort-- one of:low | :medium | :high | nil. When set, requests a specific reasoning effort for o1/o3/o4/gpt-5.:max_output_tokens-- cap on output tokens per turn. Defaults tonil(model default).:http_fn-- a 1-arity function(request_map) -> {:ok, response_map} | {:error, term}that replaces the defaultReq-backed HTTP call. Intended for tests that want to stub out the API.
See GenAgent.Backends.OpenAI for the full module docs.
This backend is deliberately minimal: text in, text out. The
Responses API supports built-in tools (web search, file search,
code interpreter) and custom function tools, but adding them
means a richer event surface and roundtripping tool results --
better served by a future version or by using gen_agent_claude
if you want tool-using agents today.
mix testUnit tests stub the HTTP layer via the :http_fn backend option,
so no tokens are burned during mix test.
Live tests (tagged :integration) hit the real API and require
OPENAI_API_KEY in the environment:
mix test --only integrationMIT. See LICENSE.