Skip to content
 
 

Latest commit

 

History

282 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chat2API

Chat2API logo

Version 1.4.0 GPL-3.0 license Electron 33+ React 18 TypeScript 5 macOS, Windows and Linux

中文 | 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.

Chat2API dashboard

Highlights

  • 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.

Supported providers

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.

Install

Desktop release

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

Build from source

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/Linux

Production packages can be built with:

npm run build
npm run build:mac
npm run build:win
npm run build:linux
npm run build:all

Docker server

The 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:server

Open 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.

Quick start

  1. Launch Chat2API, or start the Docker server.
  2. Open Providers, add a built-in provider, and enter its web credential. Credentials are stored locally; never commit them to source control.
  3. Open Proxy Settings, choose a port and routing strategy, then start the proxy.
  4. 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.

Network egress: keep provider traffic off your local proxy

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.

Automatic protection

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

Verify your own egress

# 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.net

DIRECT 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).

Clash Verge / Mihomo

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,DIRECT

Edit 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 no HTTP_PROXY, so NO_PROXY inside 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 loads chatglm.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.

Containers: check the Docker VM proxy first

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.

Do not run the full account pool from your workstation

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").

Set the storage encryption key (required)

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-secret

Pick 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_KEY

Start containers with docker compose up -d, never docker run. A container created with docker run has no compose label, so docker compose up refuses to manage it and .env is never injected — the process silently runs without the key.

If the key is missing or does not match

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-recreate

Fastest 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.

Local versus production deployment

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//data volume while both are running. They will overwrite each other's status/errorMessage fields 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=off only 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 Clash DIRECT rules 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.

Screenshots

Dashboard Providers
Dashboard Providers
Proxy settings API keys
Proxy settings API keys
Models Sessions
Models Sessions

Configuration and data

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.

Contributing

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:server

The 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.

License

Chat2API is released under the GNU General Public License v3.0.

Acknowledgements

Electron, React, TypeScript, Tailwind CSS, Zustand, and Koa.

About

OpenAI-compatible AI proxy and management app for DeepSeek, GLM, Kimi, MiniMax, Qwen, Perplexity, and more. Supports multi-account routing, streaming, tool calling, Docker, and desktop deployments.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages