deepseek-webgui
Overview
Drives the private API behind chat.deepseek.com — the same one the React frontend calls. Useful when the user wants to use their existing webgui account (free quotas, browser-based reasoning + search) from a script, TUI, or another tool, and does not want to (or cannot) move to the paid OpenAI-compatible API at platform.deepseek.com.
The hard part is the proof-of-work: every /api/v0/chat/completion call requires an x-ds-pow-response header containing a base64-encoded answer to a server-issued DeepSeekHashV1 challenge. The algorithm is implemented as an SHA3-based WASM solver inside the frontend bundle. This skill ships that WASM and drives it from Python via wasmtime so no Node.js is required at runtime.
When to use this skill
Reach for it when any of the following appears:
- "use my deepseek web account / cookies / session", "free deepseek", "no API key"
- "deepseek-tui", "deepseek4free", "wrap chat.deepseek.com"
- "x-ds-pow-response", "DeepSeekHashV1", "POW challenge", "create_pow_challenge"
- "parse deepseek SSE", "THINK fragment", "TOOL_SEARCH", "JSON-Patch deepseek"
Prefer the official OpenAI-compatible API at https://api.deepseek.com whenever the user has (or can get) a paid API key. It is faster, supported, and has no anti-bot. This skill exists for the case where that is not an option.
Quick start
# 1) Capture credentials from the browser (one-time)
# a. Log into https://chat.deepseek.com in any browser.
# b. DevTools console: copy(localStorage.userToken) # paste into auth.json
# c. Cookie-Editor / EditThisCookie: export *.deepseek.com # save as cookies.json
# 2) Place credentials next to the prompt or anywhere convenient:
echo '{"token": "<paste userToken here>"}' > auth.json
# 3) Send a prompt.
scripts/run.sh "Summarise the difference between TLS 1.2 and 1.3 in 3 bullets."
# Flags:
# --thinking enable reasoning trace (THINK fragments → think_delta events)
# --search enable web search (TOOL_SEARCH fragment → tool_search events)
# --model expert use the reasoning model (default: "default" = DeepSeek-V3)
# --raw emit every parsed StreamEvent as JSON (machine-readable mode)
# --cookies/--auth override file paths
run.sh is a thin uv run --with wasmtime --with httpx wrapper. If uv is unavailable, pip install httpx wasmtime and run python3 scripts/client.py directly.
Architecture
prompt
│
▼
DeepSeekClient.completion()
│
├─ 1. POST /api/v0/chat_session/create → chat_session.id
├─ 2. POST /api/v0/chat/create_pow_challenge → challenge {algorithm, challenge, salt, expire_at, difficulty, signature}
├─ 3. solve_pow_challenge(...) → answer (int) (scripts/pow.py + sha3.wasm)
│ base64(JSON(answer + challenge + ...))→ x-ds-pow-response
│
└─ 4. POST /api/v0/chat/completion (streaming SSE)
│
└─ _parse_sse(...) → StreamEvent("delta"|"think_delta"|"tool_search"|"search_results"|"title"|"close"|"raw"|"ready")
Three layers of credentials are required and the API will reject calls missing any one of them:
Authorization: Bearer <userToken>fromlocalStorage.userToken.- Cookies on
*.deepseek.com(aws-waf-tokenfor WAF +ds_session_idfor session). - A handful of frontend identity headers (
x-app-version,x-client-platform, etc.) — the client sets them automatically.
If a request returns {"code":40002,"msg":"Missing Token"} the userToken is missing or expired. If the response is a 202 with x-amzn-waf-action: challenge, the aws-waf-token cookie has expired — the user needs to refresh chat.deepseek.com in the browser and re-export cookies.
Files
scripts/client.py—DeepSeekClient, SSE parser, CLI. Public surface:load_cookies(path) -> dict[str,str]— parses Cookie-Editor / EditThisCookie JSON.load_auth_token(path) -> str | None— readsDEEPSEEK_TOKENenv var or{"token": "..."}file.DeepSeekClient(cookies, token=...).completion(prompt, *, thinking, search, model_type, chat_session_id, parent_message_id) -> Iterator[StreamEvent].StreamEvent.kind:"ready" | "delta" | "think_delta" | "tool_search" | "search_results" | "title" | "close" | "raw". Thetextfield ondelta/think_delta/titleis the human-visible string to print or accumulate.
scripts/pow.py—solve_pow_challenge(...). Pure-Python wrapper around the bundled WASM viawasmtime. Includes a self-test against the captured HAR challenge that asserts answer == 49586.scripts/sha3.wasm— the unmodifiedsha3_wasm_bg.wasmfromfe-static.deepseek.com/chat/static/. ~26 KB. Containswasm_solvewhich is the iteration kernel.scripts/run.sh—uv-driven launcher.references/api_reference.md— full reverse-engineered API surface: endpoints, error codes, POW field semantics, SSE patch dialect with examples. Read this when extending the client (file uploads, history listing, session deletion).
Embedding the client into another tool
To wire this into a TUI or another agent:
from scripts.client import DeepSeekClient, load_cookies, load_auth_token, StreamEvent
cookies = load_cookies("cookies.json")
token = load_auth_token("auth.json")
with DeepSeekClient(cookies, token=token) as ds:
sid = ds.create_session()
full = []
for ev in ds.completion("Hello", chat_session_id=sid, thinking=True):
if ev.kind == "delta":
print(ev.text, end="", flush=True)
full.append(ev.text)
elif ev.kind == "think_delta":
... # render in a side panel
elif ev.kind == "close":
break
print("".join(full))
completion() is a generator — it yields events as bytes arrive over the SSE stream. The first call after create_session() is the slowest because it triggers a fresh POW challenge (~tens of milliseconds in the WASM solver, plus one extra round-trip).
Known caveats
- Token mortality: the
userTokenis long-lived but not eternal. When DeepSeek invalidates it (logout, password change, server-side revoke) the user has to re-export it. - POW expiry: each challenge is valid for
expire_afterms (5 minutes in the captures). The client always solves a fresh one percompletion()call, so this only matters if you cache challenges yourself. - Rate limits / banning: this is a personal account talking to a private API. Hammering it will get the account flagged. Don't.
- Schema drift: the SSE patch dialect is internal and may change between commits of the chat frontend. The parser falls through to
rawevents for anything it doesn't recognise — log those if a previously-working stream starts producing unexpected output.
Verification
The skill was built against a HAR capture of two real chat/completion round-trips. Verified:
solve_pow_challenge(...)reproduces the capturedanswer = 49586from(challenge, salt, expire_at, difficulty)._parse_sse(...)consumes the captured 123 KB SSE stream and yields the expected fragment partitioning (162 chars of THINK reasoning + 3569 chars of visible response + the云南最好玩推荐title).
End-to-end live verification requires the user's userToken. Without it the API responds {"code":40002,"msg":"Missing Token"} immediately — there is no anonymous mode or cookie-only fallback (verified directly).