Anthropic-compatible API gateway proxying to ZCode's GLM backend with automatic captcha solving.
Project description
zcode-proxy
An Anthropic-compatible API gateway that proxies requests to ZCode's
zcode-plan GLM backend (GLM-5.2 / GLM-5-Turbo). It auto-generates Aliyun
captcha tokens in the background via CloakBrowser, exposes /v1/messages,
/v1/models, /v1/usage, and /health endpoints, and transparently handles
authentication, rate limits, retries, and quota reporting.
Point any Anthropic-compatible client (Claude Code, opencode, Cherry Studio, LibreChat, …) at it and talk to GLM models instead.
Install
pipx install zcode-proxy # install once
pipx upgrade zcode-proxy # update to latest
Requires Python ≥ 3.13. CloakBrowser downloads its own Chromium binary
(~700 MB) into ~/.cloakbrowser/ on first run — no system Chrome needed.
Quick start
# Start the proxy — a browser window opens for ZCode login on first launch
zcode-proxy
The server boots on http://127.0.0.1:8787. On first run CloakBrowser
downloads Chromium (see Install); once every captcha worker prints READY,
the proxy is live.
Point a client at it
The proxy exposes a standard Anthropic Messages API. Any client that supports
a custom base_url can connect — the proxy ignores the API key; authentication
is via the JWT obtained at startup.
Environment variables
ANTHROPIC_BASE_URL=http://127.0.0.1:8787
ANTHROPIC_API_KEY=anything
ANTHROPIC_MODEL=GLM-5.2
ANTHROPIC_BASE_URL— Do not include/v1; clients append it automatically.ANTHROPIC_API_KEY— Required by most SDKs/clients, but the proxy does not validate it. Any non-empty value works.ANTHROPIC_MODEL— Override the default model (must match a registry ID or alias from/v1/models).
SDK examples
Python (anthropic):
from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787", api_key="skip")
msg = client.messages.create(
max_tokens=256, model="GLM-5-Turbo",
messages=[{"role": "user", "content": "Hello"}],
)
print(msg.content)
TypeScript (@anthropic-ai/sdk):
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "http://127.0.0.1:8787", apiKey: "skip" });
const msg = await client.messages.create({
max_tokens: 256, model: "GLM-5-Turbo",
messages: [{ role: "user", content: "Hello" }],
});
console.log(msg.content);
Client configuration
| Client | Base URL setting | API key setting | Model setting | Notes |
|---|---|---|---|---|
| Claude Code | ANTHROPIC_BASE_URL env or settings.json → env |
ANTHROPIC_API_KEY env |
CLAUDE_MODEL env or --model flag |
Restart required after env change. |
| opencode | opencode.json → provider.anthropic.options.baseURL |
provider.anthropic.options.apiKey (supports {env:VAR}) |
provider.anthropic.models[] |
— |
| LibreChat | ANTHROPIC_REVERSE_PROXY env or librechat.yaml → endpoints[].baseURL |
ANTHROPIC_API_KEY env |
ANTHROPIC_MODELS env |
Set directEndpoint: false. |
| Cline | GUI: check "Use custom base URL" → enter URL | GUI: "Anthropic API Key" | GUI: dropdown | — |
| Continue | config.yaml → apiBase |
config.yaml → apiKey |
config.yaml → model |
Set provider: anthropic. |
| Cherry Studio | GUI: "API Address" field | GUI: "API Key" field | GUI: manually add model IDs | Provider type: Anthropic. |
| Aider | ANTHROPIC_BASE_URL env (via --set-env) |
--anthropic-api-key or ANTHROPIC_API_KEY env |
--model flag |
— |
Features
- Streaming & non-streaming — full SSE pass-through; auto-assembles SSE into a
single JSON response when
stream: false. - Automatic captcha solving — background worker pool generates Aliyun captcha tokens via CloakBrowser to minimize upstream rejections.
- JWT authentication — interactive OAuth/PKCE login in the browser, or
--authfor non-interactive use; automatic re-login on expiry. - Tool-schema compression — strips redundant JSON-Schema fields, collapses
anyOfnull unions, and rewrites tool descriptions to reduce token spend. - Quota / billing awareness — polls the ZCode billing API, surfaces Anthropic
rate-limit headers, and returns proper
429/retry-afterwhen exhausted. - Resilient — retries on upstream
1305overloaded (streaming and non-streaming), empty streams, auth-code relogin, and connection errors. - Request normalization — normalizes string content to array format, strips null values, and auto-injects thinking/effort parameters from per-model config.
- Concurrency control —
upstream.concurrencygates parallel upstream requests via a semaphore.
Configuration
zcode-proxy [--config PATH] [--port PORT] [--host HOST] [--auth JWT]
| Flag | Description |
|---|---|
--config |
Path to a TOML config file (default: bundled config). |
--port |
Override the port from config. |
--host |
Override the host from config. |
--auth |
Pass a JWT token (starting with eyJ) to skip interactive login. |
The proxy ships with a bundled zcode_proxy.toml config — every setting is
defined there with no code-level defaults. You can override it with
--config /path/to/your.toml — see the default config for a full reference
(it is installed at site-packages/zcode_proxy/zcode_proxy.toml).
| Section | Key highlights |
|---|---|
[app] |
Version, name, agent, referer, x_title headers sent upstream. |
[server] |
host / port to bind. |
[upstream] |
ZCode API URL, connect timeout, read timeout, concurrency, retry count/delay, HTTP/2 toggle. |
[billing] |
Quota endpoint + timeout + refresh cadence + usage window. |
[captcha] |
Token pool size, worker count, TTL, token timeout, refill interval, ready/shutdown timeouts, worker path. |
[models] |
Model registry (display name, token limits, capabilities) + aliases + per-model thinking budgets and effort. |
[oauth] |
PKCE OAuth endpoints, client_id, app_id, redirect_uri, timeout. |
[compress] |
Tool-schema compression toggles (see "Tool compression" below). |
[errors] |
Map of ZCode error codes → Anthropic error type + HTTP status. |
Authentication
The proxy authenticates to ZCode with a JWT. On first run (or whenever the
cached JWT has expired) it opens a browser to the ZCode OAuth authorize URL;
after you log in, ZCode redirects to http://localhost:19876/callback —
copy that entire callback URL from the browser address bar and paste it
back into the terminal. You can also paste a JWT token directly if you
have one.
If the JWT expires while the proxy is running, it automatically re-runs the login flow on the next auth-failed request.
How captcha works
ZCode's upstream rejects any request whose X-Aliyun-Captcha-Verify-Param
header is missing or stale. The proxy keeps a warm pool of solved tokens:
CaptchaPoolspawns N worker subprocesses (captcha.workers), each runningcaptcha_worker.py. A separatecaptcha.pool_sizecontrols how many pre-warmed tokens are kept ready.- Each worker loads a tiny HTML page that imports Aliyun's captcha SDK via
CloakBrowser's stealth Chromium, prints
READY, then loops on stdin. - A background
refill_loop(on acaptcha.refill_intervalcadence) keeps the token pool topped up; each request grabs a fresh token from the pool (or asks a worker synchronously with acaptcha.token_timeoutlimit). - Tokens expire after
captcha.ttlseconds.
On a fresh machine the first launch will take a minute or two while CloakBrowser downloads Chromium (see Install) — subsequent starts are instant.
Tool compression
When compress.enabled = true (as set in the shipped config), the proxy
rewrites the tools array of every incoming request before forwarding it
upstream, to cut token usage. Each sub-feature is an independent toggle —
all are defined in the config:
strip_schema_keys— strips$schemaandadditionalProperties.strip_defaults— stripsdefaultvalues.strip_format— stripsformatfields.strip_pattern— stripspatternfields.strip_dummy_bounds— stripsminimum/maximum/exclusiveMinimum/exclusiveMaximumwith JS sentinel values (9007199254740991), plus heuristicminItems/maxItemsremoval (see note below).strip_minmax_length— stripsminLength/maxLength(off by default in shipped config; these are often meaningful constraints).collapse_anyof_null— collapsesanyOf: [T, {type: "null"}]intoTwith a nullable type array (["string", "null"]).description_mode—"imperative"replaces descriptions for 14 known tools with one-line summaries;"first_sentence"truncates to the first sentence;"off"leaves descriptions untouched. Tools not in the imperative dictionary are left as-is in"imperative"mode.self_naming_no_desc— tools whose names are self-explanatory (e.g.Read,Write,Edit,Bash) have their descriptions removed entirely rather than replaced/truncated. In"imperative"mode, this only applies to tools that are also in the imperative dictionary; in"first_sentence"mode, it applies to all listed tools.defer_tools— removes tools from the request entirely so the upstream never sees them (e.g.EnterPlanMode,ExitPlanMode).
Note:
strip_dummy_boundsuses a heuristic forminItems/maxItems—minItemsof 0, andmaxItemsvalues ≥ 65536 are treated as dummy values and stripped. If a schema usesmaxItems: 65536or higher as a real constraint, setstrip_dummy_bounds = falseto preserve them.
A per-request log line shows the savings, e.g.
compress=42.3%(18→16 tools, 12,451B saved).
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /v1/messages |
Anthropic Messages API (streaming + non-streaming). |
| GET | /v1/models |
List of registered models with capabilities. |
| GET | /v1/usage |
Current per-model quota / remaining / reset time. |
| GET | /health |
Liveness probe. |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
captcha generation failed (500) |
Captcha workers couldn't produce a token in time. | On a fresh machine, CloakBrowser may still be downloading Chromium (see Install). If it persists, run python3 -m zcode_proxy.captcha_worker — it should print READY; any stderr output is the root cause (Ctrl+C to exit). |
Workers never print READY |
CloakBrowser's Chromium download is in progress, or a worker crashed during page load. | Check stderr output from the worker process. Retry after the download completes. |
authentication failed after relogin (401) |
The JWT could not be refreshed — your ZCode session may have been revoked. | Restart the proxy and complete the browser login flow again. |
exceed quota limit (429) |
Per-model quota is exhausted. | The response includes a retry-after header with the wait time. Check /v1/usage for remaining tokens and reset time. |
upstream overloaded after retries (529) |
The ZCode backend is saturated and all retry attempts failed. | The proxy retries automatically (see upstream.retries). Raise it in the config or wait and retry. |
model not found: <name> (404) |
The requested model name isn't in the registry. | Check /v1/models for valid IDs. Aliases like glm5.2 and glm-5.2 are also accepted. |
invalid JSON body (400) |
The request body couldn't be parsed. | Ensure Content-Type: application/json is set and the body is valid JSON. |
Content-Type must be application/json (400) |
The request is missing the required content-type header. | Add Content-Type: application/json to the request. |
model is required (400) |
The request is missing the model field. |
Include a valid model name in the request body. |
messages: at least one message is required (400) |
The messages field is missing or empty. |
Include at least one message in the request body. |
| Port already in use | Another process is bound to the same port. | Override at launch: zcode-proxy --port 9000. |
| HTTP/2 connection errors | Some networks/proxies don't support HTTP/2. | Set http2 = false under [upstream] in the config. |
| Login browser doesn't open | The webbrowser call failed. |
The proxy prints the authorize URL to stderr. Copy it manually into a browser, then paste the callback URL back into the terminal. |
State mismatch on login |
You pasted a callback URL from a previous login attempt (stale state param). |
Start a fresh login — do not reuse old URLs. |
Token exchange failed |
The authorization code expired or was already used. | Complete a fresh login flow. |
| Client disconnects mid-stream | The downstream client closed the connection. | The proxy logs ← client disconnected before stream start or ← connection closed during stream and closes the stream gracefully — no action needed. |
upstream connection failed (504) |
Network issue reaching ZCode. | Check connectivity; the proxy retries on ReadError/ConnectError/ReadTimeout up to upstream.retries times. |
Disclaimer
This project is not affiliated with, authorized, maintained, sponsored, or endorsed by Z.AI or any of its affiliates or subsidiaries. It is an independent and unofficial tool that interacts with ZCode's services via unofficial interfaces.
- This project may violate Z.AI's Terms of Service. Use may result in account suspension or termination.
- The proxy relies on reverse-engineered behavior that may break at any time if Z.AI changes their upstream API or captcha flow.
- No guarantees on stability, compatibility, or long-term support.
- The authors take zero liability for any consequences arising from the use of this software, including but not limited to account actions, data loss, or legal issues.
By using this software, you acknowledge these risks and accept full responsibility. Use at your own risk and ensure you comply with all applicable terms of service and laws.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zcode_proxy-3.4.0.tar.gz.
File metadata
- Download URL: zcode_proxy-3.4.0.tar.gz
- Upload date:
- Size: 28.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d699340cf6ce2e264c6214bf1be2850f05e570a32a2c87ca8162584fa297c914
|
|
| MD5 |
caa3c47eaeddd93b9172b2732759d0e4
|
|
| BLAKE2b-256 |
286fa71617f7ba550f07bd6ff1c55595387bb0bd1e266616dd06c0fddf9d5ed4
|
File details
Details for the file zcode_proxy-3.4.0-py3-none-any.whl.
File metadata
- Download URL: zcode_proxy-3.4.0-py3-none-any.whl
- Upload date:
- Size: 23.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0e9b40c48f86783f6140fe19fc07cb299d94e99d8e12a9df02bc7adf7557b51
|
|
| MD5 |
903c3fa37b2bff2f4d7f93639c4a4068
|
|
| BLAKE2b-256 |
879f76cd1a28ef64007216fa20f694470e7b8a5aee12a0e3bad5fc1ce04a838d
|