中文 | Website | Documentation
Chat2API is a cross-platform desktop app and headless server that turns web-based AI provider accounts into one local, OpenAI-compatible gateway. Configure providers and accounts once, then connect the same endpoint to OpenAI SDKs, coding agents, desktop clients, or internal tools.
- OpenAI-compatible gateway: Chat Completions at
/v1/chat/completions, Responses at/v1/responses, legacy Completions at/v1/completions, model listing, streaming SSE, API-key authentication, and multimodal message handling. Gemini-compatible generation and file routes are also available under/v1beta. - Provider and account management: Add multiple accounts per provider, validate credentials, map client model names, pin a model to a provider or account, and choose round-robin, fill-first, or failover routing.
- Tool and reasoning compatibility: Function/custom tool calls, tool-result continuations, reasoning content, web search, deep research, and provider-specific thinking modes are normalized where the upstream service supports them.
- Long-running request controls: Context compaction, request and stream deadlines, queue admission, keep-alives, bounded retries, and Qwen session/response recovery.
- Desktop and server deployments: Use the Electron UI on macOS, Windows, or Linux, or run the Koa proxy and browser admin UI in Docker without Electron.
- Operations UI: Dashboard metrics, request logs, model synchronization, API keys, proxy settings, themes, system tray access, and English/Simplified Chinese localization.
- Client bridges: Codex CLI Responses compatibility.
The built-in catalogue currently includes:
| Provider | Authentication | Built-in models |
|---|---|---|
| DeepSeek | User token | deepseek-v4-flash, deepseek-v4-pro |
| GLM | Refresh token | GLM-5.1 |
| Kimi | JWT / web token | Kimi-K2.6, Kimi-K3 |
| MiniMax | JWT | MiniMax-M2.7 |
| Mimo | Browser cookies | MiMo-V2.5-Pro, MiMo-V2.5, MiMo-V2-Flash |
| Microsoft 365 Copilot | OAuth refresh token (in-app browser login) | gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna |
| Perplexity | Session cookie | Auto |
| Qwen (China) | SSO ticket | Qwen3.6, Qwen3.7-Max, Qwen3.5-Flash, Qwen3-Max, Qwen3-Max-Thinking-Preview, Qwen3-Coder |
| Qwen AI (International) | JWT, optional cookies and login credentials | Qwen3.8-Max, Qwen3.8-Max_Fast, Qwen3.8-Max_Auto, Qwen3.8-Max_Thinking, Qwen3.7-Plus, Qwen3.7-Max |
| Z.ai | JWT | GLM-5.1, GLM-5-Turbo, GLM-5V-Turbo, GLM-5, GLM-4.7 |
Provider availability and model names follow the upstream web applications and may change. See the provider notes for credential and model-mapping details.
Download a platform package from GitHub Releases when a release is available. The source mirror is pyf-feifei/Chat2API.
| Platform | Package |
|---|---|
| macOS Apple Silicon | Chat2API-<version>-mac-arm64.dmg |
| macOS Intel | Chat2API-<version>-mac-x64.dmg |
| Windows | Chat2API-<version>-x64-setup.exe or portable build |
| Linux | Chat2API-<version>-x64.AppImage, .deb, or .tar.gz |
Requirements: Node.js 18+, npm, and Git. The Docker image uses Node.js 22.
git clone https://github.com/pyf-feifei/Chat2API.git
cd Chat2API
npm install
npm run dev:win # Windows
npm run dev # macOS/LinuxProduction packages can be built with:
npm run build
npm run build:mac
npm run build:win
npm run build:linux
npm run build:allThe server image runs the Koa proxy and browser admin UI, stores state in /data, and listens on port 8080 by default:
docker build -t chat2api:server .
docker run -d --name chat2api \
-p 8080:8080 \
-v chat2api-data:/data \
-e CHAT2API_HOST=0.0.0.0 \
-e CHAT2API_PORT=8080 \
-e CHAT2API_ENABLE_MANAGEMENT_API=true \
-e CHAT2API_MANAGEMENT_SECRET=change-me \
chat2api:serverOpen http://localhost:8080/admin/ and use the management secret to sign in. The complete Docker guide covers Compose, browser-assisted account import, storage encryption, Qwen session repair, and deployment tuning.
- Launch Chat2API, or start the Docker server.
- Open Providers, add a built-in provider, and enter its web credential. Credentials are stored locally; never commit them to source control.
- Open Proxy Settings, choose a port and routing strategy, then start the proxy.
- Point an OpenAI-compatible client at
http://127.0.0.1:8080/v1.
Example with the OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(
api_key="your-chat2api-key",
base_url="http://127.0.0.1:8080/v1",
)
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": "Hello from Chat2API"}],
)
print(response.choices[0].message.content)For Codex CLI, use the Responses endpoint and the configuration in docs/codex.md.
Chat2API must reach provider APIs over your real network path, not through a local HTTP/SOCKS proxy. If it does not, an entire class of upstream failures appears that looks like an account or content problem but is really an egress problem.
This is not hypothetical. On 2026-09-25 a Windows host with Clash Verge and
HTTP_PROXY/HTTPS_PROXY=http://127.0.0.1:7897 sent every provider request
through a hysteria2 node hosted on 195.242.178.82 — the same machine running
the production deployment. Qwen therefore saw a shared US-datacenter egress
instead of the residential IP, and Aliyun WAF answered with bxpunish /
RGV587 risk verdicts (qwen_ai_content_verdict, "egress-IP flag"). The home
IP was never banned; it was simply never used.
src/main/proxy/egressPolicy.ts runs before any network module in both the
Electron main process and the headless server, and appends the provider domains
to NO_PROXY/no_proxy. It works because proxy-from-env — the resolver axios
uses — reads process.env on every request, so the change takes effect
immediately for axios instances that already exist. You do not need to
configure anything.
Domains kept direct by default: .qwen.ai, .qianwen.com, .aliyuncs.com,
.alibabacloud.com, .alicdn.com, plus localhost and 127.0.0.1.
| Variable | Effect |
|---|---|
CHAT2API_EGRESS_DIRECT=off |
Disable the policy (route provider traffic through the proxy again) |
CHAT2API_EGRESS_DIRECT=a.com,b.com |
Replace the built-in list |
CHAT2API_EGRESS_DIRECT_EXTRA=c.com |
Append to the built-in list |
# What your app will use after the policy runs
node -e "const p=require('proxy-from-env');console.log(p.getProxyForUrl('https://chat.qwen.ai/api/v1/chat')||'DIRECT')"
# What the network really is, with every proxy bypassed
curl -s --noproxy '*' https://myip.ipip.netDIRECT on the first command is expected. An IP echo service only reports the
path taken to that service: behind a rule-based proxy such as Clash,
ipinfo.io (MATCH,PROXY) or api.ipify.org (a proxy rule set) show the proxy
node even when every provider domain goes direct. That reading has sent
diagnoses the wrong way more than once. To see what a provider domain uses,
check which rule Clash applied to it (Clash Verge → Connections), or compare an
echo service on the same rule path as the provider (for CN providers,
myip.ipip.net goes DIRECT). A datacenter AS on the --noproxy command means something below
Chat2API proxies everything (AS4837 is fine — that is a residential carrier).
The app-level policy does not cover your browser. If the same account is used in
the browser and through Chat2API on different egresses, the upstream sees the
account "hop" between IPs, which looks like a stolen account. Add the same
domains as DIRECT rules in the profile enhancement before MATCH,PROXY:
prepend:
- DOMAIN-SUFFIX,qwen.ai,DIRECT
- DOMAIN-SUFFIX,qianwen.com,DIRECT
- DOMAIN-SUFFIX,aliyuncs.com,DIRECT
- DOMAIN-SUFFIX,alibabacloud.com,DIRECT
- DOMAIN-SUFFIX,alicdn.com,DIRECTEdit the profile's rules enhancement (profiles/<uid>.yaml), not the
subscription body — the subscription is regenerated on every update and your
edits are lost. The mihomo core runs as a Windows service and cannot be killed
from an unprivileged shell, so reload the profile in the UI.
Docker note: Docker Desktop routes container traffic through the Windows system proxy (
http.docker.internal:3128) even when the container has noHTTP_PROXY, soNO_PROXYinside the container does not help. The traffic then does enter Clash, and these rules decide per domain. Measured 2026-09-30 from inside the container with the system proxy on: the provider domains above went DIRECT via the residential IP, while foreign domains matched proxy rules and went through the node. Rules only apply to the domains they list, so make sure every domain a provider page loads is covered (Z.ai, for example, also loadschatglm.cn; confirm it in Connections). See the next section for the alternative of taking Docker off the system proxy entirely.
Full setup, verification and troubleshooting: docs/network-egress.md.
If every request from a container returns an aliyun WAF challenge
(aliyun_waf_aa) while the same account works in your browser, the cause is
almost always Docker Desktop's proxy, not the provider:
docker info | grep -A2 "^ *Proxy" # prints http.docker.internal:3128 ?If it does, Docker routes all container traffic through that proxy, i.e. into
Clash. Each domain then exits wherever its Clash rule sends it: DIRECT rules
keep the residential IP, anything that falls to MATCH,PROXY exits via the
node. So first make sure every provider domain has a DIRECT rule (see above).
To take Clash out of the container path entirely, turn the
Windows system proxy off and set Docker Desktop's proxy mode to manual with
no address (stored in %APPDATA%\Docker\marlin.dat as
"proxyHTTPMode":{...,"Value":"system"} → "manual"), then restarting Docker
Desktop. Nothing set inside the container can override it — not HTTP_PROXY,
not NO_PROXY=*, not --network host.
See docs/network-egress.md §8.5 for the full procedure, plus the DNS-poisoning case that makes every Webshare key look invalid.
Providers rate-limit by egress IP, not by account. A pool of ~340 accounts driven from one IP — especially a shared datacenter one — is an anomaly shape regardless of how the accounts were obtained. Keep the full pool on the production server, and use a single account locally for functional checks.
Chat2API enforces this automatically once a verdict appears. A bxpunish /
RGV587 verdict is decided by the egress path, not by one payload, so the
per-request risk circuit is joined by a process-wide egress circuit: after
CHAT2API_QWEN_AI_EGRESS_CIRCUIT_THRESHOLD distinct payloads (default 12) are
rejected inside CHAT2API_QWEN_AI_EGRESS_CIRCUIT_WINDOW_MS (default 5 min), all
new Qwen AI traffic is refused with 503 qwen_ai_risk_circuit_open and a
Retry-After header before another account is consumed. One accepted upstream
response closes it again, so a fixed route recovers immediately instead of
waiting out the cooldown.
| Variable | Default | Effect |
|---|---|---|
CHAT2API_QWEN_AI_EGRESS_CIRCUIT_THRESHOLD |
12 |
Distinct payloads that must be rejected before the egress is parked; 0 parks on the first verdict |
CHAT2API_QWEN_AI_EGRESS_CIRCUIT_COOLDOWN_MS |
180000 |
How long the egress stays parked |
CHAT2API_QWEN_AI_EGRESS_CIRCUIT_WINDOW_MS |
300000 |
Window in which verdicts are counted |
A verdict is not always about the egress. If one request fingerprint is
rejected on every account while other requests succeed, the upstream is judging
that transcript; changing accounts or IPs will not help. See AGENTS.md
("A content verdict is not always an egress or account problem").
Account credentials are encrypted at rest. The key must be identical across every instance that shares the same data file, and it must actually reach the process.
CHAT2API_STORAGE_ENCRYPTION_KEY=change-this-to-a-long-random-secretPick a value once, keep it in .env, and use the same value for the desktop app
and every Docker deployment of the same store.
# confirm the key reached the process
docker exec chat2api printenv CHAT2API_STORAGE_ENCRYPTION_KEYStart containers with
docker compose up -d, neverdocker run. A container created withdocker runhas no compose label, sodocker compose uprefuses to manage it and.envis never injected — the process silently runs without the key.
Nothing throws. The runtime returns the c2a:v1:… ciphertext unchanged, so every
account is treated as having no session, the repair queue signs in 339 accounts
every 25 s, the upstream answers 401 email not found, a rejection storm opens
the refresh risk gate (300 s → 600 s → 1200 s → 2400 s), and every request,
including plain chat, fails 403 qwen_ai_token_refresh_gated. It presents as a
risk-control outage; it is a configuration error.
A startup self-check now catches it:
[CredentialSelfCheck] Credential data is encrypted (c2a:v1:…) but encryption is
not available. … Set CHAT2API_STORAGE_ENCRYPTION_KEY … and recreate the instance
so the variable actually reaches the process (a container started with `docker run`
never reads .env).
[Store] Initialization aborted: Credential data is encrypted but
CHAT2API_STORAGE_ENCRYPTION_KEY is not usable
Fix it and recreate the container (environment variables are read once at
process start; docker restart is not enough):
docker compose up -d --force-recreateFastest way to tell this apart from a real risk-control block — compare the session-repair line between environments:
broken: [QwenAI Session Repair] started ready=0 pending=339
healthy: [QwenAI Session Repair] started ready=339 pending=0
ready=0 pending=339 means the credentials are unreadable, not that the upstream
is blocking you. Bypass the check with CHAT2API_CREDENTIAL_SELF_CHECK=off only
for a genuinely plaintext store.
Running the desktop app and the Docker server on the same machine against the same account pool needs a little care.
| Concern | Desktop (workstation) | Docker (production) |
|---|---|---|
| Recommended pool size | 1–3 accounts, functional checks | Full pool |
| Egress | Residential IP, no proxy | Fixed server IP, no proxy |
| Never do | Drive the production pool, or run a load test from here | — |
- Do not point a local instance and the production container at the same
data.json//datavolume while both are running. They will overwrite each other'sstatus/errorMessagefields and each one's repair queue will fight the other's verdicts. - Do not run load or soak tests from a workstation. Upstream rate limiting is per egress IP, so a local load test degrades the production pool rather than measuring the code.
- Do not edit a data volume while the container is running. Stop it, edit,
then start it; otherwise the in-memory state overwrites your change on the next
save. Back up first:
docker exec chat2api cat /data/data.json > data.json.backup
- The desktop app applies the direct-egress policy automatically. If you run the
Docker image on a host that also has a proxy configured, the headless server
applies the same policy; set
CHAT2API_EGRESS_DIRECT=offonly if you deliberately want the proxy in that path. On a Docker Desktop host the container reaches Clash through the VM-level proxy, so either keep the provider domains in ClashDIRECTrules or take Docker off the system proxy as described above.
Two failure modes that look identical but are unrelated — see docs/network-egress.md:
| Symptom | Root cause |
|---|---|
| Egress shows a datacenter AS | Local proxy inherited by Docker Desktop |
| Every request 403, accounts "frozen" | Missing/mismatched storage encryption key |
See also the post-mortem for the 2026-09-25 pool outage: docs/diag-2026-09-25-qwen-egress.md.
| Dashboard | Providers |
|---|---|
![]() |
![]() |
| Proxy settings | API keys |
|---|---|
![]() |
![]() |
| Models | Sessions |
|---|---|
![]() |
![]() |
Desktop data is stored in ~/.chat2api/; Docker data is stored in the mounted /data volume.
| Path | Contents |
|---|---|
data.json |
Everything: providers, accounts (credentials encrypted), config, sessions, statistics |
qwen-ai-file-cache.json, compression-archive.json |
Provider upload cache and context-compression archive |
logs/, request-logs/, responses/ |
App logs, request logs, stored Responses state |
The server uses CHAT2API_DATA_DIR when set.
The server supports environment variables for host/port, management API, API keys, storage encryption, load balancing, request deadlines, and provider-specific controls. Start with the examples in docs/docker.md.
Issues, provider updates, tests, and documentation improvements are welcome. Please read the existing provider notes and open an issue before large adapter changes.
npm install
npm run build
npm run build:serverThe test suite (tests/) is kept out of the repository and exists only in
maintainers' working copies, so npm run test:* scripts need a local tests/
directory.
Chat2API is released under the GNU General Public License v3.0.
Electron, React, TypeScript, Tailwind CSS, Zustand, and Koa.






