Repository navigation
Releases
v0.1.4 — harness 0.2.0, and the tool-call fix it exposed
Install:
dsh plugin --profile web add github:scavanger2221/dsh-llm-opencode-go#v0.1.4Then restart the app (dsh web) and set the key in Settings → Models → OpenCode Go.
DeepSeek Harness 0.2.0-rc.2 or later is required: it is the release that ships pi-ai 0.87, whose conversion this route mirrors.
Fixed
- Tool calls were not reaching the model on harness 0.2.0. pi-ai 0.87 moved the prompt/tool fold out of the API implementations and into its
Modelsregistry —Models.streamSimplerunsnormalizeContext, which turnssystemPromptandtoolsinto the leading system message the APIs now read, and no protocol readscontext.systemPromptorcontext.toolsany more.dsh-llm-pi-aistreams through that registry; this route dispatches through the provider it builds itself, so every request went out with the user turn alone. The model answered as a bare chat model: "I don't have access to a bash tool in this environment", or its nativeDSMLtool markup as plain text.lib/adapter.jsnow folds the context with the same publicnormalizeContextthe registry uses, so the tool declaration and the system prompt reach the wire again and a streamedtool_callsreply becomes one tool-call block. - Replayed Anthropic history restored the wrong model identity. The replay envelope now records the requested model in
modeland the served one beside it inresponseModelfor every protocol, mirroringdsh-llm-pi-ai0.2.0 and the pi-ai 0.87 behaviour it follows.
Changed
- The
@earendil-works/pi-aipeer range is now^0.87.1. Harness 0.2.0-rc.2 supplies 0.87.1, which the old^0.85.1range excluded. - The documented harness floor is raised to 0.2.0-rc.2 in both READMEs, and
AGENTS.mdstates the context-normalization rule so the next pi-ai upgrade cannot repeat this.
Verified
- The adapter, cache, usage, host and card suites, run from an installed copy. The adapter suite now pins the wire request (the tool declaration and the system prompt reach it) and the streamed
tool_callsdelta; without the adapter fix it fails on "the tool declaration reaches the wire". - The live endpoint, through this adapter:
deepseek-v4.1-flash(openai-completions) andminimax-m3(anthropic-messages) each return a realbashtool call.
v0.1.3 — plan usage, a live model cache, and a pi-ai-faithful conversion
Install:
dsh plugin --profile web add github:scavanger2221/dsh-llm-opencode-go#v0.1.3Then restart the app (dsh web) and set the key in Settings → Models → OpenCode Go.
Fixed
opencode-go has no model "deepseek-v4.1-flash"no longer kills a session: a refreshed model list is cached under$DSH_HOME, so a model the pinned pi-ai release predates resolves from the first request after a plugin remount.OpenCode Go cannot represent an image in an in-history tool messageis gone: a tool result is a first-classtoolmessage, and images inuserandtoolmessages replay instead of failing the turn.- Tool results were going to the wire as user text with their
toolCallIddropped; they now reach the wire astoolmessages.
Added
- A plan-usage panel in the settings card: 5-hour, weekly and monthly windows read from
GET /v1/usagethrough an authenticated host route, with a header pill, per-window progress bars, reset countdowns and distinct states for a missing key, an unauthorized key, a malformed reply and an unreachable endpoint.
Changed
- The harness ↔ pi-ai conversion mirrors
dsh-llm-pi-ai's own — same history refusals, sametoolResultprojection, same image handling, same offload and stop/usage/error mapping — and the mirror is a contract re-diffed on every harness or pi-ai upgrade (ADR 0004). - The settings card lives at the foot of the Models page (ADR 0002) and edits per-model overrides that state only what they change (ADR 0003).
Full changelog
-
Fix: an image in an in-history tool message failed the whole turn. A tool result is a first-class
toolmessage whose own content is the tool's output, soread_imagehistory was refused withOpenCode Go cannot represent an image in an in-history tool messageand every later turn in that session died on replay. Images inuserandtoolmessages now replay — the same two roles pi-ai's own conversion accepts — and the image reaches the wire from the tool result -
Fix: tool results were sent as something the user said. The conversion looked for
tool-resultblocks that harness 0.1.7 does not produce, so atoolmessage fell through to the user branch: its text was flattened into auserturn and itstoolCallIdwas dropped. A tool message is now projected as a pi-aitoolResult(toolCallId,toolName,isError), which is what the request carries -
The conversion is now a mirror of
dsh-llm-pi-ai, and the mirror is the contract. The harness↔pi-ai seam belongs to the shipped pi-ai adapter; this route reproduces it only because it must add the session headers, so both paths now refuse and accept exactly the same history:developermessages and tool-change blocks are refused ("Developer messages are not supported yet" / "Tool-change blocks require developer role"),deferLoadingtools are refused, images are legal inuserandtoolmessages and refused everywhere else, a leadingsystemmessage folds into the prompt the same way, user turns carry the sameuserContentcollapse, and offloaded images are skipped when the request images are collected.AGENTS.mdstates the rule — on a harness or pi-ai upgrade, re-diff the context region ofdsh-llm-pi-ai/lib/index.jsagainstlib/convert.js— andtests/adapter.smoke.mjspins the resulting refusals. The decision behind it, including what delegation tollm-pi-aiwould cost now that Go is known to accept pi-ai'sx-session-affinity, is ADR 0004 -
The Models list was rebuilt so it can be read. Every row repeated its field labels ("Context window: 131072", "Max output tokens: 32768"), which made 27 models look like debug output. A row now leads with the catalog's display name (the id beside it only when the two differ), states its facts in a compact spec line (
1M ctx · 128K out · text + images, exact values in the tooltip), and keeps its actions right-aligned: Disable/Enable, Edit, and Reset for a row that carries an override. The Advertised models section is gone — its count, its endpoint query and its refresh button moved into the Models field with a new filter box, and the count line now reports what is there and where it came from (27 models · 2 overridden · 1 disabled · live catalog). The list is a fixed six-row window, so it ends on a row boundary instead of clipping one, and a disabled row dims its text but not the button that brings it back -
The header chevron was never the real icon. The card asked for
IconChevronDownOutline14, which no shipped package exports, so it silently fell back to the text glyph▾. It now usesIconChevronDownOutlineRegular, the name every shipped package uses -
A model can now be disabled. A
models:entry takesdisabled: true— which is exactly what the card's per-model Disable button writes, and nothing else — and that takes the model out of the served catalog: the provider stops advertising it, the model picker stops offering it, and a request naming it fails with an explanation instead of being sent to a model the user switched off. The id stays described (catalog.all()), so the settings card keeps rendering its row with its facts (llm.registerModelDiscoveryanswers fromall(), while the picker readslistModels), and a live refresh never re-adds it, because every configured id is owned by configuration rather than by the endpoint. Re-enabling drops the entry again when the flag was all it carried, so switching a model off and back on leaves the configuration as it was -
The card now has per-model settings instead of a list of hand-written override entries that starts empty. It lists every model the route serves with the facts a request would use (context window, max output, input modalities, and the protocol when an override states one), marks the ones carrying an override, and edits any of them in place — no ID has to be known in advance. The host's
modelDiscoveryreply carries those facts (contextWindow,maxTokens,inputModalities), which is what the list renders -
An override may state only what it changes. A field left on Inherit is absent from the entry, so editing one model's context window no longer rewrites its protocol, reasoning flag, or allowed input: the catalog merge only takes what the entry carries.
LlmDiscoveredModelis the whole discovery contract and carries neither the protocol nor the reasoning flag, so before this an editor could only ever guess them — guessing wrong switched a model's wire protocol.tests/adapter.smoke.mjspins the partial-merge behavior, and the card test asserts an inherited field never reaches the write -
Model capacities are no longer compared by
JSON.stringify: a hand-edited profile patch that spells the same override with its keys in another order is not a change worth staging a rewrite for -
Fix image requests:
readImageRequesttakes a resolved{width, height, maxBytes}target, not the route's pixel/byte budget. The budget was passed through unresolved, so every request carrying an image failed withImage request width must be a positive integer. The target is now projected from the source's intrinsic size, so a small image is still never enlarged -
Track the
@deepseek-ai/dsh-llm0.1.7 image-offload seam:offloadRequestImagesWithPolicyis gone. Prepare the request images, then fail withIMAGE_OFFLOAD_REQUIREDcarrying therequiredImageOffloadcount and let the surface offload, projecting already-offloaded occurrences withprojectOffloadedImages -
Track the 0.1.7 settings seam: the
settingsScopeservice is gone. The card injectsconfigFormsand takes its namespace's controller withconfigForms.get(NS), which keeps the samegetSnapshot/subscribe/set/mutateface -
Fix: the plugin's configuration was silently ignored on 0.1.7. The Loader now hands
applya Schemastery Config whose fields are live references (config.apiRoot.get()), not a plain object; readingconfig.apiRootreturned a field object, so every value fell back to its schema default and a settings write looked like it did nothing. Configuration is now read through those references at the moment of use, and a plain object is still accepted.tests/host.smoke.mjsmounts a realConfigwith non-default values and asserts they reach the route, the directory, and the catalog -
Track the rest of the 0.1.7 settings seam:
settings.installSectionis gone. A mounted entry's Config is projected into a settings namespace automatically (keyed by the entry id), and a plugin that ships its own page registerssettings.configure({ auto: false })instead. The route's own unsupported-apicheck is gone with it — the Config schema already rejects that value -
Move the settings card to Settings → Models, in the
settings.models.footerseat (the old seat,settings.plugin.item, is declared by no shipped package on 0.1.7 and renders nowhere). The footer needs no provider-directory row, which matters becausesettings.models.provider-cardis unreachable for this route:llm-pi-aideclares the whole pi-ai provider catalog,opencode-goincluded, so that row dispatches with thellm-pi-ainamespace and the host refuses a second declaration of the same id. The route is still declared on the configurable-provider directory on a best-effort basis, and a refused declaration is logged at info as the expected coexistence -
Fix: the settings card rendered nothing on 0.1.7. The settings service serves a namespace only for a Config schema that declares at least one
.volatile()field —volatileForm(schema)returns nothing otherwise, soconfigForms.get(NS)reported the namespace unavailable and the card returnednull.apiRoot,refreshIntervalMs,models,refreshEpoch,apiKeyEnv, anddisplayNameare no...
v0.1.2 — discovery metadata
npm keywords (dsh-plugin, deepseek-harness, …) and the dsh-plugin GitHub topic, which is what dsh-find-plugin and the curated plugin list search on.
v0.1.1 — install cleanly from GitHub
- Declare
@earendil-works/pi-aias a peer instead of a dependency. As a dependency it pulled its own transitive closure (93 packages, including build-script dependencies pnpm's supply-chain gate then refuses), which madedsh plugin add github:…fail before it could activate the bundle layer. The Harness already supplies pi-ai through the profile module fallback, so nothing is lost.
v0.1.0 — OpenCode Go provider and Settings card
Superseded by v0.1.1 — installing this tag from GitHub fails before the bundle layer activates (pnpm refuses the build scripts of pi-ai's transitive dependencies, and the non-zero exit aborts dsh plugin add). Use v0.1.1 or later.
- First release: the
opencode-goroute over the harness LLM seam, with session-affinity headers (x-opencode-sessionplus the harness-nativex-deepseek-harness-session-id) on every request and the mandatory attribution headers - Three-layer model catalog: pinned pi-ai models, per-model overrides from the
llm-opencode-gosettings section, and a live refresh of{apiRoot}/v1/modelsenriched from models.dev - Refresh triggers: mount, interval, the
/opencode-refreshchat command, and the settings card - Web card under Settings → Plugins: API key (through the credentials service), endpoint, refresh policy, advertised-model lookup, and the model override editor