Run a free DeepSeek web account as an OpenAI-compatible backend, then point opencode at it.
The DeepSeek web chat costs nothing. The metered platform API costs money. This project bridges the first one into the shape the second one has, keeps the whole thing on loopback, and paces every upstream request so an autonomous coding agent does not fire a turn every 200 ms and get the account flagged.
Two files do the work:
| file | job |
|---|---|
relay/extract.py |
reads the login token out of a dedicated Firefox profile |
relay/relay2.py |
serves POST /v1/chat/completions on 127.0.0.1:20232 |
- How it works
- The tag contract
- Requirements
- Install
- Models
- Environment variables
- Limits
- Troubleshooting
- Credits
- License
opencode speaks OpenAI. The relay translates:
- in — the harness's
systemprompt and JSON tool schemas become a plain text tag contract (below), plus a short reminder every turn so long sessions do not drift into prose. - upstream — the prompt is sent to
chat.deepseek.comwith the browser's own TLS/HTTP2 fingerprint, a solved proof-of-work header, and the chat session id from the previous turn, so the account sees one continuous conversation. - out — the reply is parsed back into assistant text or
tool_calls, and streamed to the harness as SSE deltas word by word.
Everything binds to loopback. No port forwarding, no public listener.
DeepSeek's web endpoint has no tool-calling API, so tools are described in text and read back out of text. Three reply dialects are tolerated, because the web client is not consistent about which one it produces:
<bash>{"command": "ls -la"}</bash> plain tags
<DSML||tool▁calls||> … DeepSeek markup
< calls> … reasoning model, bare
Unterminated tags are auto-closed before parsing, stray separators and broken
closers are swept, and fenced code blocks are left alone — so a model that
forgot to close a tag still yields a usable {"cmd": "ls -la"} instead of a
turn of garbage.
- Windows, macOS, or Linux
- Python 3.10+
- Firefox (real, installed — the extractor drives it)
- geckodriver on
PATH - A free chat.deepseek.com account, logged in once
git clone https://github.com/Nbill27/opencode-deepseek-web.git
cd opencode-deepseek-web
pip install -r requirements.txtrequirements.txt pulls deepseek-pow (proof-of-work), wasmtime (its
runtime), curl_cffi (browser TLS fingerprint), and selenium (token
extraction).
The relay needs a login token. It reads it from a Firefox profile you created for this, never your daily one.
-
Close every Firefox window.
-
Create a fresh profile and start it:
firefox -profile ./ff-profile -no-remote
Windows (default install path):
& "C:\Program Files\Mozilla Firefox\firefox.exe" -profile .\ff-profile -no-remote
-
In that window, go to https://chat.deepseek.com and sign in.
-
Send one message so the session actually exists.
-
Close that Firefox completely. The token lives in memory until it is flushed to disk, and the extractor will read a stale one if the window is still open.
If your Firefox lives somewhere unusual, or geckodriver is not on PATH, tell
the extractor:
export FIREFOX_BIN="/path/to/firefox"
export GECKODRIVER_PATH="/path/to/geckodriver"
export CHATBRAIN_FF_PROFILE="./ff-profile"python relay/extract.pyWrites relay/.websession.json (already in .gitignore — it is a live login,
treat it like a password). Re-run this whenever the account gets logged out.
python relay/relay2.pyIt answers on http://127.0.0.1:20232. Check it:
curl http://127.0.0.1:20232/v1/models{"object": "list", "data": [
{"id": "deepseek-chat", "object": "model"},
{"id": "deepseek-reasoner", "object": "model"}
]}A real completion:
curl http://127.0.0.1:20232/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-chat","stream":true,
"messages":[{"role":"user","content":"say hi"}]}'On Windows, start.ps1 does steps 2–3 and opens opencode for you:
./start.ps1~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"deepseek-web": {
"npm": "@ai-sdk/openai-compatible",
"name": "DeepSeek (web)",
"options": {
"baseURL": "http://127.0.0.1:20232/v1",
"apiKey": "unused-but-required"
},
"models": {
"deepseek-chat": { "name": "DeepSeek Chat" },
"deepseek-reasoner": { "name": "DeepSeek Reasoner" }
}
}
},
"model": "deepseek-web/deepseek-chat"
}Then opencode and /model to switch.
| id | upstream | notes |
|---|---|---|
deepseek-chat |
V3 chat | fast, the default; no visible reasoning |
deepseek-reasoner |
R1 | slower, thinking pass, emits the bare < calls> dialect |
Both stream. usage in the response is estimated (token counts are
approximated locally, not reported upstream) — good enough for cost display,
not for billing.
All optional. Defaults are sane.
| variable | default | meaning |
|---|---|---|
CHATBRAIN_PORT |
20232 |
listen port |
CHATBRAIN_SESSION_FILE |
relay/.websession.json |
where the token lives |
CHATBRAIN_LOG_FILE |
relay/relay2.log |
operational log |
CHATBRAIN_REPLIES_LOG |
relay/relay2-replies.log |
raw conversations — delete before sharing |
CHATBRAIN_MIN_GAP |
2.5 |
seconds between upstream calls (log-normally jittered) |
CHATBRAIN_JITTER |
1.5 |
log-normal spread of that gap |
CHATBRAIN_PAUSE_EVERY |
15 |
calls before a long idle pause |
CHATBRAIN_FAIL_COOLDOWN |
30 |
seconds to back off after an upstream error |
CHATBRAIN_IMPERSONATE |
firefox147 |
curl_cffi TLS target |
CHATBRAIN_UA |
derived | override the User-Agent (escape hatch) |
CHATBRAIN_FF_PROFILE |
./ff-profile |
extractor only |
FIREFOX_BIN, GECKODRIVER_PATH |
auto | extractor only |
Do not set CHATBRAIN_MIN_GAP to 0 and then leave an agent running. That is
exactly the pattern the pacing exists to prevent.
- This is a consumer chat account, not an API. Terms of service are on you. Do not publish the endpoint, share the box, or resell it.
- No image, file, or attachment input.
- No exact token counts —
usageis estimated. - Long conversations eventually hit the upstream context limit; the relay does not summarise for you.
- One account is one session chain. Two agents at once means interleaved context.
- Web upstream changes without notice. When replies come back as raw tags or
empty text, check
relay/relay2.logbefore assuming the relay broke.
| symptom | cause |
|---|---|
no session file at startup |
run python relay/extract.py |
| extractor finds no token | Firefox still open, or wrong profile, or not logged in |
| every request 401 / "expired" | token died — re-extract; the relay warns once in the log |
| replies empty or full of raw tags | upstream format changed; look at relay2-replies.log |
Connection refused on 20232 |
relay not running, or CHATBRAIN_PORT differs from baseURL |
| requests hang ~30 s | upstream rate limiting; raise CHATBRAIN_MIN_GAP |
| opencode shows no tools | model answered in prose; retry, and check the tag reminder in the log |
Lineage: dsh-better-bridge
by Samir Jamal — this relay is a fork of that codebase, hardened with a browser
TLS transport, session chaining, log-normal pacing, and a more forgiving reply
parser.
MIT — see LICENSE.