@dsh-plus/llm-pi 0.1.42 → 0.1.44
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 +1489 -636
- package/lib/index.d.ts +4 -8
- package/lib/index.js +943 -391
- package/package.json +35 -36
- package/src/catalog/browse.ts +297 -0
- package/src/catalog/builtin.ts +183 -35
- package/src/client/api.ts +96 -33
- package/src/client/card.tsx +180 -120
- package/src/client/client.ts +2 -2
- package/src/client/constants.ts +25 -5
- package/src/client/draft.ts +34 -13
- package/src/client/fields.tsx +18 -3
- package/src/client/i18n.ts +153 -49
- package/src/client/root-fields.tsx +64 -52
- package/src/client/route-fit.ts +117 -0
- package/src/client/styles.ts +40 -10
- package/src/client/views/catalog-browser.tsx +376 -0
- package/src/client/views/catalog-row.tsx +147 -0
- package/src/client/views/compat.tsx +31 -3
- package/src/client/views/models.tsx +189 -193
- package/src/client/views/provider-fields.tsx +2 -2
- package/src/client/views/providers.tsx +51 -11
- package/src/compat.ts +43 -19
- package/src/config-api.ts +65 -37
- package/src/config.ts +20 -28
- package/src/inherit.ts +15 -31
- package/src/kit-meta.ts +156 -0
- package/src/{compat-gates.ts → official-surface.ts} +60 -16
- package/src/profiles.ts +52 -13
- package/src/resolve-dsh.ts +291 -72
- package/src/service.ts +209 -90
- package/src/catalog/models-dev.ts +0 -261
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
BUDGET_KEYS,
|
|
12
12
|
CACHE_RETENTION_OPTIONS,
|
|
13
13
|
MODALITIES,
|
|
14
|
-
|
|
14
|
+
protocolOptions,
|
|
15
15
|
THINKING_LEVELS,
|
|
16
16
|
TRANSPORT_OPTIONS,
|
|
17
17
|
} from '../constants.ts'
|
|
@@ -158,7 +158,7 @@ export function ProviderSelectFields(props: ProviderScalarFieldsProps): ReactEle
|
|
|
158
158
|
id={`${props.id}-api`}
|
|
159
159
|
label={t('api')}
|
|
160
160
|
value={draft.api}
|
|
161
|
-
options={
|
|
161
|
+
options={protocolOptions()}
|
|
162
162
|
unsetLabel={t('compatUnset')}
|
|
163
163
|
disabled={props.disabled === true}
|
|
164
164
|
onEdit={(value) =>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider 路由编辑:每个 route
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Provider 路由编辑:每个 route 一张**受控**折叠小节(展开态由卡片持有,
|
|
3
|
+
* 以便「从内置目录添加」后自动展开目标 route、以及卡片级"全部收起")。
|
|
4
|
+
* 小节内容:基础字段 + 请求头 + 高级设置(其余字段 / retryPolicy / compat)
|
|
5
|
+
* + 模型目录(行默认折叠,见 views/models.tsx)。
|
|
5
6
|
* @module llm-pi/client/views/providers
|
|
6
7
|
*/
|
|
7
8
|
|
|
@@ -18,17 +19,25 @@ import { ProviderScalarFields, ProviderSelectFields } from './provider-fields.ts
|
|
|
18
19
|
|
|
19
20
|
export interface ProvidersSectionProps {
|
|
20
21
|
providers: Record<string, ProviderDraft>
|
|
22
|
+
/** route → 展开态(卡片持有;缺失即收起)。 */
|
|
23
|
+
openRoutes: Record<string, boolean>
|
|
21
24
|
epoch: number
|
|
22
25
|
disabled?: boolean
|
|
23
26
|
t: Translate
|
|
24
27
|
onAddRoute(key: string): void
|
|
25
28
|
onRemoveRoute(route: string): void
|
|
26
29
|
onPatchProvider(route: string, patch: Partial<ProviderDraft>): void
|
|
30
|
+
onToggleRoute(route: string, open: boolean): void
|
|
31
|
+
onCollapseAll(): void
|
|
32
|
+
onExpandAll(): void
|
|
33
|
+
/** 打开「内置模型目录」并把该 route 设为添加目标。 */
|
|
34
|
+
onAddFromCatalog(route: string): void
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
|
|
30
38
|
const [newRoute, setNewRoute] = useState('')
|
|
31
39
|
const [routeError, setRouteError] = useState('')
|
|
40
|
+
const routes = Object.keys(props.providers)
|
|
32
41
|
const submitAdd = (): void => {
|
|
33
42
|
const key = newRoute.trim()
|
|
34
43
|
if (key === '') {
|
|
@@ -45,7 +54,29 @@ export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
|
|
|
45
54
|
}
|
|
46
55
|
return (
|
|
47
56
|
<div className="lpc-section">
|
|
48
|
-
<
|
|
57
|
+
<div className="lpc-modelHead">
|
|
58
|
+
<span className="lpc-modelTitle">{props.t('providersGroup')}</span>
|
|
59
|
+
{routes.length > 1 ? (
|
|
60
|
+
<>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
64
|
+
disabled={props.disabled === true}
|
|
65
|
+
onClick={props.onCollapseAll}
|
|
66
|
+
>
|
|
67
|
+
{props.t('providersCollapseAll')}
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
72
|
+
disabled={props.disabled === true}
|
|
73
|
+
onClick={props.onExpandAll}
|
|
74
|
+
>
|
|
75
|
+
{props.t('providersExpandAll')}
|
|
76
|
+
</button>
|
|
77
|
+
</>
|
|
78
|
+
) : null}
|
|
79
|
+
</div>
|
|
49
80
|
<div className="lpc-addRoute">
|
|
50
81
|
<input
|
|
51
82
|
className={`lpc-input${routeError !== '' ? ' lpc-inputInvalid' : ''}`}
|
|
@@ -72,11 +103,14 @@ export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
|
|
|
72
103
|
key={route}
|
|
73
104
|
route={route}
|
|
74
105
|
draft={draft}
|
|
106
|
+
open={props.openRoutes[route] === true}
|
|
75
107
|
epoch={props.epoch}
|
|
76
108
|
disabled={props.disabled === true}
|
|
77
109
|
t={props.t}
|
|
110
|
+
onToggle={() => props.onToggleRoute(route, props.openRoutes[route] !== true)}
|
|
78
111
|
onRemove={() => props.onRemoveRoute(route)}
|
|
79
112
|
onPatch={(patch) => props.onPatchProvider(route, patch)}
|
|
113
|
+
onAddFromCatalog={() => props.onAddFromCatalog(route)}
|
|
80
114
|
/>
|
|
81
115
|
))}
|
|
82
116
|
</div>
|
|
@@ -86,19 +120,24 @@ export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
|
|
|
86
120
|
export interface ProviderSectionProps {
|
|
87
121
|
route: string
|
|
88
122
|
draft: ProviderDraft
|
|
123
|
+
open: boolean
|
|
89
124
|
epoch: number
|
|
90
125
|
disabled?: boolean
|
|
91
126
|
t: Translate
|
|
127
|
+
onToggle(): void
|
|
92
128
|
onRemove(): void
|
|
93
129
|
onPatch(patch: Partial<ProviderDraft>): void
|
|
130
|
+
onAddFromCatalog(): void
|
|
94
131
|
}
|
|
95
132
|
|
|
96
133
|
export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
97
|
-
const [open, setOpen] = useState(false)
|
|
98
134
|
const { route, draft, t } = props
|
|
99
135
|
const id = route.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
136
|
+
const adapter = typeof draft.extra['adapter'] === 'string' ? draft.extra['adapter'] : 'pi'
|
|
100
137
|
const summary =
|
|
101
|
-
draft.api !== '' ? draft.api : draft.extends !== '' ? `extends ${draft.extends}` :
|
|
138
|
+
draft.api !== '' ? draft.api : draft.extends !== '' ? `extends ${draft.extends}` : adapter
|
|
139
|
+
const modelCount = draft.models.length
|
|
140
|
+
const meta = modelCount > 0 ? `${modelCount}` : ''
|
|
102
141
|
const fieldProps = {
|
|
103
142
|
id,
|
|
104
143
|
draft,
|
|
@@ -112,12 +151,13 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
112
151
|
<button
|
|
113
152
|
type="button"
|
|
114
153
|
className="lpc-routeToggle"
|
|
115
|
-
aria-expanded={open}
|
|
116
|
-
onClick={
|
|
154
|
+
aria-expanded={props.open}
|
|
155
|
+
onClick={props.onToggle}
|
|
117
156
|
>
|
|
118
|
-
<ChevronDownIcon className={`lpc-chevron${open ? ' lpc-chevronOpen' : ''}`} />
|
|
157
|
+
<ChevronDownIcon className={`lpc-chevron${props.open ? ' lpc-chevronOpen' : ''}`} />
|
|
119
158
|
<span className="lpc-routeKey">{route}</span>
|
|
120
159
|
{summary !== '' ? <span className="lpc-routeApi">{summary}</span> : null}
|
|
160
|
+
{meta !== '' ? <span className="lpc-collapseMeta">{meta}</span> : null}
|
|
121
161
|
</button>
|
|
122
162
|
<button
|
|
123
163
|
type="button"
|
|
@@ -128,7 +168,7 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
128
168
|
{t('deleteRoute')}
|
|
129
169
|
</button>
|
|
130
170
|
</div>
|
|
131
|
-
{open ? (
|
|
171
|
+
{props.open ? (
|
|
132
172
|
<div className="lpc-routeBody">
|
|
133
173
|
<p className="lpc-groupLabel">{t('providerFields')}</p>
|
|
134
174
|
<ProviderScalarFields {...fieldProps} />
|
|
@@ -186,12 +226,12 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
186
226
|
<ModelsTable
|
|
187
227
|
route={route}
|
|
188
228
|
api={draft.api}
|
|
189
|
-
defaultProvider={draft.extends}
|
|
190
229
|
models={draft.models}
|
|
191
230
|
epoch={props.epoch}
|
|
192
231
|
disabled={props.disabled === true}
|
|
193
232
|
t={t}
|
|
194
233
|
onModels={(models) => props.onPatch({ models })}
|
|
234
|
+
onAddFromCatalog={props.onAddFromCatalog}
|
|
195
235
|
/>
|
|
196
236
|
</div>
|
|
197
237
|
) : null}
|
package/src/compat.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 逐协议 compat 校验:门控表与取值约束**从官方 dsh-llm-pi-ai 安装副本自动继承**
|
|
3
|
-
* (见
|
|
3
|
+
* (见 official-surface.ts),不再手抄镜像。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* 语义分两档,取决于门控表当时来自哪里:
|
|
6
|
+
* - **official(现场推导成功)**:官方门控表是兼容性的唯一事实源——按协议分型
|
|
7
|
+
* (offer/withhold),withhold 字段(官方内置目录已为对应厂商设置,如
|
|
8
|
+
* openRouterRouting/zaiToolStream/sendSessionAffinityHeaders 等)写时拒绝并提示以
|
|
9
|
+
* 目录 provider 名为 route;未知键拒绝;无值键(null/undefined)拒绝(对齐官方
|
|
10
|
+
* assertOfferedCompatFields:"写了但没生效"的表面状态不允许)。
|
|
11
|
+
* - **fallback(推导失败,用最后已知快照)**:只拒绝快照中明确 withhold 的键,
|
|
12
|
+
* **未知键放行**交给官方适配器自身校验——表落后时"误拒官方新字段"比"多放一个键"
|
|
13
|
+
* 严重得多(0.1.5-rc.1 教训:静默功能缺失、无任何报错)。
|
|
14
14
|
*
|
|
15
15
|
* pi-ai 侧消费语义:getCompat 逐字段 `??` 覆盖 detectCompat 的 baseURL/名称猜测;
|
|
16
16
|
* undefined 视为未设置(无法显式清空检测值)。
|
|
17
17
|
* @module llm-pi/compat
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
import { FALLBACK_TABLE } from './compat-gates.ts'
|
|
19
|
+
|
|
21
20
|
import type { ProtocolId } from './config.ts'
|
|
21
|
+
import type { CompatDisposition, CompatTable, CompatValue } from './official-surface.ts'
|
|
22
|
+
import { FALLBACK_TABLE } from './official-surface.ts'
|
|
22
23
|
|
|
23
|
-
export type { CompatDisposition, CompatValue } from './
|
|
24
|
+
export type { CompatDisposition, CompatValue } from './official-surface.ts'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 生效门控表:由 resolve-dsh 的套件加载流程经 {@link installCompatTable} 注入
|
|
@@ -55,6 +56,23 @@ export function compatFieldsOf(api: ProtocolId | string): readonly string[] {
|
|
|
55
56
|
)
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 生效表里出现过的协议键(运行期协议列表为空时的渲染兜底:
|
|
61
|
+
* 套件异常也要让表单能画出来)。
|
|
62
|
+
*/
|
|
63
|
+
export function compatProtocols(): string[] {
|
|
64
|
+
return Object.keys(active.gates)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 某协议被官方 withhold 的字段(内置目录已为对应厂商设置,写时拒绝)。 */
|
|
68
|
+
export function compatWithholdFieldsOf(api: ProtocolId | string): readonly string[] {
|
|
69
|
+
const gate = active.gates[api]
|
|
70
|
+
if (gate === undefined) return []
|
|
71
|
+
return Object.entries(gate).flatMap(([field, disposition]) =>
|
|
72
|
+
disposition === 'withhold' ? [field] : [],
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
/** 某协议某 offer 字段的取值约束(UI 渲染开关/下拉用)。 */
|
|
59
77
|
export function compatFieldSpec(api: string, field: string): CompatValue | undefined {
|
|
60
78
|
return active.gates[api]?.[field] === 'offer' ? active.specs[field] : undefined
|
|
@@ -114,13 +132,17 @@ export function validateCompat(
|
|
|
114
132
|
where: string,
|
|
115
133
|
): void {
|
|
116
134
|
if (compat === undefined) return
|
|
117
|
-
const
|
|
135
|
+
const table = active
|
|
136
|
+
const gate = table.gates[api]
|
|
118
137
|
if (gate === undefined) {
|
|
119
138
|
throw new Error(
|
|
120
|
-
`${where}: 协议 ${JSON.stringify(api)} 无 compat
|
|
139
|
+
`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(可配协议:${Object.keys(table.gates).join(', ')})`,
|
|
121
140
|
)
|
|
122
141
|
}
|
|
123
142
|
const offered = compatFieldsOf(api)
|
|
143
|
+
// 回退快照可能落后于官方:未知键放行给官方适配器自身校验,只拦快照里明确的
|
|
144
|
+
// withhold(详见文件头"语义分两档")。
|
|
145
|
+
const permissive = table.source === 'fallback'
|
|
124
146
|
for (const [key, value] of Object.entries(compat)) {
|
|
125
147
|
const disposition = gate[key]
|
|
126
148
|
if (disposition !== 'offer') {
|
|
@@ -130,10 +152,12 @@ export function validateCompat(
|
|
|
130
152
|
'请以目录 provider 名作为 route 名(继承目录值),或移除该字段',
|
|
131
153
|
)
|
|
132
154
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
if (!permissive) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(', ')};` +
|
|
158
|
+
`官方全部可配字段:${allOfferedFields().join(', ')})`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
137
161
|
}
|
|
138
162
|
if (value === undefined || value === null) {
|
|
139
163
|
throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`)
|
package/src/config-api.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 自定义端点 HTTP
|
|
3
|
-
* (0.1.2-alpha.1
|
|
4
|
-
*
|
|
2
|
+
* 自定义端点 HTTP 通道:配置读写走官方 remote.settings 直连
|
|
3
|
+
* (0.1.2-alpha.1 起第三方命名空间全量开放),这里只提供浏览器半拿不到的两样东西:
|
|
4
|
+
* - 运行期套件事实(生效版本、协议集合、目录规模、逐项降级诊断);
|
|
5
|
+
* - pi-ai 内置目录的搜索/筛选/分页(浏览器半读不到 pi-ai 包)。
|
|
6
|
+
*
|
|
7
|
+
* 两个端点同前缀、单次注册(按 pathname 分派,避免前缀互相遮蔽):
|
|
8
|
+
* - `GET /dsh-plus/llm-pi/catalog` 运行期事实 + provider/协议索引 + compat 字段表;
|
|
9
|
+
* - `GET /dsh-plus/llm-pi/catalog/models` 目录搜索(q/provider/api/能力/分页)。
|
|
5
10
|
* 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
|
|
6
11
|
* @module llm-pi/config-api
|
|
7
12
|
*/
|
|
@@ -10,24 +15,40 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
10
15
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
16
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
12
17
|
|
|
13
|
-
import {
|
|
14
|
-
import { compatFieldSpec, compatFieldsOf, compatTableInfo } from './compat.ts'
|
|
15
|
-
import { PROTOCOL_IDS } from './config.ts'
|
|
18
|
+
import { browseModels } from './catalog/browse.ts'
|
|
19
|
+
import { compatFieldSpec, compatFieldsOf, compatProtocols, compatTableInfo } from './compat.ts'
|
|
16
20
|
import type { LlmPiRuntime } from './service.ts'
|
|
17
21
|
|
|
18
22
|
const ROUTE_CATALOG = '/dsh-plus/llm-pi/catalog'
|
|
23
|
+
const ROUTE_MODELS = `${ROUTE_CATALOG}/models`
|
|
24
|
+
|
|
25
|
+
/** 查询参数里的布尔开关:'1'/'true' 为真。 */
|
|
26
|
+
function boolParam(value: string | null, fallback: boolean): boolean {
|
|
27
|
+
if (value === null) return fallback
|
|
28
|
+
return value === '1' || value === 'true'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 查询参数里的整数;缺失/非法返回 undefined(由消费方取默认)。 */
|
|
32
|
+
function intParam(value: string | null): number | undefined {
|
|
33
|
+
if (value === null || value.trim() === '') return undefined
|
|
34
|
+
const parsed = Number(value)
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
36
|
+
}
|
|
19
37
|
|
|
20
38
|
/**
|
|
21
39
|
* 服务端推导的 compat 字段表(协议 → 字段 → 取值约束)。浏览器半读不到官方包,
|
|
22
40
|
* 由本端点下发,UI 渲染与服务端校验同源(不再各存一份手抄镜像表)。
|
|
41
|
+
* 协议列表取运行期生效集合(不能服务的协议不渲染控件);运行期列表为空时退回
|
|
42
|
+
* 生效表自身的协议键,保证套件异常时表单仍可编辑。
|
|
23
43
|
*/
|
|
24
|
-
function compatTablePayload(): {
|
|
44
|
+
function compatTablePayload(protocols: readonly string[]): {
|
|
25
45
|
fields: Record<string, Record<string, unknown>>
|
|
26
46
|
source: string
|
|
27
47
|
problem?: string
|
|
28
48
|
} {
|
|
49
|
+
const list = protocols.length > 0 ? [...protocols] : compatProtocols()
|
|
29
50
|
const fields: Record<string, Record<string, unknown>> = {}
|
|
30
|
-
for (const api of
|
|
51
|
+
for (const api of list) {
|
|
31
52
|
const perField: Record<string, unknown> = {}
|
|
32
53
|
for (const field of compatFieldsOf(api)) {
|
|
33
54
|
const spec = compatFieldSpec(api, field)
|
|
@@ -46,38 +67,34 @@ function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
|
46
67
|
res.end(JSON.stringify(body))
|
|
47
68
|
}
|
|
48
69
|
|
|
49
|
-
/**
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
if (req.method === 'POST' && req.url?.endsWith('/refresh')) {
|
|
53
|
-
void runtime.modelsDev.refresh().then(() => {
|
|
54
|
-
sendJson(res, 200, { status: runtime.modelsDev.status() })
|
|
55
|
-
})
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
const url = new URL(req.url ?? '', 'http://localhost')
|
|
59
|
-
const provider = url.searchParams.get('provider') ?? ''
|
|
60
|
-
const source = url.searchParams.get('source') ?? 'builtin'
|
|
61
|
-
const compat = compatTablePayload()
|
|
62
|
-
if (source === 'models-dev') {
|
|
63
|
-
sendJson(res, 200, {
|
|
64
|
-
providers: runtime.modelsDev.providerIds(),
|
|
65
|
-
models: provider.length > 0 ? runtime.modelsDev.modelIds(provider) : [],
|
|
66
|
-
status: runtime.modelsDev.status(),
|
|
67
|
-
kitSource,
|
|
68
|
-
compat,
|
|
69
|
-
})
|
|
70
|
-
return
|
|
71
|
-
}
|
|
70
|
+
/** 运行期事实 + 目录索引(卡片挂载时拉一次:版本、协议、目录规模、compat 字段表)。 */
|
|
71
|
+
function handleMeta(runtime: LlmPiRuntime, res: ServerResponse): void {
|
|
72
|
+
const kit = runtime.kitInfo()
|
|
72
73
|
sendJson(res, 200, {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
compat,
|
|
74
|
+
kit,
|
|
75
|
+
providers: runtime.providerEntries(),
|
|
76
|
+
apis: runtime.apiIndex(),
|
|
77
|
+
compat: compatTablePayload(kit.protocols),
|
|
77
78
|
})
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/**
|
|
81
|
+
/** 目录搜索:q(模糊)/provider/api/reasoning/image/servable/offset/limit。 */
|
|
82
|
+
function handleModels(runtime: LlmPiRuntime, url: URL, res: ServerResponse): void {
|
|
83
|
+
const limit = intParam(url.searchParams.get('limit'))
|
|
84
|
+
const result = browseModels(runtime.kit, {
|
|
85
|
+
q: url.searchParams.get('q') ?? '',
|
|
86
|
+
provider: url.searchParams.get('provider') ?? '',
|
|
87
|
+
api: url.searchParams.get('api') ?? '',
|
|
88
|
+
reasoning: boolParam(url.searchParams.get('reasoning'), false),
|
|
89
|
+
image: boolParam(url.searchParams.get('image'), false),
|
|
90
|
+
servableOnly: boolParam(url.searchParams.get('servable'), true),
|
|
91
|
+
offset: intParam(url.searchParams.get('offset')) ?? 0,
|
|
92
|
+
...(limit === undefined ? {} : { limit }),
|
|
93
|
+
})
|
|
94
|
+
sendJson(res, 200, result)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 目录路由(webServer 缺失时由调用方保证不调用)。 */
|
|
81
98
|
export function registerCatalogApi(ctx: Context, runtime: LlmPiRuntime): void {
|
|
82
99
|
const logger = ctx.logger('llm-pi')
|
|
83
100
|
const guard = (handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> | void) => {
|
|
@@ -95,6 +112,17 @@ export function registerCatalogApi(ctx: Context, runtime: LlmPiRuntime): void {
|
|
|
95
112
|
ctx.webServer.register({
|
|
96
113
|
kind: 'prefix',
|
|
97
114
|
path: ROUTE_CATALOG,
|
|
98
|
-
handler: guard((req, res) =>
|
|
115
|
+
handler: guard((req, res) => {
|
|
116
|
+
const url = new URL(req.url ?? '', 'http://localhost')
|
|
117
|
+
if (url.pathname === ROUTE_MODELS) {
|
|
118
|
+
handleModels(runtime, url, res)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (url.pathname !== ROUTE_CATALOG) {
|
|
122
|
+
sendJson(res, 404, { error: `未知端点 ${url.pathname}` })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
handleMeta(runtime, res)
|
|
126
|
+
}),
|
|
99
127
|
})
|
|
100
128
|
}
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 与官方 llm-pi-ai 的差异:
|
|
7
7
|
* - compat 是开放 dict,物化时按协议校验(见 compat.ts),写入即拒绝未知键;
|
|
8
|
-
* - provider/model 均支持 extends 继承(见 inherit.ts
|
|
8
|
+
* - provider/model 均支持 extends 继承(见 inherit.ts),继承源只有 pi-ai 内置目录;
|
|
9
|
+
* - `api` 不在 schema 里钉死枚举:合法协议集合由已安装官方适配器现场推导
|
|
10
|
+
* (见 official-surface/resolve-dsh),写时由 profiles.ts 以明确错误拒绝不支持者,
|
|
11
|
+
* 官方将来新增协议无需改本插件。
|
|
9
12
|
* @module llm-pi/config
|
|
10
13
|
*/
|
|
11
14
|
|
|
@@ -17,13 +20,18 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
|
17
20
|
/** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
|
|
18
21
|
export const SETTINGS_NS = NS_LITERAL
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 官方适配器不可用/推导失败时的协议兜底三元组(自 0.1.2-alpha 线以来的稳定集)。
|
|
25
|
+
* 运行期生效集合来自已安装官方包的 `supportedProtocols()`(见 resolve-dsh.ts),
|
|
26
|
+
* 本常量只用于 schema 描述、浏览器半首帧兜底与推导失败回退。
|
|
27
|
+
*/
|
|
28
|
+
export const FALLBACK_PROTOCOLS = [
|
|
22
29
|
'openai-completions',
|
|
23
30
|
'openai-responses',
|
|
24
31
|
'anthropic-messages',
|
|
25
32
|
] as const
|
|
26
|
-
|
|
33
|
+
/** 线协议标识;合法集合运行期推导,故此处是开放字符串而非字面量联合。 */
|
|
34
|
+
export type ProtocolId = string
|
|
27
35
|
|
|
28
36
|
/** pi-ai 思考档位,升级序。 */
|
|
29
37
|
export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
|
|
@@ -136,18 +144,15 @@ export interface ProviderProfileConfig {
|
|
|
136
144
|
fileRefreshMarginSeconds?: number
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
/**
|
|
147
|
+
/** 插件配置根(模型目录唯一来自 pi-ai 内置目录,故无任何目录端点字段)。 */
|
|
140
148
|
export interface LlmPiConfig {
|
|
141
149
|
enabled: boolean
|
|
142
|
-
catalogUrl: string
|
|
143
|
-
catalogRefreshHours: number
|
|
144
|
-
catalogProxy: string
|
|
145
150
|
providers: Record<string, ProviderProfileConfig>
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
|
149
154
|
* schema 的宽松输入面:cordis 行级 config 与 settings 用户层都可能只给部分键
|
|
150
|
-
* (
|
|
155
|
+
* (2 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
|
|
151
156
|
*/
|
|
152
157
|
export type LlmPiConfigInput = Partial<LlmPiConfig>
|
|
153
158
|
|
|
@@ -208,18 +213,20 @@ const providerProfile = z.object({
|
|
|
208
213
|
adapter: z
|
|
209
214
|
.union(ADAPTER_KINDS)
|
|
210
215
|
.description(
|
|
211
|
-
'路由适配器:pi=PiAiAdapter
|
|
216
|
+
'路由适配器:pi=PiAiAdapter(默认,协议集合随已装官方适配器推导);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)',
|
|
212
217
|
)
|
|
213
218
|
.default('pi'),
|
|
214
219
|
extends: z
|
|
215
220
|
.string()
|
|
216
221
|
.description(
|
|
217
|
-
'provider
|
|
222
|
+
'provider 级继承:pi-ai 内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源',
|
|
218
223
|
),
|
|
219
224
|
displayName: z.string().description('选择器显示名;缺省为 route 键'),
|
|
220
225
|
api: z
|
|
221
|
-
.
|
|
222
|
-
.description(
|
|
226
|
+
.string()
|
|
227
|
+
.description(
|
|
228
|
+
'线协议(合法集合由已装官方适配器运行期推导,见配置页「内置模型目录」状态行);缺省逐模型取继承值的 api,全部一致时作为 route 协议',
|
|
229
|
+
),
|
|
223
230
|
baseURL: z.string().description('端点;缺省继承 extends 源 provider 的端点'),
|
|
224
231
|
apiKeyEnv: z.string().role('credential-ref').description('凭据引用名(凭据服务/环境变量)'),
|
|
225
232
|
headers: z.dict(z.string()).description('provider 请求头(Harness 署名头保留名优先)'),
|
|
@@ -334,21 +341,6 @@ export type LlmPiConfigFields = VolatileFields<LlmPiConfig>
|
|
|
334
341
|
|
|
335
342
|
export const Config: z<LlmPiConfigInput, LlmPiConfigFields> = z.object({
|
|
336
343
|
enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true).volatile(),
|
|
337
|
-
catalogUrl: z
|
|
338
|
-
.string()
|
|
339
|
-
.description('models.dev 目录数据端点')
|
|
340
|
-
.default('https://models.dev/api.json')
|
|
341
|
-
.volatile(),
|
|
342
|
-
catalogRefreshHours: z
|
|
343
|
-
.number()
|
|
344
|
-
.description('models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)')
|
|
345
|
-
.default(0)
|
|
346
|
-
.volatile(),
|
|
347
|
-
catalogProxy: z
|
|
348
|
-
.string()
|
|
349
|
-
.description('拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连')
|
|
350
|
-
.default('')
|
|
351
|
-
.volatile(),
|
|
352
344
|
providers: z
|
|
353
345
|
.dict(providerProfile)
|
|
354
346
|
.description('provider 路由表,键即 route 名')
|
package/src/inherit.ts
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* extends 继承解析:把 provider/model 条目上的继承引用解析为继承 base。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 数据源只有一个:**pi-ai 内置目录**(随已装 dsh 的 pi-ai 版本自动跟随,
|
|
5
|
+
* 含官方校正的 compat/thinkingLevelMap/模态)。未命中即「手写条目」,
|
|
6
|
+
* 必填字段由 route 级配置或兜底给出。models.dev 兜底源已随 0.1.43 移除——
|
|
7
|
+
* 版本漂移靠「现场推导 + 状态行显示生效版本」处理,不引入第二份可能过期的目录。
|
|
8
8
|
*
|
|
9
|
-
* 引用语法:`"provider/model"`
|
|
9
|
+
* 引用语法:`"provider/model"` 显式引用;裸 `"model"` 随 route 级 extends 源;
|
|
10
10
|
* 条目缺省 extends 时以 route extends 源下的同名模型为 base。
|
|
11
11
|
* @module llm-pi/inherit
|
|
12
12
|
*/
|
|
13
13
|
import type { ModelBase } from './catalog/builtin.ts'
|
|
14
14
|
import { builtinModelBase } from './catalog/builtin.ts'
|
|
15
|
-
import type { ModelsDevSource } from './catalog/models-dev.ts'
|
|
16
15
|
import type { ModelEntryConfig, ProviderProfileConfig } from './config.ts'
|
|
17
16
|
import type { DshKit } from './resolve-dsh.ts'
|
|
18
17
|
|
|
19
18
|
export interface BaseResolution {
|
|
20
19
|
base: ModelBase
|
|
21
|
-
source: 'builtin' | '
|
|
20
|
+
source: 'builtin' | 'none'
|
|
22
21
|
/** 实际命中的继承源 provider(诊断/错误消息用)。 */
|
|
23
22
|
sourceProvider?: string
|
|
24
23
|
}
|
|
@@ -42,19 +41,6 @@ export function parseExtendsRef(raw: string): {
|
|
|
42
41
|
return { provider, model }
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
function lookup(
|
|
46
|
-
kit: DshKit,
|
|
47
|
-
modelsDev: ModelsDevSource | undefined,
|
|
48
|
-
provider: string,
|
|
49
|
-
model: string,
|
|
50
|
-
): BaseResolution | undefined {
|
|
51
|
-
const builtin = builtinModelBase(kit, provider, model)
|
|
52
|
-
if (builtin !== undefined) return { base: builtin, source: 'builtin', sourceProvider: provider }
|
|
53
|
-
const dev = modelsDev?.lookup(provider, model)
|
|
54
|
-
if (dev !== undefined) return { base: dev, source: 'models-dev', sourceProvider: provider }
|
|
55
|
-
return undefined
|
|
56
|
-
}
|
|
57
|
-
|
|
58
44
|
/**
|
|
59
45
|
* 解析一个模型条目的继承 base。
|
|
60
46
|
* @throws ExtendsError 显式 extends 引用不存在(写入时拒绝,指明引用名)。
|
|
@@ -64,17 +50,14 @@ export function resolveModelBase(
|
|
|
64
50
|
profile: ProviderProfileConfig,
|
|
65
51
|
entry: ModelEntryConfig,
|
|
66
52
|
kit: DshKit,
|
|
67
|
-
modelsDev: ModelsDevSource | undefined,
|
|
68
53
|
): BaseResolution {
|
|
69
54
|
const where = `provider "${route}" model "${entry.id}"`
|
|
70
55
|
if (entry.extends === undefined) {
|
|
71
56
|
if (profile.extends === undefined) return { base: {}, source: 'none' }
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
)
|
|
57
|
+
const base = builtinModelBase(kit, profile.extends, entry.id)
|
|
58
|
+
return base === undefined
|
|
59
|
+
? { base: {}, source: 'none' }
|
|
60
|
+
: { base, source: 'builtin', sourceProvider: profile.extends }
|
|
78
61
|
}
|
|
79
62
|
const ref = parseExtendsRef(entry.extends)
|
|
80
63
|
const provider = ref.provider ?? profile.extends
|
|
@@ -83,11 +66,12 @@ export function resolveModelBase(
|
|
|
83
66
|
`${where}: extends ${JSON.stringify(entry.extends)} 是裸模型 id,但本 route 未配置 provider 级 extends 查找源`,
|
|
84
67
|
)
|
|
85
68
|
}
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
69
|
+
const base = builtinModelBase(kit, provider, ref.model)
|
|
70
|
+
if (base === undefined) {
|
|
88
71
|
throw new ExtendsError(
|
|
89
|
-
`${where}: extends 引用 "${provider}/${ref.model}"
|
|
72
|
+
`${where}: extends 引用 "${provider}/${ref.model}" 不在 pi-ai ${kit.versions.piAi ?? '?'} 的内置目录中` +
|
|
73
|
+
`(可继承的 provider/模型见配置页「内置模型目录」,或用裸 model id 配合 route 级 extends)`,
|
|
90
74
|
)
|
|
91
75
|
}
|
|
92
|
-
return
|
|
76
|
+
return { base, source: 'builtin', sourceProvider: provider }
|
|
93
77
|
}
|