@dsh-plus/llm-pi 0.1.41 → 0.1.43

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.
@@ -11,7 +11,7 @@ import {
11
11
  BUDGET_KEYS,
12
12
  CACHE_RETENTION_OPTIONS,
13
13
  MODALITIES,
14
- PROTOCOL_IDS,
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={PROTOCOL_IDS}
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
- * headers 键值对 + retryPolicy JSON + compat + 模型目录)。
4
- * 支持新增 route(输入键名)与删除 route。
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
- <p className="lpc-groupLabel">{props.t('providersGroup')}</p>
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={() => setOpen(!open)}
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
- * (见 compat-gates.ts),不再手抄镜像。
3
+ * (见 official-surface.ts),不再手抄镜像。
4
4
  *
5
- * 语义:官方门控表是 compat 可配性的唯一事实源——按协议分型(offer/withhold),
6
- * withhold 字段(官方内置目录已为对应厂商设置,如 openRouterRouting/zaiToolStream/
7
- * sendSessionAffinityHeaders/supportsToolSearch/supportsMidConvoEffort 等)写时
8
- * 拒绝并提示以目录 provider 名为 route;未知键拒绝;无值键(null/undefined)拒绝
9
- * (对齐官方 assertOfferedCompatFields:"写了但没生效"的表面状态不允许)。
10
- *
11
- * 历史:本表曾是手抄镜像,0.1.5-rc.1 官方扩容 offer 字段而旧表漏收,导致官方可配
12
- * 字段被本插件**误拒**(静默功能缺失、无任何报错)。现改为现场推导:官方新增字段
13
- * 即自动可用,`tests/compat-gates.test.ts` 直读官方 bundle 守门推导正确性。
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
- import type { CompatDisposition, CompatTable, CompatValue } from './compat-gates.ts'
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 './compat-gates.ts'
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 gate = active.gates[api]
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 字段表(支持:${Object.keys(active.gates).join(', ')})`,
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
- throw new Error(
134
- `${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(', ')};` +
135
- `官方全部可配字段:${allOfferedFields().join(', ')})`,
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 通道:配置读写已迁移到官方 remote.settings 直连
3
- * (0.1.2-alpha.1 起第三方命名空间全量开放),这里只保留官方传输没有的
4
- * 「模型目录」端点(含 kitSource / models-dev 运行期诊断,供卡片状态行展示)。
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 { builtinModelIds } from './catalog/builtin.ts'
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 PROTOCOL_IDS) {
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
- /** 目录查询/手动拉取:GET ?provider=&source= → 该源模型 id 列表;POST /refresh → 立即拉取。 */
50
- function handleCatalog(runtime: LlmPiRuntime, req: IncomingMessage, res: ServerResponse): void {
51
- const kitSource = runtime.kitInfo().source
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
- providers: runtime.kit.getBuiltinProviders(),
74
- models: provider.length > 0 ? builtinModelIds(runtime.kit, provider) : [],
75
- kitSource,
76
- compat,
74
+ kit,
75
+ providers: runtime.providerEntries(),
76
+ apis: runtime.apiIndex(),
77
+ compat: compatTablePayload(kit.protocols),
77
78
  })
78
79
  }
79
80
 
80
- /** 注册目录路由(webServer 缺失时由调用方保证不调用)。 */
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) => handleCatalog(runtime, 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
- /** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
21
- export const PROTOCOL_IDS = [
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
- export type ProtocolId = (typeof PROTOCOL_IDS)[number]
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
- * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
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(默认,三协议);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)',
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 级继承:内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源',
222
+ 'provider 级继承:pi-ai 内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源',
218
223
  ),
219
224
  displayName: z.string().description('选择器显示名;缺省为 route 键'),
220
225
  api: z
221
- .union(PROTOCOL_IDS)
222
- .description('线协议;缺省逐模型取继承值的 api,全部一致时作为 route 协议'),
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
- * 1. pi-ai 内置目录(含官方校正,最可信);
6
- * 2. models.dev 快照(仅内置未收录的新模型,字段保守);
7
- * 3. 都未命中 → 手写模型(无 base,必填字段由配置/兜底给出)。
4
+ * 数据源只有一个:**pi-ai 内置目录**(随已装 dsh 的 pi-ai 版本自动跟随,
5
+ * 含官方校正的 compat/thinkingLevelMap/模态)。未命中即「手写条目」,
6
+ * 必填字段由 route 级配置或兜底给出。models.dev 兜底源已随 0.1.43 移除——
7
+ * 版本漂移靠「现场推导 + 状态行显示生效版本」处理,不引入第二份可能过期的目录。
8
8
  *
9
- * 引用语法:`"provider/model"` 显式跨源;裸 `"model"` 随 route 级 extends 源;
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' | 'models-dev' | 'none'
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
- return (
73
- lookup(kit, modelsDev, profile.extends, entry.id) ?? {
74
- base: {},
75
- source: 'none',
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 hit = lookup(kit, modelsDev, provider, ref.model)
87
- if (hit === undefined) {
69
+ const base = builtinModelBase(kit, provider, ref.model)
70
+ if (base === undefined) {
88
71
  throw new ExtendsError(
89
- `${where}: extends 引用 "${provider}/${ref.model}" 在内置目录与 models.dev 快照中都不存在`,
72
+ `${where}: extends 引用 "${provider}/${ref.model}" 不在 pi-ai ${kit.versions.piAi ?? '?'} 的内置目录中` +
73
+ `(可继承的 provider/模型见配置页「内置模型目录」,或用裸 model id 配合 route 级 extends)`,
90
74
  )
91
75
  }
92
- return hit
76
+ return { base, source: 'builtin', sourceProvider: provider }
93
77
  }