A tiny self-hosted chat gateway that speaks the OpenAI API protocol.
One binary. One model. No bells and whistles.
Send standard OpenAI-format chat requests, get standard OpenAI-format responses back.
- 🗣️
POST /v1/chat/completions— stream & non-stream, OpenAI protocol - 🧠 Deep thinking by default — reasoning exposed as
reasoning_content, opt-out per request - 🖼️ Image understanding —
image_urlcontent parts (http(s) URLs & base64 data URLs) - 📋
GET /v1/models— exactly one model:deepseek-flash - 🔄 Multi-account rotation — round-robin pool with in-flight caps & health states
- 🛡️ Hardened failure paths — bounded retry ladder, stream idle watchdog, honest error termination
- 🔑 Optional API key auth —
DS_API_KEY(unset = open access) - 🧹 Async session cleanup — upstream sessions deleted after every completion, off the response path
- 📦 Single static binary — distroless Docker image, zero external Go dependencies
cp .env.example .env # optional API key
# create accounts.json:
# {"accounts":[{"mobile":"...","email":"","password":"..."}]}
chmod 600 accounts.json
docker compose up -d # serves :9879 -> 8080Point any OpenAI client at http://host:9879/v1 with model deepseek-flash:
curl http://localhost:9879/v1/chat/completions \
-H "Authorization: Bearer $DS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-flash","messages":[{"role":"user","content":"hi"}]}'| Env | Default | Purpose |
|---|---|---|
DS_ADDR |
:8080 |
listen address |
DS_ACCOUNTS |
./accounts.json |
account file (0600) |
DS_API_KEY |
unset | require Authorization: Bearer / X-Api-Key when set |
DS_MAX_INFLIGHT |
2 |
per-account concurrent request cap |
Deep thinking is on by default; opt out per request with "thinking": {"type": "disabled"}:
Reasoning arrives as reasoning_content — streamed as delta.reasoning_content
chunks before the delta.content chunks, and as message.reasoning_content on
non-stream responses. Thinking is best-effort: if the upstream model skips
thinking, you simply get an answer with no reasoning. Malformed switch values
(e.g. {"type": "banana"}) are rejected with 400.
go build -o simple-chat .
go test ./...spec.md documents the full design and upstream contract; gap-analysis.md records
what we absorbed from a survey of 14+ similar projects — and what we deliberately rejected.
Statelessness is the design: full history passed on every call, a fresh upstream session per request, deleted asynchronously after. The client owns the conversation.
MIT — see LICENSE.
{"model": "deepseek-flash", "thinking": {"type": "disabled"}, // omit or "enabled" for thinking (default) "messages": [{"role": "user", "content": "hi"}]}