Skip to content

An nginx gateway for LLM agents.

Keep provider credentials and model-routing rules at your gateway. nginx relays the stream; Ruby decides where it goes.

Explore the proxy recipes API reference

3.x · IN DEVELOPMENT

For developers running multiple agents: keep client access and routing rules at the proxy.

ngx_mruby 3.xImplemented · tested on next
CLIENTS
Coding agentsCLIs · your tools
YOUR GATEWAYnginx
mruby / Ruby
Check the keyRoute by model
Concurrent-request limitsCredential replacement
LLM UPSTREAMS
Provider Amock-model-a
Provider Bmock-model-b
Stream relayed byte for byte
Relays Messages / Responses. This diagram illustrates test rules; it sends no API request and translates no formats.

One MCP URL. Separate processing backends.

For search and document services: keep one client endpoint while Ruby applies service-specific routing rules.

AI CLIENT
POST /mcpOne endpoint
Service rulesnginx + Ruby
Searchsearch
Retrieve textread_text
Process sourceprocess_source
Use-case candidate · MCP-specific validation remains. Authorization, discovery and protocol validation belong to the MCP service.
Design requirements and implementation boundaries →

Edit Ruby. Check the next response.

Start with a small handler, then build your proxy from its configuration. The v3 development version builds from source.

01

Start

docker compose up --build --waitThe first run compiles nginx + mruby
02

Edit

hello.rbWithout cache: next request sees the edit
03

Check

curl localhost:8080/helloInspect the real response

Route by the requested model.

The Ruby handler and checked output from the integration test. Keys and upstreams are test fixtures.

Ruby handleragent_proxy_route.rb
r = Nginx::Request.new
v = Nginx::Var.new
routes = {
"mock-model-a" => ["agent_proxy_llm_a", "Bearer upstream-test-key-a"],
"mock-model-b" => ["agent_proxy_llm_b", "Bearer upstream-test-key-b"]
}
route = routes[JSON.parse(r.body)["model"]]
if route
v.agent_proxy_backend = route[0]
v.agent_proxy_upstream_auth = route[1]
Nginx.return Nginx::DECLINED
else
Nginx.return Nginx::HTTP_FORBIDDEN
end
Checked output (mock)agent_proxy_route.expected
# What test/t/cases/agent_proxy.rb compares for routing by model: status,
# port of the upstream, Authorization and x-api-key at the upstream, and
# whether the upstream got the client's body unchanged.
model_a=200 12372 Bearer upstream-test-key-a none true
model_b=200 12373 Bearer upstream-test-key-b none true
unknown_model=403
Show nginx.conf
test/conf/conf.d/60-agent-proxy.conf
upstream agent_proxy_llm_a {
server 127.0.0.1:12372;
}
upstream agent_proxy_llm_b {
server 127.0.0.1:12373;
}
# Concurrent requests per client key: agent_proxy_auth.rb sets the key.
limit_conn_zone $agent_proxy_stream_key zone=agent_proxy_streams:1m;
server {
listen 18132;
# Declares the variables that the hooks assign; never requested.
location = /.agent-proxy-vars {
internal;
set $agent_proxy_key_id "";
set $agent_proxy_stream_key "";
set $agent_proxy_backend "";
set $agent_proxy_upstream_auth "";
return 404;
}
# Most variables stay unset on most requests (a rejected key, a
# request that no hook routed).
uninitialized_variable_warn off;
# The client key, checked for every request of this server before a
# location is chosen. A request without a known key gets 401, and the
# server-level error_page gives it a JSON body.
mruby_server_rewrite_handler build/nginx/html/agent_proxy_auth.rb;
error_page 401 @agent_proxy_401;
# The upstream gets the credential that agent_proxy_route.rb picked and
# never the client's.
proxy_http_version 1.1;
proxy_set_header Connection close;
proxy_set_header Authorization $agent_proxy_upstream_auth;
proxy_set_header x-api-key "";
proxy_buffering off;
# The Anthropic Messages API, and the OpenAI Responses API below with
# the same recipes.
location = /v1/messages {
# One request at a time per client key; a second one gets 429 from
# @agent_proxy_too_many. limit_conn_status is a status that no
# upstream sends, so that an upstream's own 429 passes unchanged.
limit_conn agent_proxy_streams 1;
limit_conn_status 460;
limit_conn_log_level notice;
error_page 460 = @agent_proxy_too_many;
# Picks the upstream block and the credential by the model of the
# JSON body.
mruby_enable_read_request_body on;
mruby_access_handler build/nginx/html/agent_proxy_route.rb;
# Logs $upstream_status after the response.
mruby_log_handler build/nginx/html/agent_proxy_log.rb;
proxy_pass http://$agent_proxy_backend/v1/messages;
}
# The zone of limit_conn is per client key, so a stream of a key on
# either location holds the slot of that key on both.
location = /v1/responses {
limit_conn agent_proxy_streams 1;
limit_conn_status 460;
limit_conn_log_level notice;
error_page 460 = @agent_proxy_too_many;
mruby_enable_read_request_body on;
mruby_access_handler build/nginx/html/agent_proxy_route.rb;
mruby_log_handler build/nginx/html/agent_proxy_log.rb;
proxy_pass http://$agent_proxy_backend/v1/responses;
}
# The proxy's own error bodies, in the error format of the API that the
# client asked for: a named location keeps the request's $uri.
location @agent_proxy_401 {
default_type application/json;
if ($uri = /v1/responses) {
return 401 '{"error":{"message":"invalid API key","type":"invalid_request_error","param":null,"code":null}}';
}
return 401 '{"type":"error","error":{"type":"authentication_error","message":"invalid API key"}}';
}
location @agent_proxy_too_many {
default_type application/json;
add_header retry-after 1 always;
if ($uri = /v1/responses) {
return 429 '{"error":{"message":"too many concurrent requests","type":"rate_limit_error","param":null,"code":null}}';
}
return 429 '{"type":"error","error":{"type":"rate_limit_error","message":"too many concurrent requests"}}';
}
}

On every CI run, the case test/t/cases/agent_proxy.rb sends the requests of its client and compares the values that the client prints with the values shown on this page. The four files last changed on 2026-10-29 in commit 300e251c.

Read the full configuration and prerequisites
planned

Planned for v3

Shared usage accounting, event-level processing and first-byte deadlines. These are not available APIs yet.

Implementation status and plan

The same embedding model for HTTP and TCP.

API routing and access checks

Read request headers and choose a configured upstream.

HTTPRubyupstream
HTTP walkthrough →

Allow or refuse TCP connections

Check the client address before connecting to the backend.

TCPallow / deny
TCP walkthrough →

Documentation for implementation

HTML and Markdown from one source, with versions and validation context.

HTML →Markdown index →

Using the stable 2.7 release?

Stable installation and documentation →