# Changelog All notable changes to dsh-provider-pro. ## [0.5.4] - 2026-09-17 ### Fixed - **settings/updated handler debounce (500ms).** The handler fired `fill() + probe()` immediately on every `settings/updated` event. When the user switches models, `session-controller.saveSelection()` writes settings first; the plugin's `probe()` then grabs the same settings lock (up to 110s budget), and the next `saveSelection()` queues behind it — the settings page freezes until probe completes (gray screen). The 500ms debounce lets all writes in a rapid burst finish before the plugin reads/writes again. `sync()` (a fast read) still runs immediately; only the expensive `fill()`/`probe()` are delayed. The debounce timer is cleared on disposal. ## [0.5.3] - 2026-09-17 ### Changed - **Wire phase header/body separation.** `wirePost` now uses `AbortSignal.timeout(8s)` to gate the header phase only; the body read is NOT signal-gated, so a gateway that answers HTTP headers promptly (any status) is classified as ALIVE even if the body stalls — only a transport-level silence (status 0) is the "no evidence of life" hang signal. Previously the flat 10s abort killed slow-but-alive models and gave a false DEAD verdict (the root cause of "results are questionable"). - **Dead-model fast-fail.** Transport silence (status 0) gets ONE fast retry at 4s half-budget; the old code retried at full 10s for 20s total per hung model. A 429/5xx HTTP failure gets one retry at full 8s. Live measurement: per-hung-model cost dropped from ~20s to ~12s; HTTP error models now fail in 4–583ms instead of burning the full timeout. Total wire time on the real 13-model gateway: **95.9s → 66.7s (−30%)**. - **`imageVerdict: 'unsupported'`** distinguishes "the model has no image capability at all" (HTTP 400 "does not support image input") from "a vision-capable model refused THIS image request" (`'rejected'`). Both clear a wrong image declaration; the verdict and failure code now tell the user exactly why. Previously all image failures landed in one bucket, producing "partial" on every text-only model — the core of "对结果存疑". - **Result shape exposes `imageSupported`** for both `rejected` and `unsupported` verdicts; the client shows `'image: unsupported'` as a distinct badge text. ## [0.5.2] - 2026-09-09 ### Changed - **Sync to DSH Desktop 2.0.10** (cordis 4.0.2, dsh-llm-pi-ai 0.1.5-rc.2). Verified surface-by-surface against the installed build: settings service & RPC, `settings.section`, the bundle protocol, design tokens, pi-ai reasoning-effort semantics, and `signal` passthrough for `llm.stream` / `discoverModels`. No breaking drift — one upgrade taken: - **Host writes are now compare-and-set.** The settings service exposes `expectedRevision` (`mutate(ns, ops, expectedRevision)`, undefined = unconditional; mismatch throws `SettingsConflict`). The three host whole-array write paths — auto-fill, capacity backfill + compat fix, and image-declaration sync — re-read the namespace, write with that revision, and on conflict re-read + rebuild + retry (bounded). Previously they wrote unconditionally (the audit's P1 lost-update root cause); an external edit or another plugin writing between read and write could be silently clobbered. Now the conflict is detected and the write re-applied against the fresh document. The plugin-internal `enqueueWrite` queue is retained on top, so no interleaving can occur even without a revision-capable settings face (older DSH 2.0.x falls back to unconditional writes). - **Probe budget timer is cleared** (`clearTimeout`) as soon as the 100s race settles — a stale timer no longer keeps the event loop (and the process, in tests) alive for the full 100s. ### Smoke - Fake settings now models DSH 2.0.10: `describe()` returns `{ ns, revision }` and `mutate` enforces the CAS with the real `SettingsConflict` text. - New smoke block: a CAS conflict during auto-fill (external edit lands between the plugin's read and write) is retried — both edits survive. - All six smoke blocks pass and the process exits cleanly. ## [0.5.1] - 2026-09-08 ### Added - **Probe cancellation & supersede guard**: every probe run carries an AbortController and a generation token. The 100s host budget, plugin disposal, and each newer probe request abort the previous run's stream / wire / discovery requests, and every settings write checks `isCurrent()` before touching the document — a timed-out probe can no longer publish a stale result, unset a NEWER request slot, or clobber a newer probe's result. - **Discovery cache keyed by baseURL + credential** (not baseURL alone), so two routes sharing an endpoint with different credentials cannot reuse an incompatible `/v1/models` listing; the 10s cap ABORTS the underlying discovery request instead of just abandoning the wait. - **Plugin-internal write queue**: auto-fill, capacity backfill, compat fix, and image-declaration sync serialize their read→write cycles through one queue — the previous fill-vs-probe race could overwrite concurrent edits. ### Changed - **Role classification narrowed**: `compat.supportsDeveloperRole: false` is only written when the developer refusal carries role-specific evidence (400/422 or 5xx whose body names roles / 1214 / 角色), never for bare auth (401/403), unknown-model (404), quota, or generic 5xx — those used to produce persistent bogus compat writes. - **Image rejection detection**: a stream `finish` chunk with an error/aborted reason naming the image classifies `imageVerdict: rejected` and clears the declaration (providers encoding refusal as an error finish were previously left unclassified forever). - **Stream iterator cleanup**: `iterator.return()` is awaited with its rejection caught instead of fire-and-forget (an unhandled rejection and a dangling upstream request after timeout). - **UA baseURL matching is origin + path-boundary safe**: `https://h/api` no longer matches `/apiX` or an attacker-style `h.evil` host-prefix. - **Client probe-all**: a single model's exception no longer aborts the walk or wedges the busy flag; the button is always restored via `finally`. The timeout cleanup only unsets the request slot when it still holds THIS request — a blind unset used to delete the next model's freshly written request. - **Result freshness**: results carry a `receivedAt` stamp; the newer of a manual or bulk probe result wins. Manual probe results also feed the provider badge; the badge only counts models still in the profile. - **UA input re-syncs** with an externally changed `userAgent` without clobbering in-progress typing. - **Revision-conflict retry narrowed** to the actual conflict message ("changed since it was read" / "expected revision") instead of any error containing "revision". - **Master-switch "saved" indicator resets** at each flip start; `RpcResult.value` is typed optional; `ModelRow` no longer returns before its hooks (a deleted model row could crash React with a hook-order violation). ### Docs & packaging - **README/install examples updated**: `--profile desktop` (was `web`), tarball example 0.5.1 (was 0.3.0), feature intro "two" → "four". - **package.json**: adds `engines: node >=20` and a `verify` script (check + smoke + pack); version 0.5.1. ## [0.5.0] - 2026-09-05 ### Added - **One-button full model probe** (replaces the 0.4.0 capabilities/deep split): a single "Probe" button per model row measures three things in one pass — 1. context window/maxTokens via `discoverModels` (GET /v1/models), missing values backfilled into the model entry; 2. **message-role admission** — pi-ai's OpenAI-completions compat defaults `supportsDeveloperRole` to true, so a reasoning-capable model makes pi-ai send its system prompt as OpenAI's `developer` role, which some upstreams (GLM behind a relay, error 1214 "角色信息不正确") refuse. One minimal `developer` POST + one `system` baseline settles it; a refusal with a passing `system` auto-writes `compat.supportsDeveloperRole: false` so pi-ai falls back to `system`; 3. **image admission + latency** — a real stream carrying a 1×1 PNG through the LLM runtime, run last so it exercises the exact post-fix configuration the chat will use; the measured verdict is synced into the `input` declaration (adds/removes only `image`; other declared modalities are preserved). Each write-back re-reads the freshest settings right before its mutate and writes only fields that actually changed, so concurrent edits (UI, auto-fill) survive. Wire checks run only for `openai-completions` routes and skip silently otherwise. - Provider-wide "Probe all" now walks every model through the same full probe (it also feeds the alive badges). Each result line shows `ctx · max · role · image · first-token · written/backfilled`. Models with an active credential cooldown are skipped and keep their previous result. ### Changed - **Reasoning-effort levels are no longer wire-probed.** The 0.5.0 drafts validated low/medium/high/max with per-level requests and rewrote `reasoningEfforts` accordingly; that rewrite proved unreliable on heterogeneous relays (intermittent upstream 5xx produced "flaky" verdicts) and could drop levels the gateway normally accepts. The default five-level dictionary is instead provided by the `fillEfforts` auto-fill pass, which the probe never overwrites. - **Probe efficiency + verdict coverage** (live-fire tuned): - discovery (GET /v1/models) is cached per baseURL for 60s — a "probe all" pass over N models no longer re-fetches the same listing N times; - when the wire baseline already proves the model unreachable or hard-refused, the 30s image stream is skipped (the upstream is dead; a second, slower error adds nothing) — hung upstream models now cost ~10s instead of ~50s; - a timeout abort on the baseline is not retried (an upstream that hung 10s will not answer in the next 10); fast transport failures still get one retry; - developer-role admission treats a gateway-wrapped 5xx as a candidate refusal when `system` passes (measured: the live gateway wraps upstream 4xx refusals in 500s), so GLM-style models get the compat fix on the probe instead of failing in chat; - any streamed chunk (reasoning delta included) is first-token evidence — reasoning models that burn their token cap on thinking now get an image-acceptance verdict instead of an inconclusive stall; a clean `finish` counts too, an error/aborted finish does not; - the image stream carries a 12s first-token gate inside the 30s budget: tokens flowing → burn the budget; nothing flowing → fail fast with "no first token within 12s". - Probe summary formatting consolidated; removed the separate "Capabilities"/"Deep probe" buttons and their locale keys. - A stream that yields no first token within its budget is an explicit probe failure (the model did not answer) instead of a success with a budget note. - The provider badge reports "cooldown" only when EVERY failed model carries a defined, still-active cooldown; any hard failure counts as down. ## [0.4.0] - 2026-09-02 ### Added - **Image-input declaration**: each model card now shows a "Support image input" checkbox; checking it writes `input: [text, image]` to the model entry in `settings.yaml` so DSH allows image attachments for that model. - **Built-in probe**: each model row gains "Capabilities" (zero-cost `discoverModels` / GET /v1/models — contextWindow/maxTokens, with auto-backfill of missing fields) and "Deep probe" (fallback: a real minimal stream request carrying a 1×1 PNG to exercise image admission and measure latency). Provider-wide "Probe all" walks every model in capabilities mode by default. Host and client exchange through the `llm-pi-ai` user layer (settings IPC), so no extra service or process restart is needed. - **Alive-status UI**: each provider card header shows an up/down/untested badge, and each model row a matching status dot — aggregated from the latest probe results. ### Changed - Provider cards now render the full model list below the User-Agent field (collapsed by default). Each model row shows its declared `contextWindow`/`maxTokens` as compact chips (hand-set or probe-backfilled values — the current source of truth). - **Debt cleanup**: removed the dead `dsh-provider-probe` soft dependency, the unused `llmWire` client prop threading, and unused locale keys; probe waiting is now event-driven instead of polling `describe()`. ## [0.3.0] - 2026-09-02 ### Fixed - **Settings section failed on DSH 2.0.x** with `TypeError: Cannot read properties of undefined (reading 'settings')`: the client RPC surface moved. The section now talks to `ctx.remote.settings` (the same face the official settings pages use) — positional `mutate(ns, ops, expectedRevision)` parameters, zero-argument `describe()`, and the flat `{ok, value, error}` envelope. `connection` is no longer injected, and a missing face degrades to a clear in-section message instead of a TypeError. ### Changed - **Reasoning-level auto-fill now offers five levels** (off / low / medium / high / max); `minimal` and `xhigh` are removed by request. Models still carrying a byte-exact seven-level dictionary auto-filled by 0.1.0–0.2.0 migrate down automatically on the next fill pass, and the master switch's cleanup recognizes both shapes. Hand-customized dictionaries and explicit `false` are never touched. - The plugin now **requires DSH Desktop 2.0+** (the `remote.settings` face). ## [0.2.0] - 2026-09-02 ### Compatibility - Verified surface-by-surface against **DSH Desktop 2.0.4** (harness `0.1.2-alpha.1`, cordis `4.0.1`): the `settings.section` slot, the ModuleLoader client-bundle protocol (now hosted by `dsh-client-modules`), the settings service (`get`/`section`/`mutate` + `settings/updated`), the client RPC surface (`settings.describe`/`settings.mutate`, forwarded `settings/document-updated`/`llm/adapters-updated`), all 13 design tokens, and the pi-ai `reasoningEfforts` → `thinkingLevelMap` semantics are unchanged. No code change was required. - Attribution handling in 2.0.x moved to `@deepseek-ai/dsh-llm` (`attributionHeaders()`); pi-ai's header merge still strips case-insensitive collisions from user headers, so the fetch-level UA replacement remains both necessary and effective. ### Changed - Removed the stale `@deepseek-ai/dsh-client-runtime` reference from `dsh.client.inject` — the package no longer ships with DSH Desktop 2.0.x (the module system now lives in `dsh-client-modules`). The bundle only ever required `react`/`react/jsx-runtime` seed modules, so loading was never affected; this is a manifest cleanup. - Updated the tsdown externals allow-list to the current DSH 2.0.x platform surface (`dsh-client-runtime/client` dropped). - `prepare`/`prepack` now invoke `tsc`/`tsdown` directly instead of `npm run build`, so git installs work under any package manager. - README: bilingual compatibility note + LINUX DO community link. ## [0.1.0] - 2026-08-20 ### Added - **Request-level User-Agent override** per custom provider (stored as `llm-pi-ai.providers..userAgent`). The host half patches `globalThis.fetch` with a baseURL-prefix matcher and replaces the `User-Agent` header on matching requests — the DSH built-in attribution header is overridden, not appended. - **Official-channel-style reasoning-level switching** for custom models. A background filler gives every hand-declared custom model without a `reasoningEfforts` dictionary the full seven-level set, so the chat model picker shows the same reasoning-level row as the official channels, with the effective `defaultEffort` unset (the picker's "Default" entry is preselected; no thinking parameter is sent unless the user switches). - **Settings → 模型增强 section** (`settings.section` slot, id `provider-pro`, order 15): a master switch (`llm-pi-ai.dshProviderProAutoReasoning`, absent = on) for the auto fill — turning it off strips only byte-identical auto-filled dictionaries and returns those models to provider defaults — plus one User-Agent input per provider. Styled with the platform design tokens (theme-following). - Offline validation (`npm run check`) and a host-side smoke test (`npm run smoke`) covering the UA patch, the filler, and the master switch. ### Notes - Effort-level names inside the model picker are hardcoded English in `dsh-llm-pi-ai` and cannot be localized by a third-party plugin; the Chinese labels live in this plugin's own settings section only. - The filler touches only hand-declared `models[]` under the `llm-pi-ai` namespace; it never touches `modelOverrides` or catalog (official) models.