@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 +6 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +11 -9
- package/package.json +16 -5
- package/src/catalog/builtin.ts +17 -4
- package/src/catalog/models-dev.ts +13 -6
- package/src/client/api.ts +17 -4
- package/src/client/card.tsx +57 -20
- package/src/client/client.ts +20 -9
- package/src/client/constants.ts +5 -1
- package/src/client/draft.ts +20 -6
- package/src/client/fields.tsx +3 -1
- package/src/client/i18n.ts +4 -2
- package/src/client/scope.ts +22 -3
- package/src/client/views/compat.tsx +50 -44
- package/src/client/views/models.tsx +26 -7
- package/src/client/views/provider-fields.tsx +15 -3
- package/src/client/views/providers.tsx +31 -26
- package/src/compat.ts +18 -4
- package/src/config.ts +42 -11
- package/src/discovery.ts +30 -9
- package/src/index.ts +6 -3
- package/src/inherit.ts +11 -3
- package/src/profiles.ts +66 -21
- package/src/resolve-dsh.ts +15 -8
- package/src/service.ts +25 -12
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.
|
|
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:
|
|
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.
|
|
1293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
|
|
1294
1294
|
id: `${id}-advanced`,
|
|
1295
1295
|
title: t("advancedGroup"),
|
|
1296
1296
|
defaultOpen: false,
|
|
1297
|
-
children:
|
|
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
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
|
|
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(
|
|
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
|
-
|
|
1116
|
-
|
|
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:
|
|
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.
|
|
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
|
},
|
package/src/catalog/builtin.ts
CHANGED
|
@@ -19,7 +19,12 @@ export interface ModelBase {
|
|
|
19
19
|
compat?: Record<string, unknown>
|
|
20
20
|
contextWindow?: number
|
|
21
21
|
maxTokens?: number
|
|
22
|
-
cost?: {
|
|
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(
|
|
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
|
|
54
|
-
|
|
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 =
|
|
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({
|
|
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)
|
|
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
|
-
|
|
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')
|
|
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?: {
|
|
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(
|
|
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<{
|
|
91
|
+
export async function refreshCatalog(): Promise<{
|
|
92
|
+
status: WireModelsDevStatus
|
|
93
|
+
}> {
|
|
84
94
|
return parse<{ status: WireModelsDevStatus }>(
|
|
85
|
-
await fetch(`${ROUTE_CATALOG}/refresh`, {
|
|
95
|
+
await fetch(`${ROUTE_CATALOG}/refresh`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
credentials: 'same-origin',
|
|
98
|
+
}),
|
|
86
99
|
)
|
|
87
100
|
}
|
package/src/client/card.tsx
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
* 交互对齐官方卡片与 notify-email:折叠/展开、staged draft、未保存标记。
|
|
8
8
|
* @module llm-pi/client/card
|
|
9
9
|
*/
|
|
10
|
-
import { useEffect, useMemo,
|
|
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
|
|
14
|
-
import {
|
|
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 &&
|
|
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
|
|
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({
|
|
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({
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 ?
|
|
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">
|
|
243
|
+
<p className="lpc-statusRow">
|
|
244
|
+
{t('kitSource')}:{kitSource ?? ''}
|
|
245
|
+
</p>
|
|
221
246
|
<div className="lpc-statusRow">
|
|
222
|
-
<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
|
|
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
|
|
248
|
-
|
|
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
|
|
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>
|
package/src/client/client.ts
CHANGED
|
@@ -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 = {
|
|
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({
|
|
91
|
+
publish({
|
|
92
|
+
status: 'unavailable',
|
|
93
|
+
value: undefined,
|
|
94
|
+
revision: undefined,
|
|
95
|
+
writable,
|
|
96
|
+
})
|
|
87
97
|
return
|
|
88
98
|
}
|
|
89
|
-
publish({
|
|
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', () =>
|
package/src/client/constants.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** 协议枚举(来源:config.ts PROTOCOL_IDS)。 */
|
|
9
|
-
export const PROTOCOL_IDS = [
|
|
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
|
package/src/client/draft.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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]) => [
|
|
292
|
+
Object.entries(value.providers).map(([route, provider]) => [
|
|
293
|
+
route,
|
|
294
|
+
providerDraftFromWire(provider),
|
|
295
|
+
]),
|
|
282
296
|
),
|
|
283
297
|
}
|
|
284
298
|
}
|
package/src/client/fields.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 视觉对齐官方卡片字段(label + hint + 控件纵列),样式类前缀 lpc-。
|
|
4
4
|
* @module llm-pi/client/fields
|
|
5
5
|
*/
|
|
6
|
-
import { useEffect, useState
|
|
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])
|
package/src/client/i18n.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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',
|
package/src/client/scope.ts
CHANGED
|
@@ -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<{
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|