@dsh-plus/llm-pi 0.1.4 → 0.1.6

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/lib/client.js CHANGED
@@ -659,11 +659,11 @@ function CompatEditor(props) {
659
659
  };
660
660
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
661
661
  className: `lpc-field lpc-wide`,
662
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapseSection, {
662
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
663
663
  id: `${props.idPrefix}-collapse`,
664
664
  title: props.t("compatGroup"),
665
665
  defaultOpen: false,
666
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
666
+ children: [props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
667
667
  className: "lpc-hint",
668
668
  children: props.t("compatApiHint")
669
669
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -704,7 +704,7 @@ function CompatEditor(props) {
704
704
  }
705
705
  }, field);
706
706
  })
707
- })] })
707
+ })]
708
708
  })
709
709
  });
710
710
  }
@@ -1290,11 +1290,11 @@ function ProviderSection(props) {
1290
1290
  removeLabel: t("remove"),
1291
1291
  onEdit: (headers) => props.onPatch({ headers })
1292
1292
  }),
1293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapseSection, {
1293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
1294
1294
  id: `${id}-advanced`,
1295
1295
  title: t("advancedGroup"),
1296
1296
  defaultOpen: false,
1297
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1297
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1298
1298
  id: `${id}-retry`,
1299
1299
  label: t("retryPolicy"),
1300
1300
  hint: t("retryPolicyHint"),
@@ -1313,7 +1313,7 @@ function ProviderSection(props) {
1313
1313
  wide: true,
1314
1314
  t,
1315
1315
  onEdit: (compat) => props.onPatch({ compat })
1316
- })] })
1316
+ })]
1317
1317
  }),
1318
1318
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTable, {
1319
1319
  route,
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import z from "@deepseek-ai/schemastery";
2
1
  import * as _deepseek_ai_dsh_settings0 from "@deepseek-ai/dsh-settings";
2
+ import z from "@deepseek-ai/schemastery";
3
3
  import { Context } from "@deepseek-ai/cordis";
4
4
 
5
5
  //#region src/config.d.ts
package/lib/index.js CHANGED
@@ -1,21 +1,21 @@
1
- import z from "@deepseek-ai/schemastery";
2
1
  import { deepEqualJson, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import z from "@deepseek-ai/schemastery";
3
3
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
4
4
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
5
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
5
6
  import { request } from "node:http";
6
7
  import { request as request$1 } from "node:https";
7
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import HttpsProxyAgentModule from "https-proxy-agent";
10
10
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
11
11
  import { pathToFileURL } from "node:url";
12
+ import * as vendoredLlm from "@deepseek-ai/dsh-llm";
13
+ import * as vendoredPiAiAdapter from "@deepseek-ai/dsh-llm-pi-ai";
12
14
  import * as vendoredPiAi from "@earendil-works/pi-ai";
13
- import * as vendoredCatalog from "@earendil-works/pi-ai/providers/all";
14
15
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
15
16
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
16
17
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
17
- import * as vendoredLlm from "@deepseek-ai/dsh-llm";
18
- import * as vendoredPiAiAdapter from "@deepseek-ai/dsh-llm-pi-ai";
18
+ import * as vendoredCatalog from "@earendil-works/pi-ai/providers/all";
19
19
 
20
20
  //#region src/ns.ts
21
21
  /**
@@ -572,7 +572,7 @@ const FIELDS_BY_PROTOCOL = {
572
572
  "openai-responses": RESPONSES_FIELDS,
573
573
  "anthropic-messages": ANTHROPIC_FIELDS
574
574
  };
575
- function checkValue(api, field, spec, value, where) {
575
+ function checkValue(_api, field, spec, value, where) {
576
576
  if (spec === "boolean") {
577
577
  if (typeof value !== "boolean") throw new Error(`${where}: compat.${field} 必须是布尔值`);
578
578
  return;
@@ -1112,11 +1112,13 @@ async function startRuntime(ctx, rawConfig) {
1112
1112
  logger.warn(error);
1113
1113
  });
1114
1114
  } else try {
1115
- registrations[0].handle.replace(routes);
1116
- for (let i = 1; i < registrations.length; i += 1) registrations[i].handle.replace([]);
1115
+ const [first, ...rest] = registrations;
1116
+ if (first === void 0) throw new Error("registrations 为空(此前整批注册全部失败)");
1117
+ first.handle.replace(routes);
1118
+ for (const group of rest) group.handle.replace([]);
1117
1119
  registrations = [{
1118
1120
  routes,
1119
- handle: registrations[0].handle
1121
+ handle: first.handle
1120
1122
  }];
1121
1123
  } catch (error) {
1122
1124
  logger.error("llm-pi: 更新被拒,保留此前注册的 route");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,6 +32,20 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
+ "https-proxy-agent": "^7.0.6"
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/cordis": "^4.0.1",
39
+ "@deepseek-ai/schemastery": "^3.18.1",
40
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
41
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
42
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
43
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
44
+ "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
45
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.0-rc.8",
46
+ "@earendil-works/pi-ai": "^0.82.1"
47
+ },
48
+ "devDependencies": {
35
49
  "@deepseek-ai/cordis": "4.0.1",
36
50
  "@deepseek-ai/schemastery": "3.18.1",
37
51
  "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
@@ -40,11 +54,8 @@
40
54
  "@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
41
55
  "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.8",
42
56
  "@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.8",
43
- "@earendil-works/pi-ai": "0.82.1",
44
- "https-proxy-agent": "^7.0.6"
45
- },
46
- "devDependencies": {
47
57
  "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
58
+ "@earendil-works/pi-ai": "0.82.1",
48
59
  "@types/react": "~18.3.1",
49
60
  "react": "^18.2.0"
50
61
  },
@@ -19,7 +19,12 @@ export interface ModelBase {
19
19
  compat?: Record<string, unknown>
20
20
  contextWindow?: number
21
21
  maxTokens?: number
22
- cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }
22
+ cost?: {
23
+ input: number
24
+ output: number
25
+ cacheRead: number
26
+ cacheWrite: number
27
+ }
23
28
  headers?: Record<string, string>
24
29
  }
25
30
 
@@ -40,7 +45,11 @@ export function builtinModelIds(kit: DshKit, provider: string): string[] {
40
45
  }
41
46
 
42
47
  /** 查单个内置模型为继承 base;未命中返回 undefined。 */
43
- export function builtinModelBase(kit: DshKit, provider: string, modelId: string): ModelBase | undefined {
48
+ export function builtinModelBase(
49
+ kit: DshKit,
50
+ provider: string,
51
+ modelId: string,
52
+ ): ModelBase | undefined {
44
53
  if (!hasBuiltinProvider(kit, provider)) return undefined
45
54
  const model = kit.getBuiltinModels(provider).find((m) => m.id === modelId)
46
55
  if (model === undefined) return undefined
@@ -50,8 +59,12 @@ export function builtinModelBase(kit: DshKit, provider: string, modelId: string)
50
59
  baseUrl: model.baseUrl,
51
60
  input: [...model.input],
52
61
  reasoning: model.reasoning,
53
- ...(model.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: { ...model.thinkingLevelMap } }),
54
- ...(model.compat === undefined ? {} : { compat: { ...(model.compat as Record<string, unknown>) } }),
62
+ ...(model.thinkingLevelMap === undefined
63
+ ? {}
64
+ : { thinkingLevelMap: { ...model.thinkingLevelMap } }),
65
+ ...(model.compat === undefined
66
+ ? {}
67
+ : { compat: { ...(model.compat as Record<string, unknown>) } }),
55
68
  contextWindow: model.contextWindow,
56
69
  maxTokens: model.maxTokens,
57
70
  cost: { ...model.cost },
@@ -15,9 +15,10 @@
15
15
  * 视觉等模态须用户在条目上显式声明(防 over-claiming 导致会话重复失败请求)。
16
16
  * @module llm-pi/catalog/models-dev
17
17
  */
18
+
19
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
18
20
  import { request as httpRequest } from 'node:http'
19
21
  import { request as httpsRequest } from 'node:https'
20
- import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
21
22
  import { dirname } from 'node:path'
22
23
 
23
24
  import HttpsProxyAgentModule from 'https-proxy-agent'
@@ -73,7 +74,8 @@ function fetchJson(url: string, proxy: string, timeoutMs: number): Promise<JsonR
73
74
  return new Promise((resolve, reject) => {
74
75
  const target = new URL(url)
75
76
  const request = target.protocol === 'https:' ? httpsRequest : httpRequest
76
- const agent = target.protocol === 'https:' && proxy.length > 0 ? new HttpsProxyAgent(proxy) : undefined
77
+ const agent =
78
+ target.protocol === 'https:' && proxy.length > 0 ? new HttpsProxyAgent(proxy) : undefined
77
79
  const req = request(
78
80
  url,
79
81
  { agent, timeout: timeoutMs, headers: { accept: 'application/json' } },
@@ -89,7 +91,10 @@ function fetchJson(url: string, proxy: string, timeoutMs: number): Promise<JsonR
89
91
  chunks.push(chunk)
90
92
  })
91
93
  response.on('end', () => {
92
- resolve({ status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf8') })
94
+ resolve({
95
+ status: response.statusCode ?? 0,
96
+ body: Buffer.concat(chunks).toString('utf8'),
97
+ })
93
98
  })
94
99
  },
95
100
  )
@@ -108,7 +113,8 @@ function toModelBase(entry: ModelsDevModelEntry): ModelBase {
108
113
  const base: ModelBase = {}
109
114
  if (typeof entry.name === 'string' && entry.name.length > 0) base.name = entry.name
110
115
  const context = entry.limit?.context
111
- if (typeof context === 'number' && Number.isInteger(context) && context > 0) base.contextWindow = context
116
+ if (typeof context === 'number' && Number.isInteger(context) && context > 0)
117
+ base.contextWindow = context
112
118
  const output = entry.limit?.output
113
119
  if (typeof output === 'number' && Number.isInteger(output) && output > 0) base.maxTokens = output
114
120
  base.reasoning = entry.reasoning === true
@@ -178,7 +184,7 @@ export class ModelsDevSource {
178
184
 
179
185
  /** 全部 provider id(UI extends 选择器用)。 */
180
186
  providerIds(): string[] {
181
- return Object.keys(this.document ?? {})
187
+ return Object.keys(this.document ?? {})
182
188
  }
183
189
 
184
190
  /** 某 provider 的模型 id 列表(UI extends 选择器用)。 */
@@ -209,7 +215,8 @@ export class ModelsDevSource {
209
215
  private loadCache(): void {
210
216
  try {
211
217
  const raw = JSON.parse(readFileSync(this.cacheFile, 'utf8')) as CacheFile
212
- if (!isDocument(raw.data) || typeof raw.fetchedAt !== 'string') throw new Error('缓存形状非法')
218
+ if (!isDocument(raw.data) || typeof raw.fetchedAt !== 'string')
219
+ throw new Error('缓存形状非法')
213
220
  this.document = raw.data
214
221
  this.fetchedAt = raw.fetchedAt
215
222
  } catch {
package/src/client/api.ts CHANGED
@@ -23,7 +23,12 @@ export interface WireProvider {
23
23
  defaultMaxTokens?: number
24
24
  defaultInput?: string[]
25
25
  reasoning?: string
26
- thinkingBudgets?: { minimal: number; low: number; medium: number; high: number }
26
+ thinkingBudgets?: {
27
+ minimal: number
28
+ low: number
29
+ medium: number
30
+ high: number
31
+ }
27
32
  cacheRetention?: string
28
33
  transport?: string
29
34
  timeoutMs?: number
@@ -74,14 +79,22 @@ async function parse<T>(res: Response): Promise<T> {
74
79
  }
75
80
 
76
81
  /** 目录查询:provider 为空时只返回该源的 provider 列表。 */
77
- export async function fetchCatalog(provider: string, source: 'builtin' | 'models-dev'): Promise<CatalogResult> {
82
+ export async function fetchCatalog(
83
+ provider: string,
84
+ source: 'builtin' | 'models-dev',
85
+ ): Promise<CatalogResult> {
78
86
  const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`
79
87
  return parse<CatalogResult>(await fetch(url, { credentials: 'same-origin' }))
80
88
  }
81
89
 
82
90
  /** 手动拉取 models.dev 目录:POST /catalog/refresh → 最新快照状态。 */
83
- export async function refreshCatalog(): Promise<{ status: WireModelsDevStatus }> {
91
+ export async function refreshCatalog(): Promise<{
92
+ status: WireModelsDevStatus
93
+ }> {
84
94
  return parse<{ status: WireModelsDevStatus }>(
85
- await fetch(`${ROUTE_CATALOG}/refresh`, { method: 'POST', credentials: 'same-origin' }),
95
+ await fetch(`${ROUTE_CATALOG}/refresh`, {
96
+ method: 'POST',
97
+ credentials: 'same-origin',
98
+ }),
86
99
  )
87
100
  }
@@ -7,11 +7,18 @@
7
7
  * 交互对齐官方卡片与 notify-email:折叠/展开、staged draft、未保存标记。
8
8
  * @module llm-pi/client/card
9
9
  */
10
- import { useEffect, useMemo, useSyncExternalStore, useState, type ReactElement } from 'react'
10
+ import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
11
11
 
12
12
  import { SETTINGS_NS } from '../ns.ts'
13
- import { fetchCatalog, refreshCatalog, type ConfigValue, type WireModelsDevStatus } from './api.ts'
14
- import { draftFromValue, emptyProviderDraft, numTextOk, toPatch, type Draft, type ProviderDraft } from './draft.ts'
13
+ import { type ConfigValue, fetchCatalog, refreshCatalog, type WireModelsDevStatus } from './api.ts'
14
+ import {
15
+ type Draft,
16
+ draftFromValue,
17
+ emptyProviderDraft,
18
+ numTextOk,
19
+ type ProviderDraft,
20
+ toPatch,
21
+ } from './draft.ts'
15
22
  import { CheckRow, TextField } from './fields.tsx'
16
23
  import type { Scope, SettingsApi } from './scope.ts'
17
24
  import { ProvidersSection } from './views/providers.tsx'
@@ -75,7 +82,8 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
75
82
 
76
83
  const dirty = useMemo(
77
84
  () =>
78
- value !== undefined && draft !== null &&
85
+ value !== undefined &&
86
+ draft !== null &&
79
87
  JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))),
80
88
  [value, draft],
81
89
  )
@@ -90,16 +98,26 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
90
98
  }, [draft])
91
99
 
92
100
  if (value === undefined || draft === null) {
93
- return <li className="lpc-card"><p className="lpc-readOnly">{t('loading')}</p></li>
101
+ return (
102
+ <li className="lpc-card">
103
+ <p className="lpc-readOnly">{t('loading')}</p>
104
+ </li>
105
+ )
94
106
  }
95
107
 
96
108
  const setProvider = (route: string, patch: Partial<ProviderDraft>): void => {
97
109
  const current = draft.providers[route] ?? emptyProviderDraft()
98
- setDraft({ ...draft, providers: { ...draft.providers, [route]: { ...current, ...patch } } })
110
+ setDraft({
111
+ ...draft,
112
+ providers: { ...draft.providers, [route]: { ...current, ...patch } },
113
+ })
99
114
  setStatus(IDLE_STATUS)
100
115
  }
101
116
  const onAddRoute = (key: string): void => {
102
- setDraft({ ...draft, providers: { ...draft.providers, [key]: emptyProviderDraft() } })
117
+ setDraft({
118
+ ...draft,
119
+ providers: { ...draft.providers, [key]: emptyProviderDraft() },
120
+ })
103
121
  setStatus(IDLE_STATUS)
104
122
  }
105
123
  const onRemoveRoute = (route: string): void => {
@@ -111,11 +129,12 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
111
129
  const onSave = (): void => {
112
130
  setSaving(true)
113
131
  const revision = scope.getSnapshot().revision
114
- api.replace({
115
- ns: SETTINGS_NS,
116
- section: toPatch(draft) as unknown as Record<string, unknown>,
117
- ...(revision !== undefined ? { expectedRevision: revision } : {}),
118
- })
132
+ api
133
+ .replace({
134
+ ns: SETTINGS_NS,
135
+ section: toPatch(draft) as unknown as Record<string, unknown>,
136
+ ...(revision !== undefined ? { expectedRevision: revision } : {}),
137
+ })
119
138
  .then(async (response) => {
120
139
  if (!response.result.ok) {
121
140
  throw new Error(response.result.error?.message ?? t('saveFailed'))
@@ -170,7 +189,11 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
170
189
  </button>
171
190
  {open ? (
172
191
  <div className="lpc-body">
173
- {disabled ? <p className="lpc-readOnly" role="status">{t('readOnly')}</p> : null}
192
+ {disabled ? (
193
+ <p className="lpc-readOnly" role="status">
194
+ {t('readOnly')}
195
+ </p>
196
+ ) : null}
174
197
  <CheckRow
175
198
  id="lpc-enabled"
176
199
  label={t('enabled')}
@@ -217,9 +240,13 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
217
240
  setStatus(IDLE_STATUS)
218
241
  }}
219
242
  />
220
- <p className="lpc-statusRow">{t('kitSource')}:{kitSource ?? ''}</p>
243
+ <p className="lpc-statusRow">
244
+ {t('kitSource')}:{kitSource ?? ''}
245
+ </p>
221
246
  <div className="lpc-statusRow">
222
- <span>{t('modelsDevStatus')}:{modelsDevText(modelsDevStatus, t)}</span>
247
+ <span>
248
+ {t('modelsDevStatus')}:{modelsDevText(modelsDevStatus, t)}
249
+ </span>
223
250
  <button
224
251
  type="button"
225
252
  className="lpc-btn lpc-btnGhost lpc-btnSmall lpc-refreshBtn"
@@ -240,17 +267,27 @@ export function LlmPiCard(props: CardProps): ReactElement | null {
240
267
  />
241
268
  <div className="lpc-footer">
242
269
  {status.kind !== 'idle' ? (
243
- <p className={`lpc-status${status.kind === 'error' ? ' lpc-statusError' : ''}`} role="status">
270
+ <p
271
+ className={`lpc-status${status.kind === 'error' ? ' lpc-statusError' : ''}`}
272
+ role="status"
273
+ >
244
274
  {status.text}
245
275
  </p>
246
276
  ) : null}
247
- <button type="button" className="lpc-btn lpc-btnGhost" disabled={!dirty || saving}
248
- onClick={onDiscard}>
277
+ <button
278
+ type="button"
279
+ className="lpc-btn lpc-btnGhost"
280
+ disabled={!dirty || saving}
281
+ onClick={onDiscard}
282
+ >
249
283
  {t('discard')}
250
284
  </button>
251
- <button type="button" className="lpc-btn lpc-btnPrimary"
285
+ <button
286
+ type="button"
287
+ className="lpc-btn lpc-btnPrimary"
252
288
  disabled={!dirty || invalid || saving || disabled}
253
- onClick={onSave}>
289
+ onClick={onSave}
290
+ >
254
291
  {t(saving ? 'saving' : 'save')}
255
292
  </button>
256
293
  </div>
@@ -64,7 +64,12 @@ interface ClientContext {
64
64
  * (loopback 或 tailnet 信任域名)下行为一致。
65
65
  */
66
66
  function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
67
- let state: ScopeSnapshot = { status: 'loading', value: undefined, revision: undefined, writable: false }
67
+ let state: ScopeSnapshot = {
68
+ status: 'loading',
69
+ value: undefined,
70
+ revision: undefined,
71
+ writable: false,
72
+ }
68
73
  const listeners = new Set<() => void>()
69
74
  let generation = 0
70
75
  const publish = (next: ScopeSnapshot): void => {
@@ -73,7 +78,7 @@ function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
73
78
  }
74
79
  const load = async (): Promise<void> => {
75
80
  const gen = ++generation
76
- let response
81
+ let response: Awaited<ReturnType<SettingsApi['describe']>>
77
82
  try {
78
83
  response = await api.describe({})
79
84
  } catch {
@@ -83,10 +88,20 @@ function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
83
88
  const writable = response.result.value?.writable ?? false
84
89
  const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns)
85
90
  if (view === undefined) {
86
- publish({ status: 'unavailable', value: undefined, revision: undefined, writable })
91
+ publish({
92
+ status: 'unavailable',
93
+ value: undefined,
94
+ revision: undefined,
95
+ writable,
96
+ })
87
97
  return
88
98
  }
89
- publish({ status: 'ready', value: view.value, revision: view.revision, writable })
99
+ publish({
100
+ status: 'ready',
101
+ value: view.value,
102
+ revision: view.revision,
103
+ writable,
104
+ })
90
105
  }
91
106
  const refresh = (namespace?: unknown): void => {
92
107
  if (namespace !== undefined && namespace !== ns) return
@@ -118,7 +133,6 @@ function createApiScope(api: SettingsApi, ns: string, c: ClientContext): Scope {
118
133
  }
119
134
 
120
135
  export function apply(ctx: Context): void {
121
-
122
136
  const c = ctx as unknown as ClientContext
123
137
  const tag = injectStyle()
124
138
  c.effect(
@@ -127,10 +141,7 @@ export function apply(ctx: Context): void {
127
141
  },
128
142
  'llm-pi: style',
129
143
  )
130
- c.effect(
131
- () => c.locale.register(NS, { zh, en }),
132
- 'llm-pi: locale',
133
- )
144
+ c.effect(() => c.locale.register(NS, { zh, en }), 'llm-pi: locale')
134
145
  const api = c.get('connection').api.settings
135
146
  const scope = createApiScope(api, SETTINGS_NS, c)
136
147
  c.slots.inject('settings.plugin.item', () =>
@@ -6,7 +6,11 @@
6
6
  */
7
7
 
8
8
  /** 协议枚举(来源:config.ts PROTOCOL_IDS)。 */
9
- export const PROTOCOL_IDS = ['openai-completions', 'openai-responses', 'anthropic-messages'] as const
9
+ export const PROTOCOL_IDS = [
10
+ 'openai-completions',
11
+ 'openai-responses',
12
+ 'anthropic-messages',
13
+ ] as const
10
14
 
11
15
  /** thinking 档位(来源:config.ts THINKING_LEVELS)。 */
12
16
  export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
@@ -100,7 +100,10 @@ export function pairsToHeaders(pairs: HeaderPair[]): Record<string, string> | un
100
100
  }
101
101
 
102
102
  function inputFromWire(list: string[] | undefined): InputDraft {
103
- return { text: list?.includes('text') ?? false, image: list?.includes('image') ?? false }
103
+ return {
104
+ text: list?.includes('text') ?? false,
105
+ image: list?.includes('image') ?? false,
106
+ }
104
107
  }
105
108
 
106
109
  function inputToWire(input: InputDraft): string[] | undefined {
@@ -110,7 +113,9 @@ function inputToWire(input: InputDraft): string[] | undefined {
110
113
  return out.length > 0 ? out : undefined
111
114
  }
112
115
 
113
- function reasoningFromWire(value: false | Record<string, string | null> | undefined): ReasoningDraft {
116
+ function reasoningFromWire(
117
+ value: false | Record<string, string | null> | undefined,
118
+ ): ReasoningDraft {
114
119
  if (value === false) return { nonReasoning: true, levels: {} }
115
120
  const levels: Record<string, string> = {}
116
121
  for (const [level, line] of Object.entries(value ?? {})) levels[level] = line ?? ''
@@ -126,7 +131,9 @@ function reasoningToWire(value: ReasoningDraft): false | Record<string, string>
126
131
  return Object.keys(out).length > 0 ? out : undefined
127
132
  }
128
133
 
129
- function budgetFromWire(value: { minimal: number; low: number; medium: number; high: number } | undefined): BudgetDraft {
134
+ function budgetFromWire(
135
+ value: { minimal: number; low: number; medium: number; high: number } | undefined,
136
+ ): BudgetDraft {
130
137
  return {
131
138
  minimal: numToText(value?.minimal),
132
139
  low: numToText(value?.low),
@@ -135,13 +142,17 @@ function budgetFromWire(value: { minimal: number; low: number; medium: number; h
135
142
  }
136
143
  }
137
144
 
138
- function budgetToWire(value: BudgetDraft): { minimal: number; low: number; medium: number; high: number } | undefined {
145
+ function budgetToWire(
146
+ value: BudgetDraft,
147
+ ): { minimal: number; low: number; medium: number; high: number } | undefined {
139
148
  const out: Record<string, number> = {}
140
149
  for (const key of ['minimal', 'low', 'medium', 'high'] as const) {
141
150
  const num = toNum(value[key])
142
151
  if (num !== undefined) out[key] = num
143
152
  }
144
- return Object.keys(out).length > 0 ? (out as { minimal: number; low: number; medium: number; high: number }) : undefined
153
+ return Object.keys(out).length > 0
154
+ ? (out as { minimal: number; low: number; medium: number; high: number })
155
+ : undefined
145
156
  }
146
157
 
147
158
  /** 剔除空值:undefined / '' / 空数组 / 空对象。 */
@@ -278,7 +289,10 @@ export function draftFromValue(value: ConfigValue): Draft {
278
289
  catalogRefreshHours: String(value.catalogRefreshHours),
279
290
  catalogProxy: value.catalogProxy,
280
291
  providers: Object.fromEntries(
281
- Object.entries(value.providers).map(([route, provider]) => [route, providerDraftFromWire(provider)]),
292
+ Object.entries(value.providers).map(([route, provider]) => [
293
+ route,
294
+ providerDraftFromWire(provider),
295
+ ]),
282
296
  ),
283
297
  }
284
298
  }
@@ -3,7 +3,7 @@
3
3
  * 视觉对齐官方卡片字段(label + hint + 控件纵列),样式类前缀 lpc-。
4
4
  * @module llm-pi/client/fields
5
5
  */
6
- import { useEffect, useState, type ReactElement } from 'react'
6
+ import { type ReactElement, useEffect, useState } from 'react'
7
7
 
8
8
  import type { HeaderPair } from './draft.ts'
9
9
 
@@ -170,6 +170,7 @@ export function KeyValueEditor(props: KeyValueEditorProps): ReactElement {
170
170
  </label>
171
171
  </div>
172
172
  {props.pairs.map((pair, index) => (
173
+ // biome-ignore lint/suspicious/noArrayIndexKey: 可编辑 KV 行无稳定 id,行序即身份(增删行经 onEdit 整体回写)
173
174
  <div className="lpc-kvRow" key={index}>
174
175
  <input
175
176
  id={index === 0 ? props.id : undefined}
@@ -239,6 +240,7 @@ export interface JsonFieldProps {
239
240
 
240
241
  export function JsonField(props: JsonFieldProps): ReactElement {
241
242
  const [text, setText] = useState(() => toJsonText(props.value))
243
+ // biome-ignore lint/correctness/useExhaustiveDependencies: 仅在 epoch 递增(外部重置语义)时同步文本,避免编辑中被 value 回写打断
242
244
  useEffect(() => {
243
245
  setText(toJsonText(props.value))
244
246
  }, [props.epoch])
@@ -102,7 +102,8 @@ export const en: Record<string, string> = {
102
102
  catalogUrl: 'models.dev catalog endpoint',
103
103
  catalogUrlHint: 'Snapshot data source; usually no change needed.',
104
104
  catalogRefreshHours: 'Catalog refresh (hours)',
105
- catalogRefreshHoursHint: '0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.',
105
+ catalogRefreshHoursHint:
106
+ '0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.',
106
107
  catalogProxy: 'Fetch proxy',
107
108
  catalogProxyHint: 'HTTP proxy (e.g. http://127.0.0.1:7890); leave empty for direct.',
108
109
  kitSource: 'Module source',
@@ -151,7 +152,8 @@ export const en: Record<string, string> = {
151
152
  retryPolicyHint: 'dsh-llm RetryPolicy shape; invalid JSON is not submitted.',
152
153
  invalidJson: 'Invalid JSON (this field will not be submitted).',
153
154
  compatGroup: 'Compat overrides',
154
- compatApiHint: 'When api is unset, fields render per openai-completions; the backend validates per the effective protocol.',
155
+ compatApiHint:
156
+ 'When api is unset, fields render per openai-completions; the backend validates per the effective protocol.',
155
157
  compatUnset: 'Unset',
156
158
  modelsGroup: 'Model catalog',
157
159
  addModel: 'Add model',
@@ -31,7 +31,26 @@ export interface Scope {
31
31
 
32
32
  /** connection api.settings 的 RPC 面(本插件用到的三个方法)。 */
33
33
  export interface SettingsApi {
34
- describe(payload?: Record<string, never>): Promise<{ result: { ok: boolean; value?: { namespaces: Array<{ ns: string; secrets: Array<{ path: string[]; set: boolean }> }> }; error?: { message?: string } } }>
35
- update(request: { ns: string; patch: Record<string, unknown>; expectedRevision?: number }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
36
- replace(request: { ns: string; section: Record<string, unknown>; expectedRevision?: number }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
34
+ describe(payload?: Record<string, never>): Promise<{
35
+ result: {
36
+ ok: boolean
37
+ value?: {
38
+ namespaces: Array<{
39
+ ns: string
40
+ secrets: Array<{ path: string[]; set: boolean }>
41
+ }>
42
+ }
43
+ error?: { message?: string }
44
+ }
45
+ }>
46
+ update(request: {
47
+ ns: string
48
+ patch: Record<string, unknown>
49
+ expectedRevision?: number
50
+ }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
51
+ replace(request: {
52
+ ns: string
53
+ section: Record<string, unknown>
54
+ expectedRevision?: number
55
+ }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
37
56
  }