Proxy translating OpenAI/Anthropic Messages API to OpenCode Zen's free models. Pure stdlib Go 1.22, no external dependencies. - OpenAI Chat Completions (/v1/chat/completions) with streaming - Anthropic Messages API (/v1/messages) with streaming - Model aliases for 5 free models - Auth middleware support - Session rotation (30min per user key)
4.2 KiB
OpenCode Zen Proxy
Proxy translating OpenAI and Anthropic Messages API requests to OpenCode Zen's free model API.
Build & Run
go build -o opencode-proxy .
./opencode-proxy # no auth
./opencode-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, session mgmt, forwarding, Anthropic↔OpenAI conversion) → opencode.ai/zen/v1
- Pure stdlib Go 1.22 — no router, no deps beyond stdlib
- Entrypoint:
main.gowiresproxy.NewProxy(key)intoserver.NewServer(p) - Auth middleware in
server.go:88checksAuthorization: Bearerorx-api-keyheader - Model aliases in
internal/proxy/models.go:59— short names, company names, and Claude model names → full IDs - Session rotation: session IDs rotate every 30 min per user key (
proxy.SessionID()atmodels.go:154) - Pass-through: unknown model names are forwarded as-is, 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 models.go)
| 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 |
Source of truth is models.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 state dies on restart. No database.
deploy/start-proxy.batanddeploy/CREDENCIALES.mdcontain 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-projectheaders (set inproxy.go:107). - 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"inserver.go:126. Update bothmain.go:12andserver.go:126when bumping. - Upstream timeout:
proxy.go:167sets 10-minute HTTP client timeout — adjust if Zen models are slow on first call. - Unknown models are forwarded as-is (
models.go:140) — useful when new models appear upstream before code is updated. - Reasoning models (DeepSeek, Nemotron) need
max_tokens≥ 500 — first tokens go to reasoning, not visible content.