A lightweight, zero-dependency Python proxy for AI coding assistants (OpenCode, Claude Code, Cursor, Cline, Roo Code) that routes requests through AgentRouter or any OpenAI-compatible provider.
It automatically handles:
- Dual protocol support: Accepts both OpenAI (
/v1/chat/completions) and Anthropic (/v1/messages) formats and translates seamlessly. - Request sanitation: Strips
null-valued fields (response_format: null, etc.) that cause AI SDK type-validation crashes. - SSE stream cleaning: Filters out non-standard events like
data: nullandbilling.summary. - Content-block mitigation: Automatically trims context and retries once when upstream filters trigger a content block.
- Tailscale & remote access ready: Built-in support for running on a remote PC (e.g. Dell Wyse, mini-PC, VPS) and accessing securely from your laptop or desktop over Tailscale.
- Security by default: Strictly loopback-only (
127.0.0.1) unless configured, refuses network binds without authentication tokens, and never writes prompts or API keys to disk.
[ Your Laptop / Desktop ]
└── OpenCode / Claude Code / Cursor / Cline
│
│ (HTTP / Tailscale)
▼
[ Proxy Server ] (Local PC or Remote Wyse / Server)
├── Normalizes Anthropic & OpenAI payloads
├── Strips null JSON fields & SSE streaming noise
├── Injects upstream API key & enforces client auth token
└── Retries content-blocked responses with trimmed context
│
│ (HTTPS)
▼
[ Upstream Provider ] (AgentRouter, OpenRouter, etc.)
| Feature | Local Proxy (Single PC) | Tailscale Proxy (Two PCs / Server) |
|---|---|---|
| Where it runs | On the same PC/laptop where you code | On a secondary PC (e.g. Dell Wyse thin client, mini PC, home server) |
| Best for | Quick setup, single machine coding | Always-on 24/7 access from your laptop anywhere in the world |
| Network setup | None — loopback only (127.0.0.1) |
Tailscale installed on both machines |
| Security | Loopback only (no external traffic allowed) | Encrypted WireGuard mesh VPN + Bearer auth token |
| PC Sleep Impact | If your laptop sleeps, proxy pauses | Server sleep disabled (powercfg); stays online 24/7 |
| One-Click Setup | Double-click setup-local.bat |
Double-click setup-tailscale.bat on the server PC |
Use this when you want to run the proxy on the same PC where you run OpenCode, Claude Code, or Cursor.
- Double-click
setup-local.bat. - Enter your AgentRouter API key when prompted (
sk-...). - That's it! The script will:
- Create your
.envconfigured for127.0.0.1. - Run self-tests to ensure everything works.
- Install auto-start so the proxy automatically launches on Windows startup/reboot.
- Start the proxy immediately in the background (no black command window).
- Test connectivity at
http://127.0.0.1:4182/healthz.
- Create your
Click to view manual step-by-step instructions
- Requirements: Python 3.10 or newer.
- Configure
.env: Copy.env.exampleto.env:EditCopy-Item .env.example .env.env:PROXY_UPSTREAM=https://agentrouter.org PROXY_UPSTREAM_API_KEY=your-provider-api-key-here PROXY_LISTEN_HOST=127.0.0.1 PROXY_AUTH_TOKEN=
- Start the Proxy:
.\start-proxy.bat # Or directly with Python: python .\agentrouter-proxy.py 4182
The proxy starts on http://127.0.0.1:4182. Your local tools use base URL http://127.0.0.1:4182/v1.
Use this when you want to run the proxy 24/7 on a secondary PC (like a Dell Wyse, home lab mini-PC, Raspberry Pi, or VPS) and connect to it securely from your main laptop or desktop.
- Ensure Tailscale is installed and signed in on both your server PC and your laptop.
- On your server PC (e.g. Dell Wyse), double-click:
setup-tailscale.bat
- The script will:
- Auto-detect your server's Tailscale IP (e.g.
100.97.230.23). - Prompt for your AgentRouter API key (
sk-...) and client secret token (or auto-generate one). - Permanently disable Windows sleep on AC power (
powercfg /change standby-timeout-ac 0) so the server stays reachable 24/7 headless. - Install auto-start into Windows Startup and Scheduled Tasks.
- Launch the proxy immediately in the background.
- Display a copy-paste ready OpenCode configuration block for your laptop!
- Auto-detect your server's Tailscale IP (e.g.
Click to view manual step-by-step instructions
- Tailscale Setup:
Install Tailscale on both machines and note the server's IP via
tailscale ip -4. - Configure
.envon Server PC:PROXY_UPSTREAM=https://agentrouter.org PROXY_UPSTREAM_API_KEY=your-upstream-provider-key PROXY_LISTEN_HOST=100.97.230.23 PROXY_AUTH_TOKEN=choose-a-strong-client-secret-token
- Launch on Server PC:
Double-click
start-tailscale.bator run:.\start-proxy.bat --tailscale
From your laptop, test that the remote proxy is reachable over Tailscale:
# 1. Test unauthenticated request (should return HTTP 401 Unauthorized)
curl.exe -i http://100.97.230.23:4182/healthz
# 2. Test authenticated request (should return HTTP 200 {"status":"ok"})
curl.exe -i -H "Authorization: Bearer choose-a-strong-client-secret-token" http://100.97.230.23:4182/healthz
# 3. Check proxy stats
curl.exe -i -H "Authorization: Bearer choose-a-strong-client-secret-token" http://100.97.230.23:4182/_stats
# 4. Check token usage (today, yesterday, monthly, all-time)
curl.exe -H "Authorization: Bearer choose-a-strong-client-secret-token" http://100.97.230.23:4182/_usageOnce your proxy is running (either locally at 127.0.0.1:4182 or remotely at 100.x.y.z:4182), configure your tools:
Configure OpenCode in your workspace opencode.json (or opencode.jsonc) or globally in ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"agentrouter": {
"name": "AgentRouter (Local Proxy)",
"package": "@opencode/ai/providers/openai-compatible",
"settings": {
"baseURL": "http://127.0.0.1:4182/v1"
},
"headers": {
"Authorization": "Bearer local-proxy-token"
},
"compatibility": {
"streamUsage": true,
"requireFinishReason": true
},
"models": {
"gpt-6.1-sol": { "name": "GPT 6.1 Sol" },
"claude-opus-5": { "name": "Claude Opus 5" },
"gpt-5.5": { "name": "GPT 5.5" }
}
}
}
}Click to view OpenCode v1 Legacy Configuration (using npm & options)
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"agentrouter-proxy-wyse": {
"name": "AgentRouter (Wyse Tailscale Proxy)",
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://100.97.230.23:4182/v1",
"apiKey": "your-client-auth-token"
},
"models": {
"gpt-6.1-sol": { "name": "GPT 6.1 Sol" },
"claude-opus-5": { "name": "Claude Opus 5" },
"gpt-5.5": { "name": "GPT 5.5" }
}
}
}
}Tip
Why use this provider instead of AgentRouter (Direct Responses API)?
When OpenCode uses direct Responses API (/v1/responses), multi-turn tool calling (e.g., planning-with-files in the Build agent) sends stateful item references. Because AgentRouter load balances across distinct Azure OpenAI clusters, subsequent turns often hit a different cluster, throwing:
Error: The requested item was created under a different Azure OpenAI resource.
By selecting AgentRouter (Wyse Tailscale Proxy) or AgentRouter (Local Proxy) with npm: "@ai-sdk/openai-compatible", the proxy normalizes requests into standard stateless Chat Completions with self-contained context on every turn, completely eliminating this error!
Point Claude Code to the proxy using environment variables:
$env:ANTHROPIC_BASE_URL = "http://100.97.230.23:4182"
$env:ANTHROPIC_AUTH_TOKEN = "choose-a-strong-client-secret-token"
claude- Provider: OpenAI Compatible
- Base URL:
http://100.97.230.23:4182/v1 - API Key: Your
PROXY_AUTH_TOKEN(or dummy string if local loopback) - Model: Enter any model supported by your upstream provider (e.g.
claude-opus-5,claude-opus-4-8,gpt-5.5)
from openai import OpenAI
client = OpenAI(
base_url="http://100.97.230.23:4182/v1",
api_key="choose-a-strong-client-secret-token",
)
response = client.chat.completions.create(
model="claude-opus-5",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)To keep the proxy running 24/7 as a background service on your server PC (e.g. Dell Wyse thin client or home server):
Option A — Tailscale/server PC (run once on the machine that will serve the proxy):
Double-click setup-tailscale.bat on your server/Wyse PC. It will:
- Auto-detect your Tailscale IP and write
.envwith sensible defaults. - Create
start-tailscale.batconfigured to your Tailscale IP. - Install the silent auto-start launcher + Scheduled Task.
- Disable sleep on AC power.
- Launch the proxy immediately in the background.
- Print an OpenCode configuration snippet to paste into
opencode.json.
Option B — Local setup (run on the same PC you code on):
Double-click setup-local.bat. It will:
- Create
.envbound to127.0.0.1with a local auth token. - Install auto-start into
%APPDATA%\Startup. - Start the proxy in the background.
Option C — Manual installer (legacy):
On the server PC, double-click:
install-autostart.batThis automated installer:
- Installs a silent auto-start launcher into
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\AgentRouter-Proxy.vbs. - Registers a Windows Scheduled Task (
AgentRouterProxy) for logon triggers. - Disables Windows sleep on AC power (
powercfg /change standby-timeout-ac 0), preventing the Wyse PC from going into sleep mode while running headless. - Starts the proxy immediately in the background without opening any black command window.
- Appends all logs to
proxy.login real time.
Run status-proxy.bat or run in terminal:
# Check if proxy is running
.\status-proxy.bat
# Or view real-time logs
powershell Get-Content .\proxy.log -Wait -Tail 20To completely remove auto-start and terminate background proxy processes:
uninstall-autostart.batAll settings can be configured via .env file or environment variables:
| Variable | Default | Purpose |
|---|---|---|
PROXY_UPSTREAM |
https://agentrouter.org |
Upstream provider base URL (e.g. https://openrouter.ai/api or custom) |
PROXY_UPSTREAM_API_KEY |
(required) | Your provider API key |
PROXY_LISTEN_HOST |
127.0.0.1 |
IP address to bind to (127.0.0.1 for local, or Tailscale IP 100.x.y.z) |
PROXY_AUTH_TOKEN |
(empty) | Client Bearer token. Mandatory when listening on a non-loopback address |
PROXY_UPSTREAM_AUTH_SCHEME |
Bearer |
Upstream auth header scheme (Bearer, Basic, etc.) |
PROXY_UPSTREAM_TIMEOUT |
300 |
Upstream request timeout in seconds |
PROXY_MAX_BODY_BYTES |
20971520 (20MB) |
Maximum allowed incoming request body size |
AGENTROUTER_FORMAT |
auto |
Payload format mode: auto, openai, or anthropic |
PROXY_INJECT_PREAMBLE |
0 |
Set 1 to inject educational Mandarin preamble for strict CJK filters |
AGENTROUTER_DEBUG |
0 |
Set 1 for verbose metadata diagnostics |
Legacy variable names AGENTROUTER_API_KEY and AGENTROUTER_UPSTREAM continue to work for backwards compatibility.
The proxy tracks token consumption (prompt tokens, completion tokens, total) across all models and persists them to usage.json (never committed to git).
Double-click view-usage.bat on your laptop to see an instant dashboard:
- Today's Prompt, Completion, and Total tokens.
- Per-model breakdown (
gpt-6.1-sol,gpt-6-astra,gpt-5.6-sol,claude-opus-5-5). - Yesterday, This Month, and All-Time totals.
To monitor streaming tokens and live tokens-per-second (tok/s) while coding in OpenCode or Cursor:
.\view-usage.bat -WatchThis updates every 2 seconds live, calculating real-time output speed (tok/s).
In OpenCode's chat window, OpenCode automatically calculates and displays the exact generation speed and duration in the message header:
Build · GPT 6.1 Sol (Wyse Proxy) · 5m 27s · 25.4 tok/s
And shows active context tokens at the bottom right (82.8K (41%)).
Returns a live JSON breakdown of token usage:
# Local proxy (PowerShell)
curl.exe http://127.0.0.1:4182/_usage
# Wyse / Tailscale proxy (PowerShell)
curl.exe -H "Authorization: Bearer your-auth-token" http://100.97.230.23:4182/_usage
# Or format as pretty table in PowerShell:
(Invoke-RestMethod -Headers @{ Authorization = "Bearer your-auth-token" } -Uri "http://100.97.230.23:4182/_usage").todayExample response:
{
"today": { "prompt_tokens": 1200, "completion_tokens": 480, "total_tokens": 1680 },
"yesterday": { "prompt_tokens": 4300, "completion_tokens": 1700, "total_tokens": 6000 },
"this_month": { "prompt_tokens": 22000, "completion_tokens": 9100, "total_tokens": 31100 },
"last_month": { "prompt_tokens": 95000, "completion_tokens": 38000, "total_tokens": 133000 },
"all_time": { "prompt_tokens": 117000, "completion_tokens": 47100, "total_tokens": 164100 },
"by_model": {
"gpt-6.1-sol": { "prompt_tokens": 117000, "completion_tokens": 47100, "total_tokens": 164100 }
}
}Note: Token usage tracking works automatically for both streaming and non-streaming requests. For streaming requests, the proxy captures final usage statistics from the upstream completion event and records them into
usage.jsonwhile also forwarding an OpenAI-compatible usage chunk (choices: [], usage: {...}) for OpenCode/AI SDKstreamUsage: truesupport.
Run the test suite:
pip install pytest
python -m pytest test_proxy.py -vThe test suite includes:
- Unit tests for JSON null stripping.
- Anthropic-to-OpenAI payload normalization.
- SSE billing/null stream filtering.
- Structured content-block error detection.
- End-to-end authentication isolation and token verification against an in-process mock server.
- Why this happens: When OpenCode connects directly to AgentRouter's native
/v1/responsesendpoint (e.g.AgentRouter (Direct Responses API)), multi-turn sessions (such as theBuildagent executing tools likeplanning-with-files) send stateful item or response IDs. AgentRouter load-balances incoming requests across multiple regional Azure OpenAI clusters. When Turn 2 lands on a different Azure cluster than Turn 1, Azure rejects the request with this error. - Solution: Route through
AgentRouter-Proxy. In OpenCode's model selector, selectAgentRouter (Wyse Tailscale Proxy)orAgentRouter (Local Proxy). The proxy converts requests into standard stateless OpenAI Chat Completions payloads, sending self-contained inputs on every turn so cross-cluster Azure routing never conflicts.
- Why this happens: When reasoning models invoke parallel tool calls concurrently (e.g.
executing-plansanddocumentation-writer), naive SSE stream translators emit all argument delta chunks with hardcoded tool index0. This concatenates multiple distinct JSON argument payloads into one invalid string (e.g.{"id":"executing-plans"}{"id":"documentation-writer"}), causing the IDE tool parser to crash. - Solution:
AgentRouter-Proxyimplements a statefulResponsesStreamTranslatorthat tracksoutput_indexanditem_id, mapping each distinct parallel function call to its own sequential index (0, 1, 2...).
- Why this happens: OpenAI Chat Completions stores tool calls in
message.tool_callswithcontent: null. When forwarded directly to the Responses API, Azure/OpenAI rejects assistant messages with missingcontent. - Solution:
AgentRouter-Proxytranslates tool calls and tool execution results into top-level standalone Responses items (function_callandfunction_call_output), ensuring all message items contain valid, non-nullcontent.
- Why this happens: AgentRouter's
/v1/responsesendpoint strictly requiresmax_output_tokens >= 16. When an agent harness sends small limits (e.g.max_tokens: 5), the upstream validator returns HTTP 400. - Solution:
AgentRouter-Proxyautomatically clampsmax_output_tokenstomax(16, val).
5. "Cannot unmarshal object into Go struct field OpenAIResponsesResponse.tool_choice of type string"
- Why this happens: AgentRouter's Go backend unmarshals
tool_choicestrictly as a string. If an IDE harness sends an object (e.g.{"type": "function", "function": {"name": "read_file"}}), AgentRouter crashes with HTTP 500. - Solution:
AgentRouter-Proxysanitizes dictionarytool_choiceobjects into string format ("auto","required", or the target function name).
- Why this happens: Windows automatically enters sleep or standby mode after 15 to 30 minutes of keyboard/mouse inactivity. Since a headless server PC has no physical user input, Windows puts the machine to sleep, dropping SSH and Tailscale connections.
- Solution: Run
install-autostart.baton the server PC. It runspowercfg /change standby-timeout-ac 0to permanently disable sleep on AC power, keeping the server reachable 24/7.
- Why this happens: AgentRouter blocks or bans accounts making high volumes of API requests with generic or unauthorized user-agent headers.
- Solution:
AgentRouter-Proxyautomatically injectsUser-Agent: opencode/2.2.1into all upstream requests to ensure full compatibility with provider policies.
- Never commit
.envor real API keys: This repository is public..gitignoreis pre-configured to ignore.env,*.env, log files, and caches. - Revoke compromised keys immediately: If an API key was ever accidentally pasted in public issues, chat logs, or commits, revoke it right away in your provider dashboard.
- Use different keys: Your incoming
PROXY_AUTH_TOKEN(used between your laptop and proxy) should be different from your upstream provider API key. - Tailscale encryption: Tailscale traffic is encrypted peer-to-peer with WireGuard, keeping your remote traffic safe across networks without port forwarding.
Tip
If you don't have an AgentRouter account yet, you can support this open-source project by signing up through the official referral link below:
👉 https://agentrouter.org/register?aff=M69e
Why use AgentRouter?
- Unified Frontier LLM Access: Access the latest and most powerful reasoning and coding models from OpenAI, Anthropic, and top providers with high rate limits and resilient multi-cluster uptime.
- Built for AI Coding Agents: Seamless integration with OpenCode, Claude Code, Cursor, Cline, and Roo Code.
- Support Ongoing Maintenance: Signing up via this link helps support updates, upstream compatibility testing, and new feature development for this proxy. Thank you for your support!
MIT License. See LICENSE for details.
{ "$schema": "https://opencode.ai/config.json", "providers": { "agentrouter": { "name": "AgentRouter (Wyse Tailscale Proxy)", "package": "@opencode/ai/providers/openai-compatible", "settings": { "baseURL": "http://100.97.230.23:4182/v1" }, "headers": { "Authorization": "Bearer your-client-auth-token" }, "compatibility": { "streamUsage": true, "requireFinishReason": true }, "models": { "gpt-6.1-sol": { "name": "GPT 6.1 Sol", "limit": { "context": 200000, "output": 16384 }, "variants": [ { "id": "high", "settings": { "reasoningEffort": "high" } }, { "id": "xhigh", "settings": { "reasoningEffort": "xhigh" } } ] }, "claude-opus-5": { "name": "Claude Opus 5", "limit": { "context": 200000, "output": 8192 } }, "claude-opus-4-8": { "name": "Claude Opus 4.8", "limit": { "context": 200000, "output": 8192 } }, "gpt-5.5": { "name": "GPT 5.5", "limit": { "context": 128000, "output": 16384 } }, "glm-5.2": { "name": "GLM 5.2", "limit": { "context": 128000, "output": 8192 } } } } } }