DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

go-enols /

go-enols/dsh-plugin-opencode-free

Verified

Use OpenCode Zen's anonymous free models inside DeepSeek Harness (DSH) through a native dsh-llm adapter.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@d878cd7e

dsh-plugin-opencode-free

Use OpenCode Zen's anonymous free models inside DeepSeek Harness (DSH) through a native dsh-llm adapter — the same free lane the OpenCode CLI uses without an account or API key.

The installed OpenCode CLI is not modified or replaced. The plugin reproduces the wire shape of its requests:

  • POST https://opencode.ai/inference/openai/v1/chat/completions with Authorization: Bearer public
  • a canonical ses_ session id in x-opencode-session / x-opencode-session-id,
  • user-agent: opencode/1.19.3 (the exact inference UA the working global CLI sends),
  • x-opencode-request, x-opencode-client: cli, x-opencode-project: global,
  • a streaming body with an agent-shaped tool list (bash + read).

Zen rejects requests that do not carry this shape with 403 FreeTierError: OpenCode's free tier can only be used from within OpenCode, even when a real API key is sent. This plugin is the missing bridge.

Install

Host-only bundle, no runtime dependencies.

From a local checkout (absolute path to this directory):

plugin_manager · install_bundle · target: <absolute path to this directory>

or directly from GitHub (pnpm-style spec, the same form DSH uses for any github: dependency):

plugin_manager · install_bundle · target: github:go-enols/dsh-plugin-opencode-free

or with a DSH CLI:

dsh plugin --profile <profile> add github:go-enols/dsh-plugin-opencode-free

Restart DSH if the model picker does not refresh, then select provider opencode-free in the model picker and pick one of the *-free models (e.g. deepseek-v4.1-flash-free).

Configuration

The bundle patch inserts one row; override it from the profile's cordis.patch.yml:

- id: opencode-free
  name: 'dsh-plugin-opencode-free'
  config:
    providerId: opencode-free   # provider route shown in the model picker
    baseURL: https://opencode.ai/inference
    refreshSeconds: 600         # live catalog refresh cadence
    # userAgent: 'opencode/1.19.3'
    # modelIds: ['deepseek-v4.1-flash-free', 'big-pickle']

The plugin ships a snapshot of the free models (models.dev cost = 0) and intersects it with the live /v1/models catalog, so delisted models disappear automatically. The first four entries are live-verified.

How it works

index.js registers a structural LlmAdapter through ctx.llm.registerAdapter:

  • providerInfo, providerRetryPolicy, imageRequestPricing, listModels, resolveModel, prepareCall, stream
  • lib/ids.js — canonical ses_ id (bitwise-inverted timestamp + Base62 tail), CLI disguise headers, CLI user agent
  • lib/wire.js — OpenAI-compatible SSE ↔ DSH StreamChunk translation, free-lane gate tool injection, injected-tool-call rewriting (bash → the caller's pwsh)
  • lib/catalog.js — static free snapshot ∩ live /v1/models
  • lib/reasoning.js — per-model thinking levels and the reasoning_content echo rule
  • lib/tokens.js — prompt-token estimation calibrated against the live lane

The adapter keeps the agent's real tool list; it only appends whichever gate tools are missing. When a request carries no tools at all (title/summary calls), it adds bash + read and sends tool_choice: "none" so the model answers in plain text.

Thinking levels

Reasoning-capable free models expose a thinking-level selector in the DSH model picker. The levels are the ones models.dev declares for each model (the same metadata the OpenCode CLI builds its own selector from), handed to DSH through LlmResolvedModelInfo.reasoning:

model advertised levels
deepseek-v4.1-flash-free, glm-5.3-flash-free low, high, max
minimax-m3.1-flash-free low, medium, high, xhigh, max
nemotron-3.5-super-vl-free off, minimal, low, medium, high, xhigh
models with no declared ladder (big-pickle, mimo-v2.6-flash-free, …) minimal, low, medium, high, xhigh, max

The selected level is sent as reasoning_effort. off is sent as the wire spelling none; an id the gateway would reject is dropped instead of risking a hard 400. No defaultEffort is advertised, so the upstream default applies until the user picks a level. off is only offered where models.dev declares none — the MiniMax free models reject it outright (requires adaptive thinking).

Models that declare interleaved: reasoning_content also get their previous thinking echoed back: any assistant message carrying tool calls is sent with a reasoning_content field. That field is mandatory once thinking is on, and without it the upstream answers 400 invalid_request_error: The reasoning_content in the thinking mode must be passed back to the API. When the harness no longer has the reasoning text (a compacted turn), the empty string is sent, which the upstream accepts.

Context window

The free lane enforces two limits per request: the prompt must fit the model's input cap, and prompt + max_tokens must fit its total window. The adapter therefore advertises min(context, input + output) from models.dev (e.g. big-pickle declares a 160k input cap inside a 200k window, so it advertises 192k) and guards each call locally:

  • the prompt is estimated with live-calibrated ratios (CJK ≈ 1 token/char, English ≈ 1/4.7, JSON/code ≈ 1/2.35 — measured against the lane) and, when it cannot leave 1024 tokens of room, the adapter reports CONTEXT_WINDOW_EXCEEDED without sending the request. DSH's compaction reacts to that code by compacting and retrying the step, so a conversation that outgrew its model recovers without a wasted round trip or an upstream error in the transcript.
  • the advertised output cap is the declared one, capped at half the window: DSH's compaction reserves the full cap before sizing its message budget, so a model declaring output == context (nemotron-3.5-lightning-free) would otherwise leave zero budget and silently lose automatic compaction.
  • when the prompt fits but max_tokens would overflow the window, the output budget is clamped to the remaining room instead of failing the turn.

Switching from a large-window model (e.g. a 1M-token route) to a 200k free model while the history is already longer than 200k tokens therefore costs one compaction, not a failed turn.

Verification

node tests/selftest.mjs runs the adapter against the real free lane with a fake Cordis context and asserts a streamed reply, the gate header/session shape, the thinking-level metadata and wire mapping, the reasoning_content echo, the context-window guard and output clamp, a live reasoning_effort: max tool-call round-trip, and the error mapping for a model that upstream reports as unavailable.

Caveats

  • This uses OpenCode Zen's anonymous lane. It is quota-limited per egress IP, contended, and some models are region-blocked; 429 and 400 Model is unavailable are upstream facts, not plugin bugs.
  • The lane is not an official third-party API. Using a disguised CLI shape may be against OpenCode's terms of service; use at your own discretion.
  • Free model ids change. The shipped snapshot + live /v1/models refresh keeps the list fresh, but a model can still be delisted or region-blocked at call time.
—/ 5

No ratings yet

Verified DSH bundle

Commit d878cd7ea00b

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout