Skip to content

Releases

v0.1.4 — harness 0.2.0, and the tool-call fix it exposed

Choose a tag to compare

@scavanger2221 scavanger2221 released this 29 Sep 15:05

Install:

dsh plugin --profile web add github:scavanger2221/dsh-llm-opencode-go#v0.1.4

Then 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 Models registry — Models.streamSimple runs normalizeContext, which turns systemPrompt and tools into the leading system message the APIs now read, and no protocol reads context.systemPrompt or context.tools any more. dsh-llm-pi-ai streams 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 native DSML tool markup as plain text. lib/adapter.js now folds the context with the same public normalizeContext the registry uses, so the tool declaration and the system prompt reach the wire again and a streamed tool_calls reply becomes one tool-call block.
  • Replayed Anthropic history restored the wrong model identity. The replay envelope now records the requested model in model and the served one beside it in responseModel for every protocol, mirroring dsh-llm-pi-ai 0.2.0 and the pi-ai 0.87 behaviour it follows.

Changed

  • The @earendil-works/pi-ai peer range is now ^0.87.1. Harness 0.2.0-rc.2 supplies 0.87.1, which the old ^0.85.1 range excluded.
  • The documented harness floor is raised to 0.2.0-rc.2 in both READMEs, and AGENTS.md states 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_calls delta; 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) and minimax-m3 (anthropic-messages) each return a real bash tool call.

v0.1.3 — plan usage, a live model cache, and a pi-ai-faithful conversion

Choose a tag to compare

@scavanger2221 scavanger2221 released this 27 Sep 13:26

Install:

dsh plugin --profile web add github:scavanger2221/dsh-llm-opencode-go#v0.1.3

Then 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 message is gone: a tool result is a first-class tool message, and images in user and tool messages replay instead of failing the turn.
  • Tool results were going to the wire as user text with their toolCallId dropped; they now reach the wire as tool messages.

Added

  • A plan-usage panel in the settings card: 5-hour, weekly and monthly windows read from GET /v1/usage through 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, same toolResult projection, 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 tool message whose own content is the tool's output, so read_image history was refused with OpenCode Go cannot represent an image in an in-history tool message and every later turn in that session died on replay. Images in user and tool messages 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-result blocks that harness 0.1.7 does not produce, so a tool message fell through to the user branch: its text was flattened into a user turn and its toolCallId was dropped. A tool message is now projected as a pi-ai toolResult (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: developer messages and tool-change blocks are refused ("Developer messages are not supported yet" / "Tool-change blocks require developer role"), deferLoading tools are refused, images are legal in user and tool messages and refused everywhere else, a leading system message folds into the prompt the same way, user turns carry the same userContent collapse, and offloaded images are skipped when the request images are collected. AGENTS.md states the rule — on a harness or pi-ai upgrade, re-diff the context region of dsh-llm-pi-ai/lib/index.js against lib/convert.js — and tests/adapter.smoke.mjs pins the resulting refusals. The decision behind it, including what delegation to llm-pi-ai would cost now that Go is known to accept pi-ai's x-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 uses IconChevronDownOutlineRegular, the name every shipped package uses

  • A model can now be disabled. A models: entry takes disabled: 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.registerModelDiscovery answers from all(), while the picker reads listModels), 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 modelDiscovery reply 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. LlmDiscoveredModel is 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.mjs pins 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: readImageRequest takes 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 with Image 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-llm 0.1.7 image-offload seam: offloadRequestImagesWithPolicy is gone. Prepare the request images, then fail with IMAGE_OFFLOAD_REQUIRED carrying the requiredImageOffload count and let the surface offload, projecting already-offloaded occurrences with projectOffloadedImages

  • Track the 0.1.7 settings seam: the settingsScope service is gone. The card injects configForms and takes its namespace's controller with configForms.get(NS), which keeps the same getSnapshot/subscribe/set/mutate face

  • Fix: the plugin's configuration was silently ignored on 0.1.7. The Loader now hands apply a Schemastery Config whose fields are live references (config.apiRoot.get()), not a plain object; reading config.apiRoot returned 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.mjs mounts a real Config with 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.installSection is 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 registers settings.configure({ auto: false }) instead. The route's own unsupported-api check is gone with it — the Config schema already rejects that value

  • Move the settings card to Settings → Models, in the settings.models.footer seat (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 because settings.models.provider-card is unreachable for this route: llm-pi-ai declares the whole pi-ai provider catalog, opencode-go included, so that row dispatches with the llm-pi-ai namespace 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, so configForms.get(NS) reported the namespace unavailable and the card returned null. apiRoot, refreshIntervalMs, models, refreshEpoch, apiKeyEnv, and displayName are no...

Read more

v0.1.2 — discovery metadata

Choose a tag to compare

@scavanger2221 scavanger2221 released this 10 Sep 12:42

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

Choose a tag to compare

@scavanger2221 scavanger2221 released this 10 Sep 12:39
  • Declare @earendil-works/pi-ai as 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 made dsh 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

Choose a tag to compare

@scavanger2221 scavanger2221 released this 10 Sep 12:35

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-go route over the harness LLM seam, with session-affinity headers (x-opencode-session plus the harness-native x-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-go settings section, and a live refresh of {apiRoot}/v1/models enriched from models.dev
  • Refresh triggers: mount, interval, the /opencode-refresh chat 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