dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DeepSeek Harness plugin that puts the conversation session id on OpenCode requests, so OpenCode's free tier works without a paid key.
Use OpenCode's free models in DeepSeek Harness. No proxy, no paid key, no sidecar, no config.
English | 简体中文
You point a DeepSeek Harness provider at opencode.ai, pick one of their free models, and send a message. It fails:
429 FreeUsageLimitError: Free usage limitThe name is misleading. Nothing is wrong with your quota, your key, or your model id. OpenCode's anonymous free tier admits a request only when three independent checks all pass, and answers every failure with the same 429 FreeUsageLimitError — including a retry-after that counts down to 00:00 UTC, so a rejected request looks exactly like a spent bucket:
advertises. Node's default hello is refused; the 17 suites the OpenCode CLI itself advertises, in that exact order, are accepted — including the eight legacy suites at the end, which is why reversing the list, dropping them or capping at TLS 1.2 each answers 429. Extension order is *not* compared, so a tuned ciphers list on plain node:https is enough: no TLS-impersonation addon, no Bun, no proxy.
x-opencode-session must carry an OpenCode ses_id. The check is the value's shape: a stale clock still passes (measured: an id minted 30 days ago), while abc-123, an empty value and the missing header are all refused. The plugin mints one per conversation and renews it, which is what keeps a session's affinity intact.
read must be present, the systemprompt must contain OpenCode's two-line ` block, and stream must be true. The tool's *definition* is not compared — dsh's own read` passes.
dsh satisfies none of these today, and none of them is something a provider config can fix.
Your realistic alternatives are all worse than the fix:
One plugin, running inside dsh's own Node process, does all three: advertises the accepted ClientHello, puts a ses_ id on the wire per conversation, adapts the body to what the gate inspects, and performs the request over node:https.
dsh plugin --profile web add github:mubaid/dsh-opencode-freeaccessRestart the profile. OpenCode's free models now work in dsh.
Before:
opencode / deepseek-v4.1-flash-free
> 429 {"type":"error","error":{"type":"FreeUsageLimitError","message":"Free usage limit…"}}After the install line above, same provider, same model, same key:
opencode / deepseek-v4.1-flash-free
> streaming normallyNo new provider block. No gateway. No Bun. No native addon. No second Node version. No per-request anything.
It targets the real gates, not the error message. The 429 says "quota" and means "you are not OpenCode's client". Each of the three checks above was isolated with bracketed live measurements — every rejection is surrounded by a known-good control, so a gate result can never be confused with a closed window. See VERIFICATION.md.
It needs no dependencies. "Only the CLI's TLS client is accepted" reads like a job for a native TLS-impersonation addon, and this plugin had one. The measurements said otherwise: the gate is the *cipher list*, and Node can send that with one option on node:https. The addon is gone, so dsh plugin add still installs with no build step and no toolchain, on any Node dsh itself supports — verified live on Node 20.19.2, 25.7.0 and 26.5.0.
One conversation, one id per clock window. A ses_ id is minted from Date.now() and reused for that conversation for sessionTtlMs (default 10 minutes), then re-minted. The gateway compares the value's shape, not its clock (measured: a 30-day-old grammatical id still streams), but an id that never renews is what pins a long-lived session to a stale server-side entry, so the plugin renews it anyway. Concurrent conversations never share an id: each request is scoped with async context.
It cannot change your replies. Response bytes pass through untouched. The request-side changes are confined to opencode routes: session headers, the ` block, one placeholder read` tool *only if the caller has none*, and the transport. There is a test that diffs a matched request against an untouched control and asserts the delta is exactly what the plugin documented.
It works with whatever route you already have. A direct opencode.ai provider, or your own gateway or proxy. Point baseURLs at it.
process, with no sidecar.
AsyncLocalStorage, soparallel conversations and subagents do not bleed into each other.
ses_ id is minted from the current clock andrenewed per conversation; the gateway checks the value's shape, not its clock.
adaptBody: false sends your bytesuntouched (the free tier will refuse them — useful for debugging).
transport: "fetch" keeps Node's fetch if you mustroute through a proxy or a custom CA it handles better.
forwarded with its exact original arguments.
verbose: true logs each id, the adapted tool list, thestatus, and — for a 429 — whether the retry-after looks like quota or like one of the gates.
dsh plugin --profile web add github:mubaid/dsh-opencode-freeaccess
systemctl restart dsh-webThat is the whole setup. Defaults match opencode.ai and the standard routes.
To see it working:
journalctl -u dsh-web -f | grep opencode-freeaccessA long agentic session. Each conversation carries one id for a clock window at a time, so upstream sees a steady client instead of a new one per request, which is what prompt-cache affinity wants.
Parallel subagents. Fan out ten subagents. Each carries its own id and none contaminates the others.
Self-hosted only. If you would rather not hand traffic to an aggregator, this keeps the path from dsh to OpenCode direct, with the free tier intact.
Migrating off a proxy. Drop the proxy hop and keep the models.
What is put on an opencode request:
Change · Detail x-opencode-session · The ses_ id the gateway checks for a grammatical value x-opencode-session-id · Companion session header the CLI sends x-session-affinity, x-session-id · Affinity family, harmless duplicates x-opencode-client: cli, x-opencode-project: global · Client fingerprint headers User-Agent: opencode/latest//cli · Client fingerprint accept: text/event-stream · Set when the body was adapted body · ` block, placeholder read if absent, stream: true, tool_choice: "none" → "auto" transport · The HTTP request runs over node:https with the CLI's accepted cipher list; on a socket error the plugin retries once with fetch`
x-opencode-request is deliberately not sent: it is a v1 header and the v2 free tier refuses it.
The id format is OpenCode's own: ses_, then 12 hex characters of ~(Date.now() * 0x1000 + counter) over the low 48 bits, then 14 random base62 characters. The gate checks the shape — a ses_ prefix with both halves present, so abc-123 and an empty header are refused — plus a loose bound on the decoded time: a 30-day-old id still streams, an id that decodes to the far future does not. Minting from Date.now() satisfies both without reading a clock skew.
Everything is configurable from the profile's patch layer under the id opencode-freeaccess: providers, hosts, baseURLs, headers, extraHeaders, userAgent, sessionIdEnv, sessionTtlMs, verbose, seedSessionId, disableFetchInjection, adaptBody, transport. Defaults are documented in docs/design.md.
Implementation notes, the test suite, and an explicit list of what has not been verified are in VERIFICATION.md.
· OpenCode CLI · Proxy or aggregator · Bun sidecar · This plugin Harness · OpenCode only · Any · Any · DeepSeek Harness Free tier · Yes · Depends on the provider · Yes · Yes Extra process · No · Yes · Yes · No Native build step · No · No · Yes · No Infrastructure to maintain · None · Some · Some · None Subagents and parallel sessions · Yes · Yes · Yes · Yes
Early, and honest about it. A small plugin with a narrow, well-defined job.
0.2.0-rc.2 and 0.2.1-alpha.4(engines.dsh: ^0.2.0-rc.2), on Node 20.19.2, 25.7.0 and 26.5.0.
fetch-based protocols: openai-completions, openai-responses,anthropic-messages.
fetch and are not supported.read in the request. dsh ships one, sothis holds by default; if you disable dsh's read, the plugin adds an inert placeholder instead.
window is spent, every request — including a perfect one — gets the same 429 with a retry-after counting down to 00:00 UTC. That is upstream behaviour, not a plugin fault.
unrelated to credentials.
Issues and pull requests are welcome. If you are adding behaviour, please add a test with it, and re-measure the gates before trusting a claim about them — the gateway's error code cannot tell you which gate fired.