- TypeScript 43.3%
- JavaScript 34.8%
- CSS 12.2%
- HTML 7.1%
- Shell 2%
- Other 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
build-and-publish / docker (push) Successful in 1m32s
Update README.md to include a comprehensive overview of the fork's differences from the upstream repository, including Docker support, Forgejo CI, Apprise notifications, proxy access tokens, model blocklisting, and Go plan usage scraping. Refactor usage scraping logic to distinguish between expired auth cookies and accounts without an active Go plan. Improve proxy error handling to treat 401 and 403 upstream responses as provider-specific failures, triggering failover to the next available key instead of stalling the request. Ensure 4xx errors (excluding specific auth/rate-limit cases) do not negatively impact the key's health status in the circuit breaker. |
||
| .forgejo/workflows | ||
| .kilo | ||
| .opencode-plugin | ||
| Developers Log | ||
| resources | ||
| scripts | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| implementation_plan.md | ||
| package-lock.json | ||
| package.json | ||
| PRD.md | ||
| README.md | ||
| restart-router.sh | ||
| tsconfig.json | ||
OpenCode Go Multi-Account Router
⚠️ Educational Purpose Only — This project is provided for learning and educational purposes. It demonstrates how to build a multi-account proxy router for OpenCode Go and Zen API subscriptions. Users are responsible for complying with OpenCode's terms of service.
A native TypeScript OpenCode plugin and proxy router that pools multiple OpenCode Go + Zen API subscriptions into a single endpoint. Instead of manually swapping API keys between accounts when one runs out of quota, this router handles it automatically. It routes requests across accounts using cache-aware or load-spreading strategies, auto-starts with OpenCode in plugin mode, and includes a persistent control-room dashboard for key management, usage analytics, and live observability.
Note for end users: If you just want to use the router (not develop it), follow the Installation guide and ignore files like
restart-router.sh,AGENTS.md, andDevelopers Log/— those are for project maintainers.
💰 Support the project: Use this referral link to sign up for OpenCode Go and get $5 free credit to start. Every referral helps keep this project maintained.
Fork: changes vs upstream
This fork (git.nahorniak.cz/David-Nahorniak/opencode-go-multi-auth) is based on the upstream Rishabh-Bajpai/opencode-go-multi-auth (commit 7ebd499). Everything added beyond the original:
- Docker —
Dockerfile(multi-stagenode:22-bookworm-slim, prunesdevDependencies, installsapprise) anddocker-compose.yml. Persisted state on a/datavolume (CONFIG_DIR=/data),HEALTHCHECKvia/healthz, ports18904/18905mapped outward. - Forgejo CI —
.forgejo/workflows/build-and-publish.ymlbuilds and publishes the image to the Forgejo Container Registry on tag pushv*(andworkflow_dispatch), tagged<reg>/<owner>/<repo>:<version>and:latest. - Apprise notifications — the
ntfynotifier is replaced by the general Apprise notifier (Telegram, Discord, ntfy, email, Slack, …). New env varsNOTIFY_URLS(list of URLs separated by spaces/commas/newlines) andAPPRISE_BIN;NTFY_URLis a deprecated alias for backward compatibility.appriseis pre-installed in the Docker image; on bare-metal/plugin installs the user installs it (pip install apprise). URLs and a Test button can be changed from dashboard Settings. - Proxy access token — the proxy authenticates every incoming request with a shared bearer /
x-api-keytoken (timing-safe compare insrc/proxy/server.ts). The token is stripped from the header and the proxy then injects the real pooled upstream key; opencode sends the proxy token as the providerapiKey. Configurable viaproxyAccessToken, re-generatable from dashboard Settings. The plugin (src/plugin/index.ts) prints it in setup instructions and supplies themulti-auth-zenprovider. - Model blocklist —
disabledModelsin the config; the proxy returns404before routing processes a request for a disabled model. - Go plan usage scraping — new module
src/proxy/opencode-go-usage.tsparses the workspace dashboard HTML (both SolidJS SSR and the newerdata-slotformat) for rolling/weekly/monthly usage windows and reset times. Auth is the opencode account session cookie (OAuth login), not the Go API key. Per-accountgoUsageWorkspaceId/goUsageAuthCookieare attached to the API key and encrypted inrouter-keys.enc; managed viaKeyManager.setGoUsageCreds. Usage display moved to the Accounts page. The module detects an expired cookie (cookieExpired) and an unactivated plan (planNotActivated). - Miscellaneous —
.gitignoreadds*.key;SecureStorefixes (key add/remove fixes, missing storage API key); minor proxy server fixes.
Preview
Click any screenshot to view full resolution.
Features
- Three Routing Strategies — Priority Failover (cache-first default), Round Robin, Weighted Cycle. Each explained in the dashboard UI.
- Dual upstream — Same proxy serves OpenCode Go (paid) and OpenCode Zen (free tier). Requests with a
/zen/path prefix are forwarded tohttps://opencode.ai/zen/v1; everything else goes tohttps://opencode.ai/zen/go/v1. Both upstreams share keys, priority, weight, cooldown, and circuit breaker. - React, don't predict — The router does not estimate quota. It only marks a key as exhausted when the upstream itself returns 402/429 with a quota body, and uses the upstream-supplied cooldown. Cost is recorded for display only.
- Cost estimation — The OpenCode Go upstream does not return a
costfield, so the proxy falls back to a per-model rate card sourced from the OpenCode Go docs. Estimated costs render in yellow italic with a~prefix and a tooltip; actual upstream costs render plain. Tiered pricing (Qwen3.6/3.7 Plus, Qwen3.7 Max) applies the higher tier whencacheRead + input > 256_000tokens. - Zen model drift detection — The Models page compares the user's
opencode.jsonconfig against the live Zen upstream catalog. A banner lists new free models not yet in the user's config with a "Copy snippet" button so the user can paste them in. Re-checks every 12 hours while the page is open. - Persistent Key Settings — Enable/drain, priority, weight, and alias survive restarts. Keys are stored with stable IDs and encrypted at rest.
- Per-Key Analytics — Request count, success/error rate, average latency, token breakdown, last model used, last session ID, observed cost, and quota-error tally.
- Stream-Aware Usage Tracking — Parses token usage from both full JSON and SSE streaming completions. Injects
stream_options.include_usagefor OpenAI-compatible streams. - Circuit Breaker — Temporarily removes unhealthy keys after 3 consecutive 5xx errors, auto-recovers after 5 minutes.
- Cache-Preserving Header Passthrough — Forwards
X-Session-Id,prompt_cache_key/prompt-cache-key,cache_control/cache-control, plus both bearer andx-api-keyauth for broader model compatibility. - Local Web UI Control Room — Strategy explainer, key deck with inline editing, live usage ledger, quota-error panel, routing tape with per-request route reasons and session IDs, model catalog with visibility controls, and a drift detection banner.
- Secure Key Storage — AES-256-GCM encrypted at rest using PBKDF2-derived key from machine identity.
- Push Notifications — Optional Apprise notifications for key exhaustion, all-keys-exhausted, and circuit-breaker trips/recovery (Telegram, Discord, ntfy, email, …). Configurable from the dashboard Settings page with hot-reload.
- Auto-Starting OpenCode Plugin — Install as an OpenCode plugin so the proxy and dashboard start with OpenCode automatically.
- Standalone or Library — Keep a CLI/server mode for debugging, fallback use, or external automation.
Prerequisites
- Node.js >= 22 (required by transitive dependencies)
- npm or bun or pnpm
- OpenCode CLI installed and configured with a Go subscription (Zen is optional but recommended)
Installation
git clone https://github.com/Rishabh-Bajpai/opencode-go-multi-auth.git
cd opencode-go-multi-auth
npm install
npm run build
mkdir -p ~/.config/opencode/plugins
cat > ~/.config/opencode/plugins/opencode-go-multi-auth.js <<EOF
export { default, server, pluginModule } from "$(pwd)/dist/opencode-plugin.js"
EOF
The cloned repo must stay — the plugin loader points to its dist/ directory.
OpenCode config
Add providers that route through the proxy in ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"opencode-go": {
"options": {
"baseURL": "http://localhost:18905"
},
"models": {
"deepseek-v4-flash": {}
}
},
"multi-auth-zen": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenCode Zen (multi-auth)",
"options": {
"baseURL": "http://localhost:18905/zen"
},
"models": {
"deepseek-v4-flash-free": {},
"mimo-v2.5-free": {},
"qwen3.6-plus-free": {},
"minimax-m3-free": {},
"nemotron-3-ultra-free": {},
"north-mini-code-free": {}
}
}
}
}
The loader file in ~/.config/opencode/plugins/ is auto-loaded — no plugin array needed in the config.
Then close any existing OpenCode sessions and open a fresh one. The plugin will start or reuse one shared local router daemon, which serves:
- Proxy endpoint —
http://localhost:18905 - Dashboard —
http://localhost:18904
Open the Dashboard
Navigate to http://localhost:18904 in your browser. If the page does not open, first open a fresh OpenCode session so the plugin can start the router daemon.
Add your Go API keys
- Click Add Key
- Paste your OpenCode Go API key (found in your OpenCode account settings)
- Optionally give it an alias, priority, and weight
- Repeat for each Go account you want to pool
Standalone mode (fallback)
If you do not want plugin mode, you can still run the router manually:
npm start
Or directly:
node dist/bin.js
In standalone mode you must start the router yourself after reboot. Plugin mode avoids manual startup when opening OpenCode, but it is not an OS boot service.
Whenever you change the source locally, rebuild before restarting:
npm run build
Compatibility Notes
Note: This project has been tested on Linux. macOS has not been tested yet — if you run into issues, please open a GitHub issue.
- Some OpenCode Go models use
/messageswith Anthropic-style auth. The proxy now forwards bothAuthorization: Bearer ...andx-api-keyto maximize compatibility. - If a new model still fails, capture the exact model name and the
/messagesor/chat/completionspath from the router log.
Dual upstream: Go and Zen through the same proxy
The proxy serves both OpenCode Go (paid subscription) and OpenCode Zen (free tier + paid models) from the same localhost:18905. Requests whose path starts with /zen/ are forwarded to https://opencode.ai/zen/v1; everything else goes to https://opencode.ai/zen/go/v1. The /zen/ prefix is stripped before forwarding.
Both upstreams support Anthropic-format (/v1/messages) and OpenAI-format (/v1/chat/completions) requests. The free Zen models use the OpenAI format, so add a second custom provider alongside the built-in opencode-go you already set up (see the Installation config for the full example):
{
"provider": {
"opencode-go": {
"options": { "baseURL": "http://localhost:18905" },
"models": { "deepseek-v4-flash": {} }
},
"multi-auth-zen": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenCode Zen (multi-auth)",
"options": { "baseURL": "http://localhost:18905/zen" },
"models": {
"deepseek-v4-flash-free": {},
"mimo-v2.5-free": {},
"qwen3.6-plus-free": {},
"minimax-m3-free": {},
"nemotron-3-ultra-free": {},
"north-mini-code-free": {}
}
}
}
}
Then reference the provider from agents as multi-auth-zen/<model>, e.g.
{
"agent": {
"explore": { "model": "multi-auth-zen/deepseek-v4-flash-free" },
"general": { "model": "multi-auth-zen/deepseek-v4-flash-free" }
}
}
Important: the provider name must be unique. Do not use opencode-zen or opencode — those collide with OpenCode's built-in Zen provider and OpenCode will silently route requests directly to opencode.ai, bypassing the proxy entirely. Any other name (e.g. multi-auth-zen, proxy-zen, my-zen) works.
When new free models appear upstream, the dashboard's Models page surfaces a drift banner with a Copy snippet button so you can paste the missing models into your models block. The check runs every 12 hours while the page is open. You can also change the provider name tracked by the dashboard via the text input next to the Save button (default: multi-auth-zen).
Routing Strategies
All strategies are explained in the dashboard UI. Here is a quick reference:
| Strategy | Cache-friendly | Priority-aware | Weight-aware | Best for |
|---|---|---|---|---|
| Priority Failover (default) | Yes | Yes | No | Keep one account warm for cache reuse |
| Round Robin | No | No | No | Simple spreading when cache reuse is less important |
| Weighted Cycle | No | No | Yes | Proportional traffic distribution |
Session stickiness is applied before any strategy. If a warm session key is detected, the request is pinned to its current account regardless of the active strategy.
Note: the legacy priority_spillover and highest_remaining_quota strategies were removed when the router stopped estimating quota. Stored values for those strategies are mapped to priority_failover for backward compatibility.
Routing Tape (Log Viewer)
Each log entry now includes rich metadata visible in the UI and file logs:
- Method, path, status code, and duration
- Selected key alias and why it was chosen
- Active strategy name
- Session ID (observed or synthesized from cache key)
- Token breakdown and upstream-observed cost (if reported)
- Whether the route was chosen by session stickiness
- For quota-error rows, a structured
quotaErrorpayload with the upstream's reset time and message
Dashboard Control Room
The dashboard is organized into seven pages:
- Overview — KPI strip (enabled / active / cooldown / requests / quota errors), an "Errors told us" panel listing the keys the upstream has marked exhausted (with the upstream's reset time and message), the live token-throughput chart, a model distribution donut, and the per-window token breakdown.
- Accounts — Add, enable/drain, reorder (drag), and set priority/weight for each account. Each card shows its circuit-breaker state, quota-error tally, last quota error (status code, message, retry time), token breakdown, latency, error rate, last model, and a "Test" button that fires a live request through the proxy. Persistent to disk.
- Strategy Console — Select an active strategy and see its description, best-for recommendation, cache friendliness, and behavior.
- Tokens — Per-model and per-key time series, category share, observed cost, and 30-day rolling usage.
- Logs — Virtualized table of every routed request, with the latest entry at the top. Search across path / model / key / route reason, filter by level / "Quota only" / provider (Go / Zen / All), pause auto-scroll, paginate for long histories, right-click to copy fields, click a row to see the full metadata.
- Models — Browse the combined model catalog from both upstreams. Select which models show in the token charts. A drift banner lists free Zen models the upstream offers that are not in your
opencode.jsonand provides a "Copy snippet" button to paste them in. - Settings — Active config (ports, strategy, total tokens observed, total observed cost, quota errors caught), the Apprise notification URL(s), the notification log, and the provider config snippet to copy into
~/.config/opencode/opencode.json.
Configuration
Environment variables
Set these in your shell or service file. The app reads from process.env — it does not auto-load a .env file.
| Variable | Default | Description |
|---|---|---|
UPSTREAM_URL |
https://opencode.ai/zen/go/v1 |
Upstream OpenCode Go API base URL |
DASHBOARD_PORT |
18904 |
Web UI dashboard port |
PROXY_PORT |
18905 |
Proxy server port |
COOLDOWN_MS |
18000000 |
Fallback cooldown in ms when the upstream returns 402/429 without a Retry-After signal. Default 5 hours. |
CIRCUIT_BREAKER_THRESHOLD |
3 |
Consecutive 5xx errors before tripping |
LOG_LEVEL |
info |
Log level: error, warn, info, debug |
CONFIG_DIR |
~/.opencode |
Directory for config, encrypted key storage, and usage data |
NTFY_URL |
— | Deprecated alias for NOTIFY_URLS. Note a bare https://ntfy.sh/mytopic URL is not valid Apprise syntax — use ntfy://ntfy.sh/mytopic under NOTIFY_URLS instead. |
NOTIFY_URLS |
— | One or more Apprise URLs (Telegram, Discord, ntfy, email, …) separated by spaces, commas, or new lines. Leave empty to disable. The bundled Docker image installs apprise; on bare-metal/plugin installs run pip install apprise (or set APPRISE_BIN). |
APPRISE_BIN |
apprise |
Override the apprise executable path (e.g. a venv/pipx prefix) when running outside Docker. |
REQUEST_TIMEOUT_MS |
0 |
Wall-clock cap on the upstream HTTP round-trip in ms. 0 = disabled, matching OpenCode's no-timeout default for the opencode-go provider. |
UPSTREAM_HUNG_TIMEOUT_MS |
0 |
Safety net that aborts the upstream fetch if no response is received at all within this window. 0 = disabled. Cleared as soon as the upstream starts sending bytes. |
Ports
- 18904 — Dashboard Web UI
- 18905 — Proxy/API endpoint
Both are configurable via environment variables.
Architecture
┌──────────────┐ ┌──────────────────────────────────────┐
│ OpenCode │────▶│ Go Router Proxy (port 18905) │
│ CLI │ │ │
└──────────────┘ │ KeyManager ──▶ CircuitBreaker │
│ │ │ │
│ QuotaTracker HeaderPassthrough │
│ │ │ │
│ SecureStore LogStream ──▶ Logger │
└──────┬───────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ OpenCode │ │ OpenCode │ │ Dashboard UI │
│ Go API (A) │ │ Go API (B) │ │ (port 18904) │
└──────────────┘ └──────────────┘ └──────────────────┘
- OpenCode CLI points to the proxy at
localhost:18905 - OpenCode auto-loads the plugin at startup (plugin mode) and the plugin starts or reuses a shared detached local router daemon
- Proxy selects an API key using the active strategy (with session stickiness as a pre-filter)
- Request is forwarded to the upstream OpenCode Go API
- On 402/429: key goes on dynamic cooldown, next key is tried
- On 5xx: circuit breaker tracks consecutive errors, trips after threshold
- Token usage is parsed from full JSON or SSE streaming responses and tracked against each key
- Caching and session headers (
X-Session-Id,prompt_cache_key,cache_control) pass through unmodified - All decisions are logged with routing reasons and streamed to the dashboard in real time
Security
- API keys are never stored in plain text. They are encrypted with AES-256-GCM using a key derived from your machine's hostname and username via PBKDF2 with 600,000 iterations.
- The encrypted key file is stored at
~/.opencode/router-keys.encand is tied to your specific machine. - No keys are ever sent to any external service — all processing is local.
- The dashboard runs on localhost only and is not exposed to the network.
Push Notifications
Optional push notifications via Apprise are sent when critical events happen. Apprise supports 100+ services from a single URL syntax — Telegram (tgram://), Discord (discord://), ntfy (ntfy://), email (mailto://), Slack, Gotify, and many more.
Set the NOTIFY_URLS environment variable (one or more Apprise URLs separated by spaces, commas, or new lines) to enable:
export NOTIFY_URLS="tgram://botToken/chatId ntfy://ntfy.sh/mytopic"
npm start
The bundled Docker image ships with the apprise binary pre-installed, so notifications work out of the box. When running bare-metal / as an OpenCode plugin, install it on the host:
pip install apprise # or: pipx install apprise
You can override the executable path with APPRISE_BIN if it lives in a venv/pipx prefix. URLs are never logged (they embed secrets such as bot tokens); only the enabled/disabled state is.
Migrating from the old NTFY_URL / ntfy.sh setup: a bare https://ntfy.sh/mytopic URL is not a valid Apprise URL — Apprise uses the ntfy:// scheme (e.g. ntfy://ntfy.sh/mytopic). Replace your NTFY_URL value with its Apprise equivalent under NOTIFY_URLS. For backward compatibility a saved ntfyUrl in router-config.json is still read, but a plain ntfy.sh topic URL won't deliver until re-entered in Apprise syntax via the dashboard.
| Event | Priority | Trigger |
|---|---|---|
| Key quota exhausted | High / Urgent | A key returns 402/429 with a quota body, the router records the upstream-supplied cooldown and moves to the next key |
| All keys exhausted | Urgent | Every key in the pool is exhausted |
| Circuit breaker tripped | High | 3 consecutive 5xx errors, key removed from pool |
| Circuit breaker recovered | Low | Key is healthy again, back in the pool |
OS Boot Service
If you want the router to start when the machine boots or when you log into your desktop session, run the standalone router under your OS service manager. Plugin mode alone only starts the router when OpenCode launches.
Linux (systemd)
- Build the project once:
cd /absolute/path/to/opencode-go-multi-auth
npm install
npm run build
- Find your absolute Node path. If you use
nvm, this matters becausesystemdwill not load your interactive shell profile:
which node
- Create
~/.config/systemd/user/opencode-go-router.serviceand replace both absolute paths below:
WorkingDirectorymust be the repo rootExecStartmust use the full path to yournodebinary and the full path todist/bin.js
[Unit]
Description=OpenCode Go Multi-Account Router
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/absolute/path/to/opencode-go-multi-auth
ExecStart=/absolute/path/to/node /absolute/path/to/opencode-go-multi-auth/dist/bin.js
Restart=on-failure
RestartSec=3
Environment=NODE_ENV=production
[Install]
WantedBy=default.target
- Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now opencode-go-router.service
- Verify it:
systemctl --user status opencode-go-router.service --no-pager
curl http://127.0.0.1:18904/healthz
curl http://127.0.0.1:18905/v1/models
- If you rebuild or change the source later, rebuild and restart the service:
npm run build
systemctl --user restart opencode-go-router.service
- Point OpenCode at the same proxy config from the earlier README section. With this setup, OpenCode can reuse the already-running router after login or reboot.
Notes
- This is a user service, so it starts when your user session starts.
- If you want it to run before login, you would need system-level service configuration and user lingering, which is usually unnecessary for this app.
- If
which nodeprints annvmpath, use that exact path inExecStart.
macOS (launchd)
Use the standalone router with a user LaunchAgent that runs node /absolute/path/to/opencode-go-multi-auth/dist/bin.js in the repo directory. The important part is the same: build first, use absolute paths, and manage dist/bin.js as a long-running user service.
Development
# Watch mode with auto-reload
npm run dev
# TypeScript type checking
npm run typecheck
# Build
npm run build
# Clean build output
npm run clean
Plugin Notes
- OpenCode plugin mode is the preferred installation path.
- The plugin starts or reuses one shared local router daemon across OpenCode sessions, so opening multiple sessions should not create competing proxy/dashboard owners.
- The plugin does not replace the built-in
opencode-goprovider. It auto-starts the proxy; the provider still needs the one-timebaseURLoverride inopencode.json. - Plugin mode auto-starts the router when OpenCode launches. It does not create an OS boot service; if you want system boot behavior, run the standalone router under
systemd,launchd, or another service manager. - Standalone CLI mode remains available for debugging, system service setups, or running the router outside OpenCode.
Programmatic Usage
You can also use the router as a library:
import { createRouter } from 'opencode-go-multi-auth'
const router = await createRouter({
proxyPort: 18905,
dashboardPort: 18904,
})
// Access internal components
router.keyManager.addKey('sk-...', 'Primary')
router.circuitBreaker.getState('key-id')
router.quotaTracker.getUsage('key-id')
// Graceful shutdown
await router.shutdown()
License
MIT