Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,18 @@ deepseek fim "def f():" --suffix " return x"
Reach for `anthropic` or `respond` when the task is specifically about
those wire formats. For a plain answer, use `chat`.

Only `respond` has JSON Schema output and server-side web search:
Only `respond` has JSON Schema output:

```bash
deepseek respond "Berlin" -s "Return city and country." --schema @city.json
deepseek respond "what shipped in Go 1.26" --web-search
```

There is no server-side web search any more. DeepSeek removed the
Responses API's `web_search` tool on 2026-09-10 and now ignores it, so
`respond --web-search` exits 1 with that reason and sends nothing. To
ground an answer, search yourself and pass the results with `--file` or
stdin.

### Account and cost

```bash
Expand Down Expand Up @@ -165,15 +170,12 @@ deepseek free off # forget the enrolment on this machine
```

Free-tier limits, per UTC day: 30 requests, 60K input tokens, 20K output
tokens, 3 web searches, 4K output per call, 128KB per request body,
flash only. A request for pro is **refused, not downgraded**. `models` and `status` cost no quota; everything that can
generate a token does.

`respond --web-search` works on the free tier and spends one of the three
daily searches. It is rationed that tightly because DeepSeek reads whole
pages into the prompt — one measured search request billed 40K input
tokens, about ten ordinary turns — so treat it as a few lookups a day, not
a research loop. Other server-side tools are still refused.
tokens, 4K output per call, 128KB per request body, `deepseek-flash`
only (the retired names `deepseek-v4-flash` and
`deepseek-v4-flash-vision-exp` are the same model). A request for pro is
**refused, not downgraded**. `models` and `status` cost no quota;
everything that can generate a token does. Server-side tools are refused,
`web_search` with the reason that DeepSeek removed it.

Errors from the gateway carry `"type":"free_tier_*"` and a message that
already contains the next step — do not append DeepSeek's own advice to
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The free tier relays your prompts to DeepSeek through a gateway run by
this project. No account, no API key.

gateway https://freeseek.1lm.io
model deepseek-v4-flash
model deepseek-flash
per day 30 requests · 60k input · 20k output tokens
privacy prompts and completions are relayed to DeepSeek and are not
stored or logged by this gateway; only token counts and cost
Expand Down Expand Up @@ -189,7 +189,7 @@ One command per endpoint, named for what it does.
| --- | --- | --- |
| `chat` | `POST /chat/completions` | The default. OpenAI format, the one most tools speak. |
| `anthropic` | `POST /anthropic/v1/messages` | The format Claude Code and the Anthropic SDKs speak. |
| `respond` | `POST /responses` | The format Codex speaks. JSON Schema output and server-side `web_search` live only here. |
| `respond` | `POST /responses` | The format Codex speaks. JSON Schema output lives only here. |
| `fim` | `POST /beta/completions` | Fill in the middle — the shape editors use for inline completion. |
| `models` | `GET /models` | Available models, joined with the published rate card. |
| `balance` | `GET /user/balance` | What is left, per currency. |
Expand All @@ -209,7 +209,7 @@ One command per endpoint, named for what it does.
deepseek chat "why is the sky blue"
git diff | deepseek chat "write a commit message"
deepseek chat "explain" --file server.go --file server_test.go
deepseek chat "review this" --model deepseek-v4-pro --effort max
deepseek chat "review this" --effort max
deepseek chat "summarise" --system @house-style.md
```

Expand Down Expand Up @@ -322,11 +322,11 @@ page keeps the upstream URL it was converted from.
```bash
# Anthropic Messages. Claude model names are accepted and remapped
# server-side; the usage line shows both so cost stays traceable.
deepseek anthropic "hello" --model claude-opus-4-1
# · claude-opus-4-1→pro · 10 in · 8 out · ~$0.000011 · 0.9s
deepseek anthropic "hello" --model claude-sonnet-4-5
# · claude-sonnet-4-5→flash · 10 in · 8 out · ~$0.000006 · 0.9s

# Responses: JSON Schema output, and a web_search tool DeepSeek runs
deepseek respond "what shipped in Go 1.26" --web-search
# Responses: JSON Schema output. Its server-side web_search tool was
# removed upstream on 2026-09-10; --web-search now exits with that reason.
deepseek respond "Berlin" -s "Return city and country." --schema @city.json

# FIM: prefix in, suffix optional, the middle comes back
Expand Down Expand Up @@ -366,13 +366,13 @@ JSONL ledger:

```console
$ deepseek usage --since 7d
CALLS IN CACHED OUT COST
deepseek-v4-flash 184 2.1M 78% 94k $0.19
deepseek-v4-pro 12 88k 41% 11k $0.03
total 196 2.2M 77% 105k $0.22
CALLS IN CACHED OUT COST
deepseek-flash 184 2.1M 78% 94k $0.13
deepseek-v4-pro 12 88k 41% 11k $0.06
total 196 2.2M 77% 105k $0.19

by format: chat 170, anthropic 14, responses 8, fim 4
context cache saved ~$0.23 (1.7M of 2.2M prompt tokens replayed)
context cache saved ~$0.26 (1.7M of 2.2M prompt tokens replayed)
costs are estimates from the published USD rate card, not billed amounts
```

Expand Down
25 changes: 23 additions & 2 deletions TASTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ return genuinely different objects — `choices[].message` versus
with opposite cache conventions. A unified command has two ways out and
both are bad: normalize, and `--json` lies about what the API sent; or
leak, and one command emits three shapes depending on a flag. Meanwhile
format-specific parameters (`web_search` and JSON Schema only exist on
Responses, `prefix` only on beta chat) have to hide behind conditional
format-specific parameters (`web_search` — until DeepSeek removed it on
2026-09-10 — and JSON Schema only exist on Responses, `prefix` only on
beta chat) have to hide behind conditional
validation. One verb, four behaviours, and the user still has to know
which format they are in — the surface got smaller while the task got
harder.
Expand Down Expand Up @@ -472,3 +473,23 @@ the ration are both sized to a single measurement and should be re-measured
when the tool changes. If a search request is ever observed above 256k
input tokens in production, that is the signal to raise the allowance
rather than to quietly accept the overshoot.

**Expired 2026-09-18, by upstream.** DeepSeek removed `web_search` from
the Responses API on 2026-09-10, with V4.1 Flash: the guide now lists it
among the built-in tools that are *ignored*, and its streaming events are
gone. A probe on 2026-09-18 confirmed it — the tool was accepted and
echoed, no search ran, and the request billed 21 input tokens where the
measured search above billed 40,260. The ration and the allowance
therefore priced searches that could no longer happen, and the flag sent
a request that answered from the model's memory to a caller who believed
it had searched.

What replaced it: the gateway refuses the tool with a 400 that says why,
`respond --web-search` exits 1 with the same reason and sends nothing,
and `DailySearches` and `searchInputAllowance` are deleted. The flag is
kept, hidden, so a script that still passes it learns the reason instead
of reading "unknown flag" as a bug here. Refusing rather than stripping
the tool and forwarding is the same judgement as refusing pro rather than
downgrading it: a caller who asked for something must not be handed less
without being told. If DeepSeek brings a server-side search back, this
entry and the one above are the arithmetic to redo, not to restore.
58 changes: 38 additions & 20 deletions gateway/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ expensive seven hours.
| `DSGATE_ANON_DAILY_REQUESTS` | 30 | enough to be useful for a day's work, not enough to script against |
| `DSGATE_ANON_DAILY_INPUT_TOKENS` | 60000 | ~150 pages of context per day |
| `DSGATE_ANON_DAILY_OUTPUT_TOKENS` | 20000 | the expensive side; the real cap |
| `DSGATE_ANON_DAILY_SEARCHES` | 3 | a `web_search` request costs ~10 ordinary turns; the request counter alone would let one caller take a quarter of the day |
| `DSGATE_ANON_MAX_TOKENS` | 4096 | clamps a single response, bounding overshoot |
| `DSGATE_MAX_BODY_BYTES` | 131072 | ~32K tokens; bounds the input side of overshoot |
| `DSGATE_DAILY_BUDGET_USD` | 1.00 | the circuit breaker; the number that actually protects us |
Expand All @@ -323,22 +322,29 @@ expensive seven hours.
| `DSGATE_TOKEN_TTL_DAYS` | 7 | identities age out instead of accumulating |
| `DSGATE_BALANCE_CHECK_MINUTES` | 15 | the ledger's "we have credit" is checked against the real account |

**`web_search` is carried, and rationed.** Measured on 2026-08-07, one
search request made eleven server-side calls and billed 40,260 input
tokens with no separate per-search fee — so its whole cost arrives as
input tokens the meter already reads. What it breaks is the *reservation*,
which bounded input at one token per body byte: DeepSeek chooses how many
pages to read, so a search's input is upstream-controlled. Hence a 256k
input allowance at admission (about 6x the observed case) plus the daily
ration above. Within that allowance the budget is still a hard ceiling;
past it a search can overshoot by the difference, bounded by how many
distinct callers can be mid-search at once. Every other server-side tool
stays refused: unknown work at an unknown price, spent from donated
credit. Reasoning and the expiry condition are in `TASTE.md`.

**Free tier is flash only.** Pro is 3x the price and the request is
*rejected*, not silently downgraded — a user who asked for pro and got
flash without being told would draw wrong conclusions and blame the model.
**No server-side tool is carried.** From 2026-08-07 to 2026-09-18 the
free tier carried `web_search`, with a 3/day ration and a 256k input
allowance at admission, because one measured search billed 40,260 input
tokens. DeepSeek removed the tool from the Responses API on 2026-09-10,
with V4.1 Flash: the guide now lists `web_search` among the built-in
tools that are *ignored*, and a probe on 2026-09-18 billed 21 input
tokens where a search used to bill ~40K. Forwarding it would bill an
answer from the model's memory to a caller who believes it searched, so
it is refused with that reason, and the ration and the allowance are
gone. Every other server-side tool stays refused: unknown work at an
unknown price, spent from donated credit. The history is in `TASTE.md`.

**Free tier is `deepseek-flash` only.** It is DeepSeek's current Flash
model (V4.1, 2026-09-10); `deepseek-v4-flash` and
`deepseek-v4-flash-vision-exp` are retired names upstream serves as the
same model, so the gateway accepts them as that model and pins the
request to the name upstream lists. A `DSGATE_MODEL` that still says
`deepseek-v4-flash` is served as `deepseek-flash` for the same reason —
otherwise `/models`, which upstream now answers with the new name only,
would filter to nothing. Pro is 3.3x flash's price per output token and
the request is *rejected*, not silently downgraded — a user who asked for
pro and got flash without being told would draw wrong conclusions about
the model.

---

Expand Down Expand Up @@ -408,7 +414,7 @@ stylesheet; the donation path is a private message to a human.

The key pool answers "what if our key runs out". It does not answer the
prior question — why is a chat request costing us anything at all, when
OpenCode Zen serves `deepseek-v4-flash-free` for nothing.
OpenCode Zen served `deepseek-v4-flash-free` for nothing.

So there are two upstreams now, tried in order. The indirection is not
"a list of interchangeable backends"; a second lane earns its keep only
Expand All @@ -419,7 +425,7 @@ the model by, what it costs, and how often it says no.
Measured against Zen on 2026-08-12:

- `/chat/completions` works, reporting usage in both streamed and
buffered form. `/responses` answers but rejects a server-side
buffered form. `/responses` answers but rejected a server-side
`web_search` tool. `/anthropic/v1/messages`, `/beta/completions` and
`/user/balance` are 404.
- About one sequential request in five comes back
Expand All @@ -428,11 +434,23 @@ Measured against Zen on 2026-08-12:
- The model is `deepseek-v4-flash-free` there. Our callers never learn
that: `policy.Retarget` renames the field on the way out, and the
allowlist, the `/models` list and the client contract all keep saying
`deepseek-v4-flash`.
the served name — `deepseek-flash` since 2026-09-18.
- Zen's own docs say free-lane data **may be used to improve the model**.
That is a different promise from the paid path's and belongs in the
user-facing copy, not just here.

**Re-measured 2026-09-18: the lane is dead.** Zen still lists
`deepseek-v4-flash-free` and answers every request with
`Model is unavailable` (V4 Flash was retired upstream on 2026-09-10).
The fallback keeps the service correct — every chat still lands on the
DeepSeek key — but each one pays a refused round trip, and its prompt
still reaches Zen. So the operator step is to unset `OPENCODE_API_KEY`
until Zen serves a free V4.1 model. The rename above is why that check
cannot be skipped: callers are told `deepseek-flash` whatever the lane
runs, so a revived free lane has to be shown to serve V4.1 (the 53-token
billed prompt offset between the V4 and V4.1 families is one test)
before it goes back on.

A 20% refusal rate is why the lane is first-choice rather than the whole
service, and why the fallback must happen before a single byte reaches
the client — after `WriteHeader` the status line is spent. Any non-2xx
Expand Down
21 changes: 16 additions & 5 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ What it changes about a request, and nothing else:

| | |
|---|---|
| `model` | pinned to flash; a pro request is **refused**, not downgraded |
| `model` | pinned to `deepseek-flash`; the retired flash names are the same model; a pro request is **refused**, not downgraded |
| `max_tokens` | clamped to the free-tier ceiling |
| `n`, `best_of` | refused above 1 — they multiply the cost of one admitted request |
| server-side tools | refused; `web_search` with the reason: DeepSeek removed it on 2026-09-10 and now ignores it |
| user identity | overwritten with the token's subject |

That last one is not a nicety. DeepSeek documents `user_id` as the
Expand All @@ -63,15 +64,25 @@ Measured against Zen on 2026-08-12, which is why the lane is this narrow:
|---|---|
| refusal rate | ~20% of sequential requests, `429 FreeUsageLimitError` |
| `/chat/completions` | works; usage reported streamed and buffered |
| `/responses` | answers, but rejects a server-side `web_search` tool |
| `/responses` | answers, but rejected a server-side `web_search` tool |
| `/anthropic/v1/messages`, `/beta/completions`, `/user/balance` | 404 |
| the model's name there | `deepseek-v4-flash-free`, aliased at the last moment |
| privacy | Zen says free-lane data **may be used to improve the model** |

So it carries `chat` and nothing else. FIM, the Anthropic and Responses
formats, web search and the model list all go straight to DeepSeek, and
the caller's contract does not change: they ask for `deepseek-v4-flash`,
by that name, on every route.
formats and the model list all go straight to DeepSeek, and the caller's
contract does not change: they ask for `deepseek-flash`, by that name, on
every route.

**Measured 2026-09-18: the lane carries nothing right now.** Zen still
lists `deepseek-v4-flash-free`, but answers every request with
`Model is unavailable` — V4 Flash was retired upstream on 2026-09-10 and
Zen has no free V4.1 model. Every chat request therefore pays one refused
round trip and still lands on the DeepSeek key, and its prompt still
reaches Zen on the way. Leave `OPENCODE_API_KEY` unset until Zen serves a
free `deepseek-flash`, and check that the model it serves is V4.1 before
turning it back on: the gateway renames the model on the way out, so
callers would be told `deepseek-flash` whatever Zen actually runs.

The interesting consequence is what happens when the money runs out.
A request that the free lane can serve is admitted **past** the daily
Expand Down
6 changes: 2 additions & 4 deletions gateway/cmd/dsgate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,14 @@ behaves exactly as it did before this existed.
DSGATE_ADDR listen address (:8787)
DSGATE_STATE_DIR journal, secret, revocations (./state)
DSGATE_SECRET token signing secret, hex (generated and persisted)
DSGATE_MODEL the only model served (deepseek-v4-flash)
DSGATE_MODEL the only model served (deepseek-flash)
DSGATE_ANNOUNCE public URL, shown to clients

Per-user daily limits:

DSGATE_ANON_DAILY_REQUESTS (30)
DSGATE_ANON_DAILY_INPUT_TOKENS (60000)
DSGATE_ANON_DAILY_OUTPUT_TOKENS (20000)
DSGATE_ANON_DAILY_SEARCHES (3) server-side web searches per user
DSGATE_ANON_MAX_TOKENS (4096) per-request output cap
DSGATE_MAX_BODY_BYTES (131072) per-request body cap
DSGATE_REQUESTS_PER_MINUTE (20) per-address burst
Expand Down Expand Up @@ -152,7 +151,6 @@ func run() error {
DailyRequests: envInt("DSGATE_ANON_DAILY_REQUESTS", 30),
DailyInputTokens: envInt("DSGATE_ANON_DAILY_INPUT_TOKENS", 60000),
DailyOutputTokens: envInt("DSGATE_ANON_DAILY_OUTPUT_TOKENS", 20000),
DailySearches: envInt("DSGATE_ANON_DAILY_SEARCHES", 3),
DailyBudgetUSD: envFloat("DSGATE_DAILY_BUDGET_USD", 1.00),
TotalBudgetUSD: envFloat("DSGATE_TOTAL_BUDGET_USD", 20.00),
}
Expand Down Expand Up @@ -180,7 +178,7 @@ func run() error {
UpstreamBaseURL: env("DSGATE_UPSTREAM_BASE_URL", "https://api.deepseek.com"),
UpstreamKeys: keys,
KeyStatePath: filepath.Join(stateDir, "donated-keys.json"),
Model: env("DSGATE_MODEL", "deepseek-v4-flash"),
Model: env("DSGATE_MODEL", "deepseek-flash"),
FreeBaseURL: env("DSGATE_FREE_BASE_URL", "https://opencode.ai/zen/v1"),
FreeKeys: freeKeys,
FreeModel: env("DSGATE_FREE_MODEL", "deepseek-v4-flash-free"),
Expand Down
16 changes: 13 additions & 3 deletions gateway/deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DSGATE_UPSTREAM_KEY=sk-... dsgate
```

That listens on `:8787`, writes state to `./state`, serves
`deepseek-v4-flash`, and stops spending at **$1.00 a day / $20.00
`deepseek-flash` (DeepSeek-V4.1-Flash), and stops spending at **$1.00 a day / $20.00
total**. Point a CLI at it:

```bash
Expand All @@ -39,8 +39,18 @@ anything. `GET /v1/status` reports the share it is carrying under
Two things to know before turning it on. Zen's free lane says the prompts
it sees **may be used to improve the model**, which is a different promise
from the one the paid path makes, so say so wherever you tell users where
their prompts go. And it only carries `chat`: FIM, the Anthropic and
Responses formats, and web search still spend real credit.
their prompts go. And it only carries `chat`: FIM and the Anthropic and
Responses formats still spend real credit.

**As of 2026-09-18 Zen answers its free DeepSeek model with
`Model is unavailable` on every request** (V4 Flash was retired upstream
on 2026-09-10), so the lane saves nothing and adds a refused round trip
to every chat. Leave `OPENCODE_API_KEY` unset until that changes; see the
gateway README.

A `DSGATE_MODEL` written before 2026-09-10 (`deepseek-v4-flash`) still
works: the gateway serves it under the name upstream uses today,
`deepseek-flash`, and says so in its boot log.

## Docker

Expand Down
Loading
Loading