An LLM-gateway fleet behind LiteLLM¶
The situation¶
A platform team runs a LiteLLM proxy that every internal agent uses to reach its models. The agents are not theirs — each product team writes its own — but every model response passes through the proxy. The reeflex-litellm guardrail runs there, after the model answers and before the response goes back, and builds one Action Envelope per tool call the model proposed.
What goes wrong without a gate¶
The platform team can see that a team's agent asked the model to call
run_shell with kubectl delete namespace payments, and has no place to stop
it: the tool runs in the team's own process, after the response leaves the
proxy. Multiply by every team and every agent framework, and the one place
that sees all the proposed actions is the place that cannot refuse any of them.
Three representative actions¶
The guardrail reuses reeflex-claude's classifier: a gateway tool it can match
by name and argument shape (a shell, a file tool) is priced as that tool; one
it cannot match takes the classifier's unknown-tool path. Each envelope is
what reeflex_litellm.envelope.build_gateway_envelope emits for tenant
org-acme-demo, trimmed of transport fields.
bash with ls -la /srv/app. Matched as a shell, priced as a read.
Allowed.
Verdict: allow · rule reeflex.policy/read_only_internal
{
"agent": { "id": "agent:litellm-gateway/org-acme-demo/gpt-x",
"session_id": "litellm:org-acme-demo:sess-1" },
"action": { "namespace": "litellm-gateway", "verb": "read", "ability": "litellm/bash" },
"target": { "kind": "command", "ref": null, "environment": "production" },
"params": { "tool_name": "Bash", "verb_source": "bash_intent_parse:ls -la /srv/app",
"gateway": "litellm", "gateway_tool": "bash" },
"magnitude": { "count": 1 },
"axes": { "reversibility": "reversible", "blast_radius": "single", "externality": "internal" },
"approval": { "present": false }
}
run_shell with kubectl delete namespace payments. Matched as a shell;
the classifier recognises a namespace delete and prices it irreversible/broad.
Held. The response is withheld for up to reeflex_hold_wait seconds while the
hold is polled. If a person approves it in that window, the response is
released, and only if core then allows the resubmitted envelope. If nobody
decides in time, the call is refused, and the refusal names the open hold so
the caller can retry.
Verdict: require_approval · rule reeflex.policy/irreversible_broad_prod
{
"agent": { "id": "agent:litellm-gateway/org-acme-demo/gpt-x",
"session_id": "litellm:org-acme-demo:sess-1" },
"action": { "namespace": "litellm-gateway", "verb": "delete", "ability": "litellm/run_shell" },
"target": { "kind": "resource", "ref": "payments", "environment": "production" },
"params": { "tool_name": "Bash", "verb_source": "bash_intent_parse:kubectl delete namespace payments",
"gateway": "litellm", "gateway_tool": "run_shell" },
"magnitude": { "count": 1 },
"axes": { "reversibility": "irreversible", "blast_radius": "broad", "externality": "internal" },
"approval": { "present": false }
}
lookup_order — a tool the gateway cannot match. The unknown-tool floor
is execute / irreversible / broad / outbound, so in production even a harmless
lookup is held. That is a wrong hold, not a wrong allow, and it is fixed by
telling the guardrail what the tool is (REEFLEX_LITELLM_TOOL_MAP), not by
loosening the floor.
Verdict: require_approval · rule reeflex.policy/irreversible_broad_prod
{
"agent": { "id": "agent:litellm-gateway/org-acme-demo/gpt-x",
"session_id": "litellm:org-acme-demo:sess-1" },
"action": { "namespace": "litellm-gateway", "verb": "execute", "ability": "litellm/lookup_order" },
"target": { "kind": "resource", "ref": null, "environment": "production" },
"params": { "tool_name": "lookup_order", "verb_source": "tool_name_map:lookup_order",
"gateway": "litellm", "gateway_tool": "lookup_order" },
"magnitude": { "count": 1 },
"axes": { "reversibility": "irreversible", "blast_radius": "broad", "externality": "outbound" },
"approval": { "present": false }
}
How to deploy it¶
pip install 'reeflex-litellm[proxy]'
export REEFLEX_CORE_URL=http://your-core:8080
export REEFLEX_LITELLM_TENANCY_MAP_FILE=/etc/reeflex/tenancy.json
and in the proxy's config.yaml, the guardrail with
guardrail: reeflex_litellm.guardrail.ReeflexActionGuardrail, mode: post_call,
default_on: true. The line that matters is the tenancy map: it binds each
virtual key or team to a Reeflex org, there is no default org, and a key the
map does not name is refused without core being asked. Full guide:
LiteLLM in Docker.
What Reeflex does NOT do here¶
- It gates the proposal, not the execution. The tool runs in the client; if the client runs a tool the model never proposed through the proxy, or a client keeps a tool call it was told was refused, Reeflex does not see it.
- It does not see agents that reach a model without the proxy.
- It does not authenticate the session half of the session id. The org is
bound to the key by the tenancy map; the session within an org is what the
caller sends (
x-reeflex-session, then the request'suser, then the call id), so a caller that rotates it rotates its budgets. - It does not judge the model's text. A response with no tool call is not an action and passes untouched.