# Free IDE Proxy OpenAI/Anthropic-compatible proxy that aggregates the **free** models from multiple opencode-based editors (currently OpenCode Zen and Kilo Code) under a single endpoint. Each upstream lives behind a `Backend` implementation and the proxy routes by model name. ## Build & Run ```bash go build -o free-ide-proxy . ./free-ide-proxy # no auth ./free-ide-proxy -api-key "secret" # with auth ``` Auth reads `OPENCODE_PROXY_KEY` env var; `-api-key` flag takes precedence. Defaults: `127.0.0.1:6446`. Use `-host 0.0.0.0` for external access. ## 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 deps beyond stdlib - **Entrypoint:** `main.go` wires `proxy.NewProxy(key)` into `server.NewServer(p)` - **Multi-backend:** `internal/proxy/backend.go` defines the `Backend` interface. The `Proxy` holds an ordered list and routes each request to the first backend that recognises the model. Unknown models fall back to the default backend (opencode) and are forwarded as-is. - **Backends:** `opencode.go` (OpenCode Zen, `opencode.ai/zen/v1`, `Bearer public`) and `kilo.go` (Kilo gateway, `api.kilo.ai/api/gateway`, no auth). Add a new file implementing `Backend` and register it in `NewProxy` (`models.go`) to support another upstream. - **Auth middleware** in `server.go:88` checks `Authorization: Bearer` or `x-api-key` header - **Model aliases** live in each backend (`opencode.go` and `kilo.go`) — short names, company names, and Claude model names → full IDs - **Session rotation:** session IDs rotate every 30 min per user key (`proxy.SessionID()`) - **Auto rate-limit jailing:** models returning HTTP 429 are automatically hidden from `/v1/models` and re-probed every 30 min until they recover. See `models.go` (`modelHealth`, `healthLoop`, `pingModel`, `markRateLimited`, `markActive`, `isRateLimited`, `Models()` filtering). - **Pass-through:** unknown model names are forwarded as-is to the default backend, not rejected ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/health` | Health check (returns hardcoded `"1.0.0"`) | | `GET` | `/v1/models` | List available free models (auth required if key set) | | `GET` | `/v1/models/{id}` | Single model detail | | `POST` | `/v1/chat/completions` | OpenAI Chat Completions (streaming + non-streaming) | | `POST` | `/v1/messages` | Anthropic Messages API | | `POST` | `/v1/v1/messages` | Same as above — workaround for Claude Code double-path bug | ## Anthropic Messages API Fully implemented in `internal/proxy/anthropic.go`. Converts Anthropic → OpenAI format upstream, then converts responses back to Anthropic SSE (streaming) or JSON (non-streaming). **Claude Code quirk:** Claude Code appends `/v1/messages` to `ANTHROPIC_BASE_URL` automatically. Set `ANTHROPIC_BASE_URL` to `http://127.0.0.1:6446` (without `/v1` suffix) to avoid double-path. The proxy also handles `/v1/v1/messages` as a fallback (`server.go:62`). **Claude model name aliases** are defined in `models.go:77-88` — Claude Code validates model names client-side, so you must set `ANTHROPIC_MODEL` to a valid Claude name like `claude-sonnet-4-6` (maps to `deepseek-v4-flash-free`). ## Available Models (from `opencode.go` + `kilo.go`) ### OpenCode Zen backend (`opencode.go` → `opencode.ai/zen/v1`, `Bearer public`) | 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 | ### Kilo gateway backend (`kilo.go` → `api.kilo.ai/api/gateway`, no auth) | 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 | **Source of truth is the code (`opencode.go`, `kilo.go`), not `README.md`.** The README may list models that differ from the code (e.g. `minimax-m2.5-free`, `kimi-k2.5-free`, `gpt-5-nano`, `nemotron-3-super-free`, `qwen3.6-plus-free` are NOT in the code). The code controls what works. ## Quirks & Gotchas - **No tests exist.** Any change is untested unless you add them. - **No CI/CD, no Makefile, no lint config.** All manual. - **In-memory only.** Session and health state dies on restart. The health map auto-rebuilds via initial scan. - **`deploy/start-proxy.bat`** and **`deploy/CREDENCIALES.md`** contain a hardcoded API key — do not commit them. - **`opencode-proxy.exe~`** in root is a backup artifact; ignore. - Upstream Zen API expects `x-opencode-request`, `x-opencode-session`, `x-opencode-client`, `x-opencode-project` headers (set in `opencode.go` `OpenCodeBackend.Headers`). Kilo needs no auth — its gateway returns HTTP 200 for free models with no `Authorization` header. - **Server logs request bodies to stdout** (truncated to 500 chars in `server.go:265-271`). Verbose by design. - **Health endpoint** returns hardcoded version `"1.0.0"` in `server.go:126`. Update both `main.go:12` and `server.go:126` when bumping. - **Upstream timeout:** `proxy.go` sets a 10-minute HTTP client timeout — adjust if Zen models are slow on first call. - **Unknown models are forwarded as-is** via the default backend (opencode) — useful when new models appear upstream before code is updated. - **Reasoning models** (DeepSeek, Nemotron, StepFun) need `max_tokens` ≥ 500 — first tokens go to reasoning, not visible content. With `max_tokens:30` they return empty content; that is model behaviour, not a proxy bug. - **Rate limit jailing:** `proxy.go:164` and `anthropic.go:713` check for 429 and call `markRateLimited` immediately. `healthLoop()` in `models.go` does a full scan on startup, then re-checks caged models every 30 min. `Models()` filters out rate-limited models — they reappear once recovered.