@dsh-plus/llm-pi 0.1.13 → 0.1.14
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 +48 -50
- package/lib/index.d.ts +3 -2
- package/lib/index.js +634 -222
- package/package.json +28 -24
- package/src/auth-inline.ts +177 -0
- package/src/catalog/official.ts +0 -2
- package/src/client/api.ts +4 -3
- package/src/client/card.tsx +5 -13
- package/src/client/client.ts +16 -18
- package/src/client/constants.ts +11 -16
- package/src/compat.ts +136 -62
- package/src/config-api.ts +3 -3
- package/src/config.ts +25 -4
- package/src/deepseek-routes.ts +25 -0
- package/src/ns.ts +1 -1
- package/src/profiles-deepseek.ts +12 -6
- package/src/profiles.ts +439 -171
- package/src/resolve-dsh.ts +170 -13
- package/src/service.ts +26 -0
package/src/profiles.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* profile
|
|
2
|
+
* profile 构建器(0.1.2-alpha.1 适配版):插件 Config → 官方 schema 形状的
|
|
3
|
+
* 「前置数据源归一化」→ 官方 resolveProfiles(或插件等价兜底)→
|
|
4
|
+
* ResolvedPiAiProviderProfile。
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 适配决策(resolveProfiles 复用评估,2026-08):
|
|
7
|
+
* - 官方 resolveProfiles 仅在 @deepseek-ai/dsh-llm-pi-ai 的 src/config.ts 子路径
|
|
8
|
+
* 导出,npm 发布形态不携带 src/(lib/index.js 单 bundle),包根也不导出该
|
|
9
|
+
* 符号——真实部署(npm 布局 dsh 树)与 vendored 兜底都无法引用官方解析链;
|
|
10
|
+
* - 因此:dsh 树 dev 布局(src 存在)时经 src 子路径复用官方 resolveProfiles
|
|
11
|
+
* (resolve-dsh.ts 探测),其余形态走本文件的 resolveProfilesFallback——
|
|
12
|
+
* 语义逐行对齐官方 config.ts:resolveProfiles + catalog.ts:resolveRouteModels
|
|
13
|
+
* (协议限定本插件三协议),compat 门控以官方 catalog.ts COMPAT_GATES 为
|
|
14
|
+
* 唯一事实源(见 compat.ts);
|
|
15
|
+
* - 两条路径的输入相同:插件独占的 extends/models.dev/草稿路由等语义在此
|
|
16
|
+
* 「前置归一化」为全显式官方 schema 形状(模型条目字段全部落定),官方链
|
|
17
|
+
* 不再需要目录查找;normalizeRoute 的产物经 kit.resolveProfiles 进入
|
|
18
|
+
* 官方 adapter 消息链路。
|
|
8
19
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
20
|
+
* 与官方链的既有缝隙(记录在案):
|
|
21
|
+
* - 模型级 maxTokens 在官方 schema 中兼作「能力值」与「显式配置的请求默认
|
|
22
|
+
* cap」;归一化把继承值物化进条目后官方链会全部计入 configuredMaxTokens,
|
|
23
|
+
* 此处按插件原语义回填为「仅用户显式配置」——请求默认 cap 行为不变;
|
|
24
|
+
* - 官方 schema 无模型级 baseUrl/headers/cost:端点收敛为 route 级单一值
|
|
25
|
+
* (模型间端点不一致写时拒绝),headers/cost 不继承(0.84.2 目录无模型级
|
|
26
|
+
* headers;harness 不消费 cost 元数据,与官方 NO_COST 同口径)。
|
|
12
27
|
* @module llm-pi/profiles
|
|
13
28
|
*/
|
|
14
29
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
15
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
PiAiModelProfile,
|
|
32
|
+
PiAiProviderProfile,
|
|
33
|
+
ResolvedPiAiProviderProfile,
|
|
34
|
+
} from '@deepseek-ai/dsh-llm-pi-ai'
|
|
16
35
|
|
|
17
36
|
import {
|
|
18
37
|
builtinProviderBaseUrl,
|
|
@@ -20,11 +39,13 @@ import {
|
|
|
20
39
|
type ModelBase,
|
|
21
40
|
} from './catalog/builtin.ts'
|
|
22
41
|
import type { ModelsDevSource } from './catalog/models-dev.ts'
|
|
23
|
-
import { mergeCompat, validateCompat } from './compat.ts'
|
|
42
|
+
import { compatDispositionOf, compatFieldsOf, mergeCompat, validateCompat } from './compat.ts'
|
|
24
43
|
import {
|
|
25
44
|
DEFAULT_CONTEXT_WINDOW,
|
|
26
45
|
DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
27
46
|
DEFAULT_MAX_TOKENS,
|
|
47
|
+
DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
48
|
+
DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
28
49
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
29
50
|
MAX_TIMER_DELAY_MS,
|
|
30
51
|
type ModelEntryConfig,
|
|
@@ -63,97 +84,98 @@ function declaredInput(
|
|
|
63
84
|
return configured === undefined || configured.length === 0 ? undefined : [...configured]
|
|
64
85
|
}
|
|
65
86
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
* 显式 dict → 全档位确定的 thinkingLevelMap(未声明档位置 null);
|
|
69
|
-
* false → 非推理模型;缺省 → 保留继承源的 reasoning 能力。
|
|
70
|
-
*/
|
|
71
|
-
function resolveModelReasoning(
|
|
87
|
+
/** 显式 reasoningEfforts 字典的合法性校验(官方 resolveModelReasoning 同款规则)。 */
|
|
88
|
+
function validateReasoningEfforts(
|
|
72
89
|
provider: string,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
):
|
|
76
|
-
const efforts = entry.reasoningEfforts
|
|
77
|
-
if (efforts === undefined) {
|
|
78
|
-
if (base.reasoning === undefined) return { reasoning: false }
|
|
79
|
-
return {
|
|
80
|
-
reasoning: base.reasoning,
|
|
81
|
-
...(base.thinkingLevelMap === undefined
|
|
82
|
-
? {}
|
|
83
|
-
: {
|
|
84
|
-
thinkingLevelMap: base.thinkingLevelMap as Record<string, string | null>,
|
|
85
|
-
}),
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (efforts === false) return { reasoning: false }
|
|
90
|
+
entryId: string,
|
|
91
|
+
efforts: Record<string, string | null>,
|
|
92
|
+
): void {
|
|
89
93
|
if (Object.keys(efforts).length === 0) {
|
|
90
|
-
invalid(provider, `model "${
|
|
94
|
+
invalid(provider, `model "${entryId}" 的 reasoningEfforts 为空:声明档位、置 false 或缺省继承`)
|
|
91
95
|
}
|
|
92
96
|
for (const level of THINKING_LEVELS) {
|
|
93
|
-
const wire =
|
|
97
|
+
const wire = efforts[level]
|
|
94
98
|
if (wire === undefined) continue
|
|
95
99
|
if (wire === null) {
|
|
96
100
|
if (level !== 'off')
|
|
97
|
-
invalid(provider, `model "${
|
|
101
|
+
invalid(provider, `model "${entryId}" reasoningEfforts.${level} 需要线值;仅 off 可留空`)
|
|
98
102
|
} else if (wire.length === 0) {
|
|
99
|
-
invalid(provider, `model "${
|
|
103
|
+
invalid(provider, `model "${entryId}" reasoningEfforts.${level} 不能为空字符串`)
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
|
-
const declared = THINKING_LEVELS.filter(
|
|
103
|
-
(level) => (efforts as Record<string, unknown>)[level] !== undefined,
|
|
104
|
-
)
|
|
106
|
+
const declared = THINKING_LEVELS.filter((level) => efforts[level] !== undefined)
|
|
105
107
|
if (!declared.some((level) => level !== 'off')) {
|
|
106
|
-
invalid(provider, `model "${
|
|
108
|
+
invalid(provider, `model "${entryId}" reasoningEfforts 只有 off;声明思考档位或置 false`)
|
|
107
109
|
}
|
|
108
|
-
const map: Record<string, string | null> = {}
|
|
109
|
-
for (const level of THINKING_LEVELS) {
|
|
110
|
-
const wire = (efforts as Record<string, string | null | undefined>)[level]
|
|
111
|
-
if (wire === undefined) map[level] = null
|
|
112
|
-
else if (wire !== null) map[level] = wire
|
|
113
|
-
}
|
|
114
|
-
return { reasoning: true, thinkingLevelMap: map }
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
/**
|
|
113
|
+
* 继承源的 reasoning 能力 → 官方 reasoningEfforts 字典(全显式归一化的一环)。
|
|
114
|
+
* - base 无推理能力 → false(与官方"手写模型缺省不推理"同语义);
|
|
115
|
+
* - base 带 thinkingLevelMap → 逐档位翻译:线值照抄;null(不支持)不声明
|
|
116
|
+
* (dict 未声明档位置 null = 不支持,语义一致);缺省档位(off/minimal/
|
|
117
|
+
* low/medium/high)线值 = 档位名(pi-ai dispatch 的 `map?.[level] ?? level`),
|
|
118
|
+
* xhigh/max 缺省 = 不支持(pi-ai 对两档的缺省语义)——翻译后支持档位集合
|
|
119
|
+
* 与线值与内置目录逐档位一致;
|
|
120
|
+
* - base 只有 reasoning:true 无表(models.dev 保守源)→ 声明基档位为默认
|
|
121
|
+
* 线值、xhigh/max 不支持——与 pi-ai 无表默认语义一致。
|
|
122
|
+
*/
|
|
123
|
+
function reasoningEffortsFromBase(
|
|
124
|
+
provider: string,
|
|
125
|
+
entry: ModelEntryConfig,
|
|
126
|
+
base: ModelBase,
|
|
127
|
+
): false | Record<string, string | null> {
|
|
128
|
+
if (entry.reasoningEfforts !== undefined) {
|
|
129
|
+
if (entry.reasoningEfforts === false) return false
|
|
130
|
+
validateReasoningEfforts(provider, entry.id, entry.reasoningEfforts)
|
|
131
|
+
return entry.reasoningEfforts
|
|
126
132
|
}
|
|
133
|
+
if (base.reasoning !== true) return false
|
|
134
|
+
const map = base.thinkingLevelMap
|
|
135
|
+
if (map === undefined) {
|
|
136
|
+
return { off: null, minimal: 'minimal', low: 'low', medium: 'medium', high: 'high' }
|
|
137
|
+
}
|
|
138
|
+
const dict: Record<string, string | null> = {}
|
|
139
|
+
for (const level of THINKING_LEVELS) {
|
|
140
|
+
const wire = map[level]
|
|
141
|
+
if (wire === undefined) {
|
|
142
|
+
if (level === 'xhigh' || level === 'max') continue // 未声明 = 不支持
|
|
143
|
+
dict[level] = level
|
|
144
|
+
} else if (wire !== null) {
|
|
145
|
+
dict[level] = wire
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return dict
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
/** 归一化物化的单模型条目(含解析期事实,供 route 级唯一性检查)。 */
|
|
152
|
+
interface MaterializedEntry {
|
|
153
|
+
entry: PiAiModelProfile
|
|
132
154
|
api: string
|
|
133
|
-
|
|
134
|
-
baseUrl: string
|
|
135
|
-
reasoning: boolean
|
|
136
|
-
thinkingLevelMap?: Record<string, string | null>
|
|
137
|
-
input: ('text' | 'image')[]
|
|
138
|
-
cost: typeof NO_COST
|
|
139
|
-
contextWindow: number
|
|
140
|
-
maxTokens: number
|
|
141
|
-
headers?: Record<string, string>
|
|
142
|
-
compat?: Record<string, unknown>
|
|
155
|
+
baseUrl: string | undefined
|
|
143
156
|
}
|
|
144
157
|
|
|
145
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* 物化单个模型为官方 schema 条目(全显式)。lenient 下缺 api/baseURL 时
|
|
160
|
+
* 返回 null(调用方跳过该模型)。
|
|
161
|
+
*/
|
|
146
162
|
function materializeModel(
|
|
147
163
|
route: string,
|
|
148
164
|
profile: ProviderProfileConfig,
|
|
149
165
|
entry: ModelEntryConfig,
|
|
150
166
|
base: ModelBase,
|
|
151
167
|
routeApi: string | undefined,
|
|
152
|
-
providerBaseUrl: string | undefined,
|
|
153
168
|
defaultInput: ('text' | 'image')[],
|
|
154
169
|
deps: BuildDeps,
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
): MaterializedEntry | null {
|
|
171
|
+
const legacyImageDetail = (entry as ModelEntryConfig & { imageDetail?: unknown }).imageDetail
|
|
172
|
+
if (legacyImageDetail !== undefined) {
|
|
173
|
+
invalid(
|
|
174
|
+
route,
|
|
175
|
+
`model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;` +
|
|
176
|
+
'请改用 imagePixelBudget/imageMaxBytes(仅 adapter: deepseek)并删除 imageDetail',
|
|
177
|
+
)
|
|
178
|
+
}
|
|
157
179
|
const api = profile.api ?? base.api ?? routeApi
|
|
158
180
|
if (api === undefined) {
|
|
159
181
|
if (deps.lenient) {
|
|
@@ -164,7 +186,8 @@ function materializeModel(
|
|
|
164
186
|
}
|
|
165
187
|
invalid(route, `model "${entry.id}" 需要 api:继承源未提供,请在 route 上设置 api`)
|
|
166
188
|
}
|
|
167
|
-
const baseUrl =
|
|
189
|
+
const baseUrl =
|
|
190
|
+
profile.baseURL ?? base.baseUrl ?? builtinProviderBaseUrl(deps.kit, profile.extends ?? '')
|
|
168
191
|
if (baseUrl === undefined) {
|
|
169
192
|
if (deps.lenient) {
|
|
170
193
|
deps.warn?.(
|
|
@@ -174,6 +197,7 @@ function materializeModel(
|
|
|
174
197
|
}
|
|
175
198
|
invalid(route, `model "${entry.id}" 需要 baseURL:继承源未提供,请在 route 上设置 baseURL`)
|
|
176
199
|
}
|
|
200
|
+
const name = entry.name ?? base.name ?? entry.id
|
|
177
201
|
const contextWindow =
|
|
178
202
|
entry.contextWindow ??
|
|
179
203
|
base.contextWindow ??
|
|
@@ -181,7 +205,8 @@ function materializeModel(
|
|
|
181
205
|
DEFAULT_CONTEXT_WINDOW
|
|
182
206
|
const maxTokens =
|
|
183
207
|
entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
|
|
184
|
-
|
|
208
|
+
const input = declaredInput(entry.input) ?? base.input ?? [...defaultInput]
|
|
209
|
+
// compat 校验(官方门控表 + schema 值约束)→ 合并(继承值仅同协议继承)
|
|
185
210
|
validateCompat(api, profile.compat as Record<string, unknown> | undefined, `provider "${route}"`)
|
|
186
211
|
validateCompat(
|
|
187
212
|
api,
|
|
@@ -189,40 +214,44 @@ function materializeModel(
|
|
|
189
214
|
`provider "${route}" model "${entry.id}"`,
|
|
190
215
|
)
|
|
191
216
|
const compat = mergeCompat(
|
|
192
|
-
base.api === api ? base.compat : undefined,
|
|
217
|
+
base.api === api ? (base.compat as Record<string, unknown> | undefined) : undefined,
|
|
193
218
|
profile.compat as Record<string, unknown> | undefined,
|
|
194
219
|
entry.compat as Record<string, unknown> | undefined,
|
|
195
220
|
)
|
|
221
|
+
if (compat !== undefined) {
|
|
222
|
+
validateCompat(api, compat, `provider "${route}" model "${entry.id}"`)
|
|
223
|
+
}
|
|
196
224
|
return {
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
entry: {
|
|
226
|
+
id: entry.id,
|
|
227
|
+
...(name === entry.id ? {} : { name }),
|
|
228
|
+
...(contextWindow === DEFAULT_CONTEXT_WINDOW ? {} : { contextWindow }),
|
|
229
|
+
...(maxTokens === DEFAULT_MAX_TOKENS ? {} : { maxTokens }),
|
|
230
|
+
...(input.length === 1 && input[0] === 'text' ? {} : { input }),
|
|
231
|
+
reasoningEfforts: reasoningEffortsFromBase(route, entry, base),
|
|
232
|
+
...(compat === undefined ? {} : { compat }),
|
|
233
|
+
},
|
|
199
234
|
api,
|
|
200
|
-
provider: route,
|
|
201
235
|
baseUrl,
|
|
202
|
-
...resolveModelReasoning(route, entry, base),
|
|
203
|
-
input: declaredInput(entry.input) ?? base.input ?? [...defaultInput],
|
|
204
|
-
cost: base.cost ?? NO_COST,
|
|
205
|
-
contextWindow,
|
|
206
|
-
maxTokens,
|
|
207
|
-
...(base.headers === undefined ? {} : { headers: { ...base.headers } }),
|
|
208
|
-
...(compat === undefined ? {} : { compat }),
|
|
209
236
|
}
|
|
210
237
|
}
|
|
211
238
|
|
|
212
|
-
/**
|
|
213
|
-
*
|
|
239
|
+
/**
|
|
240
|
+
* 物化一个 route 的全部模型为官方条目;返回 route 级唯一 api/baseURL 与
|
|
241
|
+
* 用户显式配置的请求 cap 表。lenient 下 route 无任何可服务模型时返回 null。
|
|
242
|
+
*/
|
|
214
243
|
function materializeRouteModels(
|
|
215
244
|
route: string,
|
|
216
245
|
profile: ProviderProfileConfig,
|
|
217
246
|
deps: BuildDeps,
|
|
218
247
|
defaultInput: ('text' | 'image')[],
|
|
219
248
|
): {
|
|
220
|
-
models:
|
|
249
|
+
models: PiAiModelProfile[]
|
|
250
|
+
api: string
|
|
251
|
+
baseUrl: string
|
|
221
252
|
configuredMaxTokens: Map<string, number>
|
|
222
253
|
} | null {
|
|
223
254
|
const configuredMaxTokens = new Map<string, number>()
|
|
224
|
-
const providerBaseUrl =
|
|
225
|
-
profile.extends === undefined ? undefined : builtinProviderBaseUrl(deps.kit, profile.extends)
|
|
226
255
|
const entries: { id: string; entry?: ModelEntryConfig; base: ModelBase }[] = []
|
|
227
256
|
if (profile.models !== undefined && profile.models.length > 0) {
|
|
228
257
|
const seen = new Set<string>()
|
|
@@ -243,6 +272,8 @@ function materializeRouteModels(
|
|
|
243
272
|
)
|
|
244
273
|
base = {}
|
|
245
274
|
}
|
|
275
|
+
// 仅用户显式配置的 maxTokens 是"部署选择"(请求默认 cap);继承值只作能力。
|
|
276
|
+
if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
|
|
246
277
|
entries.push({ id: entry.id, entry, base })
|
|
247
278
|
}
|
|
248
279
|
} else if (profile.extends !== undefined) {
|
|
@@ -261,22 +292,12 @@ function materializeRouteModels(
|
|
|
261
292
|
.filter((api): api is string => api !== undefined),
|
|
262
293
|
)
|
|
263
294
|
const routeApi = apis.size === 1 ? [...apis][0] : undefined
|
|
264
|
-
const
|
|
295
|
+
const materialized = entries
|
|
265
296
|
.map(({ id, entry, base }) =>
|
|
266
|
-
materializeModel(
|
|
267
|
-
route,
|
|
268
|
-
profile,
|
|
269
|
-
entry ?? { id },
|
|
270
|
-
base,
|
|
271
|
-
routeApi,
|
|
272
|
-
providerBaseUrl,
|
|
273
|
-
defaultInput,
|
|
274
|
-
deps,
|
|
275
|
-
configuredMaxTokens,
|
|
276
|
-
),
|
|
297
|
+
materializeModel(route, profile, entry ?? { id }, base, routeApi, defaultInput, deps),
|
|
277
298
|
)
|
|
278
|
-
.filter((model): model is
|
|
279
|
-
if (
|
|
299
|
+
.filter((model): model is MaterializedEntry => model !== null)
|
|
300
|
+
if (materialized.length === 0) {
|
|
280
301
|
if (deps.lenient) {
|
|
281
302
|
deps.warn?.(
|
|
282
303
|
`llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`,
|
|
@@ -285,14 +306,27 @@ function materializeRouteModels(
|
|
|
285
306
|
}
|
|
286
307
|
invalid(route, 'route 内没有可服务的模型')
|
|
287
308
|
}
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
309
|
+
const apisFinal = new Set(materialized.map((m) => m.api))
|
|
310
|
+
if (apisFinal.size > 1) {
|
|
311
|
+
invalid(
|
|
312
|
+
route,
|
|
313
|
+
`route 内模型协议不一致(${[...apisFinal].join(', ')});一个 route 只能服务一种协议`,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
const baseUrls = new Set(materialized.map((m) => m.baseUrl))
|
|
317
|
+
if (baseUrls.size > 1) {
|
|
290
318
|
invalid(
|
|
291
319
|
route,
|
|
292
|
-
`route
|
|
320
|
+
`route 内模型端点不一致(${[...baseUrls].join(', ')});` +
|
|
321
|
+
'官方 schema 无模型级端点,请以 route 级 baseURL 统一',
|
|
293
322
|
)
|
|
294
323
|
}
|
|
295
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
models: materialized.map((m) => m.entry),
|
|
326
|
+
api: materialized[0]?.api as string,
|
|
327
|
+
baseUrl: materialized[0]?.baseUrl as string,
|
|
328
|
+
configuredMaxTokens,
|
|
329
|
+
}
|
|
296
330
|
}
|
|
297
331
|
|
|
298
332
|
/**
|
|
@@ -308,94 +342,328 @@ export function isDraftRoute(profile: ProviderProfileConfig): boolean {
|
|
|
308
342
|
)
|
|
309
343
|
}
|
|
310
344
|
|
|
345
|
+
interface NormalizedRoute {
|
|
346
|
+
profile: PiAiProviderProfile
|
|
347
|
+
/** 用户显式配置的每请求输出 cap(仅 raw entry.maxTokens 显式设置的条目)。 */
|
|
348
|
+
configuredMaxTokens: Map<string, number>
|
|
349
|
+
}
|
|
350
|
+
|
|
311
351
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
352
|
+
* 前置数据源归一化:一个 route → 官方 schema 形状的全显式 profile。
|
|
353
|
+
* 不可服务的 route(deepseek 路由 / 草稿 / lenient 下无模型)返回 undefined。
|
|
354
|
+
* 校验规则对齐官方 resolveProfiles(baseURL/displayName 非空、idle 超时边界、
|
|
355
|
+
* 图片限额正整数、defaultInput 非空)。
|
|
316
356
|
*/
|
|
317
|
-
|
|
318
|
-
|
|
357
|
+
function normalizeRoute(
|
|
358
|
+
route: string,
|
|
359
|
+
profile: ProviderProfileConfig,
|
|
319
360
|
deps: BuildDeps,
|
|
361
|
+
): NormalizedRoute | undefined {
|
|
362
|
+
// adapter: deepseek 的 route 由 profiles-deepseek.ts 物化(官方 DeepSeekAdapter 链路)
|
|
363
|
+
if ((profile.adapter ?? 'pi') === 'deepseek') return undefined
|
|
364
|
+
if (isDraftRoute(profile)) return undefined // 草稿路由:占位待补,不进 adapter
|
|
365
|
+
if (route.length === 0) throw new Error('llm-pi: provider 名不能为空')
|
|
366
|
+
// 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
|
|
367
|
+
// route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
|
|
368
|
+
// 暴露(DUPLICATE_ADAPTER),由 service 层逐个 route 注册降级处理。
|
|
369
|
+
if (profile.baseURL !== undefined && profile.baseURL.length === 0) invalid(route, 'baseURL 为空')
|
|
370
|
+
if (profile.displayName !== undefined && profile.displayName.length === 0)
|
|
371
|
+
invalid(route, 'displayName 为空')
|
|
372
|
+
const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
373
|
+
if (
|
|
374
|
+
!Number.isFinite(streamIdleTimeoutMs) ||
|
|
375
|
+
streamIdleTimeoutMs <= 0 ||
|
|
376
|
+
streamIdleTimeoutMs > MAX_TIMER_DELAY_MS
|
|
377
|
+
) {
|
|
378
|
+
invalid(route, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`)
|
|
379
|
+
}
|
|
380
|
+
const maxRequestImageBytes = profile.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES
|
|
381
|
+
if (!Number.isInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) {
|
|
382
|
+
invalid(route, 'maxRequestImageBytes 必须是正整数')
|
|
383
|
+
}
|
|
384
|
+
const requestImagePixelBudget =
|
|
385
|
+
profile.requestImagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET
|
|
386
|
+
if (!Number.isSafeInteger(requestImagePixelBudget) || requestImagePixelBudget <= 0) {
|
|
387
|
+
invalid(route, 'requestImagePixelBudget 必须是正整数')
|
|
388
|
+
}
|
|
389
|
+
const requestImageMaxBytes = profile.requestImageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES
|
|
390
|
+
if (!Number.isSafeInteger(requestImageMaxBytes) || requestImageMaxBytes <= 0) {
|
|
391
|
+
invalid(route, 'requestImageMaxBytes 必须是正整数')
|
|
392
|
+
}
|
|
393
|
+
const defaultInput = [...(profile.defaultInput ?? ['text'])] as ('text' | 'image')[]
|
|
394
|
+
if (defaultInput.length === 0) invalid(route, 'defaultInput 至少要声明一种模态')
|
|
395
|
+
const materialized = materializeRouteModels(route, profile, deps, defaultInput)
|
|
396
|
+
if (materialized === null) return undefined // lenient 下 route 无模型可服务,跳过注册
|
|
397
|
+
const { api, baseUrl, models, configuredMaxTokens } = materialized
|
|
398
|
+
const factory = deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
|
|
399
|
+
if (factory === undefined) {
|
|
400
|
+
invalid(
|
|
401
|
+
route,
|
|
402
|
+
`api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
const displayName = profile.displayName ?? route
|
|
406
|
+
return {
|
|
407
|
+
profile: {
|
|
408
|
+
...(profile.apiKeyEnv === undefined ? {} : { apiKeyEnv: profile.apiKeyEnv }),
|
|
409
|
+
displayName,
|
|
410
|
+
api,
|
|
411
|
+
...(baseUrl === undefined ? {} : { baseURL: baseUrl }),
|
|
412
|
+
models,
|
|
413
|
+
...(profile.defaultContextWindow === undefined
|
|
414
|
+
? {}
|
|
415
|
+
: { defaultContextWindow: profile.defaultContextWindow }),
|
|
416
|
+
...(profile.defaultMaxTokens === undefined
|
|
417
|
+
? {}
|
|
418
|
+
: { defaultMaxTokens: profile.defaultMaxTokens }),
|
|
419
|
+
defaultInput,
|
|
420
|
+
...(profile.headers === undefined ? {} : { headers: profile.headers }),
|
|
421
|
+
...(profile.reasoning === undefined ? {} : { reasoning: profile.reasoning }),
|
|
422
|
+
...(profile.thinkingBudgets === undefined
|
|
423
|
+
? {}
|
|
424
|
+
: { thinkingBudgets: profile.thinkingBudgets }),
|
|
425
|
+
...(profile.cacheRetention === undefined ? {} : { cacheRetention: profile.cacheRetention }),
|
|
426
|
+
...(profile.transport === undefined ? {} : { transport: profile.transport }),
|
|
427
|
+
...(profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs }),
|
|
428
|
+
...(profile.websocketConnectTimeoutMs === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: { websocketConnectTimeoutMs: profile.websocketConnectTimeoutMs }),
|
|
431
|
+
...(profile.streamIdleTimeoutMs === undefined
|
|
432
|
+
? {}
|
|
433
|
+
: { streamIdleTimeoutMs: profile.streamIdleTimeoutMs }),
|
|
434
|
+
...(profile.maxRequestImageBytes === undefined
|
|
435
|
+
? {}
|
|
436
|
+
: { maxRequestImageBytes: profile.maxRequestImageBytes }),
|
|
437
|
+
...(profile.requestImagePixelBudget === undefined
|
|
438
|
+
? {}
|
|
439
|
+
: { requestImagePixelBudget: profile.requestImagePixelBudget }),
|
|
440
|
+
...(profile.requestImageMaxBytes === undefined
|
|
441
|
+
? {}
|
|
442
|
+
: { requestImageMaxBytes: profile.requestImageMaxBytes }),
|
|
443
|
+
...(profile.retryPolicy === undefined ? {} : { retryPolicy: profile.retryPolicy }),
|
|
444
|
+
} as PiAiProviderProfile,
|
|
445
|
+
configuredMaxTokens,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Harness 自管凭据的 api-key auth(对齐官方 harnessApiKeyAuth)。 */
|
|
450
|
+
function harnessApiKeyAuth(name: string) {
|
|
451
|
+
return {
|
|
452
|
+
name,
|
|
453
|
+
resolve: ({ credential }: { credential?: { key?: string } }) =>
|
|
454
|
+
Promise.resolve({
|
|
455
|
+
auth: credential?.key === undefined ? {} : { apiKey: credential.key },
|
|
456
|
+
source: name,
|
|
457
|
+
}),
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** resolveProfilesFallback 的套件依赖窄面(与 kit 同源,见 resolve-dsh.ts)。 */
|
|
462
|
+
export interface ResolverDeps {
|
|
463
|
+
createProvider: DshKit['createProvider']
|
|
464
|
+
protocolFactories: DshKit['protocolFactories']
|
|
465
|
+
resolveRetryPolicy: DshKit['resolveRetryPolicy']
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** 官方 resolveModelReasoning 的插件侧镜像:dict → {reasoning, thinkingLevelMap}。 */
|
|
469
|
+
function resolveModelReasoning(entry: PiAiModelProfile): {
|
|
470
|
+
reasoning: boolean
|
|
471
|
+
thinkingLevelMap?: Record<string, string | null>
|
|
472
|
+
} {
|
|
473
|
+
const efforts = entry.reasoningEfforts
|
|
474
|
+
if (efforts === undefined || efforts === false) return { reasoning: false }
|
|
475
|
+
const map: Record<string, string | null> = {}
|
|
476
|
+
for (const level of THINKING_LEVELS) {
|
|
477
|
+
const wire = (efforts as Record<string, string | null | undefined>)[level]
|
|
478
|
+
if (wire === undefined) map[level] = null
|
|
479
|
+
else if (wire !== null) map[level] = wire
|
|
480
|
+
}
|
|
481
|
+
return { reasoning: true, thinkingLevelMap: map }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** 官方 resolveModelCompat 的插件侧镜像:门控表过滤 + 模型级拒绝不适配字段。 */
|
|
485
|
+
function resolveModelCompat(
|
|
486
|
+
provider: string,
|
|
487
|
+
entry: PiAiModelProfile,
|
|
488
|
+
api: string,
|
|
489
|
+
): { compat: Record<string, unknown> } | Record<string, never> {
|
|
490
|
+
const configured: Record<string, unknown> = {}
|
|
491
|
+
for (const [field, value] of Object.entries(entry.compat ?? {})) {
|
|
492
|
+
if (compatDispositionOf(api as never, field) !== 'offer') {
|
|
493
|
+
const offered = compatFieldsOf(api as never)
|
|
494
|
+
invalid(
|
|
495
|
+
provider,
|
|
496
|
+
`model "${entry.id}" 的 compat.${field} 不适用于协议 ${api};` +
|
|
497
|
+
`该协议可配置字段:${offered.length === 0 ? '无' : offered.join(', ')}`,
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
if (value === undefined || value === null) {
|
|
501
|
+
invalid(provider, `model "${entry.id}" 的 compat.${field} 未设置值;给出值或移除该键`)
|
|
502
|
+
}
|
|
503
|
+
configured[field] = value
|
|
504
|
+
}
|
|
505
|
+
if (Object.keys(configured).length === 0) return {}
|
|
506
|
+
return { compat: configured }
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 官方 resolveProfiles 的插件等价实现(npm 形态无 src/config.ts 可导入时兜底)。
|
|
511
|
+
* 输入为 normalizeRoute 的全显式产物(api/baseURL/条目字段全部落定),语义与
|
|
512
|
+
* 官方 config.ts:resolveProfiles + catalog.ts:resolveRouteModels 逐行对齐:
|
|
513
|
+
* 默认值(streamIdleTimeoutMs/图片限额/容量兜底)、retryPolicy 解析、
|
|
514
|
+
* configuredMaxTokens(此处按全显式条目计入,buildProfiles 再回填为用户显式
|
|
515
|
+
* 集)、createProvider 构建 piProvider(harness api-key auth)。
|
|
516
|
+
*/
|
|
517
|
+
export function resolveProfilesFallback(
|
|
518
|
+
providers: Readonly<Record<string, PiAiProviderProfile>> | undefined,
|
|
519
|
+
deps: ResolverDeps,
|
|
320
520
|
): Map<string, ResolvedPiAiProviderProfile> {
|
|
321
521
|
const resolved = new Map<string, ResolvedPiAiProviderProfile>()
|
|
322
|
-
for (const [
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (profile.baseURL !== undefined && profile.baseURL.length === 0)
|
|
331
|
-
invalid(route, 'baseURL 为空')
|
|
332
|
-
if (profile.displayName !== undefined && profile.displayName.length === 0)
|
|
333
|
-
invalid(route, 'displayName 为空')
|
|
334
|
-
const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
522
|
+
for (const [provider, source] of Object.entries(providers ?? {})) {
|
|
523
|
+
if (source.baseURL !== undefined && source.baseURL.length === 0) {
|
|
524
|
+
invalid(provider, 'baseURL 为空')
|
|
525
|
+
}
|
|
526
|
+
if (source.displayName !== undefined && source.displayName.length === 0) {
|
|
527
|
+
invalid(provider, 'displayName 为空')
|
|
528
|
+
}
|
|
529
|
+
const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
335
530
|
if (
|
|
336
531
|
!Number.isFinite(streamIdleTimeoutMs) ||
|
|
337
532
|
streamIdleTimeoutMs <= 0 ||
|
|
338
533
|
streamIdleTimeoutMs > MAX_TIMER_DELAY_MS
|
|
339
534
|
) {
|
|
340
|
-
invalid(
|
|
535
|
+
invalid(provider, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`)
|
|
536
|
+
}
|
|
537
|
+
const maxRequestImageBytes = source.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES
|
|
538
|
+
if (!Number.isInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) {
|
|
539
|
+
invalid(provider, 'maxRequestImageBytes 必须是正整数')
|
|
540
|
+
}
|
|
541
|
+
const requestImagePixelBudget =
|
|
542
|
+
source.requestImagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET
|
|
543
|
+
if (!Number.isSafeInteger(requestImagePixelBudget) || requestImagePixelBudget <= 0) {
|
|
544
|
+
invalid(provider, 'requestImagePixelBudget 必须是正整数')
|
|
545
|
+
}
|
|
546
|
+
const requestImageMaxBytes = source.requestImageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES
|
|
547
|
+
if (!Number.isSafeInteger(requestImageMaxBytes) || requestImageMaxBytes <= 0) {
|
|
548
|
+
invalid(provider, 'requestImageMaxBytes 必须是正整数')
|
|
341
549
|
}
|
|
342
|
-
const defaultInput = [...(
|
|
343
|
-
if (defaultInput.length === 0) invalid(
|
|
344
|
-
const displayName =
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
550
|
+
const defaultInput = [...(source.defaultInput ?? ['text'])]
|
|
551
|
+
if (defaultInput.length === 0) invalid(provider, 'defaultInput 至少要声明一种模态')
|
|
552
|
+
const displayName = source.displayName ?? provider
|
|
553
|
+
const api = source.api
|
|
554
|
+
if (api === undefined) {
|
|
555
|
+
invalid(
|
|
556
|
+
provider,
|
|
557
|
+
'需要 api:本插件仅支持 openai-completions/openai-responses/anthropic-messages',
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
const factory = deps.protocolFactories[api as keyof DshKit['protocolFactories']]
|
|
352
561
|
if (factory === undefined) {
|
|
353
562
|
invalid(
|
|
354
|
-
|
|
563
|
+
provider,
|
|
355
564
|
`api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`,
|
|
356
565
|
)
|
|
357
566
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
567
|
+
if (source.baseURL === undefined) {
|
|
568
|
+
invalid(provider, '需要 baseURL:官方 schema 无模型级端点,端点只能 route 级表达')
|
|
569
|
+
}
|
|
570
|
+
// 模型物化:全显式条目 + route 级 api/baseURL(官方 resolveRouteModels 镜像,
|
|
571
|
+
// 无目录继承——归一化已把继承值落定在条目里)
|
|
572
|
+
const configuredMaxTokens = new Map<string, number>()
|
|
573
|
+
const seen = new Set<string>()
|
|
574
|
+
const models = (source.models ?? []).map((entry) => {
|
|
575
|
+
if (entry.id.length === 0) invalid(provider, '存在空 id 的模型条目')
|
|
576
|
+
if (seen.has(entry.id)) invalid(provider, `模型 "${entry.id}" 重复列出`)
|
|
577
|
+
seen.add(entry.id)
|
|
578
|
+
const contextWindow =
|
|
579
|
+
entry.contextWindow ?? source.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW
|
|
580
|
+
if (!Number.isInteger(contextWindow) || contextWindow <= 0) {
|
|
581
|
+
invalid(provider, `model "${entry.id}" contextWindow 必须是正整数`)
|
|
582
|
+
}
|
|
583
|
+
const maxTokens = entry.maxTokens ?? source.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
|
|
584
|
+
if (!Number.isInteger(maxTokens) || maxTokens <= 0) {
|
|
585
|
+
invalid(provider, `model "${entry.id}" maxTokens 必须是正整数`)
|
|
586
|
+
}
|
|
587
|
+
if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
|
|
588
|
+
return {
|
|
589
|
+
id: entry.id,
|
|
590
|
+
name: entry.name ?? entry.id,
|
|
591
|
+
api,
|
|
592
|
+
provider,
|
|
593
|
+
baseUrl: source.baseURL as string,
|
|
594
|
+
input: [...(entry.input ?? defaultInput)],
|
|
595
|
+
cost: NO_COST,
|
|
596
|
+
contextWindow,
|
|
597
|
+
maxTokens,
|
|
598
|
+
...resolveModelReasoning(entry),
|
|
599
|
+
...resolveModelCompat(provider, entry, api),
|
|
600
|
+
}
|
|
370
601
|
})
|
|
371
|
-
|
|
372
|
-
|
|
602
|
+
if (models.length === 0) invalid(provider, 'route 内没有可服务的模型')
|
|
603
|
+
const { apiKeyEnv, retryPolicy, models: _models, displayName: _displayName, ...rest } = source
|
|
604
|
+
resolved.set(provider, {
|
|
605
|
+
...rest,
|
|
606
|
+
provider,
|
|
373
607
|
displayName,
|
|
374
|
-
...(
|
|
608
|
+
...(apiKeyEnv === undefined ? {} : { apiKeyEnv: credentialRef(apiKeyEnv) }),
|
|
375
609
|
streamIdleTimeoutMs,
|
|
376
|
-
maxRequestImageBytes
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
610
|
+
maxRequestImageBytes,
|
|
611
|
+
requestImagePixelBudget,
|
|
612
|
+
requestImageMaxBytes,
|
|
613
|
+
retryPolicy: deps.resolveRetryPolicy(
|
|
614
|
+
retryPolicy,
|
|
615
|
+
`llm-pi: provider "${provider}" retryPolicy`,
|
|
380
616
|
),
|
|
381
|
-
...(
|
|
382
|
-
...(
|
|
383
|
-
...(profile.thinkingBudgets === undefined
|
|
384
|
-
? {}
|
|
385
|
-
: { thinkingBudgets: { ...profile.thinkingBudgets } }),
|
|
386
|
-
...(profile.cacheRetention === undefined ? {} : { cacheRetention: profile.cacheRetention }),
|
|
387
|
-
...(profile.transport === undefined ? {} : { transport: profile.transport }),
|
|
388
|
-
...(profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs }),
|
|
389
|
-
...(profile.websocketConnectTimeoutMs === undefined
|
|
617
|
+
...(rest.headers === undefined ? {} : { headers: { ...rest.headers } }),
|
|
618
|
+
...(rest.thinkingBudgets === undefined
|
|
390
619
|
? {}
|
|
391
|
-
: {
|
|
392
|
-
configuredMaxTokens
|
|
393
|
-
piProvider
|
|
620
|
+
: { thinkingBudgets: { ...rest.thinkingBudgets } }),
|
|
621
|
+
configuredMaxTokens,
|
|
622
|
+
piProvider: deps.createProvider({
|
|
623
|
+
id: provider,
|
|
624
|
+
name: displayName,
|
|
625
|
+
...(source.baseURL === undefined ? {} : { baseUrl: source.baseURL }),
|
|
626
|
+
auth: { apiKey: harnessApiKeyAuth(displayName) },
|
|
627
|
+
models,
|
|
628
|
+
api: factory() as never,
|
|
629
|
+
}),
|
|
394
630
|
} as ResolvedPiAiProviderProfile)
|
|
395
631
|
}
|
|
396
632
|
return resolved
|
|
397
633
|
}
|
|
398
634
|
|
|
635
|
+
/**
|
|
636
|
+
* 校验并物化全部 pi route。任一 route 不可服务即整体抛错——
|
|
637
|
+
* settings 写入校验与运行期 profiles 回调共用本函数,
|
|
638
|
+
* 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
|
|
639
|
+
* 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
|
|
640
|
+
*/
|
|
641
|
+
export function buildProfiles(
|
|
642
|
+
providers: Record<string, ProviderProfileConfig> | undefined,
|
|
643
|
+
deps: BuildDeps,
|
|
644
|
+
): Map<string, ResolvedPiAiProviderProfile> {
|
|
645
|
+
const resolved = new Map<string, ResolvedPiAiProviderProfile>()
|
|
646
|
+
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
647
|
+
let normalized: NormalizedRoute | undefined
|
|
648
|
+
try {
|
|
649
|
+
normalized = normalizeRoute(route, profile, deps)
|
|
650
|
+
if (normalized === undefined) continue
|
|
651
|
+
const single = deps.kit.resolveProfiles({ [route]: normalized.profile })
|
|
652
|
+
for (const [name, built] of single) {
|
|
653
|
+
// configuredMaxTokens 回填为用户显式集(官方链把物化继承值也计入请求默认 cap,
|
|
654
|
+
// 与插件原语义不符;见文件头"既有缝隙")。
|
|
655
|
+
resolved.set(name, { ...built, configuredMaxTokens: normalized.configuredMaxTokens })
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (!deps.lenient) throw error
|
|
659
|
+
deps.warn?.(
|
|
660
|
+
`llm-pi: provider "${route}" 不可服务(${error instanceof Error ? error.message : String(error)}),已跳过注册`,
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return resolved
|
|
665
|
+
}
|
|
666
|
+
|
|
399
667
|
/** settings 写入校验钩子:完整试跑解析,非法配置在写入处拒绝。 */
|
|
400
668
|
export function assertServiceable(
|
|
401
669
|
config: { providers?: Record<string, ProviderProfileConfig> },
|