proxyagent
Run any agent — Claude, Codex, custom — on any machine, with no API key on the machine.
A secure, self-hosted proxy for models and tools. Your keys live in one hardened place; every machine holds only a scoped, revocable token.
Agents need model access (and tool access) to do anything. Today that means scattering
real API keys across every machine an agent runs on — a security nightmare. proxyagent
fixes it: stand up one proxy that holds the real credentials, and point every agent at
it. The machine gets a throwaway token; the real key never leaves the proxy.
remote machine proxy (you host) upstream
┌────────────────┐ token only ┌──────────────────┐ real key ┌───────────┐
│ claude / codex │ ───────────► │ proxyagent serve │ ─────────► │ Anthropic │
│ (no real key) │ ◄─────────── │ scope·log·tools │ ◄───────── │ OpenAI │
└────────────────┘ stream └──────────────────┘ └───────────┘
How it works
Every harness honours *_BASE_URL, so the shim is trivial: point the base URL at the
proxy and use the machine token as the "api key." The proxy authenticates the token,
checks its scope, swaps in the real key, forwards upstream, and logs the call. The
machine never sees a real credential.
Try it with zero keys (local)
pip install proxyagent && proxyagent serve # prints an admin token
proxyagent token new local # works locally, no admin token needed # mint a token
# call the built-in `mock` model — full pipeline (auth, scope, usage, cost, log), no real key:
curl -s localhost:8080/anthropic/v1/messages -H "x-api-key: pa_…" \
-d '{"model":"mock","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'
Quickstart
1. Run the proxy (on a box you control — it holds the real keys):
pip install proxyagent
export ANTHROPIC_API_KEY=sk-ant-… # and/or OPENAI_API_KEY=sk-…
proxyagent serve # prints an admin token + a dashboard at :8080
2. Mint a machine token (scoped + revocable):
proxyagent token new macbook-01 --scope "anthropic:claude-*" # local: no admin token needed
3. Run a real agent on any machine — no real key there. Token + prompt, it just runs the actual Claude Code / Codex CLI locally, routing every model call through the proxy:
import proxyagent
proxyagent.run("build a SwiftUI todo app and run the tests",
token="pa_…", proxy="https://proxy.you.com") # harness="codex" for Codex
# same from the CLI:
PROXYAGENT_TOKEN=pa_… proxyagent run codex --goal "fix the failing tests" --proxy https://proxy.you.com
Install the agent CLI you want first: npm i -g @anthropic-ai/claude-code or npm i -g @openai/codex.
proxyagent wires each one to the proxy automatically (Claude Code via ANTHROPIC_BASE_URL; Codex via a
one-off model provider). Both are verified end-to-end — every call lands on the proxy, keyless.
The dashboard & docs
proxyagent serve ships a dashboard at / and a full "how to run it" docs page at /docs
(install → serve → add a key → mint a token → point any agent/SDK/curl at the proxy, with
copy-paste snippets pre-filled with your host). Reveal the admin token with
proxyagent admin-token. The dashboard:
- Access keys — the credentials you create. Each is a provider + an auth type (Anthropic · API key, Anthropic · Bedrock, OpenAI · Azure, …); pick the type, enter the key/fields, done. Listed with provider logo · auth type · masked key · test · disable · remove. Test pings the real upstream and shows ok / auth-failed / unreachable — catch a bad key the moment you add it. Disable pauses a credential (it drops out of the failover pool) without deleting it, so you can re-enable later.
- Machine tokens — mint (scoped / TTL / budget / note / IP allow-list), search (by
label/id/scope), edit (retune in place, no re-mint), clone (duplicate config, new
secret), revoke, and revoke-expired (one-click cleanup). Minting shows a sample curl + copy-
.env. Click any machine to open its drill-down page — that machine's requests, spend, budget, and an inspectable log of every prompt it sent and the model's output. - Model routing — add/remove model remaps (e.g.
* → mockfor offline). - Activity — spend-by-token breakdown (requests · tokens · cost · budget %), a live request log with usage + cost, plus Export CSV and Trim of the audit trail. Click a request to inspect the exact prompt (system + messages) and the output it returned.
Prompt & output capture
The proxy records each call's request/response bodies so the dashboard can show what actually
went through — the per-machine page and the Activity feed both expand to a side-by-side
Prompt → Output inspector. Bodies are redacted (secret-shaped strings → sk-***) and
size-capped (~16 KB each) before they're stored. On by default; turn it off to keep only
metadata (tokens/cost/status):
export PROXYAGENT_CAPTURE_BODIES=0 # store metadata only — never persist prompts/outputs
Proxied tools — the same trick, for tools
The proxy can also hold your tool keys and hand agents governed tools — so an agent gets web search (and custom tools) without ever holding the tool's credential.
export TAVILY_API_KEY=tvly-… # web_search uses this; agents never see it
export PROXYAGENT_TOOLS='[{"name":"crm","url":"https://hooks.you.com/crm","headers":{"Authorization":"Bearer …"}}]'
# then send requests with header x-proxyagent-tools: on → tool defs are injected;
# the proxy executes calls to managed tools server-side (keys stay here).
Server-side agentic loop. With x-proxyagent-tools: on (non-streaming), the proxy runs
the whole tool loop for you: model asks to use a tool → proxy executes it server-side
(its credentials never leave the proxy) → appends the tool_result → re-calls the model →
repeats until a final answer (capped at 6 steps). The agent just sends one request and gets
the finished answer back; the response carries x-proxyagent-tool-steps: <n>. Works on both
Anthropic (tool_use) and OpenAI (tool_calls) shapes. Try it offline with model: "mock" —
the mock emits a real tool_use, so the loop runs end-to-end with no keys.
The step budget defaults to 6 (PROXYAGENT_MAX_TOOL_STEPS) and is overridable per-request with
x-proxyagent-tool-steps-max: <n>. Set it to 0 to get the model's tool request back
without executing it — for clients that want to run the tool themselves.
Credentials, storage & cost
By default provider keys come from the environment and stay local. Or add them
once and they're stored encrypted (proxy_agent_keys) — locally in SQLite, or in
Postgres if you point at one. Either way the machine never sees them.
export PROXYAGENT_SECRET_KEY=… # enables at-rest encryption (Fernet)
proxyagent provider add anthropic --key sk-ant-… # stored, encrypted
proxyagent provider add openai --key sk-… --kind api_key
# OAuth: store an access token (+ refresh_token/token_url in meta → auto-refreshed before expiry)
proxyagent provider ls
proxyagent provider test <cred-id> # ping the upstream: ok / auth-failed / unreachable
# Postgres-backed (shared, multi-instance): tables proxy_agent_keys / _tokens / _calls
export PROXYAGENT_DATABASE_URL=postgresql://user:pass@host/db # pip install 'proxyagent[postgres]'
Every call is traced in proxy_agent_calls with token usage, latency, and computed
cost (per-model pricing, override via PROXYAGENT_PRICING). See it live:
proxyagent doctor # diagnose setup: providers · encryption · db · admin token
proxyagent usage # totals: requests · tokens · $ cost
proxyagent logs # per-request trace incl. cost
proxyagent usage-by-token # per-token spend breakdown (who's costing what)
proxyagent logs-export -o audit.csv # dump the audit trail to CSV
proxyagent logs-trim 30 # delete traces older than 30 days
Per-token & per-model spend. See exactly which machine token and which model is costing
what (GET /admin/usage-by-token, GET /admin/usage-by-model, both surfaced side-by-side in the
dashboard's Activity tab). Keep the audit table bounded and exportable:
export PROXYAGENT_LOG_RETENTION_DAYS=30 # trim traces older than 30d on startup
curl -XPOST localhost:8080/admin/logs/trim?days=30 -H "x-admin-token: pa_admin_…" # trim on demand
curl localhost:8080/admin/logs/export -H "x-admin-token: pa_admin_…" -o audit.csv # CSV for SIEM/archival
Deploy
docker compose up -d # proxy at :8080; reveal admin token via `docker compose logs`
# or with shared Postgres:
docker compose --profile postgres up -d
A Dockerfile (with a /healthz HEALTHCHECK) and docker-compose.yml (proxy + optional Postgres,
persistent volume) ship in the repo. Bring keys via a .env file. Verified: container builds,
/healthz green, mock call + dashboard serve. GET /readyz is a readiness probe that pings the
backing store and returns 503 if the DB is unreachable — wire it to your load balancer / k8s
readiness check so a broken instance is pulled from rotation. Set PROXYAGENT_REQUIRE_PROVIDER=1
to also fail readiness until at least one provider is configured (keeps a fresh/misconfigured
instance out of rotation). Each token's most recent error is surfaced in /admin/tokens + the dashboard.
Rate limits
Per-token limits (mint with --rate) and per-provider limits protect your upstreams:
export PROXYAGENT_PROVIDER_RATE_LIMITS='{"anthropic": 600, "openai": 1000}' # requests/min
export PROXYAGENT_RATE_LIMIT_DEFAULT=300 # fallback for the rest
Over the limit → 429.
Response cache
Off by default. Set PROXYAGENT_CACHE_TTL=<seconds> and identical (provider + body) non-streaming
requests are served from memory — saving upstream cost + latency. Cache hits return
x-proxyagent-cache: hit; bypass per-request with header x-proxyagent-cache: no. Hits/size are
in /metrics.
Request tracing
W3C trace context (traceparent / tracestate / baggage) sent by the client is forwarded
to the upstream, so a distributed trace spans the proxy hop. Every proxied response also carries
x-proxyagent-request-id. Send your own
(x-proxyagent-request-id: <id>) and the proxy honours + echoes it; omit it and the proxy mints
one (req_…). The id is stored on the call trace (proxy_agent_calls.request_id, in logs and
the CSV export), so a client log line ties straight to a row in the audit trail.
Operational summary
GET /admin/stats (or proxyagent.Admin(...).stats()) returns a one-shot snapshot — version,
uptime, cache (enabled/ttl/hits/size), latency p50/p95, active+total tokens, credentials,
configured providers, per-tool execution counts, total requests and spend. GET /admin/summary
(or Admin.summary()) returns a shareable Markdown status report (totals + top providers/models).
GET /admin/usage-by-day?days=14 returns a daily timeseries (requests/tokens/cost per UTC day). The
dashboard's Activity tab shows the stat strip, a 14-day requests chart, and each token's
expiry countdown, and auto-refreshes when you focus the tab.
Observability — Prometheus
GET /metrics exposes proxyagent_requests_total, proxyagent_responses_total{status},
proxyagent_tokens_total{direction}, proxyagent_cost_usd_total{provider},
proxyagent_active_tokens, proxyagent_credentials, and a
proxyagent_request_duration_ms histogram (latency buckets + sum/count). Admin-gated by default; set
PROXYAGENT_METRICS_PUBLIC=1 for unauthenticated scraping on an internal network.
Resilience
On a network fault the proxy fails over to the next credential in the pool; if they're all
exhausted it returns a clean 504 (upstream timeout) or 502 (connection error) with a
descriptive body — never a raw 500. Tune the upstream timeout with PROXYAGENT_REQUEST_TIMEOUT.
Cap request size with PROXYAGENT_MAX_BODY_BYTES (over → 413; 0 = unlimited). Cap in-flight
proxied requests with PROXYAGENT_MAX_CONCURRENCY (over → 503; 0 = unlimited) to shield upstreams. The dashboard's
Test all button (and Admin.test_all_credentials()) health-sweeps every stored credential
concurrently and reports ok / auth-failed / unreachable per credential. Responses are gzip-
compressed when the client accepts it (e.g. model lists / audit logs shrink ~75%), and the
dashboard has a light/dark theme toggle (persisted).
Browser clients (CORS)
Off by default. Set PROXYAGENT_CORS_ORIGINS to a comma-separated allowlist (or *) and the
proxy answers OPTIONS preflight + echoes Access-Control-Allow-Origin, exposing the
x-proxyagent-* headers — so a browser-based agent or dashboard can call it directly.
export PROXYAGENT_CORS_ORIGINS="https://app.you.com,https://staging.you.com"
Security model
- Real keys never leave the proxy — read from env, never persisted, never logged, never returned.
- Machine tokens are stored hashed (SHA-256); plaintext shown once. A stolen DB yields nothing usable.
- Scoped (
provider:modelglobs), expiring (TTL), revocable, rate-limited, and optionally IP-locked (mint withallowed_ipsCIDRs; off-list clients get403, honouringX-Forwarded-Forbehind a proxy). - Constant-time token comparison; sensitive headers redacted from logs, and upstream error bodies passed through a secret redactor (api keys, bearer tokens, AWS/Google keys, emails) before they touch the audit log.
- Admin API + dashboard gated by a separate admin token. Run it behind TLS.
SDK — token + prompt, it just runs
The headline call: hand it a token and a prompt, and it runs a real agent (Claude Code by default) on this machine against that prompt — no API key here, the proxy holds the key.
import proxyagent
proxyagent.run("build a SwiftUI todo app and run the tests",
token="pa_…", proxy="https://proxy.you.com")
# harness="codex" for Codex, command="my-agent {goal}" for any custom agent.
# token also reads PROXYAGENT_TOKEN; proxy reads PROXYAGENT_PROXY.
Manage the proxy programmatically too — mint tokens, manage credentials, host it:
import proxyagent
app = proxyagent.create_app() # ASGI app — embed in your own service
admin = proxyagent.Admin("https://proxy.you.com", "pa_admin_…")
token = admin.mint("laptop", scope=["anthropic:claude-*"], ttl_seconds=3600)
proxyagent.run("build the app", token=token, proxy="https://proxy.you.com") # ← run an agent with it
Harnesses & auth modes
Two things are separate: which agent CLI you run on the machine, and how the proxy
authenticates upstream. The machine only ever holds a pa_ token; the proxy holds the real
credential in whatever auth mode you configure.
Built-in harnesses (run the real CLI locally, keyless): claude-code, codex, and any
custom command (--command "my-agent {goal}") — anything that respects *_BASE_URL, e.g. aider/Cline.
On the proxy side, a provider credential can be any of these auth modes — all wired:
| Auth mode | How the proxy uses it |
|---|---|
| API key | x-api-key / Authorization: Bearer to the provider endpoint |
| OAuth | stored access token, auto-refreshed via refresh_token before expiry |
| AWS Bedrock | the proxy SigV4-signs the Claude-on-Bedrock request itself (no boto3) |
| Azure | api-key header to your Azure deployment URL |
| Google Vertex | service-account JSON → JWT → access token → Claude-on-Vertex |
For Bedrock/Vertex the proxy holds the AWS/GCP credentials and signs upstream, so the machine needs
no cloud creds at all. Add any of them in the dashboard's Access keys tab or via proxyagent provider add … --kind.
# the cloud-credential paths — the machine that runs the harness holds none of these:
proxyagent provider add anthropic --kind bedrock --key <AWS_SECRET> # + meta: access_key, region
proxyagent provider add openai --kind azure --key <AZURE_KEY> # + meta: endpoint
proxyagent provider add anthropic --kind oauth --key <OAUTH_TOKEN>
proxyagent provider add anthropic --kind vertex --key "$(cat sa.json)" # + meta: region
Credential pools & failover
A provider isn't one key — it's a pool. Add as many credentials as you want, across
auth types (several API keys, OAuth tokens, …); each is managed individually in the
dashboard. The proxy rotates through the pool, failing over to the next credential on
any 429 / 5xx — so a rate-limited or dead key never takes you down.
proxyagent provider add anthropic --key sk-ant-aaa # additive — builds the pool
proxyagent provider add anthropic --key sk-ant-bbb
proxyagent provider add anthropic --key <oauth> --kind oauth
Budgets — per-token and per-provider
Cap what any token can spend; once its summed cost crosses the cap, the proxy returns 402.
proxyagent token new ci --budget 5.00 # this token may spend at most $5
Or cap a whole provider — a spend ceiling across all tokens, so one runaway agent can't blow your bill no matter which token it holds:
export PROXYAGENT_PROVIDER_BUDGETS='{"anthropic": 200, "openai": 50}' # $ ceilings; over → 402
Get alerted when any cap is crossed — the proxy POSTs a webhook (deduped per token/provider with a cooldown) right before the 402:
export PROXYAGENT_BUDGET_WEBHOOK=https://hooks.slack.com/… # {event,type,id,cap_usd,spend_usd}
export PROXYAGENT_BUDGET_WEBHOOK_COOLDOWN=300 # seconds between repeat alerts (default 300)
export PROXYAGENT_EVENT_WEBHOOK=https://hooks.you.com/… # token_created / token_revoked lifecycle events
export PROXYAGENT_WEBHOOK_SECRET=… # sign all webhooks: X-Proxyagent-Signature: sha256=HMAC(body)
Supported providers
anthropic · openai · gemini · groq · openrouter · mistral · deepseek ·
xai · together — Anthropic uses its Messages API; the rest are OpenAI-compatible.
Point a harness/agent at https://proxy.you.com/<provider>/v1 and it routes there.
Add or override any endpoint with PROXYAGENT_<NAME>_ENDPOINT. A GET /v1/models (and
/<provider>/v1/models) returns the routable catalog in OpenAI list shape, so harnesses that
probe for available models just work.
Model remap — rename or reroute models
Rewrite the requested model before forwarding — rename it, or reroute it to a totally different provider:
proxyagent alias set gpt-4o anthropic:claude-sonnet-4-5 # send "gpt-4o" calls to Claude
proxyagent alias set '*' mock # force EVERYTHING offline (no keys)
proxyagent alias ls
The '*' → mock trick is the offline harness unlock: point claude-code at the
proxy, map everything to mock, and it runs end-to-end with zero keys and zero spend —
perfect for local dev, demos, and CI.
Supported harnesses
claude-code, codex, and any custom command (--command "my-agent {goal}"). Adding one
is a few lines — it just needs to respect *_BASE_URL.
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file proxyagent-0.62.1.tar.gz.
File metadata
- Download URL: proxyagent-0.62.1.tar.gz
- Upload date:
- Size: 174.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37759f37ba6a840fd4247b9544ca03e9bbaa249128d29398dc3dccaee094d751
|
|
| MD5 |
f0d6f681b8ecf2470f737adf1828375b
|
|
| BLAKE2b-256 |
0da570c4bfdcd70fc3505275035985cce895b15528b9ad50c40871b41a803881
|
File details
Details for the file proxyagent-0.62.1-py3-none-any.whl.
File metadata
- Download URL: proxyagent-0.62.1-py3-none-any.whl
- Upload date:
- Size: 94.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f266cfe0d42865882adf0c2d14dc1395145080d957c2dfee8a3b1c21df69ad19
|
|
| MD5 |
f5864072a0c9eb331aa66e3ce8057cf5
|
|
| BLAKE2b-256 |
768614581ea5a7dca0516d276334a36e0a3f7a355c4f85a9407ca7a1261aa65a
|