Skip to content

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
· 2 commits to main since this release

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 now volatile, which is exactly the set the card reads and writes; providerId and the deployment-only fields stay ordinary. Because a volatile write commits into the running Config instead of remounting the entry, the plugin now rebuilds its catalog, refresher, and cached provider from loader/volatile-update and re-announces the route. tests/host.smoke.mjs pins both the volatile metadata and the in-place update path