Skip to content

About

Turn chat.deepseek.com into a local, OpenAI-compatible HTTP API. Single-file Python CLI: one-time browser login captures your session, the server replays it for every request. Works with openai SDK, OpenCode, DeepSeek Harness, Claude Code

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

🔄 deepseek-web2api

Python 3.10+ License: MIT Tests: 116 passing Inspired by gemini-web2api

Convert chat.deepseek.com into a local, OpenAI-compatible HTTP API.

Run any OpenAI client — the official openai SDK, OpenCode, the DeepSeek Harness, LangChain, Cursor, Aider, Claude Code (via OPENAI_BASE_URL) — against your own signed-in DeepSeek account, with no separate DeepSeek API key.

Inspired by Sophomoresty/gemini-web2api. Single-file entry point, but adapted for DeepSeek's web protocol (PoW challenge, SSE patches, localStorage.userToken).


┌──────────────┐  OpenAI /v1   ┌──────────────────────┐  chat.deepseek.com  ┌────────────┐
│  any client  │ ────────────► │ deepseek_web2api.py  │ ──────────────────► │  DeepSeek  │
│ OpenCode,    │ ◄──────────── │  (FastAPI + uvicorn) │ ◄────────────────── │   web      │
│ Claude Code, │     SSE       │                      │       SSE          └────────────┘
│ harness...   │               └──────────────────────┘
└──────────────┘                         ▲
                                         │  captured once via Chromium
                                         ▼
                                  ┌──────────────┐
                                  │  your login  │
                                  └──────────────┘

✨ Features

  • OpenAI Chat Completions API (/v1/chat/completions) — streaming + non-streaming
  • OpenAI Responses API (/v1/responses) — Codex CLI compatible
  • Auto session refresh — once you've logged in, future runs reuse the persistent browser profile
  • PoW solving in-process — runs DeepSeek's own sha3_wasm_bg.wasm via wasmtime, no round-trip to a real browser for every request
  • THINK / RESPONSE fragment filtering — only the user-facing reply is exposed as content; reasoning is filtered out
  • Multi-turn via conversation_id — DeepSeek thread lineage preserved across requests
  • Multiple model ids — deepseek-chat, deepseek-reasoner, deepseek-expert, deepseek-expert-reasoner, plus OpenAI-style aliases
  • Optional API-key auth (--api-key) — turn the proxy into a private relay
  • Cross-platform — Linux, macOS, Windows; Chromium via Playwright
  • Zero auth bypass — this isn't a free DeepSeek. It uses your account, your quota, your normal web login.

⚠️ Honest status

What Status
OpenAI Chat Completions wire format ✅ verified — 28/28 checks pass with the official openai Python SDK
OpenAI Responses API (Codex CLI) ✅ basic mapping (input + instructions + previous_response_id + stream)
Unit tests ✅ 9 groups / 51 assertions pass
Server boots, /v1/models, /healthz ✅
Chromium login + cookie/userToken capture ✅ implemented; login subcommand handles the AWS WAF human-check via a visible window
Temp-mail signup (mail.tm / tempmailc) ✅ implemented; signup subcommand drives the DeepSeek signup form
End-to-end with a real captured session ⚠️ requires interactive login on a desktop (captcha). Implemented, not auto-runnable in headless sandboxes.
PoW round-trip against real DeepSeek ⚠️ the bundled WASM is DeepSeek's own module. Works against real challenges; synthetic tests in this sandbox can't fully verify it.

See tests/VERIFICATION.md for the full honest list.


📦 Install

Requires Python 3.10+ and a Chromium-capable OS (Linux/macOS/Windows; WSL works).

git clone https://github.com/<you>/deepseek-web2api.git
cd deepseek-web2api
pip install -r requirements.txt
playwright install chromium

requirements.txt

playwright>=1.44
httpx>=0.27
fastapi>=0.111
uvicorn[standard]>=0.30
pydantic>=2.7
curl_cffi>=0.7
wasmtime>=21.0

🚀 Usage

1. One-time: log in

python deepseek_web2api.py login
# → a Chromium window opens on https://chat.deepseek.com/sign_in
#   log in with your email + password (or Google), solve the AWS WAF human-check
#   the script polls localStorage.userToken, saves it under data/session.json, exits

Subsequent runs reuse the persistent profile in data/profile/, so the login step is only needed once (or when the session expires after ~6 hours — python deepseek_web2api.py refresh silently re-captures).

2. Start the server

python deepseek_web2api.py serve --host 127.0.0.1 --port 8082
# → 🚀 DeepSeek Web2API listening on http://127.0.0.1:8082
#    OpenAI base URL: http://127.0.0.1:8082/v1

Flags:

Flag Default What it does
--host 127.0.0.1 Bind address (use 0.0.0.0 to expose on the network)
--port 8082 HTTP port
--api-key (none) If set, require Authorization: Bearer <key> on /v1/*
--log-level info debug / info / warning / error
--no-interactive-login off Never pop a browser; return 503 if no cached session

3. Point any OpenAI client at it

Python (openai SDK)

from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8082/v1", api_key="not-needed")

for chunk in client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "hi"}],
    stream=True,
):
    print(chunk.choices[0].delta.content or "", end="")

curl

curl http://127.0.0.1:8082/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-reasoner",
    "messages": [{"role": "user", "content": "Reply with the word ready."}]
  }'

OpenCode — drop in ~/.config/opencode/opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "deepseek-local": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "DeepSeek (local web2api)",
      "options": {
        "baseURL": "http://127.0.0.1:8082/v1",
        "apiKey": "anything"
      },
      "models": {
        "deepseek-chat": {},
        "deepseek-reasoner": {},
        "deepseek-expert": {},
        "deepseek-expert-reasoner": {}
      }
    }
  }
}

DeepSeek Harness — in $DSH_HOME/settings.yaml:

llm-pi-ai:
  providers:
    deepseek-local:
      apiKeyEnv: ANYTHING
      api: openai-completions
      baseURL: http://127.0.0.1:8082/v1
      models:
        - id: deepseek-chat
        - id: deepseek-reasoner
        - id: deepseek-expert

Claude Code — set two env vars:

export OPENAI_BASE_URL=http://127.0.0.1:8082/v1
export OPENAI_API_KEY=anything
export OPENAI_MODEL=deepseek-chat

📡 API endpoints

Method Path Notes
GET /healthz liveness probe
GET /v1/models list model ids
POST /v1/chat/completions OpenAI Chat Completions (stream + non-stream)
POST /v1/responses OpenAI Responses API (Codex CLI compatible)

Response shape

Non-streaming reply:

{
  "id": "chatcmpl-…",
  "object": "chat.completion",
  "created": 1700000000,
  "model": "deepseek-chat",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "Hello! How can I help?"},
    "finish_reason": "stop"
  }],
  "usage": {"prompt_tokens": 4, "completion_tokens": 6, "total_tokens": 10},
  "conversation_id": "abcd-1234-…:42"
}

Streaming follows the standard OpenAI chat.completion.chunk shape: a role chunk first, then a sequence of content deltas, then a finish chunk carrying the conversation_id, and a final data: [DONE].

Models

id DeepSeek model_type thinking
deepseek-chat default off
deepseek-reasoner default on (DeepThink)
deepseek-expert expert off
deepseek-expert-reasoner expert on
deepseek-v3 alias for deepseek-chat
deepseek-r1 alias for deepseek-reasoner

Unknown model ids are accepted (fall through to deepseek-chat) — better than hard-failing a friendly typo. To add a new id, edit MODELS in deepseek_web2api/format.py.

Multi-turn

Each non-streaming reply (and the final streaming chunk) carries a conversation_id of the form <chat_session_id>:<last_message_id>. Pass it back in the next request's conversation_id field to keep the same DeepSeek thread — the server resumes with the thread's existing parent_message_id chain.

reply1 = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Tell me a joke."}],
)
print(reply1.choices[0].message.content)

reply2 = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Another one."}],
    extra_body={"conversation_id": reply1.conversation_id},
)
print(reply2.choices[0].message.content)

Note: a thread's model is fixed when it's created. Resuming keeps the original model; passing a different model on resume is silently ignored.


🧠 How it works

  1. Login — Playwright opens a persistent Chromium profile at data/profile/, navigates to chat.deepseek.com/sign_in, waits for the user to sign in (solving the AWS WAF human-check), and reads the bearer token from localStorage.userToken plus the cookies. Saved to data/session.json with 0o600 permissions.

  2. Refresh — subsequent calls (or the first server request) try a headless capture from the same profile. Only a full expiry (no token found, all cookies gone) triggers a visible re-login.

  3. Chat — the server's pure-HTTP client calls DeepSeek's internal API:

    POST /api/v0/chat_session/create        → new thread id
    POST /api/v0/chat/create_pow_challenge  → get a PoW challenge
    solve it via the bundled WASM
    POST /api/v0/chat/completion            → SSE stream (with x-ds-pow-response header)
    
  4. SSE parsing — DeepSeek's stream is JSON-patch format:

    {"v": {"response": {"fragments": [
      {"type": "THINK",    "content": "Let me think..."},
      {"type": "RESPONSE", "content": "The answer is"}
    ], "message_id": 42}}}
    {"p": "response/fragments/-1/content", "o": "APPEND", "v": " 42"}
    {"v": "."}

    We track which paths point at RESPONSE fragments and skip THINK/SEARCH content so the user only sees the actual reply.

  5. PoW — DeepSeek gates /api/v0/chat/completion behind a proof-of-work header (x-ds-pow-response). The algorithm ("DeepSeekHashV1") is shipped as a WebAssembly module on DeepSeek's own CDN (fe-static.deepseek.com/.../sha3_wasm_bg.wasm). We bundle the same module in deepseek_web2api/sha3_wasm_bg.wasm and run it via wasmtime — same code the browser runs, no round-trip, no reimplementation.


🗂 File layout

deepseek_web2api/
├── deepseek_web2api.py         # single-file CLI entry point
├── deepseek_web2api/           # package — the actual implementation
│   ├── pow.py                  # PoW solver (runs DeepSeek's WASM via wasmtime)
│   ├── auth.py                 # Chromium login + session capture + CLI
│   ├── client.py               # pure-HTTP DeepSeek client
│   ├── format.py               # OpenAI <-> DeepSeek message conversion
│   ├── server.py               # FastAPI app
│   ├── tempmail.py             # mail.tm + tempmailc.com helpers (test only)
│   ├── signup.py               # browser-driven signup automation (test only)
│   └── sha3_wasm_bg.wasm       # DeepSeek's PoW module, bundled
├── tests/
│   ├── test_units.py           # 51 unit assertions
│   ├── test_openai_compat.py   # 28 OpenAI SDK compat assertions
│   ├── test_orchestration.py   # 37 full-code-path assertions with mock client
│   ├── test_e2e.py             # signup → capture → serve → query (needs real account)
│   ├── run_all.py              # runs all 3 deterministic suites sequentially
│   └── VERIFICATION.md         # what works, what doesn't, why
├── data/                       # gitignored — your session + browser profile live here
├── README.md
├── requirements.txt
└── .gitignore

🧪 Development

Run the tests

# 3 deterministic suites (no DeepSeek account needed)
python -m tests.run_all

# individually
python -m tests.test_units           # 9 groups / 51 assertions
python -m tests.test_openai_compat   # 28 assertions (boots server, drives openai SDK)
python -m tests.test_orchestration   # 37 assertions (boots server, mock client)

The test_e2e.py script drives a real DeepSeek signup (with a temp-mail address) and tests the full flow. It needs a real Chromium environment where you can solve the AWS WAF captcha — it won't run end-to-end in a headless sandbox.

Project conventions

  • Single-file CLI at the root (deepseek_web2api.py) with subcommands.
  • Implementation lives in the deepseek_web2api/ package.
  • All file I/O writes data/ with 0o600 / 0o700 for credential material.
  • Bearer tokens are never logged.
  • Public functions have docstrings; types are PEP 604 (X | None).
  • Server is FastAPI on uvicorn, with explicit CORS-free defaults (it's a local proxy — don't expose it on the public internet without a reverse proxy and auth).

🔐 Security

  • data/session.json contains your bearer token. Keep it private. The file is created with 0o600 perms.
  • The bundled sha3_wasm_bg.wasm belongs to DeepSeek — included only because it's required to talk to their public web API.
  • Default bind is 127.0.0.1. Don't expose this server on the public internet without putting it behind a reverse proxy (nginx, caddy, etc.) and a real auth layer.
  • --api-key is intentionally minimal — a Bearer token check via a Starlette middleware. It is not a substitute for TLS, rate limiting, or audit logging.
  • authorization: Bearer <your-token> is sent on every upstream request to chat.deepseek.com. If you set log_requests: true (future feature) or share a core dump, that token is exposed. Don't.

🧯 Troubleshooting

Symptom Likely cause / fix
playwright._impl._errors.Error: Executable doesn't exist Run playwright install chromium
LoginRequired: No DeepSeek session found Run python deepseek_web2api.py login once to capture a session, or set --no-interactive-login + load data/session.json
RuntimeError: PoW solver returned no answer The bundled WASM is stale — DeepSeek shipped a new one. Set DS_POW_WASM=/path/to/new.wasm after downloading from https://fe-static.deepseek.com/chat/static/sha3_wasm_bg.*.wasm
openai.AuthenticationError: 401 from upstream Session expired (cookies, token, or both). Run python deepseek_web2api.py refresh to silently re-capture, or login again.
403 Request blocked from chat.deepseek.com AWS WAF thinks you're a bot. Run login in a visible window and complete the captcha.
No message arrived for <email> within 120s DeepSeek's signup email went to spam, or the email service is down. Try a different temp-mail provider.
Server returns 502 upstream_error DeepSeek's protocol changed or you're being rate-limited. Check data/session.json is fresh.

📜 License

MIT — see LICENSE.

The bundled deepseek_web2api/sha3_wasm_bg.wasm belongs to DeepSeek and is included only because it's required to talk to their public web API.


🙏 Acknowledgements


⚖️ Disclaimer

This is unofficial, not affiliated with DeepSeek, and not endorsed by them. Use at your own risk. The DeepSeek web protocol is not a public API and can change without notice. This tool reuses your signed-in session to talk to your own account — it does not bypass authentication, does not create free DeepSeek usage, and does not scale to industrial workloads. Read DeepSeek's Terms of Service before using.

About

Turn chat.deepseek.com into a local, OpenAI-compatible HTTP API. Single-file Python CLI: one-time browser login captures your session, the server replays it for every request. Works with openai SDK, OpenCode, DeepSeek Harness, Claude Code

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages