Local OpenAI-compatible API proxy for DeepSeek Web Chat
Quick Start • Features • Examples • Models • Endpoints • Open WebUI
FreeDeepseekAPI runs a local API server for DeepSeek Web Chat (chat.deepseek.com) and lets you connect DeepSeek Web to Open WebUI, LiteLLM, Hermes, Claude Code, OpenAI SDK-style clients, and other OpenAI-compatible tools.
The project works through your regular logged-in DeepSeek account in a separate Chrome profile. The local server accepts API requests and then talks to DeepSeek Web through the saved browser session.
⚠️ This is an experimental web-chat proxy. DeepSeek may change the internal Web API without warning. For production use cases, the official paid DeepSeek API is more reliable.
ForgetMeAI: https://t.me/forgetmeai
- What this gives you
- Features
- Quick Start
- Windows launch
- Linux / Chromium launch
- VPS / headless launch
- Rootless Podman
- Diagnostics / doctor
- Session reuse and chat reset
- Multi-account pool
- Console auth ideas
- Verify it works
- Request examples
- Models
- Endpoints
- Open WebUI
- Update login
- Tests
- Project status
- Use DeepSeek Web as a local API endpoint.
- Connect DeepSeek to Open WebUI and other OpenAI-compatible clients.
- Get regular JSON responses or streaming SSE.
- Use reasoning models with separate
reasoning_content. - Work with Anthropic Messages API shim for Claude Code / Anthropic SDK.
- Use OpenAI Responses API shim for new OpenAI/Codex-style clients.
- Keep separate web sessions for different agents/users.
- OpenAI-compatible API:
POST /v1/chat/completions - Anthropic-compatible shim:
POST /v1/messages - OpenAI Responses shim:
POST /v1/responses - Streaming: SSE chunks and regular non-stream JSON responses
- Reasoning output: separate
reasoning_contentfor thinking models - Tool calling: parsing OpenAI tools, Anthropic tools, and Responses function tools
- Model capabilities:
GET /v1/model-capabilitieswith alias → real web mode - Agent sessions: separate DeepSeek session per
user/ agent id - Session recovery: auto-reset of stale chains/sessions
- Zero dependencies: Node.js 18+, no npm dependencies
git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git
cd FreeDeepseekAPI
npm run auth
npm startnpm run auth opens the auth menu:
- select item
1; - log in to DeepSeek in a separate Chrome profile;
- send a short message like
ok; - return to terminal and press Enter.
npm start shows the launch menu:
1— authorize / update DeepSeek login2— show models and statuses3— start proxy4— exit
For headless/CI launch without menu:
NON_INTERACTIVE=1 npm start
# or
SKIP_ACCOUNT_MENU=1 npm startBy default the server listens on:
http://localhost:9655
By default the proxy is only accessible from the same machine. To allow network access, explicitly set the bind address and a proxy API key:
HOST=0.0.0.0 PROXY_API_KEY='replace-with-a-long-random-value' npm startThen pass the key as Authorization: Bearer <key>. Without PROXY_API_KEY
non-health endpoints remain unauthenticated, so do not expose such an
instance to the network.
Browser requests are allowed from loopback origins. If the UI is opened on a
different address, add its exact origin via comma, e.g.
PROXY_CORS_ORIGINS=https://ui.example.com,http://192.168.1.20:3000.
git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git
cd FreeDeepseekAPI
npm run auth
npm startIf Chrome is installed in a non-standard location, specify the path explicitly:
$env:CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
npm run authIf Chrome is not found, npm run auth now prints ready-to-use instructions for Windows/macOS/Linux instead of a cryptic stack trace.
git clone https://github.com/ForgetMeAI/FreeDeepseekAPI.git
cd FreeDeepseekAPI
CHROME_PATH=$(which chromium) npm run auth
npm startIf Chromium has a different name:
CHROME_PATH=$(which chromium-browser) npm run auth
# or
CHROME_PATH=$(which google-chrome) npm run authThe most reliable flow without Chrome on the server:
- On your home PC (where you have GUI/Chrome):
npm run auth- Copy
deepseek-auth.jsonto VPS:
scp deepseek-auth.json user@your-vps:/opt/FreeDeepseekAPI/deepseek-auth.json- On VPS import/verify the file and set safe permissions:
cd /opt/FreeDeepseekAPI
npm run auth:import -- --input ./deepseek-auth.json
npm run doctor -- --offline- Start proxy without interactive menu:
NON_INTERACTIVE=1 npm startYou can import not only a ready-made deepseek-auth.json, but also a browser cookie export:
DEEPSEEK_TOKEN="<token>" npm run auth:import -- --input ./cookies.jsonImportant:
deepseek-auth.jsongives access to your DeepSeek Web login. Do not commit, do not publish, store with0600permissions.
The container is intended for non-interactive proxy launch only. Perform
browser-based authorization on the host with npm run auth: auth scripts and
deepseek-auth.json are not copied into the image.
Run Podman as a regular user, without sudo.
- Build the local image:
podman build --tag localhost/free-deepseek-api:local --file Containerfile .- Pass DeepSeek auth and a separate proxy API key via Podman secrets:
podman secret create --replace free-deepseek-auth ./deepseek-auth.json
printf 'Proxy API key: '
IFS= read -r -s PROXY_API_KEY
printf '\n'
printf '%s' "$PROXY_API_KEY" |
podman secret create --replace free-deepseek-proxy-key -Use a long random key. The value stays in the current shell's PROXY_API_KEY
variable for API verification; it does not end up in the image or the Podman
command line.
- Run the container with minimal privileges:
podman run --detach \
--name free-deepseek-api \
--publish 127.0.0.1:9655:9655 \
--secret free-deepseek-auth,target=deepseek-auth.json,uid=1000,gid=1000,mode=0400 \
--secret free-deepseek-proxy-key,target=proxy-api-key,uid=1000,gid=1000,mode=0400 \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges \
localhost/free-deepseek-api:localInside the container, NON_INTERACTIVE=1, HOST=0.0.0.0, and paths to both
secrets are pre-configured. REQUIRE_PROXY_API_KEY=1 prevents the container
from starting if the key secret is missing or empty. On the host, the port is
only published to 127.0.0.1; do not remove this address without a separate
network firewall/access policy.
- Verify liveness, account readiness, and the protected endpoint:
podman healthcheck run free-deepseek-api
curl --fail http://127.0.0.1:9655/readyz
curl --fail \
-H "Authorization: Bearer $PROXY_API_KEY" \
http://127.0.0.1:9655/v1/modelsThe built-in healthcheck verifies the local /health (whether the process is
alive). /readyz additionally returns 503 if no DeepSeek auth account is
currently ready to serve requests. Container diagnostics:
podman logs free-deepseek-api
podman inspect --format '{{.State.Health.Status}}' free-deepseek-apiStop and remove the container along with the saved Podman secrets:
podman stop free-deepseek-api
podman rm free-deepseek-api
podman secret rm free-deepseek-auth free-deepseek-proxy-key
unset PROXY_API_KEYWhen rotating auth or proxy key, replace the corresponding secret and recreate the container so that behavior does not depend on the Podman version.
npm run doctor
# without network requests to DeepSeek:
npm run doctor -- --offlinedoctor checks:
- whether
deepseek-auth.json/DEEPSEEK_AUTH_DIRis found; - whether JSON is valid;
- whether
token,cookie,wasmUrlexist; - whether file permissions are safe on macOS/Linux (
0600); - on normal run — whether DeepSeek PoW endpoint is reachable.
If you see data.biz_data is null, fetch failed, 401/403/429 or Hermes/OpenCode doesn't see models — first run npm run doctor.
FreeDeepseekAPI doesn't create a new DeepSeek chat on every HTTP request without reason. The logic is:
- one
x-agent-session,session, oruser→ one DeepSeek chat session; - if session id already exists — proxy reuses it and continues chain via
parent_message_id; - auto-reset happens on TTL, DeepSeek session error, or too long message chain;
- local history is saved as short context so new DeepSeek session can continue the conversation.
- long agent requests are trimmed before sending via
DEEPSEEK_MAX_PROMPT_CHARS(default 80,000 characters): the beginning of the task, fresh tool results, and tool adapter are preserved; - if the client already sent multi-turn history, local recovery-history is not added a second time;
- empty responses are retried up to
DEEPSEEK_MAX_RETRIEStimes (default 2), with context shrinking on each retry.
Explicitly set agent/session:
curl -X POST http://localhost:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-agent-session: my-agent" \
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hi"}]}'View active sessions:
curl http://localhost:9655/v1/sessionsReset a single session:
curl -X POST "http://localhost:9655/reset-session?agent=my-agent"Reset all sessions:
curl -X POST "http://localhost:9655/reset-session?agent=all"Clear sessions on server restart:
# Start server with sessions cleared (fresh context each restart)
DEEPSEEK_CLEAR_SESSIONS_ON_START=1 npm startThis deletes sessions.json on startup, so the model starts with no memory of previous conversations. Useful for development/testing when you want clean context each restart.
Debug tool loading:
# Log all MCP tool names loaded per agent (for debugging)
DEEPSEEK_LOG_TOOLS=1 npm startShows which tools are being sent by the client and cached by the server. Disabled by default.
Why chats still appear in DeepSeek Web: proxy works through internal Web Chat API, and DeepSeek stores real chat sessions on their side. This is normal for web-proxy. The task of session reuse is not to spawn new chats unnecessarily and to reset cleanly only when the chain has gone stale/broken.
You can connect multiple auth files. Correct model: sticky account per agent/session — proxy doesn't switch account inside a live DeepSeek session. If an account gets 401/403/429 and goes into cooldown, the session is safely reset and a new request may switch to another available account.
Option 1 — directory with auth files:
mkdir -p accounts
cp deepseek-auth-main.json accounts/main.json
cp deepseek-auth-backup.json accounts/backup.json
chmod 600 accounts/*.json
DEEPSEEK_AUTH_DIR=./accounts NON_INTERACTIVE=1 npm startOption 2 — file list:
DEEPSEEK_AUTH_PATH="./accounts/main.json,./accounts/backup.json" NON_INTERACTIVE=1 npm startHow the pool works:
- new agent/session gets an available account round-robin;
- selected account is pinned to session (
sticky); - on
401,403,429account goes into cooldown; - if sticky-account session went into cooldown, old DeepSeek session is reset to avoid hammering rate-limited/expired account;
- account status visible in
/healthwithout auth file paths or file names; - auth files must be stored with
0600permissions.
Configure cooldown:
DEEPSEEK_ACCOUNT_COOLDOWN_MS=600000 npm startPassword flow from PR #3 can be done, but safer not to store password and not make it default. Normal implementation:
npm run auth:consoleasks for email/phone and password via hidden prompt.- Password stays only in process memory, not written to files/logs/history.
- Script replicates Web login flow via
fetch/CDP: gets captcha/verify challenge, gives human link/code, waits for confirmation. - After successful login, only standard-format
deepseek-auth.jsonis saved. - If DeepSeek asks for captcha/2FA — script honestly says "open link, pass check, press Enter", doesn't try to bypass protection.
- For VPS better mode
auth:console --no-save-password --output deepseek-auth.json.
Minimal safe MVP: console auth only interactive, no env password. Acceptable automation variant: DEEPSEEK_EMAIL=... npm run auth:console, but password still entered via hidden prompt.
curl http://localhost:9655/
curl http://localhost:9655/v1/models
curl http://localhost:9655/v1/model-capabilitiesIf all good, /health returns server status, list of supported aliases, and config_ready: true.
curl -X POST http://localhost:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Hi! Reply with one phrase."}],
"stream": false
}'curl -X POST http://localhost:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-reasoner",
"messages": [{"role": "user", "content": "Solve briefly: why is the sky blue?"}],
"stream": false
}'For reasoning models, API returns the reasoning chain separately from the final answer:
- non-stream:
choices[0].message.reasoning_content - stream:
choices[0].delta.reasoning_content - usage:
usage.completion_tokens_details.reasoning_tokens
reasoning_tokens — approximate estimate based on extracted DeepSeek Web THINK text, because web stream doesn't return official token usage for reasoning separately.
curl -X POST http://localhost:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat-search",
"messages": [{"role": "user", "content": "Find a fresh fact about DeepSeek and reply briefly."}],
"stream": false
}'curl -N -X POST http://localhost:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Write a short joke."}],
"stream": true
}'curl -X POST http://localhost:9655/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat",
"max_tokens": 512,
"messages": [{"role": "user", "content": "Reply exactly OK"}],
"stream": false
}'For Claude Code you can specify backend directly:
export ANTHROPIC_BASE_URL="http://127.0.0.1:9655"
export ANTHROPIC_AUTH_TOKEN="dummy-key"
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
claude --model deepseek-chatcurl -X POST http://localhost:9655/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat",
"input": "Reply exactly OK",
"stream": false
}'FreeDeepseekAPI accepts:
- OpenAI
tools; - Anthropic
tools; - Responses API function tools.
Proxy asks DeepSeek to return strict JSON tool call, but also parses fallback formats:
TOOL_CALL:- fenced JSON with an explicit
tool_call,tool_calls, orfunction_callenvelope <tool_call>...</tool_call>- DeepSeek DSML (
<|DSML|tool_calls>...) and web variant with<||DSML|| Tool Calls>
GET /v1/models returns only aliases that are currently verified and working through this proxy.
| Alias | Web mode | Reasoning | Web search | Notes |
|---|---|---|---|---|
deepseek-chat |
Fast / default |
no | no | basic chat |
deepseek-v3 |
Fast / default |
no | no | compatibility alias |
deepseek-default |
Fast / default |
no | no | compatibility alias |
deepseek-reasoner |
Fast / default |
yes | no | thinking_enabled=true |
deepseek-r1 |
Fast / default |
yes | no | R1-compatible alias |
deepseek-chat-search |
Fast / default |
no | yes | web search |
deepseek-default-search |
Fast / default |
no | yes | web search alias |
deepseek-reasoner-search |
Fast / default |
yes | yes | reasoning + search |
deepseek-r1-search |
Fast / default |
yes | yes | R1-compatible + search |
deepseek-expert |
Expert / expert |
no | no | Expert mode |
deepseek-v4-pro |
Expert / expert |
yes | no | Expert + reasoning |
Full mapping:
curl http://localhost:9655/v1/model-capabilitiesAccording to the official DeepSeek V4 Preview page, deepseek-chat and deepseek-reasoner are currently routed to deepseek-v4-flash non-thinking/thinking. In chat.deepseek.com direct stream the exact checkpoint name is not returned (model: ""), so the proxy records both the web mode (default / Fast) and the current official routing (DeepSeek-V4-Flash).
Current DeepSeek Web remote config shows these web modes:
default/ UIFast— works; supportsthinking_enabledandsearch_enabled.expert/ UIExpert— works via the current web contract (x-client-version=2.0.0) and supportsthinking_enabled. In/v1/models,deepseek-expertis served without reasoning anddeepseek-v4-proas Expert + reasoning.vision/ UIRecognition— visible in remote config, but currently the direct Web API returnsbackend_err_by_model(Vision is temporarily unavailable). Thereforedeepseek-visionis hidden from/v1/models.
Search for Expert is unavailable per remote config, so deepseek-expert-search remains unsupported.
| Method | Path | Purpose |
|---|---|---|
GET |
/ or /health |
proxy status |
GET |
/v1/models |
list of working OpenAI-compatible aliases |
GET |
/v1/model-capabilities |
full alias mapping, real model, capabilities |
POST |
/v1/chat/completions |
OpenAI-compatible Chat Completions |
POST |
/v1/messages |
Anthropic Messages API shim |
POST |
/v1/responses |
OpenAI Responses API shim |
GET |
/v1/sessions |
active local agent sessions |
POST |
/reset-session?agent=<id> |
reset a single session |
POST |
/reset-session?agent=all |
reset all sessions |
Base URL for Open WebUI in Docker:
http://host.docker.internal:9655/v1
For local launch without Docker:
http://localhost:9655/v1
If PROXY_API_KEY is not set, any API key can be used. If the key is set,
the client must send exactly that value — proxy verifies the bearer token
before granting access to models, sessions, and completions.
npm run auth
npm startIf DeepSeek starts returning 401, 403 or asks for a new PoW/session — re-run npm run auth to refresh the saved browser session.
Local auth files must not be committed to GitHub:
deepseek-auth.json.chrome-profile-deepseek/.env
They are already in .gitignore.
Syntax check for the project:
npm testLive smoke tests against a running local proxy:
BASE_URL=http://127.0.0.1:9655 MODEL=deepseek-chat npm run test:liveFreeDeepseekAPI is an experimental web-chat proxy for local use and integrations. It depends on the current DeepSeek Web Chat contract, so when DeepSeek makes changes, auth/session logic or model mapping may need updating.
If something stops working:
- refresh login via
npm run auth; - check
/v1/model-capabilities; - retry the request on a fresh session;
- if the problem persists — DeepSeek likely changed the internal Web API.
ForgetMeAI · Telegram