@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.
- package/lib/client.js +1528 -767
- 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 +183 -133
- package/src/client/client.ts +4 -4
- 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/ns.ts +2 -2
- 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
package/src/kit-meta.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行期套件元信息:插件实际在用哪一份 pi-ai / 官方适配器,以及版本是否落在
|
|
3
|
+
* 本插件验证过的区间内。
|
|
4
|
+
*
|
|
5
|
+
* 存在的理由:插件**按文件路径**动态加载已装 dsh 树里的 pi-ai(版本随 dsh 升级
|
|
6
|
+
* 自动变化,见 resolve-dsh.ts),因此"当前生效版本"不是 manifest 上写的版本。
|
|
7
|
+
* 配置页把它显示出来,版本漂移一眼可见;`VERIFIED_PI_AI_RANGE` 只用于**提示**,
|
|
8
|
+
* 永不阻断——能不能用由运行期形状自检(assertKitShape)与逐项降级决定。
|
|
9
|
+
* @module llm-pi/kit-meta
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
/** 实际生效的三个版本;拿不到即省略(读文件失败不视为错误)。 */
|
|
17
|
+
export interface KitVersions {
|
|
18
|
+
/** pi-ai(目录与协议实现的来源)。 */
|
|
19
|
+
piAi?: string
|
|
20
|
+
/** 官方 dsh-llm-pi-ai(PiAiAdapter 与 compat 门控的来源)。 */
|
|
21
|
+
piAiAdapter?: string
|
|
22
|
+
/** dsh 本体(安装树根 package.json,仅在根就是 dsh 包时可得)。 */
|
|
23
|
+
dsh?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 本插件开发/测试覆盖过的 pi-ai 区间(semver range,仅用于状态行提示)。 */
|
|
27
|
+
export const VERIFIED_PI_AI_RANGE = '>=0.85.1 <0.88.0'
|
|
28
|
+
|
|
29
|
+
/** 读 package.json 的 version;文件不存在/非 JSON/无 version 都返回 undefined。 */
|
|
30
|
+
export function readPackageVersion(manifestPath: string): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
const parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
33
|
+
const version = (parsed as { version?: unknown } | null)?.version
|
|
34
|
+
return typeof version === 'string' && version.length > 0 ? version : undefined
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 读包目录的版本(`<dir>/package.json`)。 */
|
|
41
|
+
export function packageVersionOf(packageDir: string): string | undefined {
|
|
42
|
+
return readPackageVersion(join(packageDir, 'package.json'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 只读包名(判断安装树根是不是 dsh 本体)。 */
|
|
46
|
+
function readPackageName(manifestPath: string): string | undefined {
|
|
47
|
+
try {
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
49
|
+
const name = (parsed as { name?: unknown } | null)?.name
|
|
50
|
+
return typeof name === 'string' ? name : undefined
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 收集 dsh 安装树内实际加载的那几个包的版本。 */
|
|
57
|
+
export function collectTreeVersions(root: string): KitVersions {
|
|
58
|
+
const nm = join(root, 'node_modules')
|
|
59
|
+
const piAi = packageVersionOf(join(nm, '@earendil-works', 'pi-ai'))
|
|
60
|
+
const adapter = packageVersionOf(join(nm, '@deepseek-ai', 'dsh-llm-pi-ai'))
|
|
61
|
+
// 树根 package.json 是 dsh 本体时才算 dsh 版本(源码仓库布局下根可能是别的包)。
|
|
62
|
+
const rootManifest = join(root, 'package.json')
|
|
63
|
+
const dsh =
|
|
64
|
+
readPackageName(rootManifest) === '@deepseek-ai/dsh' ? packageVersionOf(root) : undefined
|
|
65
|
+
return {
|
|
66
|
+
...(piAi === undefined ? {} : { piAi }),
|
|
67
|
+
...(adapter === undefined ? {} : { piAiAdapter: adapter }),
|
|
68
|
+
...(dsh === undefined ? {} : { dsh }),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 由模块入口文件向上找包目录(最多 3 层,兼容 `<pkg>/{lib,dist}/index.js`
|
|
74
|
+
* 与根目录入口两种布局);找不到返回 undefined。
|
|
75
|
+
*/
|
|
76
|
+
function packageDirOf(entryFile: string): string | undefined {
|
|
77
|
+
let dir = dirname(entryFile)
|
|
78
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
79
|
+
if (readPackageName(join(dir, 'package.json')) !== undefined) return dir
|
|
80
|
+
const parent = dirname(dir)
|
|
81
|
+
if (parent === dir) return undefined
|
|
82
|
+
dir = parent
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 解析已安装包的目录(本进程内解析,只用于读 package.json)。
|
|
89
|
+
* 拿不到即返回 undefined(该版本号留空,不影响运行)。
|
|
90
|
+
*/
|
|
91
|
+
export function installedPackageDir(specifier: string): string | undefined {
|
|
92
|
+
try {
|
|
93
|
+
return packageDirOf(fileURLToPath(import.meta.resolve(specifier)))
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 收集插件 vendored 兜底副本的版本(回退路径下"生效版本"即这两份)。 */
|
|
100
|
+
export function collectVendoredVersions(): KitVersions {
|
|
101
|
+
const piAiDir = installedPackageDir('@earendil-works/pi-ai')
|
|
102
|
+
const adapterDir = installedPackageDir('@deepseek-ai/dsh-llm-pi-ai')
|
|
103
|
+
const piAi = piAiDir === undefined ? undefined : packageVersionOf(piAiDir)
|
|
104
|
+
const adapter = adapterDir === undefined ? undefined : packageVersionOf(adapterDir)
|
|
105
|
+
return {
|
|
106
|
+
...(piAi === undefined ? {} : { piAi }),
|
|
107
|
+
...(adapter === undefined ? {} : { piAiAdapter: adapter }),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 已验证区间的解析端点(与 {@link VERIFIED_PI_AI_RANGE} 同步维护)。 */
|
|
112
|
+
const VERIFIED_MIN: readonly [number, number, number] = [0, 85, 1]
|
|
113
|
+
const VERIFIED_MAX_EXCLUSIVE: readonly [number, number, number] = [0, 88, 0]
|
|
114
|
+
|
|
115
|
+
/** 解析 `x.y.z[-pre][+build]` 的数值三元组;形状不符返回 undefined。 */
|
|
116
|
+
function numericTriple(version: string): [number, number, number] | undefined {
|
|
117
|
+
const core = version.split(/[-+]/, 1)[0] ?? ''
|
|
118
|
+
const parts = core.split('.')
|
|
119
|
+
if (parts.length !== 3) return undefined
|
|
120
|
+
const nums = parts.map((part) => (/^\d+$/.test(part) ? Number.parseInt(part, 10) : Number.NaN))
|
|
121
|
+
const [a, b, c] = nums
|
|
122
|
+
if (a === undefined || b === undefined || c === undefined) return undefined
|
|
123
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || !Number.isInteger(c)) return undefined
|
|
124
|
+
return [a, b, c]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareTriple(
|
|
128
|
+
left: readonly [number, number, number],
|
|
129
|
+
right: readonly [number, number, number],
|
|
130
|
+
): number {
|
|
131
|
+
for (let i = 0; i < 3; i += 1) {
|
|
132
|
+
const diff = (left[i] as number) - (right[i] as number)
|
|
133
|
+
if (diff !== 0) return diff
|
|
134
|
+
}
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 生效 pi-ai 版本不在已验证区间时的提示文案(**提示而非阻断**)。
|
|
140
|
+
* 区间内或版本号形状无法解析时返回 undefined。
|
|
141
|
+
*/
|
|
142
|
+
export function piAiVersionNotice(version: string | undefined): string | undefined {
|
|
143
|
+
if (version === undefined) return undefined
|
|
144
|
+
const parsed = numericTriple(version)
|
|
145
|
+
if (parsed === undefined) return undefined
|
|
146
|
+
if (compareTriple(parsed, VERIFIED_MIN) < 0) {
|
|
147
|
+
return `当前生效 pi-ai ${version} 低于本插件验证过的下限 ${VERIFIED_PI_AI_RANGE}`
|
|
148
|
+
}
|
|
149
|
+
if (compareTriple(parsed, VERIFIED_MAX_EXCLUSIVE) >= 0) {
|
|
150
|
+
return (
|
|
151
|
+
`当前生效 pi-ai ${version} 超出本插件验证过的区间 ${VERIFIED_PI_AI_RANGE};` +
|
|
152
|
+
'已启用现场推导与形状自检,若出现异常请先看诊断行'
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
return undefined
|
|
156
|
+
}
|
package/src/ns.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
3
3
|
* 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 对命名空间做编译期
|
|
4
4
|
* 语法校验,见 config.ts);浏览器半把它作为配置卡片的注册键
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* (插件页配置卡片挂在 row/bundle config 槽位上,与命名空间解耦,
|
|
6
|
+
* 见 shared/config-slots),
|
|
7
7
|
* 同一字面量两处共用,防止漂移。
|
|
8
8
|
* @module llm-pi/ns
|
|
9
9
|
*/
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 0.1.5-rc.1 教训)。
|
|
2
|
+
* 官方适配面的**自动继承**:从已安装的官方副本(`dsh-llm-pi-ai` + `pi-ai`)
|
|
3
|
+
* 现场推导三样东西,取代早期的手抄镜像:
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 1. **线协议集合**:官方包根导出的 `supportedProtocols()`(官方 PROTOCOLS 表
|
|
6
|
+
* 的唯一投影)。官方新增协议时本插件自动可用,不再需要改代码;
|
|
7
|
+
* 2. **compat 门控与取值约束**:门控取自运行期 bundle 的 `COMPAT_GATES`
|
|
8
|
+
* (事实源:运行期拒绝哪些字段由它决定),取值约束取自导出的 Config schema
|
|
9
|
+
* 的 `providers.*.compat` 节点(boolean / number.step(1) / enum / 对象);
|
|
10
|
+
* 3. **模型条目字段集**:Config schema 的 `providers.*.models` 键集——继承时按此
|
|
11
|
+
* 透传 pi-ai 目录的同名字段,官方将来新增模型级字段即自动跟随。
|
|
8
12
|
*
|
|
9
|
-
* 1.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 命名常量与内联对象两种形态)。门控表取自这里而非 d.ts,因为它是**事实源**:
|
|
13
|
-
* 运行期拒绝哪些字段由它决定。
|
|
14
|
-
* 2. **导出的 Config schema**(schemastery 标准 schema):`providers.*.compat`
|
|
15
|
-
* 节点逐字段给出取值约束(boolean/number.step(1)/enum/对象),据此推导
|
|
16
|
-
* 取值校验,不再手写 VALUE_SPECS。
|
|
13
|
+
* 历史教训(0.1.5-rc.1):compat 门控曾是官方 catalog.ts 的手抄镜像,官方扩容
|
|
14
|
+
* offer 字段而旧表漏收,导致官方可配字段被本插件**误拒**(静默功能缺失、无报错)。
|
|
15
|
+
* 现改为现场推导,`tests/official-surface.test.ts` 直读官方副本守门推导正确性。
|
|
17
16
|
*
|
|
18
|
-
* 推导失败(官方改布局/改打包形态)时回退到
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* 推导失败(官方改布局/改打包形态)时回退到 FALLBACK_*:compat 表用最后已知快照
|
|
18
|
+
* 且**放宽未知键**(见 compat.ts:宁可放行交给官方自身校验,也不误拒官方新字段),
|
|
19
|
+
* 协议回退三元组、模型字段回退已知七键,各自给出诊断——绝不因推导失败弄挂启动。
|
|
20
|
+
* @module llm-pi/official-surface
|
|
21
21
|
*/
|
|
22
|
+
import { FALLBACK_PROTOCOLS } from './config.ts'
|
|
22
23
|
|
|
23
24
|
/** 字段可配性:offer = 官方允许写;withhold = 官方为厂商内置、写时拒绝。 */
|
|
24
25
|
export type CompatDisposition = 'offer' | 'withhold'
|
|
@@ -161,6 +162,49 @@ export function deriveSpecs(module: Record<string, unknown>): Record<string, Com
|
|
|
161
162
|
return out
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* 从官方适配器模块推导线协议集合:官方包根导出的 `supportedProtocols()`
|
|
167
|
+
* (`Object.keys(PROTOCOLS)` 的投影,顺序即官方表顺序)。
|
|
168
|
+
* 非函数/返回空/含非字符串项都视为推导失败,调用方回退内置三元组。
|
|
169
|
+
*/
|
|
170
|
+
export function deriveProtocols(module: Record<string, unknown>): string[] | undefined {
|
|
171
|
+
const reader = module['supportedProtocols']
|
|
172
|
+
if (typeof reader !== 'function') return undefined
|
|
173
|
+
const value = (reader as () => unknown)()
|
|
174
|
+
if (!Array.isArray(value) || value.length === 0) return undefined
|
|
175
|
+
const ids = value.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
176
|
+
return ids.length === value.length ? ids : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 从官方 Config schema 推导模型条目字段集(`providers.*.models` 的元素键集)。
|
|
181
|
+
* 这些是官方适配器**接受**的模型级键;继承时按此透传 pi-ai 目录的同名字段。
|
|
182
|
+
*/
|
|
183
|
+
export function deriveModelEntryFields(module: Record<string, unknown>): string[] | undefined {
|
|
184
|
+
const config = module['Config'] as SchemaNode | undefined
|
|
185
|
+
const models = config?.dict?.['providers']?.inner?.dict?.['models']
|
|
186
|
+
const fields = models?.inner?.dict
|
|
187
|
+
if (fields === undefined) return undefined
|
|
188
|
+
const keys = Object.keys(fields)
|
|
189
|
+
return keys.length > 0 ? keys : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 推导失败时的模型条目字段兜底(0.2.x 线实测键集)。 */
|
|
193
|
+
export const FALLBACK_MODEL_ENTRY_FIELDS = [
|
|
194
|
+
'id',
|
|
195
|
+
'name',
|
|
196
|
+
'contextWindow',
|
|
197
|
+
'maxTokens',
|
|
198
|
+
'input',
|
|
199
|
+
'reasoningEfforts',
|
|
200
|
+
'compat',
|
|
201
|
+
] as const
|
|
202
|
+
|
|
203
|
+
/** 协议推导失败时的兜底三元组(转出为可变数组,便于调用方直接持有)。 */
|
|
204
|
+
export function fallbackProtocols(): string[] {
|
|
205
|
+
return [...FALLBACK_PROTOCOLS]
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
/** 官方门控表不可解析时的最后已知快照(0.1.5-rc.2 实测值)。 */
|
|
165
209
|
export const FALLBACK_TABLE: CompatTable = {
|
|
166
210
|
source: 'fallback',
|
package/src/profiles.ts
CHANGED
|
@@ -35,10 +35,10 @@ import type {
|
|
|
35
35
|
|
|
36
36
|
import {
|
|
37
37
|
builtinProviderBaseUrl,
|
|
38
|
+
hasBuiltinProvider,
|
|
38
39
|
inheritedCatalogEntries,
|
|
39
40
|
type ModelBase,
|
|
40
41
|
} from './catalog/builtin.ts'
|
|
41
|
-
import type { ModelsDevSource } from './catalog/models-dev.ts'
|
|
42
42
|
import { mergeCompat, validateCompat } from './compat.ts'
|
|
43
43
|
import {
|
|
44
44
|
DEFAULT_CONTEXT_WINDOW,
|
|
@@ -61,9 +61,8 @@ const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
|
61
61
|
|
|
62
62
|
export interface BuildDeps {
|
|
63
63
|
kit: DshKit
|
|
64
|
-
modelsDev?: ModelsDevSource
|
|
65
64
|
/**
|
|
66
|
-
* 运行期宽松模式:数据源漂移(
|
|
65
|
+
* 运行期宽松模式:数据源漂移(dsh 树升级换 pi-ai 版本导致内置目录变化)使已写入的
|
|
67
66
|
* extends 引用失效时,降级/跳过并告警,而不是抛错把整个 route 弄挂。
|
|
68
67
|
* 写时校验(assertServiceable)保持严格(缺省),非法引用在写入处拒绝。
|
|
69
68
|
*/
|
|
@@ -84,6 +83,29 @@ function declaredInput(
|
|
|
84
83
|
return configured === undefined || configured.length === 0 ? undefined : [...configured]
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 归一化条目里"本层不处理、需原样进 pi-ai Model"的字段(继承自目录的未来
|
|
88
|
+
* 模型级字段,如 samplingParams)。官方 schema 当前的模型条目键(id/name/
|
|
89
|
+
* contextWindow/maxTokens/input/reasoningEfforts/compat)在此全部排除:
|
|
90
|
+
* 它们要么由本层显式翻译,要么不能出现在 pi-ai Model 上。
|
|
91
|
+
*/
|
|
92
|
+
function extraEntryFields(entry: PiAiModelProfile): Record<string, unknown> {
|
|
93
|
+
const known = new Set([
|
|
94
|
+
'id',
|
|
95
|
+
'name',
|
|
96
|
+
'contextWindow',
|
|
97
|
+
'maxTokens',
|
|
98
|
+
'input',
|
|
99
|
+
'reasoningEfforts',
|
|
100
|
+
'compat',
|
|
101
|
+
])
|
|
102
|
+
const out: Record<string, unknown> = {}
|
|
103
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
104
|
+
if (!known.has(key) && value !== undefined) out[key] = value
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
87
109
|
/** 显式 reasoningEfforts 字典的合法性校验(官方 resolveModelReasoning 同款规则)。 */
|
|
88
110
|
function validateReasoningEfforts(
|
|
89
111
|
provider: string,
|
|
@@ -223,6 +245,9 @@ function materializeModel(
|
|
|
223
245
|
// 用户手写层(上面两次 validateCompat)。
|
|
224
246
|
return {
|
|
225
247
|
entry: {
|
|
248
|
+
// 继承源的其余官方可继承字段先落定(官方 schema 接受的键集运行期推导),
|
|
249
|
+
// 表单显式字段随后覆盖:官方将来新增模型级字段即自动跟随,无需改本插件。
|
|
250
|
+
...(base.extra ?? {}),
|
|
226
251
|
id: entry.id,
|
|
227
252
|
...(name === entry.id ? {} : { name }),
|
|
228
253
|
...(contextWindow === DEFAULT_CONTEXT_WINDOW ? {} : { contextWindow }),
|
|
@@ -230,7 +255,7 @@ function materializeModel(
|
|
|
230
255
|
...(input.length === 1 && input[0] === 'text' ? {} : { input }),
|
|
231
256
|
reasoningEfforts: reasoningEffortsFromBase(route, entry, base),
|
|
232
257
|
...(compat === undefined ? {} : { compat }),
|
|
233
|
-
},
|
|
258
|
+
} as PiAiModelProfile,
|
|
234
259
|
api,
|
|
235
260
|
baseUrl,
|
|
236
261
|
}
|
|
@@ -261,7 +286,7 @@ function materializeRouteModels(
|
|
|
261
286
|
seen.add(entry.id)
|
|
262
287
|
let base: ModelBase
|
|
263
288
|
try {
|
|
264
|
-
base = resolveModelBase(route, profile, entry, deps.kit
|
|
289
|
+
base = resolveModelBase(route, profile, entry, deps.kit).base
|
|
265
290
|
} catch (error) {
|
|
266
291
|
// 运行期数据源漂移:已写入的引用在当前目录 miss。严格模式(写时校验)保持拒绝;
|
|
267
292
|
// lenient 模式降级为手写条目并告警,避免整个 route 不可服务。
|
|
@@ -366,6 +391,20 @@ function normalizeRoute(
|
|
|
366
391
|
// 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
|
|
367
392
|
// route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
|
|
368
393
|
// 暴露(DUPLICATE_ADAPTER),由 service 层逐个 route 注册降级处理。
|
|
394
|
+
if (profile.extends !== undefined && !hasBuiltinProvider(deps.kit, profile.extends)) {
|
|
395
|
+
// 继承源只有 pi-ai 内置目录(models.dev 兜底已移除):写错 provider 名过去会
|
|
396
|
+
// 静默退化成手写条目,现在写入即拒绝并给出可用集合,避免"配了没生效"。
|
|
397
|
+
if (!deps.lenient) {
|
|
398
|
+
invalid(
|
|
399
|
+
route,
|
|
400
|
+
`extends ${JSON.stringify(profile.extends)} 不是 pi-ai ${deps.kit.versions.piAi ?? '?'} 的内置 provider;` +
|
|
401
|
+
`可用:${deps.kit.getBuiltinProviders().join(', ')}`,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
deps.warn?.(
|
|
405
|
+
`llm-pi: provider "${route}" 的 extends ${JSON.stringify(profile.extends)} 不是当前 pi-ai 的内置 provider,已忽略该继承源`,
|
|
406
|
+
)
|
|
407
|
+
}
|
|
369
408
|
if (profile.baseURL !== undefined && profile.baseURL.length === 0) invalid(route, 'baseURL 为空')
|
|
370
409
|
if (profile.displayName !== undefined && profile.displayName.length === 0)
|
|
371
410
|
invalid(route, 'displayName 为空')
|
|
@@ -395,11 +434,11 @@ function normalizeRoute(
|
|
|
395
434
|
const materialized = materializeRouteModels(route, profile, deps, defaultInput)
|
|
396
435
|
if (materialized === null) return undefined // lenient 下 route 无模型可服务,跳过注册
|
|
397
436
|
const { api, baseUrl, models, configuredMaxTokens } = materialized
|
|
398
|
-
const factory = deps.kit.protocolFactories[api
|
|
437
|
+
const factory = deps.kit.protocolFactories[api]
|
|
399
438
|
if (factory === undefined) {
|
|
400
439
|
invalid(
|
|
401
440
|
route,
|
|
402
|
-
`api ${JSON.stringify(api)}
|
|
441
|
+
`api ${JSON.stringify(api)} 本插件无法服务(当前生效 pi-ai ${deps.kit.versions.piAi ?? '?'} 支持的协议:${deps.kit.protocols.join('/')})`,
|
|
403
442
|
)
|
|
404
443
|
}
|
|
405
444
|
const displayName = profile.displayName ?? route
|
|
@@ -462,6 +501,8 @@ function harnessApiKeyAuth(name: string) {
|
|
|
462
501
|
export interface ResolverDeps {
|
|
463
502
|
createProvider: DshKit['createProvider']
|
|
464
503
|
protocolFactories: DshKit['protocolFactories']
|
|
504
|
+
/** 当前生效协议集合(写时错误消息用;与 protocolFactories 的键同源)。 */
|
|
505
|
+
protocols: string[]
|
|
465
506
|
resolveRetryPolicy: DshKit['resolveRetryPolicy']
|
|
466
507
|
}
|
|
467
508
|
|
|
@@ -550,16 +591,13 @@ export function resolveProfilesFallback(
|
|
|
550
591
|
const displayName = source.displayName ?? provider
|
|
551
592
|
const api = source.api
|
|
552
593
|
if (api === undefined) {
|
|
553
|
-
invalid(
|
|
554
|
-
provider,
|
|
555
|
-
'需要 api:本插件仅支持 openai-completions/openai-responses/anthropic-messages',
|
|
556
|
-
)
|
|
594
|
+
invalid(provider, `需要 api:本插件只服务官方适配器声明的协议(${deps.protocols.join('/')})`)
|
|
557
595
|
}
|
|
558
|
-
const factory = deps.protocolFactories[api
|
|
596
|
+
const factory = deps.protocolFactories[api]
|
|
559
597
|
if (factory === undefined) {
|
|
560
598
|
invalid(
|
|
561
599
|
provider,
|
|
562
|
-
`api ${JSON.stringify(api)}
|
|
600
|
+
`api ${JSON.stringify(api)} 本插件无法服务(当前支持的协议:${deps.protocols.join('/')})`,
|
|
563
601
|
)
|
|
564
602
|
}
|
|
565
603
|
if (source.baseURL === undefined) {
|
|
@@ -589,6 +627,7 @@ export function resolveProfilesFallback(
|
|
|
589
627
|
}
|
|
590
628
|
if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
|
|
591
629
|
return {
|
|
630
|
+
...extraEntryFields(entry),
|
|
592
631
|
id: entry.id,
|
|
593
632
|
name: entry.name ?? entry.id,
|
|
594
633
|
api,
|