Local OpenAI-compatible API proxy for DeepSeek Web Chat
Start here • Quick start • What you get • Examples • Models • Endpoints • Open WebUI • Coding agents
What it is. You have a free DeepSeek account on chat.deepseek.com. FreeDeepseekAPI runs on your computer and turns that account into an API at http://127.0.0.1:9655, the same kind of address paid AI services give you. Any app that can talk to OpenAI or Anthropic can then use DeepSeek through it: Open WebUI, Cursor, Claude Code, Codex, your own scripts. No paid API key: your own account, on your own machine.
You need: Node.js 18 or newer, Git, Google Chrome, and a free DeepSeek account.
git clone --branch stable https://github.com/dekrezz/FreeDeepseekAPI.git
cd FreeDeepseekAPI
npm run auth
npm run dashboardnpm run auth: choose Sign in with Chrome, log in at chat.deepseek.com in the window it opens, send a short message such asok, then go back to the terminal. Your login is saved todeepseek-auth.json.npm run dashboard: starts the proxy and openshttp://127.0.0.1:9655/dashboard. You can chat with DeepSeek right there.
| Setting | Value |
|---|---|
| Base URL | http://127.0.0.1:9655/v1 |
| API key | any text (if you started the proxy with PROXY_API_KEY, use that key) |
| Model | deepseek-v4-flash answers with web search, deepseek-v4-flash-thinking thinks first; add -nosearch to either to turn search off |
Open WebUI running in Docker: use http://host.docker.internal:9655/v1.
While the proxy is running:
npm run setup:agentsIt sets up Claude Code, Codex, OpenCode, Hermes, OpenClaw and Cursor as an extra profile; whatever you use today stays as it is. You can also do it from the dashboard: Settings → Agents. Then start, for example, claude --settings ~/.claude/freedeepseek.settings.json or codex --profile freedeepseek.
Something wrong? npm run doctor checks your setup, and curl http://127.0.0.1:9655/health shows whether the proxy is up. Everything else is below.
FreeDeepseekAPI runs a local API server in front of DeepSeek Web Chat (chat.deepseek.com). Point Open WebUI, LiteLLM, Hermes, Claude Code, Codex, OpenCode, OpenClaw, Cursor, or any OpenAI-compatible client at it.
The project uses your normal logged-in DeepSeek account. The local server accepts API requests, then continues that saved Web session. There is no paid api.deepseek.com key.
One model is on the site today: DeepSeek-V4.1-Flash. The native Web Search built into chat.deepseek.com is on by default, and that search is what answers live internet questions. -thinking turns DeepThink on. -nosearch or "web_search": false turns search off.
This is an experimental Web-chat proxy. DeepSeek can change its private Web API at any time. When it does, the project is updated to follow, and this guide may change with it. Keep your copy on the newest version: Settings → Updates in the dashboard, or see Updates.
- Start here
- What you get
- Features
- Quick start
- Windows
- Linux / Chromium
- VPS / headless
- Rootless Podman
- Diagnostics / doctor
- Session reuse
- Multi-account pool
- Dashboard
- Updates
- Sign-in
- Coding agents
- Check that it works
- Request examples
- Models
- Endpoints
- Environment
- Errors
- Open WebUI
- Refresh the login
- Tests
- Project status
- Use DeepSeek Web as a local API endpoint.
- Connect DeepSeek to Open WebUI and other OpenAI-compatible clients.
- Get a normal JSON body or a streaming SSE response.
- Turn DeepThink on with
-thinkingand readreasoning_content. - Talk to Claude Code through the Anthropic Messages shim.
- Talk to Codex through the OpenAI Responses shim.
- Keep a separate Web chat for each agent or
userid. - Let several local tools run in one reply, up to eight.
- Search the live web with DeepSeek's own search, not with bash or a harness websearch tool.
- OpenAI-compatible API:
POST /v1/chat/completions - Anthropic-compatible shim:
POST /v1/messages - OpenAI Responses shim:
POST /v1/responses - Streaming: SSE chunks, and a normal non-stream JSON body
- DeepThink:
reasoning_contentwhen-thinkingis on - Native search: chat.deepseek.com Web Search, on by default. Off with
-nosearchor"web_search": false - Tool calling: OpenAI tools, Anthropic tools, and Responses function tools, returned as a batch
- Model capabilities:
GET /v1/model-capabilities - Agent sessions: one DeepSeek chat per
x-agent-sessionoruser - Session recovery: an empty reply, a broken tool call, or a timeout keeps the same chat
- Zero dependencies: Node.js 18+, no npm packages
- License: Apache-2.0
git clone --branch stable https://github.com/dekrezz/FreeDeepseekAPI.git
cd FreeDeepseekAPI
npm run auth
npm startnpm run auth opens the sign-in menu. Choose Chrome sign-in, log in at chat.deepseek.com in the profile it opens, send a short message such as ok, then return to the terminal and continue. The saved file is deepseek-auth.json, mode 0600.
npm start opens the launch menu:
- Start proxy
- Start proxy + open dashboard (also
npm run dashboard) - Sign in with Chrome
- Import an auth file or a browser cookie export
- Show the DeepSeek-V4.1-Flash model ids
- Quit
Headless or CI, with no menu:
NON_INTERACTIVE=1 npm start
# or
SKIP_ACCOUNT_MENU=1 npm startThe server listens on:
http://127.0.0.1:9655
By default only this computer can reach it. To publish it on a network, set an address and a proxy key:
HOST=0.0.0.0 PROXY_API_KEY='replace-with-a-long-random-value' npm startSend that key as Authorization: Bearer <key>. Without PROXY_API_KEY, the API has no authentication of its own. Do not put that instance on a network.
Browser calls are allowed from loopback origins. If the UI is on another origin, list the exact origins, comma-separated:
PROXY_CORS_ORIGINS='https://ui.example.com,http://192.168.1.20:3000'git clone --branch stable https://github.com/dekrezz/FreeDeepseekAPI.git
cd FreeDeepseekAPI
npm run auth
npm startIf Chrome is not in the usual place:
$env:CHROME_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
npm run authWhen Chrome is missing, npm run auth prints the install path for Windows, macOS, or Linux instead of a raw stack trace.
git clone --branch stable https://github.com/dekrezz/FreeDeepseekAPI.git
cd FreeDeepseekAPI
CHROME_PATH=$(which chromium) npm run auth
npm startIf the binary has another name:
CHROME_PATH=$(which chromium-browser) npm run auth
# or
CHROME_PATH=$(which google-chrome) npm run authThe reliable path does not run Chrome on the server.
- On a machine that has a GUI and Chrome:
npm run auth- Copy
deepseek-auth.jsonto the VPS:
scp deepseek-auth.json user@your-vps:/opt/FreeDeepseekAPI/deepseek-auth.json- Import it and check the file:
cd /opt/FreeDeepseekAPI
npm run auth:import -- --input ./deepseek-auth.json
npm run doctor -- --offline- Start the proxy with no menu:
NON_INTERACTIVE=1 npm startA browser cookie export works too, if you also have the token:
DEEPSEEK_TOKEN="<token>" npm run auth:import -- --input ./cookies.jsondeepseek-auth.json is access to your DeepSeek Web login. Do not commit it, do not paste it into a chat, and keep it mode 0600.
There is no password login command. The proxy never asks for your DeepSeek password and never stores one. Sign-in is Chrome, a saved auth file, or a cookie export plus token.
The container only runs the proxy. Sign in on the host with npm run auth. Auth scripts and deepseek-auth.json are not copied into the image.
Run Podman as your user, not as root.
- Build the image:
podman build --tag localhost/free-deepseek-api:local --file Containerfile .- Store the DeepSeek auth file and a separate proxy key as 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 this shell so you can call the API. It is not baked into the image or the Podman command line.
- Start the container with the privileges dropped:
podman run --detach \
--name free-deepseek-api \
--publish 127.0.0.1:9655:9655 \
--secret free-deepseek-auth,type=mount,target=/run/secrets/deepseek-auth.json,mode=0400 \
--secret free-deepseek-proxy-key,type=mount,target=/run/secrets/proxy-api-key,mode=0400 \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges \
localhost/free-deepseek-api:localInside the image, NON_INTERACTIVE=1, HOST=0.0.0.0, and REQUIRE_PROXY_API_KEY=1 are already set. The container refuses to serve if the proxy-key secret is missing or empty. On the host the port is published only on 127.0.0.1. Do not drop that address unless you have a firewall in front of it.
- Check liveness, account readiness, and the protected model list:
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 hits local /health and only tells you the process is up. /readyz returns 503 when no DeepSeek login can serve a chat right now.
podman logs free-deepseek-api
podman inspect --format '{{.State.Health.Status}}' free-deepseek-apiStop the container and remove the 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 you rotate the login or the proxy key, replace the secret and recreate the container.
npm run doctor
# no network call to DeepSeek:
npm run doctor -- --offlinedoctor checks:
- whether
deepseek-auth.jsonorDEEPSEEK_AUTH_DIRis present; - whether the JSON parses;
- whether
token,cookie, andwasmUrlare set; - whether the file mode is
0600on macOS and Linux; - on a normal run, whether the DeepSeek PoW endpoint answers.
If you see data.biz_data is null, fetch failed, 401, 403, 429, or a coding agent that cannot list models, run npm run doctor first.
FreeDeepseekAPI does not open a new DeepSeek chat on every HTTP request.
- One
x-agent-session,session, oruservalue is one DeepSeek chat. - When that chat already exists, the proxy continues it with
parent_message_id. - The standing system prompt and the tool list go upstream once. Later turns from OpenCode, Claude Code, and Codex send only the new user or tool turn.
- A changed system prompt is sent again.
- An empty reply, a broken tool-call, or a timeout keeps the live chat. Those failures used to open a new chat and resend the whole prompt. They do not anymore.
- A new chat starts when the session is missing, the chain hits 100 messages, the chat is older than 2 hours, DeepSeek says the prompt is too long, or you call
/reset-session. - Long prompts are cut to
DEEPSEEK_MAX_PROMPT_CHARS(default 80,000 characters) before they are sent. The start of the task, the latest tool results, and the tool reminder stay. - If the client already sent a multi-turn history, the proxy does not paste its own recovery history a second time.
- An empty reply is retried up to
DEEPSEEK_MAX_RETRIEStimes (default 2). Each retry uses a smaller slice of context, still on the same chat when that chat is alive.
Set the agent explicitly:
curl -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-agent-session: my-agent" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Hello"}]}'List active sessions:
curl http://127.0.0.1:9655/v1/sessionsReset one session:
curl -X POST "http://127.0.0.1:9655/reset-session?agent=my-agent"Reset every session:
curl -X POST "http://127.0.0.1:9655/reset-session?agent=all"A reset keeps the proxy's copy of the conversation and replays it into the next chat when the client sends only one message. Add &history=drop when the client will send its own full transcript, for example after editing an earlier message, so the replaced turns do not come back.
You will still see chats in the DeepSeek website. The proxy uses the Web Chat API, and DeepSeek stores the real chats. Session reuse only stops the proxy from opening a new one when the current chain is still good.
A message whose text is exactly /new resets that agent's remote chat instead of sending the text to the model.
DeepSeek can ban a login for days if two chats send on that same login at the same time. The pool rule is: one in-flight chat per login. A second request for a busy login waits. It does not overlap.
Sticky means the proxy does not hop accounts in the middle of a live chat, except when that login is rate-limited or loses auth. If the login gets 401 or 403, or DeepSeek rate-limits it, the login goes into cooldown and the old remote session is dropped. A rate-limited request does not fail: it moves right away to another ready login, on a new chat that gets the full transcript. After 401/403, the next request moves.
Directory of auth files:
mkdir -p accounts
npm run auth:import -- --input ~/Downloads/deepseek-auth.json --output ./accounts/worker-2.json
chmod 600 accounts/*.json deepseek-auth.json
DEEPSEEK_AUTH_DIR=./accounts NON_INTERACTIVE=1 npm startOr a comma-separated list:
DEEPSEEK_AUTH_PATH="./accounts/main.json,./accounts/backup.json" NON_INTERACTIVE=1 npm startHow the pool behaves:
- a new agent gets a free login, round-robin;
- that login stays stuck to the session;
401,403, and rate limits put the login in cooldown (DEEPSEEK_ACCOUNT_COOLDOWN_MS, default 10 minutes). A rate limit is HTTP429, a "too frequent" message in an HTTP400or JSON body (for exampleСлишком частые сообщения), or a DeepSeek stream hint withfinish_reason: rate_limit_reached.Retry-Afteris honored when DeepSeek sends it;- the rate-limited request moves to another ready login on a new chat. Only when every login is cooling down does the client get
429 rate_limit; - other
400errors do not cause a cooldown. The chat is recreated on the same login; - two requests and two free logins go to different accounts;
- the same
x-agent-sessionoverlapping waits on its login (DEEPSEEK_ACCOUNT_LOCK_WAIT_MS, default 120 seconds); - when every login is busy, the request waits on the sticky or least-recently-used login;
- OpenCode's session-title request is answered locally and does not occupy a login;
/healthshows account status without auth-file paths or file names;/dashboardshows every login and lets you pause, resume, clear a cooldown, and reload auth files without a restart (see Dashboard);- auth files must be mode
0600.
You cannot pin a client to a chosen file. The proxy picks a free login.
DEEPSEEK_ACCOUNT_COOLDOWN_MS=600000 npm startOpen http://127.0.0.1:9655/dashboard. It lists every login with its state (ready, busy, cooldown, disabled, no_credentials), the cooldown countdown and reason, consecutive and total failures, the last upstream error, and usage. From there you can:
- Disable / Enable a login. This is a runtime pause: in-flight work finishes, and a restart forgets it.
"enabled": falsein the auth file is the persistent switch; a login disabled in its file cannot be enabled from the dashboard. - Clear cooldown to put a cooling login back in rotation now.
- Reload accounts to re-read the auth files. New files are added and deleted files are removed. A kept login keeps its cooldown and counters. A login whose token or cookie changed starts fresh.
To add a login, import it and press Reload:
npm run auth:import -- --input ~/Downloads/deepseek-auth.json --output ./accounts/worker-3.jsonThe page is static and holds no data. It reads the admin API:
| Method | Path | Result |
|---|---|---|
GET |
/admin/accounts |
{ now, pool, accounts } |
POST |
/admin/accounts/<id>/disable |
{ account, pool } |
POST |
/admin/accounts/<id>/enable |
{ account, pool }. 409 disabled_in_file if the file says "enabled": false |
POST |
/admin/accounts/<id>/clear-cooldown |
{ account, pool } |
POST |
/admin/accounts/reload |
{ added, removed, kept, errors, accounts, pool }. 422 no_accounts_found leaves the pool unchanged |
POST |
/admin/accounts/restore |
Body { archived_as } from a DELETE. Renames the file back and reloads: { restored, errors, accounts, pool }. 404 archive_not_found, 409 restore_target_exists |
Access rules:
- With
PROXY_API_KEYset,/admin/*needs the same bearer as/v1/*. The page asks for the key and keeps it in the tab's session storage only. - Without a key,
/admin/*answers only direct loopback clients. A request that carriesX-Forwarded-For,Forwarded, orX-Real-IPis refused with403 admin_forbidden, even from127.0.0.1. So is a non-localhostHost, or a browserOriginother than the proxy's own (another local web app cannot pause logins).PROXY_ADMIN_ALLOW_REMOTE=1lifts only the client address rule. TheHostmust still be loopback, an IP address, or a host listed inPROXY_CORS_ORIGINS, and a browserOriginmust still be the proxy's own. Anyone who can reach the port can still pause logins, so prefer a key on a network bind. - Browser POSTs still pass the origin guard. To use the dashboard from a non-loopback address, add that origin to
PROXY_CORS_ORIGINS. - Admin responses never contain tokens, cookies,
hif_*values, or auth-file names.
The container image ships the dashboard. With --read-only, actions that write auth files (add, rename, remove, restore) fail with 500 auth_file_write_failed; pause, resume, and clear cooldown work.
DeepSeek changes its Web chat from time to time, and FreeDeepseekAPI follows. Keep your copy current.
Two channels, both branches on GitHub:
| Channel | What it gets |
|---|---|
stable |
A release once it has been checked. The default, and what Quick start clones. |
latest |
Every release as soon as it is out. |
From the dashboard: Settings → Updates. Pick a channel, Check for updates shows what is new, Install moves your copy to it, Restart now loads it. The restart is automatic when the proxy runs with npm start or npm run dashboard; otherwise stop it and start it again.
The dashboard only moves your copy forward. It stops and shows the command to run by hand when the folder has uncommitted changes to tracked files or your channel branch has commits of its own. A container image or a downloaded archive has no git history: pull a new image or download the new release.
By hand:
git fetch origin && git switch stable && git merge --ff-only origin/stableThen restart the proxy.
| Command | What it does |
|---|---|
npm run auth |
Menu: Chrome sign-in, import, status, delete the local file |
npm run deepseek:auth |
Chrome capture of token and cookies |
npm run auth:import |
Import deepseek-auth.json or a browser cookie export |
npm run doctor |
Check the files. Drop --offline to also hit PoW |
Chrome extension: load chrome-extension/ on an open chat.deepseek.com tab. Collect until Token is Ready, then save the file. Do not paste that JSON into a chat.
Fields: token, cookie, wasmUrl (an https://*.deepseek.com address). Optional: hif_dliq, hif_leim, name, and enabled (false pauses that login). See auth.example.json.
The password of the DeepSeek account is not an input. There is no auth:console command, and none should store a password on disk.
npm run setup:agents
npm run setup:agents -- --all --model deepseek-v4-flash
npm run setup:agents -- --target claude-code --model deepseek-v4-flash-thinking
npm run setup:agents -- --target codex --model deepseek-v4-flashThe proxy must already be listening. The default origin is http://127.0.0.1:9655. Override it with --base-url. If PROXY_API_KEY is set, it is copied into the agent config.
Setup adds FreeDeepseekAPI as an opt-in profile. It does not replace Claude, GPT, or whatever you already use.
| Agent | What is written | How you select it |
|---|---|---|
| Claude Code | ~/.claude/freedeepseek.settings.json |
claude --settings ~/.claude/freedeepseek.settings.json |
| Codex | ~/.codex/freedeepseek.config.toml and a model catalog |
codex --profile freedeepseek |
| OpenCode | a freedeepseek provider, plus autonomy rules in AGENTS.md |
pick freedeepseek/<id> |
| Hermes | ~/.hermes/freedeepseek.yaml |
that profile file |
| OpenClaw | a freedeepseek provider |
pick it explicitly |
| Cursor | a snippet and launcher under integrations/cursor/ |
editor settings stay as they are |
--model is always DeepSeek-V4.1-Flash, with native chat.deepseek.com search on. -thinking turns DeepThink on. -nosearch turns search off; the agent then keeps its own web tools. An old -search id maps to the same id without the suffix.
Codex setup does not set forced_login_method = api. On Codex CLI that line logs you out of ChatGPT. Normal codex stays on ChatGPT. The FreeDeepseek profile talks to this proxy.
Images:
- Claude Code: paste or attach. The Anthropic
imageblock is uploaded to DeepSeek Web. - Codex:
codex -i screenshot.png. Repeat-ifor more files./v1/responsesacceptsinput_image. - OpenCode: paste or drop an image. The generated config keeps attachments.
- Other OpenAI clients: send
image_urlas a base64 data URL or a public HTTPS URL.
Restore a previous setup from its backup:
node scripts/setup-agents.js --restore ~/.freedeepseek-api/backups/<stamp>Two agents at once need two Web logins in accounts/. Details: docs/agents.md.
curl http://127.0.0.1:9655/health
curl http://127.0.0.1:9655/readyz
curl http://127.0.0.1:9655/v1/models
curl http://127.0.0.1:9655/v1/model-capabilities/health is liveness. /readyz is 200 only when at least one login can serve. /v1/models lists the four DeepSeek-V4.1-Flash ids.
curl -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-agent-session: my-agent" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Answer in one sentence."}],
"stream": false
}'-thinking is the DeepThink switch from the chat composer. The model is still DeepSeek-V4.1-Flash. It reasons before the answer.
curl -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash-thinking",
"messages": [{"role": "user", "content": "Why is the sky blue? Keep it short."}],
"stream": false
}'Where the reasoning shows up:
- non-stream:
choices[0].message.reasoning_content - stream:
choices[0].delta.reasoning_content - usage:
usage.completion_tokens_details.reasoning_tokens
reasoning_tokens is an estimate from the extracted DeepThink text. The Web stream does not report official reasoning-token usage.
A tool-call turn does not attach reasoning to the message. Some agents treat any text next to a tool call as the final answer and stop.
The native Web Search of chat.deepseek.com is on by default, for every model id. Internet questions go through that search. Do not ask the model to use bash, curl, or a harness websearch / webfetch tool for the live web.
curl -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Find a current fact about DeepSeek and answer briefly."}],
"stream": false
}'To answer without search, send "web_search": false, or use an id ending in -nosearch:
curl -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash-thinking",
"web_search": false,
"messages": [{"role": "user", "content": "Explain how a B-tree splits a node."}],
"stream": false
}'web_search must be true or false; any other value returns 400 invalid_request_error. It overrides the id: "web_search": true on a -nosearch id turns search back on. The field works on /v1/chat/completions, /v1/messages, and /v1/responses.
With search off, a coding agent keeps its own websearch / webfetch tools and is not told that native search is on.
curl -N -X POST http://127.0.0.1:9655/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Tell a short joke."}],
"stream": true
}'The last SSE chunk carries usage, then data: [DONE].
curl -X POST http://127.0.0.1:9655/v1/messages \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"max_tokens": 512,
"messages": [{"role": "user", "content": "Reply with exactly OK"}],
"stream": false
}'Claude Code, after npm run setup:agents -- --target claude-code:
claude --settings ~/.claude/freedeepseek.settings.jsonNames such as claude-sonnet-* and claude-opus-* are mapped onto DeepSeek-V4.1-Flash and DeepSeek-V4.1-Flash with DeepThink, so a leftover Claude id still runs.
curl -X POST http://127.0.0.1:9655/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"input": "Reply with exactly OK",
"stream": false
}'Codex uses this route. After setup:
codex --profile freedeepseekThe proxy accepts OpenAI tools, Anthropic tools, and Responses function tools.
DeepSeek Web has no native tool-call array. The proxy writes the tool list into the prompt and parses the reply back into OpenAI tool_calls. Accepted shapes:
{"tool_call":{"name":"...","arguments":{...}}}{"tool_calls":[{"name":"...","arguments":{...}}]}— several independent calls, up to 8TOOL_CALL:plus a JSON object- a fenced JSON envelope with
tool_call,tool_calls, orfunction_call <tool_call>...</tool_call>- DeepSeek DSML, including the doubled-bar Web variant
execute_code and web_search are DeepSeek's own tools. They are not run on your computer. If they appear next to a real local tool such as read_file, they are dropped and the local calls are kept. A half-written tool block is not executed. One repair retry asks for strict JSON. If that is still broken, the turn returns 502 malformed_tool_call and the chat stays open.
While native search is on, harness websearch, webfetch, and WebFetch tools are stripped and the live web is the native chat.deepseek.com search. With search off they reach the agent unchanged.
PNG, JPEG, WebP, and GIF. Up to 10 images, 20 MiB decoded each. The JSON body defaults to 30 MiB.
The proxy uploads the image to DeepSeek Web, waits until parsing finishes, then sends the file id. A provider file_id is rejected. A URL must be public HTTPS. Redirects into local, private, or link-local addresses are rejected.
OpenAI Chat Completions uses image_url. Responses uses input_image. Anthropic uses an image block with base64. See docs/api.md.
GET /v1/models lists only DeepSeek-V4.1-Flash.
| ID | DeepThink | Native web search | What it does |
|---|---|---|---|
deepseek-v4-flash |
off | on | DeepSeek-V4.1-Flash. deepseek-flash is the same id |
deepseek-v4-flash-thinking |
on | on | DeepThink, then the answer |
deepseek-v4-flash-nosearch |
off | off | No native search |
deepseek-v4-flash-thinking-nosearch |
on | off | DeepThink without native search |
These suffixes are the two switches in the DeepSeek chat composer. They do not select another model. "web_search": true|false in the request body overrides the search switch of any id.
The old deepseek-v4-flash-search and deepseek-v4-flash-thinking-search ids still work. They mean deepseek-v4-flash and deepseek-v4-flash-thinking and are no longer listed in /v1/models.
Older ids are not registered and return 400 invalid_model:
deepseek-chat, deepseek-reasoner, deepseek-r1, deepseek-v3, deepseek-instant, deepseek-expert, deepseek-v4-pro, deepseek-chat-search, and vision.
curl http://127.0.0.1:9655/v1/model-capabilitiesFull table: docs/models.md.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Process is up. Account status if no proxy key, or the bearer matches |
GET |
/readyz |
200 only if a login can serve now |
GET |
/dashboard |
Account dashboard (static page) |
GET |
/admin/accounts |
Pool and per-login status. See Dashboard |
POST |
/admin/accounts/<id>/{disable,enable,clear-cooldown} |
Pause, resume, or un-cool one login |
POST |
/admin/accounts/reload |
Re-read auth files without a restart |
GET |
/v1/models |
The four DeepSeek-V4.1-Flash ids |
GET |
/v1/model-capabilities |
Ids, DeepThink, and native search |
POST |
/v1/chat/completions |
OpenAI Chat Completions |
POST |
/v1/messages |
Anthropic Messages |
POST |
/v1/responses |
OpenAI Responses |
GET |
/v1/sessions |
Local agent sessions |
POST |
/reset-session?agent=<id> |
Drop one Web chat |
POST |
/reset-session?agent=all |
Drop every Web chat |
| Variable | Default | Meaning |
|---|---|---|
HOST |
127.0.0.1 |
Bind address. A non-loopback bind without a proxy key prints a warning |
PORT |
9655 |
Listen port |
PROXY_API_KEY |
off | Bearer required on /v1/* and /admin/* when set |
REQUIRE_PROXY_API_KEY |
off | Refuse to start if the key is missing. The container sets this |
PROXY_CORS_ORIGINS |
loopback | Extra exact browser origins |
PROXY_ADMIN_ALLOW_REMOTE |
off | If 1, /admin/* answers non-loopback clients even without PROXY_API_KEY |
DEEPSEEK_AUTH_PATH |
./deepseek-auth.json |
One file, or a comma-separated list |
DEEPSEEK_AUTH_DIR |
./accounts when that directory exists |
Every *.json in it |
DEEPSEEK_MAX_PROMPT_CHARS |
80000 |
Cap on what is sent upstream. Minimum 16000 |
DEEPSEEK_MAX_RETRIES |
2 |
Empty-reply retries on the same chat |
DEEPSEEK_REQUEST_DEADLINE_MS |
120000 |
Per-request deadline |
DEEPSEEK_MAX_CONCURRENT |
24 |
Process-wide cap. Real parallelism is the number of idle logins |
DEEPSEEK_ACCOUNT_LOCK_WAIT_MS |
120000 |
How long a second chat waits for a busy login |
DEEPSEEK_ACCOUNT_COOLDOWN_MS |
600000 |
Cooldown after 401, 403, or a rate limit without Retry-After |
TRUST_PROXY |
off | If 1, the client IP is taken from X-Forwarded-For |
MAX_REQUEST_BODY_BYTES |
31457280 |
Maximum JSON body, including base64 images |
NON_INTERACTIVE / SKIP_ACCOUNT_MENU |
off | Skip the launch menu |
| Status | error.type |
What to do |
|---|---|---|
| 400 | invalid_model |
Use an id from GET /v1/models |
| 400 | context_length_exceeded |
The prompt was still too long after compaction |
| 401 | authentication_error |
The proxy key does not match |
| 403 | admin_forbidden |
/admin/* without a key from a non-loopback or proxied client. See Dashboard |
| 429 | concurrent_chat_blocked |
The wait for a busy login ran out. Add another login or retry |
| 429 | rate_limit |
Every login is in cooldown (rate-limited or auth failure). Honor Retry-After |
| 429 | rate_limit_error |
DeepSeek rate-limited the login and the request ran out of time or the client left before a failover finished. Honor Retry-After |
| 502 | malformed_tool_call |
Tool markup was still broken after one repair. The chat was kept |
| 502 | empty_response |
DeepSeek returned nothing after the retries |
| 503 | overloaded / no_auth |
Too many in-flight requests, or no auth file |
| 504 | request_timeout |
The deadline in DEEPSEEK_REQUEST_DEADLINE_MS was hit |
Base URL when Open WebUI runs in Docker on the same machine:
http://host.docker.internal:9655/v1
Local, no Docker:
http://127.0.0.1:9655/v1
If PROXY_API_KEY is unset, the API key field can be anything. If the key is set, the client must send that exact bearer. The proxy checks it before models, sessions, and completions.
Pick deepseek-v4-flash for a normal answer with native chat.deepseek.com search, deepseek-v4-flash-thinking for DeepThink, and a -nosearch id when you want an answer without search.
npm run auth
npm startIf DeepSeek answers 401 or 403, or the PoW challenge fails, sign in again and replace deepseek-auth.json.
These paths stay out of git:
deepseek-auth.jsonaccounts/*.json- Chrome profile directories used by the auth script
.env
Syntax and unit tests, no network:
npm testLive smoke tests against a proxy that is already running:
BASE_URL=http://127.0.0.1:9655 MODEL=deepseek-v4-flash npm run test:livenpm test runs node --check on the entry points and node --test on tests/unit.test.js and tests/account-failover.test.js. It does not call DeepSeek: the failover tests serve every upstream call from an in-process mock.
FreeDeepseekAPI is a local Web-chat proxy. It depends on the current chat.deepseek.com contract. When that contract changes, auth or the request body may need an update.
If a call stops working:
- Update to the newest version (Updates).
- Refresh the login with
npm run auth. - Run
npm run doctor. - Read
GET /v1/model-capabilitiesand use a DeepSeek-V4.1-Flash id. - If the same chat keeps failing,
POST /reset-session?agent=<id>and try once more. - If it still fails, DeepSeek has likely changed the private Web API.
Security reports go to a private GitHub advisory, not a public issue. See SECURITY.md. The maintainer list is CONTRIBUTORS.md.
Apache-2.0. Copyright 2026 dekrezz.

