Why Ultiproxy?
The problem: You pay for GitHub Copilot ($10/mo), ChatGPT Plus/Codex ($20/mo), Claude Pro ($20/mo), Google Antigravity, xAI Grok, DeepSeek, and Z.ai. That's $100+/month in flat-rate AI subscriptions.
Yet every time you spin up an autonomous coding agent--OpenCode, Hermes, Claude Code, Cursor, or Aider--you are forced to supply raw, pay-as-you-go API keys that burn money per token. Meanwhile, your monthly subscriptions sit idle 85% of the day.
The solution: Ultiproxy pools all your subscriptions into ONE local endpoint at :9050 that natively speaks both OpenAI (POST /v1/chat/completions) and Anthropic (POST /v1/messages) -- and exposes its entire control plane over MCP, so an agent can wire it up itself.
- Zero-Config Startup:
ultiproxy serveis the whole installation step. Nothing to author, nothing to edit, nothing to reload: lanes, aliases and timeouts are added at runtime over MCP and persist in the daemon's data dir (providers.json,aliases.json,timeouts.json). - MCP-Native Control Plane: every knob an operator would touch is an MCP tool at
http://localhost:9050/mcp--add_provider,set_model_alias,set_provider_timeout,get_quota_status,initiate_oauth_login, ... An agent can bootstrap the whole gateway on its own. There is no separate admin CLI to install or remember: the daemon binary only serves, and MCP is the sole control plane. - Zero-Marginal-Cost Pooling: Run agents 24/7 on your flat subscriptions. Stop paying per-token API bills for workloads you've already paid for.
- Quota Monitoring, Not Quota Routing: live per-provider quota windows and credit state via the MCP
get_quota_statustool,/api/quota, andquota.txt. Operators and agents decide routing; the proxy never reroutes on quota by itself. - Per-API-Key Accounting: scoped client keys (
sk-up-agent-a,sk-up-ci-worker) with granular tracking of prompt tokens, completion tokens, cached tokens, requests and cost. - Reasoning & Tool Passthrough: flawless streaming for thinking tokens (
reasoning_contentin o1/DeepSeek-R1), tool calls, and multi-image vision inputs. - Agent-First, Zero GUI Bloat: a single high-performance Go daemon. No Electron runtime, no dashboard to click through, sub-millisecond routing latency.
Agent-First Quickstart
Ultiproxy starts with zero configuration: nothing to author, nothing to reload, and nothing that has to be in place before the first request. You boot the daemon, then you (or your agent) configure everything else over MCP while it runs.
0. Install (once)
# Precompiled binary into ~/.local/bin (no root required):
curl -fsSL https://raw.githubusercontent.com/smhanov/ultiproxy/main/dist/install.sh | sh
# ...or build from source (this repo only):
git clone https://github.com/smhanov/ultiproxy
cd ultiproxy
go build -o ultiproxy ./cmd/ultiproxy
The installer also installs a hardened systemd user unit on Linux. Review it first with bash dist/install.sh --dry-run if you prefer.
Building from source needs only this repository and Go 1.25 or newer
(go.mod declares go 1.25.0). The provider library
github.com/smhanov/llmhub is consumed as a normal published Go module
pinned in go.mod -- no extra checkouts required:
git clone https://github.com/smhanov/ultiproxy
cd ultiproxy
go build -o ultiproxy ./cmd/ultiproxy
1. Start the daemon
# Directly:
ultiproxy serve
# Or as a hardened systemd user service (what the installer sets up on Linux):
systemctl --user enable --now ultiproxy
# Or bound to all interfaces for remote agents, still with no configuration:
ULTIPROXY_ADDR=0.0.0.0:9050 ultiproxy serve
Verify it is alive:
curl http://localhost:9050/healthz # {"status":"ok"}
By default the daemon listens on 127.0.0.1:9050 and accepts every request, which is the right posture for a localhost-only install. Declaring client keys turns on authentication and gives you per-key attribution; upstream credentials are never exposed to clients either way. Two spellings are accepted on every route (including /api/quota and the stats endpoints): Authorization: Bearer <key> for OpenAI-style clients and x-api-key: <key> for Anthropic SDKs and Claude Code.
2. Connect your agent to the embedded MCP server
Streamable HTTP at http://localhost:9050/mcp (legacy SSE at http://localhost:9050/mcp/sse):
{
"mcpServers": {
"ultiproxy": {
"url": "http://localhost:9050/mcp",
"headers": { "Authorization": "Bearer sk-up-local-agent-key" }
}
}
}
Raw JSON-RPC works too, which is handy for scripts:
curl -s http://localhost:9050/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
3. Register upstream providers
Subscription lanes log in over OAuth; API lanes take a key:
curl -s http://localhost:9050/mcp -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"add_provider","arguments":{
"name":"deepseek","base_url":"https://api.deepseek.com/v1","api_key":"sk-..."
}}}'
curl -s http://localhost:9050/mcp -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"add_provider","arguments":{"name":"antigravity","kind":"antigravity"}}}'
For OAuth lanes, drive the flow instead of pasting keys:
# 1) get the sign-in URL (and user_code for device flows such as xai)
{"name":"initiate_oauth_login","arguments":{"provider":"antigravity"}}
# 2a) auth-code flow: copy the code=... value out of the browser callback URL
{"name":"submit_oauth_code","arguments":{"provider":"antigravity","code":"4/0A..."}}
# 2b) device flow: poll until it answers "completed"
{"name":"check_oauth_login","arguments":{"provider":"xai"}}
An OAuth lane must be added with auth_via_oauth_manager so its stored credential (refresh token included) actually drives the lane. Static api_key lanes never refresh, which is how a lane dies overnight:
curl -s http://localhost:9050/mcp -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":5,"method":"tools/call",
"params":{"name":"add_provider","arguments":{
"name":"xai","kind":"openaicompat","base_url":"https://api.x.ai","api_key":"",
"quirks":{"auth_via_oauth_manager":true}
}}}
'
Re-adding a lane this way reuses the credential file already in the daemon's data dir (<data_dir>/credentials/<lane>/); only a dead refresh token needs another initiate_oauth_login.
4. Map client-visible model names
curl -s http://localhost:9050/mcp -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"set_model_alias","arguments":{
"alias":"qwenpoint-3.8",
"provider":"vllm",
"upstream":"Qwen/Qwen3.8-Instruct-AWQ",
"context_limit":131072,
"max_output":8192,
"pricing_tag":"local-gpu"
}}}'
Aliases, lanes and timeouts all survive a restart: the daemon re-reads its data dir and rebuilds the registry, so nothing has to be re-applied.
5. Point your agents at the endpoint
- OpenAI clients:
http://localhost:9050/v1 - Anthropic clients:
http://localhost:9050
MCP Tools
Everything below is available on tools/list at http://localhost:9050/mcp, with full self-describing docstrings -- an agent never needs this README to operate the proxy.
Point Your Clients at Ultiproxy
OpenCode
Add to ~/.config/opencode/opencode.json:
{
"providers": {
"ultiproxy": {
"name": "Ultiproxy Universal Gateway",
"type": "openai",
"baseURL": "http://localhost:9050/v1",
"apiKey": "sk-up-local-agent-key",
"models": [
"copilot/gpt-4o",
"anthropic/claude-3-5-sonnet",
"deepseek/deepseek-r1",
"vllm/qwenpoint-3.8"
]
}
}
}
Hermes Agent
Give Hermes the OpenAI-compatible base URL and a model id that exists on the proxy:
{
"llm": {
"provider": "custom",
"base_url": "http://localhost:9050/v1",
"api_key": "sk-up-local-agent-key",
"model": "anthropic/claude-3-5-sonnet",
"max_tokens": 8192,
"temperature": 0.2,
"extra_headers": { "X-Client-Id": "hermes-agent-primary" }
}
}
Claude Code
Run Claude Code against your pooled subscriptions using the native Anthropic endpoint:
export ANTHROPIC_BASE_URL="http://localhost:9050"
export ANTHROPIC_AUTH_TOKEN="sk-up-local-agent-key"
# Launch Claude Code
claude
Cursor
Settings -> Models -> add an OpenAI-compatible provider:
- Base URL:
http://localhost:9050/v1 - API Key:
sk-up-local-agent-key
OpenAI SDKs (and anything else OpenAI-shaped)
import OpenAI from "openai";
const openai = new OpenAI({
baseURL: "http://localhost:9050/v1",
apiKey: "sk-up-local-agent-key",
});
const response = await openai.chat.completions.create({
model: "copilot/gpt-4o",
messages: [{ role: "user", content: "Implement quota-aware load balancer in Go." }],
stream: true,
});
curl -s http://localhost:9050/v1/chat/completions \
-H "Authorization: Bearer sk-up-local-agent-key" \
-H "Content-Type: application/json" \
-d '{"model":"copilot/gpt-4o","messages":[{"role":"user","content":"Ping"}]}'
HTTP Surface
Supported Providers (11 + Local)
Ultiproxy fronts 11 cloud subscription providers and local self-hosted runtimes. Lanes are added at runtime over MCP -- OAuth subscription lanes through initiate_oauth_login, keyed API lanes through add_provider:
Any other OpenAI-compatible upstream (OpenRouter-style gateways, llama.cpp, TGI, another proxy) is one add_provider call away.
Security & Architecture
- Single Key Surface: Clients talk to Ultiproxy with one client key (
sk-up-...). Upstream subscription credentials and OAuth refresh tokens stay inside the daemon's credential store and never reach a client or a network trace. - Localhost by Default: the daemon binds
127.0.0.1:9050;ULTIPROXY_ADDRis the only switch needed to expose it to remote agents. - Per-API-Key Accounting: track token usage by agent, workload or team member via
get_client_usageandGET /api/stats/summary. - State Belongs to the Daemon: runtime changes made over MCP are persisted atomically under the data dir (
providers.json,aliases.json,timeouts.json) and restored on the next start -- no restart ritual, no drift between what you asked for and what runs. - System Hardening: the systemd user service (
dist/ultiproxy.service) enforces strict process boundaries (ProtectSystem=strict,NoNewPrivileges=true, isolatedStateDirectory=ultiproxy). - Automatic Token Refresh: OAuth credentials never hard-expire. A background refresher walks every lane every 5 minutes and refreshes any credential with less than 10 minutes of life left -- no inbound request needed, so an idle lane still works the next morning. If the upstream rejects a credential anyway (401/403
bad-credentials), the lane invalidates it, mints a fresh one and retries exactly once, before the first byte of a response or stream; a second consecutive rejection is returned to the client honestly. Refresh log lines carry the lane name and the new expiry, never a token. - Honest Routing: an unknown model is a 404
unknown_model, never a silent detour to another vendor; tools against a lane without tool support is a 409model_does_not_support_tools. Failover happens only before the first byte.
Troubleshooting
bind: address already in use on startup. The daemon exits non-zero when its listen address is taken and prints a hint naming ULTIPROXY_ADDR. The usual collision is the default port: 127.0.0.1:9050 is also Tor's SOCKS port, so on a machine running Tor (or a second ultiproxy) bind something else instead — e.g. ULTIPROXY_ADDR=127.0.0.1:9051 ultiproxy serve. The default stays 127.0.0.1:9050 everywhere else in these docs. A client pointed at a port owned by another service gets that service's answer (Tor replies with an HTML "This is a SOCKS proxy" page), so if responses look alien, check what actually owns the port before debugging the proxy.
Contributing
Pull requests are welcome! Ensure you adhere to the project standards:
- API changes must update
docs/openapi.yamlandllms.txt. - MCP tool docstrings in
pkg/mcp/tools.goare the agent-facing documentation -- keep them complete and self-contained. - Verification gates must pass before submitting PRs (
go build ./...,go test ./...,go vet ./...).
License
MIT (c) 2026 Ultiproxy Contributors.