feat: multi-backend proxy with auto rate-limit jailing
Refactor from monolithic proxy to multi-backend architecture (Backend interface: OpenCode Zen + Kilo gateway). - Add auto rate-limit jailing: models returning HTTP 429 are hidden from /v1/models immediately and re-probed every 30 min - Backend interface supports model aliases, custom headers, and per-backend routing - Full Anthropic Messages API support with OpenAI format conversion - 9 free models across two backends with Claude name aliases
This commit is contained in:
@@ -1,30 +1,36 @@
|
||||
# OpenCode Zen Proxy
|
||||
# Free IDE Proxy
|
||||
|
||||
OpenAI-compatible proxy for [OpenCode](https://opencode.ai)'s free Zen models. Use DeepSeek V4 Flash, MiniMax M2.5, Kimi K2.5, and more — for free — in any OpenAI-compatible tool (Cursor, Claude Code, Continue, etc.).
|
||||
OpenAI/Anthropic-compatible proxy that aggregates **free** models from [OpenCode Zen](https://opencode.ai) and [Kilo Code](https://kilo.ai) under a single endpoint. Use DeepSeek V4 Flash, StepFun, Nemotron, OpenRouter free, and more — for free — in any OpenAI-compatible tool (Cursor, Claude Code, Continue, Cline, etc.).
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Your tool → opencode-proxy (localhost:6446) → opencode.ai/zen/v1
|
||||
Your tool → free-ide-proxy (localhost:6446) → { opencode.ai/zen/v1 | api.kilo.ai/api/gateway }
|
||||
```
|
||||
|
||||
The proxy injects the required `x-opencode-*` headers that OpenCode Zen's free API expects. Your tools talk standard OpenAI protocol to the proxy; the proxy handles the rest.
|
||||
The proxy routes by model name to the matching backend, injecting the required headers. OpenCode Zen expects `x-opencode-*` headers with `Bearer public`; Kilo's gateway needs no auth for free models. Your tools talk standard OpenAI protocol to the proxy; the proxy handles the rest.
|
||||
|
||||
### Auto rate-limit jailing
|
||||
|
||||
When an upstream starts returning HTTP 429 (rate limit) for a model, the proxy automatically **cages** it — the model disappears from `/v1/models` immediately. A background checker re-probes caged models every 30 minutes; if they recover, they reappear.
|
||||
|
||||
This means your client just needs to query `/v1/models` before each request and pick an available model. No hardcoded model names, no manual retries.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o opencode-proxy .
|
||||
go build -o free-ide-proxy .
|
||||
|
||||
# Run (no auth)
|
||||
./opencode-proxy
|
||||
./free-ide-proxy
|
||||
|
||||
# Run with API key protection
|
||||
./opencode-proxy -api-key "your-secret-key"
|
||||
./free-ide-proxy -api-key "your-secret-key"
|
||||
|
||||
# Or via env var
|
||||
export OPENCODE_PROXY_KEY="your-secret-key"
|
||||
./opencode-proxy
|
||||
./free-ide-proxy
|
||||
```
|
||||
|
||||
The server starts on `http://127.0.0.1:6446`.
|
||||
@@ -44,9 +50,12 @@ Also reads `OPENCODE_PROXY_KEY` environment variable if `-api-key` is not set.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/health` | Health check |
|
||||
| `GET` | `/v1/models` | List available free models |
|
||||
| `POST` | `/v1/chat/completions` | Chat completions (streaming + non-streaming) |
|
||||
| `GET` | `/health` | Health check (version, active model count, auth status) |
|
||||
| `GET` | `/v1/models` | List currently **active** (not rate-limited) free models |
|
||||
| `GET` | `/v1/models/{id}` | Single model detail (404 if rate-limited) |
|
||||
| `POST` | `/v1/chat/completions` | OpenAI Chat Completions (streaming + non-streaming) |
|
||||
| `POST` | `/v1/messages` | Anthropic Messages API |
|
||||
| `POST` | `/v1/v1/messages` | Workaround for Claude Code double-path bug |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -56,58 +65,60 @@ If `-api-key` is set, all requests require one of:
|
||||
|
||||
## Available Models
|
||||
|
||||
| Model ID | Alias | Notes |
|
||||
|----------|-------|-------|
|
||||
| `deepseek-v4-flash-free` | `deepseek`, `deepseek-v4` | Solid, recommended |
|
||||
| `big-pickle` | `pickle` | Stealth model (= DeepSeek V4 Flash) |
|
||||
| `minimax-m2.5-free` | `minimax`, `m2.5` | Strong coding model |
|
||||
| `kimi-k2.5-free` | `kimi`, `k2.5` | Best free model |
|
||||
| `gpt-5-nano` | `nano`, `gpt5` | OpenAI-powered free |
|
||||
| `nemotron-3-super-free` | `nemotron` | Hit or miss |
|
||||
| `qwen3.6-plus-free` | `qwen` | Intermittent |
|
||||
### OpenCode Zen (5 models) — `opencode.ai/zen/v1`
|
||||
|
||||
All support streaming, tool calls, and system messages.
|
||||
| Model ID | Aliases | Context | Max Output |
|
||||
|----------|---------|---------|------------|
|
||||
| `deepseek-v4-flash-free` | `deepseek`, `deepseek-v4`, `ds` | 1M | 384K |
|
||||
| `big-pickle` | `pickle` | 200K | 32K |
|
||||
| `mimo-v2.5-free` | `mimo`, `mimo-v2.5`, `xiaomi` | 1M | 32K |
|
||||
| `north-mini-code-free` | `north`, `north-mini`, `cohere` | 256K | 64K |
|
||||
| `nemotron-3-ultra-free` | `nemotron`, `nemotron-3`, `nvidia` | 1M | 16K |
|
||||
|
||||
**API auth:** `Bearer public` (no real token needed).
|
||||
|
||||
### Kilo gateway (4 models) — `api.kilo.ai/api/gateway`
|
||||
|
||||
| Model ID | Aliases | Context | Max Output |
|
||||
|----------|---------|---------|------------|
|
||||
| `stepfun/step-3.7-flash:free` | `stepfun`, `stepfun-free` | 256K | 32K |
|
||||
| `poolside/laguna-m.1:free` | `poolside`, `poolside-free`, `laguna` | 256K | 32K |
|
||||
| `nvidia/nemotron-3-ultra-550b-a55b:free` | — | 1M | 16K |
|
||||
| `openrouter/free` | `openrouter` | 256K | 32K |
|
||||
|
||||
**API auth:** None needed for free models.
|
||||
|
||||
All models support streaming, tool calls, and system messages. Reasoning models (DeepSeek, Nemotron, StepFun) need `max_tokens` ≥ 500 or they return empty content (first tokens go to reasoning).
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code appends `/v1/messages` to `ANTHROPIC_BASE_URL` automatically. Set:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL=http://127.0.0.1:6446
|
||||
export ANTHROPIC_MODEL=claude-sonnet-4-6 # maps to deepseek-v4-flash-free
|
||||
```
|
||||
|
||||
The proxy also handles `/v1/v1/messages` as a fallback for the double-path issue.
|
||||
|
||||
Claude model name aliases (Claude Code validates model names client-side):
|
||||
|
||||
| Claude Model | Maps to |
|
||||
|-------------|---------|
|
||||
| `claude-sonnet-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4` | `deepseek-v4-flash-free` |
|
||||
| `claude-opus-4-8`, `claude-opus-4-5`, `claude-opus-4` | `deepseek-v4-flash-free` |
|
||||
| `claude-haiku-4-5`, `claude-haiku-4` | `north-mini-code-free` |
|
||||
| `claude-3.5-sonnet` | `north-mini-code-free` |
|
||||
| `claude-3.5-haiku` | `big-pickle` |
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### Cursor / Continue / Cline
|
||||
|
||||
- **Base URL**: `http://127.0.0.1:6446/v1`
|
||||
- **API Key**: your proxy key (or `public` if no auth)
|
||||
- **API Key**: your proxy key (or empty if no auth)
|
||||
- **Model**: `deepseek-v4-flash-free` (or any alias)
|
||||
|
||||
### Claude Code
|
||||
|
||||
Claude Code uses the Anthropic Messages API natively. For now, use an OpenAI-compatible bridge or configure via:
|
||||
|
||||
```bash
|
||||
# In Claude Code, use as custom provider
|
||||
claude config set provider_base_url http://127.0.0.1:6446/v1
|
||||
```
|
||||
|
||||
### OpenCode CLI
|
||||
|
||||
Add to `~/.config/opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": {
|
||||
"free": {
|
||||
"name": "free",
|
||||
"type": "openai",
|
||||
"apiKey": "public",
|
||||
"baseURL": "http://127.0.0.1:6446/v1",
|
||||
"models": {
|
||||
"free/deepseek": {
|
||||
"id": "deepseek-v4-flash-free",
|
||||
"name": "free/deepseek"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Any OpenAI SDK
|
||||
|
||||
```python
|
||||
@@ -115,7 +126,7 @@ from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="http://127.0.0.1:6446/v1",
|
||||
api_key="your-proxy-key" # or "public" if no auth
|
||||
api_key="your-proxy-key"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
@@ -128,35 +139,31 @@ response = client.chat.completions.create(
|
||||
|
||||
```bash
|
||||
# Build for Linux
|
||||
GOOS=linux GOARCH=amd64 go build -o opencode-proxy .
|
||||
GOOS=linux GOARCH=amd64 go build -o free-ide-proxy .
|
||||
|
||||
# Copy and run
|
||||
scp opencode-proxy user@vps:/home/user/
|
||||
ssh user@vps './opencode-proxy -api-key "secure-key" -host 0.0.0.0'
|
||||
|
||||
# Or use systemd (create /etc/systemd/system/opencode-proxy.service)
|
||||
scp free-ide-proxy user@vps:/home/user/
|
||||
ssh user@vps './free-ide-proxy -api-key "secure-key" -host 0.0.0.0'
|
||||
```
|
||||
|
||||
systemd unit:
|
||||
### systemd service
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=OpenCode Zen Proxy
|
||||
Description=Free IDE Proxy
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/user/opencode-proxy -api-key "${PROXY_KEY}" -host 0.0.0.0
|
||||
ExecStart=/home/user/free-ide-proxy -api-key "${PROXY_KEY}" -host 0.0.0.0
|
||||
Restart=always
|
||||
User=user
|
||||
EnvironmentFile=/etc/opencode-proxy.env
|
||||
EnvironmentFile=/etc/free-ide-proxy.env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Local SSH tunnel
|
||||
|
||||
If you don't want to expose the port publicly:
|
||||
### Local SSH tunnel
|
||||
|
||||
```bash
|
||||
ssh -L 6446:127.0.0.1:6446 user@your-vps
|
||||
@@ -164,6 +171,28 @@ ssh -L 6446:127.0.0.1:6446 user@your-vps
|
||||
|
||||
Then point your tools at `http://127.0.0.1:6446/v1`.
|
||||
|
||||
## Quirks & Gotchas
|
||||
|
||||
- **No tests** exist yet. All manual testing.
|
||||
- **In-memory only** — session and health state dies on restart. Health map auto-rebuilds via initial scan.
|
||||
- **Health check logs** show rate-limit activity: `sudo journalctl -u free-ide-proxy | grep health`
|
||||
- **Unknown models** are forwarded as-is via the default backend (opencode) — catch-all for new models.
|
||||
- **Reasoning model caveat:** DeepSeek, Nemotron and StepFun need `max_tokens` ≥ 500; with very low values they return empty content. This is model behaviour, not a proxy bug.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client → internal/server/ (HTTP, auth, routing)
|
||||
→ internal/proxy/ (model resolve, backend router, session mgmt,
|
||||
forwarding, Anthropic↔OpenAI conversion)
|
||||
→ Backend → upstream
|
||||
```
|
||||
|
||||
- **Pure stdlib Go 1.22** — no router, no dependencies beyond stdlib.
|
||||
- **Multi-backend** via `Backend` interface (`internal/proxy/backend.go`). Add a new backend by implementing the interface and registering it in `NewProxy` (`models.go`).
|
||||
- **Model aliases** live in each backend (`opencode.go`, `kilo.go`).
|
||||
- **Auto rate-limit jailing** built into `Models()` — models returning 429 are hidden until they recover.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Reference in New Issue
Block a user