# Middleware
Middleware lets you inject logic before and after each provider sample in the agentic loop — without modifying the loop itself. Common uses: logging, token budget enforcement, content filtering, rate limiting, cost tracking.
## The interface
Implement the `OpenResponses.Middleware` behaviour:
```elixir
defmodule MyApp.Middleware.AuditLog do
@behaviour OpenResponses.Middleware
@impl OpenResponses.Middleware
def before_sample(loop_state) do
# Called before each provider request.
# Return {:cont, loop_state} to proceed,
# or {:halt, reason} to stop the loop.
Logger.info("Sampling from #{loop_state.response.model}", response_id: loop_state.response.id)
{:cont, loop_state}
end
@impl OpenResponses.Middleware
def after_sample(loop_state, items) do
# Called after each provider response is received.
# Return {:cont, loop_state, items} to proceed with (possibly modified) items,
# or {:halt, loop_state, reason} to stop.
{:cont, loop_state, items}
end
end
```
## Registering middleware
In `config/config.exs`:
```elixir
config :open_responses, :middlewares, [
MyApp.Middleware.AuditLog,
MyApp.Middleware.TokenBudget,
MyApp.Middleware.ContentFilter
]
```
Middleware runs in order. If any `before_sample/1` returns `{:halt, reason}`, the loop stops and the response transitions to `failed`. Later middleware modules are not called.
## Examples
### Token budget enforcement
```elixir
defmodule MyApp.Middleware.TokenBudget do
@behaviour OpenResponses.Middleware
@max_tokens 50_000
@impl OpenResponses.Middleware
def before_sample(%{response: %{usage: %{"total_tokens" => used}}} = state)
when used > @max_tokens do
{:halt, :token_budget_exceeded}
end
def before_sample(state), do: {:cont, state}
@impl OpenResponses.Middleware
def after_sample(state, items), do: {:cont, state, items}
end
```
### Content filtering
```elixir
defmodule MyApp.Middleware.ContentFilter do
@behaviour OpenResponses.Middleware
@impl OpenResponses.Middleware
def before_sample(state), do: {:cont, state}
@impl OpenResponses.Middleware
def after_sample(state, items) do
filtered = Enum.reject(items, &contains_pii?/1)
{:cont, state, filtered}
end
defp contains_pii?(item) do
text = get_text(item)
String.match?(text, ~r/\b\d{3}-\d{2}-\d{4}\b/)
end
defp get_text(%{"content" => [%{"text" => text} | _]}), do: text
defp get_text(_), do: ""
end
```
### Rate limiting
```elixir
defmodule MyApp.Middleware.RateLimit do
@behaviour OpenResponses.Middleware
@impl OpenResponses.Middleware
def before_sample(state) do
user_id = state.response.metadata["user_id"]
case MyApp.RateLimiter.check(user_id) do
:ok -> {:cont, state}
{:error, :rate_limited} -> {:halt, :rate_limited}
end
end
@impl OpenResponses.Middleware
def after_sample(state, items), do: {:cont, state, items}
end
```
## Execution order
Middleware runs in list order:
```
before_sample: [AuditLog, TokenBudget, ContentFilter]
│ → AuditLog.before_sample
│ → TokenBudget.before_sample (halts here if over budget)
│ → ContentFilter.before_sample
[provider samples]
after_sample: [AuditLog, TokenBudget, ContentFilter]
│ → AuditLog.after_sample
│ → TokenBudget.after_sample
│ → ContentFilter.after_sample (filters items here)
```
On halt in `before_sample`, the remaining middleware and the provider call are skipped. On halt in `after_sample`, the loop terminates after the current items are processed.