@huaweidevtools/aishell-dsh-llm 0.0.1 → 0.0.2

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.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  OpenAI-compatible LLM adapter for DSH. Registers the `aishell` provider route on `ctx.llm`, routing model calls through the aishell gateway with direct AK/SK signing.
4
4
 
5
+ Besides the live route, the plugin declares `aishell` in the configurable-provider directory (`ctx.llm.registerConfigurableProviders`) — the directory the Models page joins with live routes, so the route shows up as a configurable provider row with its own settings address. The plugin also carries a Config schema; the settings mirror only lists namespaces whose plugin declares one, so without it the Models page would not show the row.
6
+
5
7
  ## How it works
6
8
 
7
9
  ```
@@ -21,6 +23,29 @@ User selects aishell/<model>
21
23
 
22
24
  No intermediate API-key exchange or config fetch — every request is signed with the user's AK/SK. The credential store (OS keyring with fingerprint + memory fallback) holds the AK/SK; this adapter only reads them.
23
25
 
26
+ ## Image input
27
+
28
+ The adapter carries images for models whose gateway catalog entry lists `image` in `modalities`. A model the catalog describes as text-only never receives one: `dsh-llm` projects such an image to a stable placeholder before the adapter sees it.
29
+
30
+ For an image-capable model the pipeline is:
31
+
32
+ ```
33
+ harness history (durable image *references*, not bytes)
34
+ → prepareRequestImages: one request version per attachment, via ctx.attachments
35
+ → byte budget check → IMAGE_OFFLOAD_REQUIRED when over
36
+ → projectOffloaded: occurrences an earlier offload decision marked
37
+ → serialize: [handle text, base64 data URL] parts on the user message
38
+ ```
39
+
40
+ Two properties are worth knowing:
41
+
42
+ - **Bytes are read from the attachment service, not the block.** An image block holds an opaque `attachmentId` that is deliberately neither a path nor a URL, so the adapter resolves bytes through `ctx.attachments.readImageRequest`, which also normalizes and re-encodes to the route's target size. A deployment without that service fails image requests with `UNSUPPORTED_CONTENT` rather than sending an empty turn.
43
+ - **Tool-result images ride a separate user turn.** OpenAI's `tool` message takes text only, so an image a tool returned is hoisted into a following `user` message — the same shape the ecosystem's OpenAI-path serializer produces.
44
+
45
+ Over-budget requests fail with `IMAGE_OFFLOAD_REQUIRED` and are retried by `@deepseek-ai/dsh-compaction-image-offload`, which `dsh-base` mounts: it marks the oldest occurrences offloaded, after which they travel as placeholder text. This adapter never offloads on its own.
46
+
47
+ Image token accounting uses the token meter's structural estimate — this adapter implements no `imageRequestPricing`, matching `dsh-llm-pi-ai`. Image-heavy sessions are therefore approximated rather than priced per model.
48
+
24
49
  ## What it does NOT touch
25
50
 
26
51
  - `ctx.credentials` — DSH's own credential provider stays `dsh-credentials-local`. User-configured models and `.credentials.yaml` keep working.
@@ -41,8 +66,13 @@ dsh plugin --profile <name> add @huaweidevtools/aishell-dsh-base
41
66
 
42
67
  | Field | Type | Default | Description |
43
68
  |---|---|---|---|
44
- | `baseUrl` | `string?` | `https://api.aishell.openbuildlab.com` | Chat API base URL. `/chat/completions` and `/models` are appended. |
69
+ | `baseUrl` | `string?` | `https://api.aishell.openbuildlab.com/v1` | Chat API base URL. `/chat/completions` and `/models` are appended, so a base carrying its own path segment is signed and requested in full. |
45
70
  | `keyringService` | `string?` | `aishell-dsh` | Credential store service name. |
71
+ | `requestImagePixelBudget` | `number?` | `4194304` | Total-pixel cap one request image is scaled into. |
72
+ | `requestImageMaxBytes` | `number?` | `1048576` | Encoded-byte target for one request image. |
73
+ | `maxRequestImageBytes` | `number?` | `20971520` | Accumulated base64 image bytes one request accepts before the oldest images must be offloaded. |
74
+
75
+ The three image fields share `dsh-llm-pi-ai`'s defaults: the gateway publishes no image budget of its own, so the route supplies one.
46
76
 
47
77
  ## Storing credentials
48
78
 
package/lib/client.js ADDED
@@ -0,0 +1,221 @@
1
+ window.__ModuleLoader__.load({ id: "@huaweidevtools/aishell-dsh-llm", factory: (require) => {
2
+ "use strict";
3
+ var module = { exports: {} }; var exports = module.exports;
4
+ //#region rolldown:runtime
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
+ key = keys[i];
14
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
22
+ value: mod,
23
+ enumerable: true
24
+ }) : target, mod));
25
+
26
+ //#endregion
27
+ const __deepseek_ai_dsh_client_store = __toESM(require("@deepseek-ai/dsh-client-store"));
28
+ const react = __toESM(require("react"));
29
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
30
+
31
+ //#region src/client/ProviderCard.tsx
32
+ /**
33
+ * Render the aishell family's card extension.
34
+ *
35
+ * The parent renders this inside the host editor's card (below its fallback
36
+ * hint), so the visual style follows the surrounding card rather than opening
37
+ * a panel of its own: one readiness line and one catalog list, no buttons —
38
+ * the host card's Cancel stays the only control. Readiness folds the
39
+ * credential fact into a single conclusion (the question this page asks is
40
+ * "can I use this provider", not "what are my credentials"); details and
41
+ * management of credentials belong to the AI Shell settings section, which
42
+ * the unconfigured hint names.
43
+ */
44
+ /**
45
+ * What an empty catalog means, and therefore what the card says about it.
46
+ *
47
+ * An empty model list has two unrelated causes that look identical on the
48
+ * wire: the gateway returned nothing, or the request was never signed because
49
+ * no credentials are stored. Only the second one is actionable, and it is the
50
+ * state every new user starts in — so it has to name the place to fix it.
51
+ *
52
+ * Extracted from the render so the branch is testable without a DOM: the
53
+ * repo's client-side tests cover pure logic, not components.
54
+ *
55
+ * @param catalog - the route body, or `undefined` while it is still loading.
56
+ * @returns the text to show, which the caller renders as-is.
57
+ */
58
+ function catalogStatusText(catalog) {
59
+ if (catalog === void 0) return "获取中…";
60
+ if (catalog.models.length > 0) return "";
61
+ return catalog.credentialsPresent ? "网关未返回任何模型" : "需要凭据才能获取(凭据在 设置 → AI Shell 中配置)";
62
+ }
63
+ function AishellProviderCard(props) {
64
+ const status = props.useCredentials((snapshot) => snapshot);
65
+ const [catalog, setCatalog] = (0, react.useState)(void 0);
66
+ const [catalogError, setCatalogError] = (0, react.useState)(void 0);
67
+ (0, react.useEffect)(() => {
68
+ let cancelled = false;
69
+ fetch(MODELS_ROUTE).then(async (response) => {
70
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
71
+ return response.json();
72
+ }).then((body) => {
73
+ if (!cancelled) {
74
+ setCatalog(body);
75
+ setCatalogError(void 0);
76
+ }
77
+ }).catch((error) => {
78
+ if (!cancelled) setCatalogError(error instanceof Error ? error.message : String(error));
79
+ });
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, []);
84
+ const configured = status.credentialsStored;
85
+ const readinessStyle = {
86
+ display: "flex",
87
+ alignItems: "center",
88
+ gap: 8,
89
+ fontSize: 12,
90
+ lineHeight: "18px",
91
+ fontWeight: 500,
92
+ color: "var(--dsw-alias-label-secondary)"
93
+ };
94
+ const readinessTextStyle = {
95
+ fontWeight: 400,
96
+ color: configured ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)"
97
+ };
98
+ const rowStyle = {
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ gap: 2
102
+ };
103
+ const catalogLabelStyle = {
104
+ fontSize: 12,
105
+ lineHeight: "18px",
106
+ fontWeight: 500,
107
+ color: "var(--dsw-alias-label-secondary)"
108
+ };
109
+ const valueStyle = {
110
+ fontSize: 12,
111
+ lineHeight: "18px",
112
+ color: "var(--dsw-alias-label-tertiary)"
113
+ };
114
+ const sectionStyle = {
115
+ display: "flex",
116
+ flexDirection: "column",
117
+ gap: 10,
118
+ borderTop: "0.5px solid var(--dsw-alias-border-l4)",
119
+ marginTop: 12,
120
+ paddingTop: 12
121
+ };
122
+ const modelStyle = {
123
+ display: "flex",
124
+ alignItems: "center",
125
+ gap: 8,
126
+ fontSize: 13,
127
+ lineHeight: "20px",
128
+ color: "var(--dsw-alias-label-primary)"
129
+ };
130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
131
+ style: sectionStyle,
132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
133
+ style: readinessStyle,
134
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
135
+ style: {
136
+ width: 6,
137
+ height: 6,
138
+ borderRadius: "50%",
139
+ flex: "none",
140
+ background: configured ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-warn-label)"
141
+ },
142
+ "aria-hidden": "true"
143
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
144
+ style: readinessTextStyle,
145
+ children: configured ? "已就绪 — 凭据已验证,可直接对话" : "未就绪 — 凭据未配置(在 设置 → AI Shell 中保存 AK/SK)"
146
+ })]
147
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
148
+ style: rowStyle,
149
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
150
+ style: catalogLabelStyle,
151
+ children: "模型目录(来自 aishell 网关,只读)"
152
+ }), catalogError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
153
+ style: valueStyle,
154
+ children: ["目录获取失败:", catalogError]
155
+ }) : catalog === void 0 || catalog.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
156
+ style: valueStyle,
157
+ children: catalogStatusText(catalog)
158
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
159
+ style: {
160
+ margin: 0,
161
+ padding: 0,
162
+ listStyle: "none",
163
+ display: "flex",
164
+ flexDirection: "column",
165
+ gap: 4
166
+ },
167
+ children: catalog.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
168
+ style: modelStyle,
169
+ children: [
170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
171
+ "aria-hidden": "true",
172
+ children: "·"
173
+ }),
174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.id }),
175
+ model.inputModalities.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
176
+ style: {
177
+ fontSize: 11,
178
+ color: "var(--dsw-alias-label-quaternary)"
179
+ },
180
+ children: [model.inputModalities.join(" / "), " 输入"]
181
+ }) : null
182
+ ]
183
+ }, model.id))
184
+ })]
185
+ })]
186
+ });
187
+ }
188
+ /**
189
+ * Host route, written document-relative like the credential routes the
190
+ * sibling plugin uses: the served index carries `<base href="./">`, so the
191
+ * request lands on the same origin that serves the page — which carries the
192
+ * browser-session cookie the route's authentication fence requires.
193
+ */
194
+ const MODELS_ROUTE = "api/aishell/models";
195
+
196
+ //#endregion
197
+ //#region src/client/index.ts
198
+ /** The settings namespace aishell-credentials owns (the card reads its status). */
199
+ const AISHELL_CREDENTIALS_NS = "aishell-client-credentials";
200
+ /** Required services. */
201
+ const inject = ["slots", "configForms"];
202
+ function apply(ctx) {
203
+ const form = ctx.configForms.get(AISHELL_CREDENTIALS_NS);
204
+ const status = (0, __deepseek_ai_dsh_client_store.createSnapshotStore)({ credentialsStored: false });
205
+ const derive = () => {
206
+ status.set({ credentialsStored: form.getSnapshot().value?.credentialsStored === true });
207
+ };
208
+ ctx.effect(() => form.subscribe(derive), "aishell-llm: credential presence for the models card");
209
+ derive();
210
+ ctx.effect(() => ctx.configForms.whileServed([AISHELL_CREDENTIALS_NS], () => ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
211
+ name: "settings.models.provider-card",
212
+ key: "aishell-llm",
213
+ inject: () => ({ hooks: { credentials: status } })
214
+ }, AishellProviderCard))), "aishell-llm: models page provider card");
215
+ }
216
+
217
+ //#endregion
218
+ exports.apply = apply
219
+ exports.inject = inject
220
+ return module.exports; } });
221
+ //# sourceMappingURL=client.js.map