Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,78 @@ config :req_llm,
{:ok, response} = ReqLLM.stream_text(model, messages, finch_name: MyApp.CustomFinch)
```

## Req Plugins

ReqLLM builds a `Req.Request` struct internally for each operation. The `:req_plugins` option lets you inject custom middleware into this request before it's sent, enabling custom headers, logging, metrics, retries, or any other `Req` step.

Each plugin is a function `(Req.Request.t() -> Req.Request.t())` applied in order. If a plugin raises, the operation returns `{:error, ...}` without sending the request.

### Adding Custom Headers

```elixir
ReqLLM.generate_text("openai:gpt-4o", "Hello",
req_plugins: [
fn req -> Req.Request.put_header(req, "x-request-id", UUID.uuid4()) end
]
)
```

### Composing Multiple Plugins

Plugins are applied left to right:

```elixir
auth_plugin = fn req ->
Req.Request.put_header(req, "x-custom-auth", "Bearer #{my_token()}")
end

logging_plugin = fn req ->
Logger.info("Sending request to #{req.url}")
req
end

ReqLLM.generate_text("openai:gpt-4o", messages,
req_plugins: [auth_plugin, logging_plugin]
)
```

### Reusable Plugin Modules

For plugins shared across your application, define a module:

```elixir
defmodule MyApp.ReqPlugins do
def request_id(req) do
Req.Request.put_header(req, "x-request-id", UUID.uuid4())
end

def telemetry(req) do
Req.Request.register_options(req, [:telemetry_metadata])
Req.Request.append_request_steps(req, telemetry: &emit_start/1)
end

defp emit_start(req) do
:telemetry.execute([:my_app, :llm, :request], %{}, %{url: req.url})
req
end
end

ReqLLM.generate_text("openai:gpt-4o", messages,
req_plugins: [&MyApp.ReqPlugins.request_id/1, &MyApp.ReqPlugins.telemetry/1]
)
```

### Supported Operations

The `:req_plugins` option works with all non-streaming operations:

- `ReqLLM.generate_text/3`
- `ReqLLM.generate_object/4`
- `ReqLLM.embed/3` (single and batch)
- `ReqLLM.generate_image/3`

Streaming operations (`stream_text/3`) build their requests through a different pipeline and do not currently support `:req_plugins`.

## API Key Configuration

Keys are loaded with clear precedence: per-request → in-memory → app config → env vars → .env files.
Expand Down
2 changes: 2 additions & 0 deletions lib/req_llm/embedding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ defmodule ReqLLM.Embedding do
:ok <- validate_input(text),
{:ok, provider_module} <- ReqLLM.provider(model.provider),
{:ok, request} <- provider_module.prepare_request(:embedding, model, text, opts),
{:ok, request} <- ReqLLM.Provider.Defaults.apply_req_plugins(request, opts),
{:ok, %Req.Response{status: status, body: decoded_response}} when status in 200..299 <-
Req.request(request) do
extract_single_embedding(decoded_response)
Expand All @@ -200,6 +201,7 @@ defmodule ReqLLM.Embedding do
:ok <- validate_input(texts),
{:ok, provider_module} <- ReqLLM.provider(model.provider),
{:ok, request} <- provider_module.prepare_request(:embedding, model, texts, opts),
{:ok, request} <- ReqLLM.Provider.Defaults.apply_req_plugins(request, opts),
{:ok, %Req.Response{status: status, body: decoded_response}} when status in 200..299 <-
Req.request(request) do
extract_multiple_embeddings(decoded_response)
Expand Down
2 changes: 2 additions & 0 deletions lib/req_llm/generation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ defmodule ReqLLM.Generation do
with {:ok, model} <- ReqLLM.model(model_spec),
{:ok, provider_module} <- ReqLLM.provider(model.provider),
{:ok, request} <- provider_module.prepare_request(:chat, model, messages, opts),
{:ok, request} <- ReqLLM.Provider.Defaults.apply_req_plugins(request, opts),
{:ok, %Req.Response{status: status, body: decoded_response}} when status in 200..299 <-
Req.request(request) do
{:ok, decoded_response}
Expand Down Expand Up @@ -240,6 +241,7 @@ defmodule ReqLLM.Generation do
opts_with_schema = Keyword.put(opts, :compiled_schema, compiled_schema),
{:ok, request} <-
provider_module.prepare_request(:object, model, messages, opts_with_schema),
{:ok, request} <- ReqLLM.Provider.Defaults.apply_req_plugins(request, opts),
{:ok, %Req.Response{status: status, body: decoded_response}} when status in 200..299 <-
Req.request(request) do
# For models with json.strict = false, coerce response types to match schema
Expand Down
1 change: 1 addition & 0 deletions lib/req_llm/images.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ defmodule ReqLLM.Images do
{:ok, provider_module} <- ReqLLM.provider(model.provider),
{:ok, request} <-
provider_module.prepare_request(:image, model, prompt_or_messages, opts),
{:ok, request} <- ReqLLM.Provider.Defaults.apply_req_plugins(request, opts),
{:ok, %Req.Response{status: status, body: response}} when status in 200..299 <-
Req.request(request) do
{:ok, response}
Expand Down
40 changes: 39 additions & 1 deletion lib/req_llm/provider/defaults.ex
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,49 @@ defmodule ReqLLM.Provider.Defaults do
:operation,
:receive_timeout,
:max_retries,
:req_http_options
:req_http_options,
:req_plugins
]

Keyword.drop(opts, internal_keys)
end

@doc """
Applies user-provided Req plugins to a built request.

Each plugin is a function `(Req.Request.t() -> Req.Request.t())` applied in order.
A crashing plugin returns `{:error, ...}` rather than propagating the exception.
"""
@spec apply_req_plugins(Req.Request.t(), keyword()) ::
{:ok, Req.Request.t()} | {:error, Exception.t()}
def apply_req_plugins(request, opts) do
plugins = Keyword.get(opts, :req_plugins, [])

result =
Enum.reduce_while(plugins, request, fn plugin, req ->
try do
{:cont, plugin.(req)}
rescue
error ->
{:halt, {:error, error, plugin}}
end
end)

case result do
{:error, error, plugin} ->
{:error,
ReqLLM.Error.Unknown.Unknown.exception(
error:
RuntimeError.exception(
"Req plugin #{inspect(plugin)} failed: #{Exception.message(error)}"
)
)}

%Req.Request{} = req ->
{:ok, req}
end
end

@spec default_attach(module(), Req.Request.t(), term(), keyword()) :: Req.Request.t()
def default_attach(provider_mod, %Req.Request{} = request, model_input, user_opts) do
{:ok, %LLMDB.Model{} = model} = ReqLLM.model(model_input)
Expand Down Expand Up @@ -405,6 +442,7 @@ defmodule ReqLLM.Provider.Defaults do
:tools,
:tool_choice,
:req_http_options,
:req_plugins,
:stream,
:frequency_penalty,
:system_prompt,
Expand Down
1 change: 1 addition & 0 deletions lib/req_llm/provider/options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ defmodule ReqLLM.Provider.Options do
:on_unsupported,
:fixture,
:req_http_options,
:req_plugins,
:compiled_schema,
:operation,
:text,
Expand Down
Loading