Skip to main content

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_URLDo 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.jsonenv ANTHROPIC_API_KEY env CLAUDE_MODEL env or --model flag Restart required after env change.
opencode opencode.jsonprovider.anthropic.options.baseURL provider.anthropic.options.apiKey (supports {env:VAR}) provider.anthropic.models[]
LibreChat ANTHROPIC_REVERSE_PROXY env or librechat.yamlendpoints[].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.yamlapiBase config.yamlapiKey config.yamlmodel 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. Defaults to non-streaming (matching the Anthropic API spec).
  • 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 --auth for non-interactive use; automatic re-login on expiry.
  • Tool-schema compression — strips redundant JSON-Schema fields, collapses anyOf null 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-after when exhausted.
  • Resilient — retries on upstream overloaded errors (configurable via [errors]), 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 controlupstream.concurrency gates 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 (except upstream.jwt, which is acquired at runtime via login). 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, max_items_threshold (see "Tool compression" below).
[errors] Map of ZCode error codes → Anthropic error type + HTTP status, plus default fallback.

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/callbackcopy 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:

  1. CaptchaPool spawns N worker subprocesses (captcha.workers), each running captcha_worker.py. A separate captcha.pool_size controls how many pre-warmed tokens are kept ready.
  2. Each worker loads a tiny HTML page that imports Aliyun's captcha SDK via CloakBrowser's stealth Chromium, prints READY, then loops on stdin.
  3. A background refill_loop (on a captcha.refill_interval cadence) keeps the token pool topped up; each request grabs a fresh token from the pool (or asks a worker synchronously with a captcha.token_timeout limit).
  4. Tokens expire after captcha.ttl seconds.

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 $schema and additionalProperties.
  • strip_defaults — strips default values.
  • strip_format — strips format fields.
  • strip_pattern — strips pattern fields.
  • strip_dummy_bounds — strips minimum/maximum/exclusiveMinimum/exclusiveMaximum with JS sentinel values (9007199254740991), plus heuristic minItems/maxItems removal (see note below).
  • strip_minmax_length — strips minLength/maxLength (off by default in shipped config; these are often meaningful constraints).
  • collapse_anyof_null — collapses anyOf: [T, {type: "null"}] into T with 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_bounds uses a heuristic for minItems/maxItemsminItems of 0, and maxItems values ≥ the max_items_threshold config value (default: 65536) are treated as dummy values and stripped. If a schema uses a high maxItems as a real constraint, set strip_dummy_bounds = false to 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zcode_proxy-3.5.0.tar.gz (29.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zcode_proxy-3.5.0-py3-none-any.whl (23.5 kB view details)

Uploaded Python 3

File details

Details for the file zcode_proxy-3.5.0.tar.gz.

File metadata

  • Download URL: zcode_proxy-3.5.0.tar.gz
  • Upload date:
  • Size: 29.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for zcode_proxy-3.5.0.tar.gz
Algorithm Hash digest
SHA256 977002040a580c251f241f5e5103642051cb15342a785a324c7a14300aeeb2b8
MD5 078a930978de9ca587c4532d2813cd75
BLAKE2b-256 8e7fcc59a561b068e8374e860f5631c547ad1735a256fe4175b81f5f395bcbd8

See more details on using hashes here.

File details

Details for the file zcode_proxy-3.5.0-py3-none-any.whl.

File metadata

  • Download URL: zcode_proxy-3.5.0-py3-none-any.whl
  • Upload date:
  • Size: 23.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for zcode_proxy-3.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd53cddb81be95ed2bccb9a04402c329cefabef1f1d0fa21a270126a0713fa70
MD5 ca99978340e9320eeed44237466a29ae
BLAKE2b-256 8146788d17b66ac728337700fad35e05f565d4932cb93e8892805e4c1dc6ce5c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page