Skip to content

Releases: artorias-zj/dsh-opencode-free-model

v2.3.0 — a verdict is an answer about one path, one model and one session

Choose a tag to compare

@artorias-zj artorias-zj released this 26 Oct 05:49

Ports the upstream free-lane corrections that apply to this plugin, from upstream v2.5.9..v2.7.2 (48 commits).

What was ported

One probe session per model. Quota on this lane is accounted per upstream session, so a round that shared one session spent the whole allowance on its first model and read every model after it back as 429 throttled — dropping models the egress could serve, over quota they never touched (upstream #561). Each probe now derives its session from the model id, which also keeps one model's probe session stable across rounds and restarts.

A verdict is only true of the path it was measured on. An egress change used to just add the new address and leave the old answers standing, so a region refusal collected behind a VPN could outlive the VPN. Rows are now marked stale on a generation bump — kept as history, but read as "not measured" — and a round still in flight when the path changes is superseded rather than joined. The slowest models in a round land last, which is exactly when they would otherwise overwrite fresh verdicts with obsolete ones. The throttle backoff is reset for the same reason: it described the old path's quota.

A refusal is about one model, not the roster. A mid-turn RegionError used to force a full probe round, spending one probe per model out of the same quota pool as your own turns to confirm something the refusal had already said about exactly one of them. It now records that single verdict directly, with no probe request and no invented latencyMs/startedAt.

What was deliberately not ported

The other ~44 commits in that range serve subsystems this fork does not carry: the magpie engine, token savers, the subscription lane, the native Gemini gateway, the workbench, combos, the self-updater, the inbound forward listener, the standalone UI. More to the point, none of the ten modules backing this plugin's actual primitives — stream, http, catalog, upstream, messages, recovery, effort, channel, route classification, failure — was touched upstream at all in that range. There is no hidden SSE, usage-accounting, fingerprint or window change hiding in there.

One upstream change was evaluated and declined on its merits: 6da81a3 ("disable all automatic probing") assumes a manual reprobe button and a settings UI that this Host-only plugin has no surface for. Ported literally it would leave every model reading unknown and make exposeRegionModels inert. The quota concern behind it is real, and the third fix above addresses the fork's actual instance of it.

Verification

  • 13/13 modules pass node --check and import cleanly.
  • 15/15 behaviour assertions through a real cordis mount over a stubbed transport: three models produce three distinct derived sessions and never the old shared one; egressGeneration starts at 0; a region refusal fires zero probes; an egress flip bumps the generation and marks prior rows stale; a late old-generation round resolves to {} and leaves the store unchanged.
  • A latent mount-breaking bug was caught in review: returning an object from apply makes cordis _execute throw TypeError: Invalid effect and fails the plugin entirely. The handles are now parked in a WeakMap and read via mountHandle(ctx). Confirmed ACTIVE against cordis 4.0.0-rc.10.

v2.2.0 — sync the free lane with upstream, and fix the per-model catalog

Choose a tag to compare

@artorias-zj artorias-zj released this 23 Oct 02:37

Ported from dsh-our-free-model f0b979f..482500f (v2.5.3 → v2.5.9), keeping only what belongs to the anonymous free lane. Upstream's other changes hang off subsystems this plugin does not ship (EAC, Kilo, ten account channels, the local OpenAI gateway, combos, the self-updater, the settings UI), so they were refused rather than copied.

Usage accounting

A free-lane multimodal turn can stamp a prompt larger than the model's own window (~619k total on a 256k model after a short vision turn), and the host soft-budgets from that sample, then fail-closes with CONTEXT_WINDOW_EXCEEDED while the real conversation is a few thousand tokens. The input side is now dropped before it is metered; output and reasoning counts survive, and once one attempt's prompt is rejected the turn holds input at 0 because no later sum rebuilds an exact prompt.

Upstream gates this on one known-bad router; the equivalent gate here is where the window came from — a real capability row, never the conservative fallback, because a stamp above a fallback proves the fallback is undersized rather than that the stamp is wrong.

Per-model catalog facts

Added the exact-id override layer and corrected the four ids this lane serves that the family patterns mis-stated:

id before after
deepseek-v4.1-flash-free 128000 / 64000 / no vision 1048576 / 384000 / vision
glm-5.3-flash-free 131072 / 32768 / no vision 1048576 / 131072 / vision
nemotron-3.5-super-vl-free 128000 / 32768 / no vision 1048576 / 131072 / vision
big-pickle fallback 262144 / 131072 / vision

This was not cosmetic: the understated window made checkpointFits reject recovery continuations ~8× more eagerly on the model the README advertises first, maxOutput capped budgetFor 6× below what the gateway accepts, and three ids that accept images were advertised as text-only — one of them literally named -vl. Seeds now track the live roster (8 models).

Smaller ports

  • Probe verdicts record when the probe started rather than when it answered, so a slow reply cannot read as fresher than a later round.
  • A finish chunk carries upstreamFinish when the fallback mapped an abnormal ending onto stop.
  • The Chat projection passes an assistant reasoning_content string through verbatim (empty string included) while still never projecting internal reasoning blocks.

Deliberately not copied

The thinking menus stay this plugin's own 2026-10-18 measurement rather than upstream's narrower declared sets. Both are true — GLM answers 200 to none and spends reasoning tokens anyway — so a rung that is accepted but not honoured is a product decision, not a bug.

Verification

All 13 modules parse and import; 40 unit assertions over the clamp, the overrides, the effort invariants and the reasoning echo; and an end-to-end run through the real adapter against a local SSE fixture (clamp fires on a measured window, stays inert on the fallback, no-op on an honest stamp, and correctly lets a 619k stamp through on the corrected 1M window).

No breaking change for callers.

Install

dsh plugin --profile desktop add github:artorias-zj/dsh-opencode-free-model

v2.1.0 — thinking levels are the real control

Choose a tag to compare

@artorias-zj artorias-zj released this 18 Oct 05:15

Why this release exists

The effort menu was built on a probe that concluded this lane accepts reasoning_effort and then ignores it, leaving max_tokens as the only real control — so a level was implemented as an output-token budget.

A re-probe shows that conclusion no longer holds. The gateway now validates the field, and an undeclared value answers 400 naming the accepted set:

literal['none','minimal','low','medium','high','xhigh','max']   # deepseek-v4.1-flash-free
reasoning_effort: Invalid option: expected one of "max"|…        # nemotron ids

And the level moves reasoning volume. With max_tokens pinned at 8192, three samples per rung, median reasoning tokens on deepseek-v4.1-flash-free:

level reasoning tokens
none 0
low 793
high ~1000
xhigh ~1600
max ~2000

big-pickle: none 0 → max 1013. glm-5.3-flash-free: none 7 → max 6135.

The bug this fixed

The old light rung sent max_tokens: 2048. That ceiling no longer bounded the thinking — it truncated the turn before the answer started:

light (2048)   finish=length   reasoning 4807 chars   answer 0 chars

Every level now returns a complete turn.

What changed

  • effort.js rewritten. Ids are the upstream's own level names (none…max), sent verbatim as reasoning_effort. The token ceiling is independent of the level: a caller-supplied maxTokens still lowers it, nothing raises it above the model.
  • The menu is per model, because the accepted sets genuinely differ: big-pickle refuses minimal/xhigh; space-bunny-free refuses none (so it has no Off rung and reports canDisableThinking: false). Sets were measured by sending every rung to every live free id — published tables are narrower than the wire (models.dev lists three rungs for deepseek-v4.1-flash-free and glm-5.3-flash-free; both accept all seven).
  • A level the model does not declare is never forwarded. It resolves to that model's default instead of drawing a 400.
  • defaultEffort is per model and always a rung in that model's menu — the harness rejects the whole roster otherwise.
  • The level rides continuations too. A recovery segment that dropped it would spend its ceiling on thinking again, which is the turn it is rescuing.
  • describe() names the menu instead of a token figure, which was a number the level does not honour.
  • jev-1.13-free leaves the seed roster: every wire answers 400 ModelProtocolUnsupported, so the gateway lists a model it will not serve.

Breaking change

light / balanced / deep are gone in favour of the lane's own rungs. A saved stale preference is not an error — an unknown level resolves to the model's default — so it degrades quietly rather than failing a turn.

Verified

  • 26 module-level checks: per-model menus, defaults always in menu, unsupported levels fall back, budget no longer varies with level, descriptions promise no token figures.
  • 13 adapter checks through a stub gateway: reasoning_effort reaches the wire, max_tokens is the full ceiling, space-bunny-free + none → low, default → medium, defaultEffort valid for every model.
  • Live turns on all five rungs of deepseek-v4.1-flash-free: every one finish=stop with a complete answer; big-pickle/space-bunny-free/glm-5.3-flash-free likewise.

Full changelog: v2.0.0...v2.1.0

v2.0.0 — the free lane works again

Choose a tag to compare

@artorias-zj artorias-zj released this 17 Oct 04:27

Required upgrade

1.0.1 and every earlier release fail every turn with 403 FreeTierError. The gateway tightened two gates, and each one on its own is enough to refuse a request.

The session id no longer decoded

The gateway reads the first twelve hex digits of a ses_… as ~(time × 0x1000 + counter) and requires the counter to stay small: 0–63 answers, 64 and up is refused. The old derivation emitted a raw sha256 prefix, which decodes to counters in the hundreds (846, 203, 2479 for three test seeds), so every conversation was refused.

Derivation now builds the time field from the digest, pins the counter to 1, inverts it, and keeps the base62 tail from the digest — a canonical shape that is also stable across turns, which the per-session quota accounting needs.

The attribution User-Agent carried a refused token

The harness mandates an attribution User-Agent and states nothing can suppress it. This lane refuses exactly that product token — deepseek-harness/<version>, case-insensitively — while accepting the bare word, the same words inside a URL, and deepseek_harness/….

The plugin now sends opencode/1.18.31 dsh/<version> (+https://github.com/deepseek-ai/deepseek-harness): the client token the lane identifies, plus the attribution with only its product name respelled. lib/kernel.js still reports the harness's own string, so attribution stays honest at the seam; the deviation is documented in both READMEs.

Also in this release

  • FreeTierError is reported as CLIENT_ERROR with fingerprintRejected: true, not INVALID_CREDENTIAL. This lane has no per-user key, so credential guidance sent users looking for something that cannot exist.
  • The catalog matches the live roster: ten free ids, current display names, big-pickle admitted as free despite having no -free marker, retired seeds removed.
  • Docs record that these wire facts are measured, not published — the gate can tighten again, and a FreeTierError means update, not re-authenticate.

Verified

Against the live lane: both gates fixed, 200 across four conversation seeds plus the catalog and probe sessions; a real streamed turn completes with a stop finish and correct token accounting (inputTokens 34, outputTokens 16, totalTokens 50, reasoningTokens 14, answer "OK").

Notes

Not gated, and deliberately not changed: /zen/v1 and /inference/v1 both answer, and x-opencode-client / x-opencode-request / x-opencode-session-id / x-opencode-project are each individually dispensable. Porting those cosmetics would have added surface with no measured effect.

Full changelog: v1.0.1...v2.0.0

v1.0.1 — upstream robustness fixes

Choose a tag to compare

@artorias-zj artorias-zj released this 12 Oct 07:51

Ports the free-lane robustness fixes from the upstream reference project:

Stream decoding

  • omit an unknown tool-call name from deltas instead of emitting undefined (the kernel lossless-JSON snapshot rejected the whole chunk)
  • downgrade a turn whose call never learned its name (issue #92: an answered nameless call poisoned every later turn)
  • read streamed thinking under four spellings (reasoning, reasoning_content, reasoning_text, thinking) plus reasoning_details
  • scrub leaked DSML control markup with a hold-back buffer
  • accumulate capped answer text for max-tokens continuation

Recovery

  • widen the total turn window to 30 minutes (issues #69/#70)
  • continue once from the checkpoint when a stopped turn held only its reasoning (silent stop)
  • continue automatically when finish length cut the answer (issue #28)

Messages

  • drop nameless calls in repairToolPairing even when answered
  • accept OpenAI-wrapped tool defs in toToolDefs
  • buffer tool-result images so every result precedes user content

HTTP

  • classify HTML error pages, signature rejections and 4xx failures by their real cause (issues #63/#50)
  • append the socket-level cause chain to transport failures (issue #79)

v1.0.0 — initial release

Choose a tag to compare

@artorias-zj artorias-zj released this 12 Oct 07:51

The first release of dsh-opencode-free-model: use the OpenCode Zen free model lane from DeepSeek Harness with no API key.

  • llm adapter registration with three wire protocols (chat / responses / messages)
  • live model catalog with alphabetical picker order
  • reasoning-effort ladder as an output-token budget
  • reasoning-checkpoint continuation for cut streams
  • fingerprint quartet with tool renaming
  • MIT licensed