dsh-astudio-connect 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -12,16 +12,13 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
12
12
 
13
13
  - [Installation](#installation)
14
14
  - [Features](#features)
15
- - [Command Line](#command-line)
16
- - [How It Works](#how-it-works)
17
15
  - [Configuration](#configuration)
18
16
  - [Model Catalog](#model-catalog)
19
17
  - [Reasoning Effort and Probing](#reasoning-effort-and-probing)
20
18
  - [Model Visibility](#model-visibility)
21
19
  - [Points Balance Badge](#points-balance-badge)
20
+ - [Billing and Points](#billing-and-points)
22
21
  - [Settings Card](#settings-card)
23
- - [Update Check and Host Heartbeat](#update-check-and-host-heartbeat)
24
- - [Location Discovery (Windows)](#location-discovery-windows)
25
22
  - [DSH Version Support](#dsh-version-support)
26
23
  - [Known Limitations](#known-limitations)
27
24
  - [Security](#security)
@@ -41,29 +38,31 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
41
38
 
42
39
  ### Install the plugin
43
40
 
44
- The plugin registers its manifest through `cordis.patch.yml`, and DSH loads it at startup.
41
+ **Command line**:
45
42
 
46
43
  ```bash
47
- # Install from the local directory
48
- npm install -D ./dsh-astudio-connect
44
+ dsh plugin --profile web add dsh-astudio-connect@latest
49
45
  ```
50
46
 
51
- Or add this directory path in the DSH plugin manager.
47
+ Replace `web` with the profile you actually boot.
48
+
49
+ > The plugin version must match your DSH kernel generation, or the host refuses to load it. Installing the latest is enough; if it is still refused, report your DSH kernel version and the full error on [Issues](https://github.com/Cwentor/dsh-astudio-connect/issues) rather than bypassing the host check.
52
50
 
53
51
  ### Verify
54
52
 
55
- After installing you must **fully quit and restart DSH** (DSH loads plugins at startup; refreshing the page is not enough), then confirm:
53
+ After installing you must **fully quit and restart DSH**, then confirm:
56
54
 
57
55
  1. An **AStudio** group appears in the model picker;
58
56
  2. Pick a model from that group and complete a short conversation that answers normally.
59
57
 
60
- Both must hold for the install to count as successful. If the group does not appear, start with `doctor` in the [Command Line](#command-line) section to check sign-in state and the config path.
58
+ Both must hold for the install to count as successful. If the group does not appear, run `npx dsh-astudio-connect doctor` inside the profile directory (`$DSH_HOME/profiles/<name>`) to check sign-in state.
61
59
 
62
60
  > You can also hand the install to an agent you already have locally (DSH Agent, Codex, Claude Code, Cursor, …) by sending it this paragraph verbatim:
63
61
  >
64
- > Please install and verify the DSH (DeepSeek Harness) plugin dsh-astudio-connect (local directory: `<repo path>`):
62
+ > Please install and verify the DSH (DeepSeek Harness) plugin dsh-astudio-connect (npm package: `dsh-astudio-connect`):
65
63
  > Read the project README first, and confirm which interface I actually use (Web / Desktop / TUI) and the DSH kernel version;
66
- > Install from the local directory using the `cordis.patch.yml` registration, without modifying dependency packages or bypassing host validation;
64
+ > On Desktop use **Add plugin** in the Plugins panel and enter `dsh-astudio-connect`; for a command-line profile use `dsh plugin --profile <name> add dsh-astudio-connect@latest`, without modifying dependency packages or bypassing host validation;
65
+ > If the host refuses it because the kernel generation does not match, give me my DSH kernel version and the full error;
67
66
  > Ask me for consent and back up any local file or configuration change;
68
67
  > Once installed, verify that an AStudio group appears in the model picker, and run a short conversation with me.
69
68
 
@@ -73,84 +72,14 @@ Both must hold for the install to count as successful. If the group does not app
73
72
  - **Credential reuse**: reads AStudio's `experimental_bearer_token`; no separate OAuth flow.
74
73
  - **Standard protocol**: maas-api speaks standard OpenAI-compatible protocol, which pi-ai supports natively — no protocol translation.
75
74
  - **Full capability bridge**: non-streaming, streaming SSE, tool calls, `reasoning_content` from reasoning models, and context compaction all reuse the DSH host's own implementations.
76
- - **Model catalog**: taken from AStudio's own model-manager endpoint (the same document the AStudio UI renders), carrying real context windows and max output, with a three-tier fallback of live → saved → built-in.
77
- - **Reasoning-effort probing**: for models that declare no levels, sends a minimal sentinel-bearing request under your explicit consent, confirming the upstream actually validates `reasoning_effort` before accepting which levels it accepts.
78
- - **Model visibility**: pick which models appear in the picker from the settings card; preferences are stored per signed-in account, and hiding only affects selectability, not sessions already using that model.
75
+ - **Model catalog**: taken entirely from AStudio's own model-manager endpoint, carrying real context windows and max output; the plugin ships no roster of its own and reports "No models available" when it cannot fetch one.
76
+ - **Reasoning-effort probing**: for models that declare no levels, sends a minimal sentinel-bearing request under your explicit consent, first confirming the upstream really validates `reasoning_effort`, then which levels it accepts.
77
+ - **Model visibility**: pick which models appear in the picker from the settings card; a model you switch off leaves the picker immediately, and a session using it must select another before it can send.
79
78
  - **Balance at a glance**: with an AStudio model selected, the composer shows the account's general and Spark points balances beside the model picker, with a ↻ manual refresh and no automatic polling.
80
- - **Settings card**: the AStudio Connect card under the sidebar Plugins panel, split into Account / Context / Probe sections, with manual catalog refresh and probing.
79
+ - **Settings card**: the AStudio Connect card under the sidebar Plugins panel, split into Account / Models / Probe sections, with manual catalog refresh and per-model probing.
81
80
  - **Command-line diagnostics**: `status`, `doctor` and `logout` — never emit credential material.
82
81
  - **Update check**: compares against npm's public dist-tags to decide whether a newer release exists, and enriches with GitHub release notes.
83
82
 
84
- ## Command Line
85
-
86
- The plugin ships a `dsh-astudio-connect` executable (the `bin` in `package.json`). It **does not start the host and does not load pi-ai**, so it can run standalone and be used to troubleshoot from anywhere:
87
-
88
- ```
89
- Usage: dsh-astudio-connect <doctor|status|logout> [--provider astudio] [--json]
90
-
91
- doctor secret-free sign-in and environment diagnostics
92
- status sign-in state, catalog source, and model count
93
- logout clear the plugin in-memory credential cache
94
-
95
- --provider which provider to inspect; defaults to astudio
96
- --json emit one secret-free JSON document (doctor/status only)
97
- ```
98
-
99
- Run it from a local checkout (when not globally installed):
100
-
101
- ```bash
102
- node lib/bin.js status
103
- node lib/bin.js doctor --json
104
- node lib/bin.js logout
105
- ```
106
-
107
- | Command | Output | Exit code |
108
- |---|---|---|
109
- | `doctor` | Config file path and presence, sign-in state, whether the model-manager token exists, remediation hints | `0` when `signed-in` **and** the config file exists, otherwise `1` |
110
- | `status` | Sign-in state, catalog source, model count | `0` when `signed-in`, otherwise `1` |
111
- | `logout` | Clears the plugin's own in-memory credential cache | Always `0` |
112
-
113
- Behaviour guarantees:
114
-
115
- - **Writes nothing**: none of the three commands writes AStudio's `config.toml`; `logout` clears only the plugin's own in-memory cache, leaves the desktop app's sign-in alone, and does not promise to make the model group disappear (it still applies as long as the app's credential file is present).
116
- - **Never echoes credentials**: output carries only `tokenLength` and "present / absent", never the token itself; JWT-shaped strings in error text are replaced with `[redacted token]`.
117
- - **`--provider` accepts only `astudio`** — anything else errors out.
118
-
119
- > **Known behaviour**: the catalog source reported by `status` is always `built-in`. The live refresh happens inside the host process, and a standalone CLI process does not issue that request, so it reflects the built-in roster rather than the catalog the host is actually using. For the host's real source, see the Account section of the settings card.
120
-
121
- ## How It Works
122
-
123
- The plugin registers maas-api's models into DSH's `llm` seam through a three-layer bridge:
124
-
125
- 1. **Credential layer** (`auth.ts`): reads `experimental_bearer_token` from AStudio's `config.toml`, caches it in memory, never writes it to disk.
126
- 2. **Local shim** (`shim.ts`): starts an OpenAI-compatible endpoint on `127.0.0.1`, validates four inbound guards, then forwards to maas-api.
127
- 3. **pi-ai adapter** (`adapter.ts`): registers the shim's endpoint as DSH's `astudio` provider using `openAICompletionsApi()` + `PiAiAdapter`.
128
-
129
- ```
130
- DSH (llm seam) → AStudioPiAiAdapter → 127.0.0.1 shim → maas-api (Bearer token)
131
- ```
132
-
133
- The model catalog and the points balance travel over two separate read-only paths that do not go through the shim:
134
-
135
- ```
136
- model-manager API ← modelBearerToken (model roster and context parameters)
137
- account points API ← ssoSessionId (general / Spark points balance)
138
- ```
139
-
140
- All three credentials come from AStudio's own files, but they serve different purposes and are not interchangeable.
141
-
142
- ### Shim routes
143
-
144
- | Method | Path | Purpose |
145
- |---|---|---|
146
- | `GET` | `/healthz` | Liveness probe |
147
- | `GET` | `/v1/models` | Model roster, **hidden entries filtered out** |
148
- | `POST` | `/v1/chat/completions` | Forwarded to maas-api, request body and streaming response passed through |
149
-
150
- Request bodies are capped at 64 MB; a broken stream gets a terminating `data: [DONE]\n\n`; a single stream idles out after 300 seconds.
151
-
152
- The port the shim binds avoids the ports the fetch standard refuses to connect to (`FETCH_BLOCKED_PORTS` in `loopback.ts`): the OS may hand out an ephemeral port that fetch will not dial, in which case the shim listens successfully yet every request fails with `TypeError: fetch failed`. So it rebinds, up to 16 attempts.
153
-
154
83
  ## Configuration
155
84
 
156
85
  Plugin configuration is managed through DSH's plugin page (on 0.2.x: sidebar **Plugins** panel → AStudio Connect):
@@ -164,29 +93,20 @@ Plugin configuration is managed through DSH's plugin page (on 0.2.x: sidebar **P
164
93
 
165
94
  ## Model Catalog
166
95
 
167
- The model roster and context parameters come from AStudio's **model-manager** endpoint (`GET /api/v1/model-manager/models`, production base `https://astronstudio-api-volces-prod.xf-yun.com/api/v1/model-manager`), which is the same document the AStudio UI renders. Authentication uses `modelBearerToken` from `astron-session.json` — a separate credential from the chat `experimental_bearer_token`.
168
-
169
- > `/v1/models` is deliberately not used: that OpenAI-compatible discovery endpoint answers with a smaller, staler subset and would drop models that are in fact callable.
170
-
171
- Catalog parsing rules:
96
+ The model roster and context parameters come from AStudio's **model-manager** endpoint.
172
97
 
173
- - Only models with `visibility` of `list` are kept; those with `supported_in_api` of `false` are dropped;
174
- - Context window and max output come from the upstream's real values (e.g. `spark-x2.6-preview` is 262144 / 100000);
175
- - The upstream's `none` in `supported_reasoning_levels` is translated into a "thinking can be switched off" capability flag rather than treated as an effort level;
176
- - When merging, the built-in roster's order and names win; models unique to the upstream are appended at the end.
98
+ **Catalog parsing rules:**
177
99
 
178
- **Fallback order**:
100
+ - Only models with `visibility` of `list` are kept; those with `supported_in_api` of `false` are dropped, as are rows missing a context window or output budget;
101
+ - Context window and max output come from the upstream's real values (e.g. `spark-x2.6-preview` is 262144 / 100000).
179
102
 
180
- 1. Live catalog
181
- 2. Last successful snapshot
182
- 3. Built-in roster
103
+ **The plugin ships no model roster at all.** The list is whatever upstream actually returned:
183
104
 
184
- Snapshots are written to `$DSH_HOME/.astudio-connect/catalog.json`, stored per local identity. The catalog source (`live` / `saved` / `fallback`) and the fetch time are both shown on the settings card.
105
+ 1. Live catalog — the models this fetch returned;
106
+ 2. Last successful snapshot — that same upstream answer, when this fetch could not be made (network, auth, malformed response);
107
+ 3. Neither — the list is empty.
185
108
 
186
- The current built-in roster covers:
187
-
188
- - **Reasoning models**: `spark-x2.5`, `spark-x2.6-preview` (thinking switchable, `low` / `high` / `max`)
189
- - **Chat models**: `spark-x2.5-4b`, `xopglm52`, `xopdeepseekv4pro`, `xopdeepseekv4pro0813`, `xopdsv4flash0731in`, `astronclaw-auto`
109
+ Case 3 is not filled in with a built-in roster, a placeholder or a default model: the settings card says "No models available" and names the reason (signed out / endpoint error / unreadable response / unreachable / upstream listed none), and the model picker shows the host's own "No models available". An upstream answer that is **explicitly empty** is treated the same way and written to the snapshot — otherwise a later failed refresh would resurrect models upstream has withdrawn. A response whose `models` field is missing, of the wrong type, or whose rows are all unusable is treated as a malformed response rather than as "upstream withdrew every model", and leaves the existing snapshot untouched.
190
110
 
191
111
  ## Reasoning Effort and Probing
192
112
 
@@ -201,9 +121,9 @@ So this plugin uses a **user-consented, dynamically discovered** approach:
201
121
  3. Upstream accepts → recorded as `validating`, storing only the accepted levels; upstream rejects with `invalid_parameter_value` → recorded as `rejected` / `non-validating`;
202
122
  4. Results are persisted per `account:modelId` and expire after 24 hours.
203
123
 
204
- Probing consumes a small amount of points. A result only means the current upstream accepts that level — **it does not promise that reasoning quality, speed, or point consumption will change**.
124
+ Probing sends one real upstream request, consuming a small number of upstream tokens (like every other call the plugin makes, that counts against upstream usage, not AStudio points — see [Billing and Points](#billing-and-points)). A result only means the current upstream accepts that level — **it does not promise that reasoning quality or speed will change**.
205
125
 
206
- The mapping from `reasoning_effort` to pi-ai levels only exposes levels the upstream declared; `off` is exposed only when the model declares `none` (i.e. thinking can be switched off).
126
+ The mapping from `reasoning_effort` to pi-ai levels only exposes levels the upstream declared; `off` is exposed only when the model declares `none` (i.e. thinking can be switched off), and **what actually goes out on the wire is `none`, not `off`** — upstream's vocabulary is only `none` / `high` / `max`, and a literal `off` gets the whole request refused with 10005.
207
127
 
208
128
  ## Model Visibility
209
129
 
@@ -224,7 +144,7 @@ General 1393 · Spark 24621
224
144
 
225
145
  The two balances are shown separately and are **never added together**, because they pay for different things:
226
146
 
227
- - **General points** (`totalBalance`): every model, plus plugins and Skill calls.
147
+ - **General points** (`totalBalance`): every model, plus plugins and Skill calls (AStudio's own plugins and Skills — **not this plugin**).
228
148
  - **Spark points** (`sparkTotalBalance`): Spark-branded models only.
229
149
 
230
150
  The data comes from the account endpoint the AStudio desktop app itself uses (`https://agent.xfyun.cn/xingchen-studio/points/balance`). AStudio's settings page requests its own loopback service, which forwards to the account API carrying the desktop session cookie; this plugin reaches the same upstream directly using the `ssoSessionId` already present in `astron-session.json`, so it **adds no location discovery and does not require AStudio to be running**.
@@ -248,15 +168,39 @@ Behaviour guarantees:
248
168
 
249
169
  > **Known limitation**: the account endpoint only provides aggregate balances, with no per-transaction detail — the AStudio desktop app itself is the same way (it directs users to the web to view details). So the badge can reflect *balance changes* but cannot say *exactly how much this conversation consumed*.
250
170
 
171
+ > **This number will not move while you chat through the plugin.** An unchanged balance does not mean nothing was spent — it means the call never entered AStudio's ledger at all. See [Billing and Points](#billing-and-points).
172
+
251
173
  If the badge shows "Unavailable" and clicking ↻ does nothing, first confirm the plugin artifacts match the running DSH version: DSH loads plugins at startup, so after rebuilding you must **fully quit and restart DSH** (refreshing the page is not enough).
252
174
 
175
+ ## Billing and Points
176
+
177
+ > **The short version**: calls made by this plugin **do not consume AStudio points**. AStudio's points are not charged by the model gateway. The desktop app settles them per *turn*, from usage it reports itself. The plugin is a separate process and is never on that path.
178
+
179
+ The thing that actually charges you is the desktop app, not `maas-api`:
180
+
181
+ 1. The acode runtime opens a turn (carrying `turn_id` / `thread_id` / `session_id`).
182
+ 2. Each model request goes through the desktop's local model gateway to `model_providers.astron-spark.base_url`; the gateway records that request's input / output / cached tokens as a runtime event.
183
+ 3. When the turn ends, the desktop app aggregates those events into a `ClientTurnUsageReport` (schema 1.3) and writes it to `turn_billing_outbox` in `state.sqlite`.
184
+ 4. `DirectPointsGateway.settle()` POSTs that report to `{studioApiBaseUrl}points/deduce-balance` with the desktop session cookie; the server rates it into `ratedPoints` and deducts `GENERAL_POINTS` / `SPARK_POINTS`.
185
+
186
+ Three conditions must hold at once to be charged: the request belongs to an acode turn, that turn finishes as `completed`, and the settlement request is submitted successfully. Miss any one and nothing is charged — **merely leaving through that gateway is not enough**.
187
+
188
+ ### This is not the same as "free"
189
+
190
+ - **Upstream tokens are still consumed** — they just never reach AStudio's ledger.
191
+ - **The balance gate does not cover the plugin**: when its own balance hits 0, AStudio rejects requests with `402 astron_points_exhausted` — but that check lives in its own local gateway, which the plugin does not traverse. In theory the plugin still works at zero balance (inferred from the current implementation; not tested).
192
+ - **Upstream can move metering into the gateway at any time**, keying it on uid or token. When that happens this path closes immediately, with no code change on our side.
193
+ - **Do not try to "report back" on the plugin's behalf**: the `session_id` / `thread_id` / `call_ids` / `capability_usages` in `ClientTurnUsageReport` can only be produced by the acode runtime, and the server keeps `turn_billing_usage_reviews` as a missing-usage audit. A fabricated report only lands in the audit table; it does not make the books right.
194
+
195
+ The badge reads AStudio's own ledger, so of course it stays still while you chat through the plugin — **an unchanged balance does not mean nothing was spent, only that the spend missed that ledger**. Read the [Disclaimer](#disclaimer) before using this.
196
+
253
197
  ## Settings Card
254
198
 
255
199
  Sidebar **Plugins** panel → AStudio Connect. One card split into three sections, switching between Chinese and English with the plugin language:
256
200
 
257
- - **Account**: sign-in state, config file path, catalog source (live / saved / built-in), model count, and a one-click catalog refresh.
258
- - **Context**: each model's context window and max output, plus the model visibility toggles.
259
- - **Probe**: probe state and accepted levels for models with no declared levels, with manual probe / re-probe.
201
+ - **Account**: sign-in state, catalog source (live / saved / no models), model count, and a one-click catalog refresh.
202
+ - **Models**: a visibility switch per model, committed by **Save** — a model switched off disappears from DSH's model picker right away. A session that was using that model has to select another one before it can send again. Until then the card says "n unsaved" in its header and "once saved, n of m exposed to DSH" in the body; **Discard** drops the draft.
203
+ - **Probe**: probe state and accepted levels per model, with per-model probe / re-probe. A press turns the button into "Probing…" and answers with a pressed-in displacement plus a haptic tick where the platform has one.
260
204
 
261
205
  The card reads the status document once on mount and then every 60 seconds; all data comes from the same-origin `GET /astudio/status` and carries no credential material.
262
206
 
@@ -267,69 +211,46 @@ The card reads the status document once on mount and then every 60 seconds; all
267
211
  | `/astudio/status` | `GET` | Status document | loopback Host + Origin |
268
212
  | `/astudio/points` | `GET` | Points balance (`?refresh=1` bypasses the cache) | loopback Host + Origin |
269
213
  | `/astudio/update` | `GET` | Version and update info | loopback Host + Origin |
270
- | `/astudio/probe/{modelId}` | `POST` | Reasoning-effort probing | loopback Host + Origin + in-process probe key |
271
-
272
- The probe route separately requires an in-process key (handed out with the status document, rotated per process, never persisted): the status route's loopback check stops a DNS-rebinding **page**, and that is not the same as authorising a **state-changing** action.
214
+ | `/astudio/probe/{modelId}` | `POST` | Reasoning-effort probing | loopback Host + Origin + in-process control key |
215
+ | `/astudio/visibility/{modelId}` | `POST` | Model visibility (one per changed model, sent by Save) | loopback Host + Origin + in-process control key |
273
216
 
274
- ## Update Check and Host Heartbeat
275
-
276
- **Update check** (`update.ts`): reads npm's public dist-tags first to decide whether an update exists — looking only at the `latest` tag, since pre-release channels are not this plugin's release line; only once an update is confirmed does it query GitHub releases for the version notes. Both requests have a timeout (8 seconds) and a response body size cap, and the browser re-validates the version numbers, release URL and range with `parseAStudioUpdateResult` before rendering; any mismatch suppresses rendering.
277
-
278
- **Host heartbeat** (`host-heartbeat.ts`): after the plugin registers the provider, it writes a JSON document containing `pid` and `registeredAt` to `$DSH_HOME/.astudio-host-heartbeat.json`; the host deletes it on `ctx.dispose()`.
279
-
280
- The heartbeat distinguishes "the host is still running" from "the host crashed and left the file behind". Checking the PID alone is not enough — after a crash the OS may hand the same PID to an unrelated process, and that uncleared heartbeat would falsely report "host alive". So the condition is that the PID exists **and** the process holding it started no later than `registeredAt`; when the start time cannot be read, it degrades to a plain PID check.
281
-
282
- The browser half writes no files and reports failure only through `console.error`. That asymmetry is intentional: the host is the load-bearing half, and a missing heartbeat unambiguously means the host never started.
283
-
284
- ## Location Discovery (Windows)
285
-
286
- The plugin does not assume AStudio lives at a fixed path. It locates the data directory in this order:
287
-
288
- 1. A path explicitly set in the `configPath` setting (used verbatim, not probed)
289
- 2. The `ASTUDIO_DATA_HOME` environment variable (used verbatim, not probed)
290
- 3. AStudio's own registry record: `ProductionDataRoot` / `ProductionAcodeHome` under `HKCU\Software\AStudioStorage`
291
- 4. The `<install directory name> Data` directory beside the install directory
292
-
293
- Item 3 is the record AStudio itself writes, so moving drives or reinstalling needs no configuration at all. A record that exists but is incomplete or of an unsupported schema version raises an explicit error rather than silently degrading to "not connected" — an older install this plugin cannot read must not win over a newer one.
294
-
295
- Registry value-name prefixes are split by release channel: `production` → `Production*`, `canary` → `Canary*`, `development` → `Development*`; item 4's directory name follows as `AStudio Data` / `AStudio Canary Data` / `AStudio Dev Data`.
296
-
297
- Automatic discovery is not implemented on non-Windows platforms; set `ASTUDIO_DATA_HOME` explicitly there.
217
+ Both write routes require the same in-process control key (handed out with the status document, rotated per process, never persisted): the status route's loopback check stops a DNS-rebinding **page**, and that is not the same as authorising a **state-changing** action.
298
218
 
299
219
  ## DSH Version Support
300
220
 
301
- The plugin declares host dependencies in two places, both pinned to seam package version **`0.2.0-rc.2`**.
221
+ | Plugin version | DSH kernel it needs |
222
+ |---|---|
223
+ | `0.1.1` | `0.2.1-rc.2` |
224
+ | `0.1.0` | `0.2.0-rc.2` |
302
225
 
303
- **Host-side `peerDependencies`** (run in the host process):
226
+ The host refuses to load the plugin when the kernel generation does not match. `0.1.0` shipped against `0.2.0-rc.2`, which was the real-machine environment; from `0.1.1` on the plugin targets `0.2.1-rc.2`, where provider registration, the settings card and the points badge are covered by local unit and integration tests, and the exact desktop build number still needs re-confirming on the new kernel.
304
227
 
305
- | Package | Purpose |
306
- |---|---|
307
- | `dsh-llm` / `dsh-llm-pi-ai` | Register the `astudio` provider |
308
- | `dsh-host-webserver` | Routes for the settings card and points badge |
309
- | `dsh-settings` | Plugin configuration |
310
- | `dsh-home-paths` | Where snapshots, visibility preferences and the heartbeat are written |
311
- | `dsh-attachment` | Attachment passing |
312
- | `@deepseek-ai/cordis` | `^4.0.2` (`4.x`) |
313
- | `@deepseek-ai/schemastery` | `^3.18.2` |
314
- | `react` | `^18.2.0` |
228
+ ## When the upstream refuses a request
229
+
230
+ maas-api answers a non-conforming request with a bare `10005 invalid request` and never names the field. The plugin heals the request in the forwarding layer, in this order, without DSH or pi-ai having to change:
315
231
 
316
- **Browser-side `dsh.client.inject`** (host packages the client bundle depends on):
232
+ 1. **Wire repair** — `tool_calls[].function.arguments` that is not a string, a `tool` message with no `tool_call_id`, `content` set to `null`. Every OpenAI-compatible server rejects these, so they are fixed in place and logged at warn level.
233
+ 2. **Effort guard** — a `reasoning_effort` the model does not declare (or any effort for a model that does not reason) is dropped. The gateway validates the level per model and answers 400 for the whole request otherwise.
234
+ 3. **Shape fallback** — drop platform-only fields (`store`, `stream_options`, `prompt_cache_key`, …) and rewrite `developer` as `system`; if that still earns a 400, flatten text-only content arrays to strings. The first accepted shape is reused for the rest of the process, so later requests cost no extra round trip.
235
+ 4. **Self-diagnosis (once per process)** — when every shape is refused, the plugin asks the gateway instead of guessing: it changes one dimension at a time (`max_tokens`, the whole `tools` array, `reasoning_effort`, the message history), then bisects whichever one is at fault. The conclusion is included in the error handed back to DSH (`diagnosis: …`), and what can be fixed is fixed and resent, staying fixed for the process:
236
+ - `max_tokens` above the limit → clamped to 8192;
237
+ - a refused tool definition → dropped by name (logged), the rest still apply;
238
+ - an unsupported effort → removed.
317
239
 
318
- `dsh-client-ui-renderer`, `dsh-client-ui-conversation`, `dsh-client-ui-model-selection`, `dsh-client-locale`, `dsh-api-remotes`, `dsh-api-session-controller`.
240
+ `store` is never sent (`supportsStore: false`), so steps 1–3 normally do nothing and cost no requests.
319
241
 
320
- | Status | Version | Notes |
321
- |---|---|---|
322
- | **Supported** | DSH Desktop **2.0.17** (seam `0.2.0-rc.2`) | Current development and verification environment: provider registration, settings card and points badge all confirmed on a real machine |
242
+ A refused body is also written to `%TEMP%\dsh-astudio-connect\failed-requests\` (five files at most, 16 MB each) because only the real body shows what the gateway objects to. Those files contain conversation text and stay on the machine; set `ASTUDIO_DUMP_FAILED=0` to disable this, or `ASTUDIO_DUMP_FAILED_DIR` to relocate it.
323
243
 
324
244
  ## Known Limitations
325
245
 
326
246
  - [ ] **Image input is not enabled yet**: the adapter already maps a model to `input: ['text', 'image']` based on the catalog declaration, but model-manager parsing currently hardcodes `supportsImages: false` (and the upstream document has no corresponding field), so every model accepts plain text only.
327
- - [ ] **The update check requires the package to be published on npm**: the version judgement reads npm's public dist-tags, so with the package unpublished that step 404s and the check is always "unavailable". The repository URL also once named a nonexistent org, making the GitHub release enrichment 404 as well; both are corrected in `src/version.ts` and `package.json`'s `repository`. The enrichment path only takes effect once the package is published.
328
- - [ ] **Automatic location discovery is implemented only on Windows**: macOS / Linux need `ASTUDIO_DATA_HOME` set explicitly.
247
+ - [ ] **Windows only for now**: macOS / Linux are neither adapted nor verified, so they are unsupported.
329
248
  - [ ] **A single local identity**: AStudio's token carries no stable uid, so catalog snapshots, visibility preferences and probe records all hang off `astudio-local`, with no multi-account isolation.
330
- - [ ] **No per-transaction billing detail**: the account endpoint only provides aggregate balances, so the badge can only reflect balance changes.
331
- - [ ] **No long-term compatibility guarantee for upstream private endpoints**: the model catalog, account endpoint and reasoning-level information are the AStudio client's own interfaces rather than an official open API, and upstream changes may break the plugin. In that case it degrades through "last successful catalog → built-in roster" and labels the source on the card, but long-term compatibility cannot be promised.
332
- - [ ] **The CLI's `status` does not reflect the live catalog**: see the note in [Command Line](#command-line).
249
+ - [ ] **Plugin calls are not billed to AStudio points**: points are settled by the desktop app per turn, and the plugin creates no turn, so nothing is deducted. For better or worse this is an unmetered path — see [Billing and Points](#billing-and-points). The plugin deliberately offers no way to "report back".
250
+ - [ ] **The balance gate does not cover the plugin**: AStudio itself returns `402` at zero balance, but that check lives inside its own local gateway, which the plugin bypasses (inferred, not tested).
251
+ - [ ] **No per-transaction billing detail**: the account endpoint only provides aggregate balances, so the badge can only reflect balance changes; plugin calls produce no billing records at all, let alone detail.
252
+ - [ ] **No long-term compatibility guarantee for upstream private endpoints**: the model catalog, account endpoint and reasoning-level information are the AStudio client's own interfaces rather than an official open API, and upstream changes may break the plugin. In that case it degrades to "last successful catalog", and says so plainly when there is no snapshot either, but long-term compatibility cannot be promised.
253
+ - [ ] **The CLI's `status` now issues one live fetch**: it waits for a real request so it can report the actual catalog source and model count; offline or signed out, it reports `empty` with the matching failure.
333
254
  - [ ] **No account-switch following**: unlike WorkBuddy's two variants, AStudio is a single product, so the plugin does not replicate variant-level capabilities.
334
255
 
335
256
  ## Security
@@ -347,7 +268,7 @@ The plugin declares host dependencies in two places, both pinned to seam package
347
268
  ```bash
348
269
  npm install
349
270
  npm run check # typecheck + test + build
350
- npm test # run 263 tests (28 test files)
271
+ npm test # run 302 tests (29 test files)
351
272
  npm run build # produce lib/ (the directory DSH actually loads)
352
273
  ```
353
274
 
@@ -361,6 +282,8 @@ This project is **for personal learning and research only**. It drives only the
361
282
 
362
283
  Users must comply with AStudio's terms of service; any consequence arising from use of this project (including but not limited to account restrictions, cleared credits, or service interruption) is borne by the user alone.
363
284
 
285
+ **This plugin implements neither AStudio's usage reporting nor its points settlement**: calls made through it never enter AStudio's points ledger (mechanism in [Billing and Points](#billing-and-points)). That gap follows from the other side's client-reported billing model — it is not a feature of this project, and it must not be used to evade billing. If you want to pay for these calls, use the official AStudio client.
286
+
364
287
  The authors accept no liability for any direct or indirect damages arising from the use or misuse of this project.
365
288
 
366
289
  This project is not affiliated with, authorised by, or endorsed by iFLYTEK, AStudio, or DeepSeek. Names appearing in this document are used only to describe compatibility; their trademark rights belong to their respective owners.
@@ -369,7 +292,7 @@ This project is not affiliated with, authorised by, or endorsed by iFLYTEK, AStu
369
292
 
370
293
  - **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** (MIT) — the sibling DSH bridge plugin, aimed at the WorkBuddy desktop app. This project follows its overall structure, with AStudio's maas-api as the upstream instead.
371
294
 
372
- Refer to that project's own documentation for supported versions and installation requirements. It is maintained independently by a third party; listing it is not an endorsement, and is not a stability or compatibility promise about this plugin's internal interfaces.
295
+ > Refer to that project's own documentation for supported versions and installation requirements. It is maintained independently by a third party; listing it is not an endorsement, and is not a stability or compatibility promise about this plugin's internal interfaces.
373
296
 
374
297
  ## Acknowledgements
375
298