A tiny local gateway that makes OpenCode Zen's free models (glm-5.3-flash-free,
deepseek-v4.1-flash-free, …) usable from any OpenAI Chat Completions–compatible client.
No dependencies. Just Node.js ≥ 18.
中文说明见 README.zh-CN.md
OpenCode Zen serves its free models only to requests that look like the real OpenCode client — anything else gets:
403 FreeTierError: OpenCode's free tier can only be used from within OpenCode.
A static API key, an OAuth login, or a different User-Agent alone does not help.
Reverse-engineering the gate (by capturing the real opencode CLI and reading
magpie's source) shows a free-tier request must satisfy
all of the following:
| # | Requirement | Detail |
|---|---|---|
| 1 | OpenCode client headers | User-Agent: opencode/<version> ai-sdk/… runtime/bun/…, x-opencode-session, x-opencode-session-id, x-opencode-request, x-opencode-client: cli, x-opencode-project: global — and no x-session-affinity |
| 2 | Well-formed IDs | Session/message IDs must follow opencode's descending() layout: 26 chars = 12 hex chars of ~(ms<<12) + 14 random chars |
| 3 | Streaming + bash/read tools |
stream: true is mandatory; the tools array must offer lowercase bash and read with non-empty parameter schemas |
| 4 | The right URL | Free models must be called at https://opencode.ai/inference/v1/... (since mid-October 2026, /zen/v1 returns 403 for free models) |
zen-pool does all of this transparently, so your client doesn't have to.
- Accepts OpenAI Chat Completions requests on
http://127.0.0.1:3425/v1. - Injects the OpenCode identity headers and generates valid IDs per request.
- Offers the client's
Bash/Readtools to Zen under lowercasebash/read(or adds harmless stubs when absent) and maps tool-call names back in the reply. - Streams through to
/inference/v1for free models (*-free, plusbig-pickleandgrok-code), and to/zen/v1for everything else. - Aggregates the upstream stream into a single JSON reply for non-streaming clients.
- Multi-account rotation: on
401/402/quota-style errors it moves to the next API key / OAuth account automatically.
git clone https://github.com/<you>/zen-pool.git
cd zen-pool
node zen-pool.mjs serve # listens on 127.0.0.1:3425Then point any OpenAI-compatible client at it:
- Base URL:
http://127.0.0.1:3425/v1 - API key: anything (e.g.
local) — the gateway supplies the real credential - Model:
glm-5.3-flash-free,deepseek-v4.1-flash-free,cat-jam-free, …
No account or API key is required for free models — zen-pool asks Zen
anonymously (Bearer public) in OpenCode's shape.
curl http://127.0.0.1:3425/v1/chat/completions \
-H "content-type: application/json" \
-d '{
"model": "glm-5.3-flash-free",
"messages": [
{"role": "system", "content": "You are a helpful coding agent."},
{"role": "user", "content": "Say hello in one sentence."}
]
}'Tip: a
systemmessage is not required by the gateway, but agents normally send one — keep it, it makes the request look natural.
- Start the gateway:
node zen-pool.mjs serve - In ZCode: Settings → Model Providers → add a custom provider
- API type:
OpenAI Chat Completions - Base URL:
http://127.0.0.1:3425/v1 - API key: anything (e.g.
local) - Add the model IDs you want (
glm-5.3-flash-free, …)
- API type:
- Restart ZCode and pick the model.
(The equivalent for ZCode's config file lives at <zcode-data-dir>/v2/provider_config.json
— an opencode-zen-chat provider entry with api.baseUrl set to the gateway.)
In ~/.codex/config.toml:
[model_providers.zen-pool]
name = "zen-pool"
base_url = "http://127.0.0.1:3425/v1"
wire_api = "chat"
[profiles.free]
model = "glm-5.3-flash-free"
model_provider = "zen-pool"Then codex --profile free.
Free models need no account. If you also want paid models through the gateway, or want multiple free accounts pooled, provide credentials:
# Option A — API keys (paid models). Copy the example and fill in your keys:
cp keys.example.json keys.json
# Get keys at https://opencode.ai/auth
# Option B — OAuth device login (repeat per account):
node zen-pool.mjs login --name work
node zen-pool.mjs login --name personal
# Inspect state:
node zen-pool.mjs status
curl http://127.0.0.1:3425/zen-pool/statusRotation rules: free models try anonymous first, then fall back to every
configured account/key; paid models rotate through keys and accounts on
401/402/quota errors. Every response carries an x-zen-pool-account header
saying which account served it. OAuth tokens are refreshed automatically and
persisted in accounts.json.
- Windows:
Win+R→shell:startup→ put a shortcut tonode <path>\zen-pool\zen-pool.mjs servethere. - macOS/Linux: a
systemd --userunit or a launchd plist runningnode /path/to/zen-pool/zen-pool.mjs serve.
- The upstream free pool occasionally answers
429 Endpoint is unavailable(capacity, not your account) — just retry. - Free-tier data policy is permissive (prompts may be retained / used for training). Don't send sensitive code to free models.
keys.jsonandaccounts.jsoncontain plaintext credentials — they are gitignored; keep it that way.- The gateway binds
127.0.0.1only. - The free-tier gate is a server-side rule and can change any time. If you see
FreeTierErroragain, check the constants at the top ofzen-pool.mjsagainst magpie'sinternal/provider/opencode_client.go.
- Captured the real
opencodeCLI with a local HTTP recorder (it sendsBearer publicwhen nobody is signed in). - Compared every captured header/body field against a minimal working request
to isolate the required set (headers → ID layout →
stream→bash/readtools →/inference/v1URL). - Cross-checked with magpie's MIT-licensed source, which documents the same
rules in
internal/gateway/zenfree.go.
- opencode — the CLI whose request shape is emulated.
- magpie by yetone (MIT) — prior art for this exact integration; several constants and the tool-renaming strategy follow it.
MIT — see LICENSE.