@dsh-plus/llm-pi 0.1.5 → 0.1.7
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 +1881 -1845
- package/lib/index.d.ts +21 -4
- package/lib/index.js +573 -135
- package/package.json +16 -11
- package/src/catalog/builtin.ts +17 -4
- package/src/catalog/models-dev.ts +13 -6
- package/src/catalog/official.ts +71 -0
- package/src/client/api.ts +17 -4
- package/src/client/card.tsx +57 -20
- package/src/client/client.ts +20 -9
- package/src/client/constants.ts +5 -1
- package/src/client/draft.ts +110 -38
- package/src/client/fields.tsx +3 -1
- package/src/client/i18n.ts +4 -2
- package/src/client/scope.ts +22 -3
- package/src/client/views/compat.tsx +50 -44
- package/src/client/views/models.tsx +26 -7
- package/src/client/views/provider-fields.tsx +15 -3
- package/src/client/views/providers.tsx +31 -26
- package/src/compat.ts +18 -4
- package/src/config.ts +147 -11
- package/src/deepseek-routes.ts +139 -0
- package/src/discovery.ts +63 -11
- package/src/index.ts +6 -3
- package/src/inherit.ts +11 -3
- package/src/profiles-deepseek.ts +282 -0
- package/src/profiles.ts +71 -21
- package/src/resolve-dsh.ts +104 -8
- package/src/service.ts +64 -13
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deepseek 路由物化:adapter: deepseek 的 provider 配置 → 官方 DeepSeekAdapter
|
|
3
|
+
* 可直接消费的 DeepSeekConnectionOptions。
|
|
4
|
+
*
|
|
5
|
+
* 语义对齐官方 llm-deepseek 的 resolveAdapterOptions,但继承源换为官方内置
|
|
6
|
+
* 目录(catalog/official.ts):模型条目只写 id 即继承同名官方模型的模态、
|
|
7
|
+
* 像素预算等能力;extends: 'deepseek' 时全量继承官方目录。
|
|
8
|
+
*
|
|
9
|
+
* 物化产物最终再过一遍官方 resolveAdapterOptions 校验/补默认值,等于把
|
|
10
|
+
* 官方插件的配置闸门原样复用;文件通道(Files API 上传、file_id 引用、
|
|
11
|
+
* 失败降级 base64、配额清理)全部在 DeepSeekAdapter 内部,此处只喂配置。
|
|
12
|
+
* @module llm-pi/profiles-deepseek
|
|
13
|
+
*/
|
|
14
|
+
import type { DeepSeekConnectionOptions } from '@deepseek-ai/dsh-llm-deepseek'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type OfficialModelBase,
|
|
18
|
+
officialBaseUrl,
|
|
19
|
+
officialModelBase,
|
|
20
|
+
officialModelIds,
|
|
21
|
+
} from './catalog/official.ts'
|
|
22
|
+
import type { ModelEntryConfig, ProviderProfileConfig } from './config.ts'
|
|
23
|
+
import type { DshKit } from './resolve-dsh.ts'
|
|
24
|
+
|
|
25
|
+
export interface BuildDeepseekDeps {
|
|
26
|
+
kit: DshKit
|
|
27
|
+
/** 运行期宽松模式:数据源漂移时跳过 route 并告警,而非抛错弄挂注册循环。 */
|
|
28
|
+
lenient?: boolean
|
|
29
|
+
warn?: (message: string) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 物化产物:route 的展示名与适配器连接事实(注册期事实另见 connection.retryPolicy)。 */
|
|
33
|
+
export interface ResolvedDeepseekRoute {
|
|
34
|
+
route: string
|
|
35
|
+
displayName: string
|
|
36
|
+
connection: DeepSeekConnectionOptions
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 报告不可服务的 route,命名出错配置键(与 profiles.ts 的 invalid 同格式)。 */
|
|
40
|
+
function invalid(provider: string, detail: string): never {
|
|
41
|
+
throw new Error(`llm-pi: provider "${provider}" ${detail}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** pi 专有字段在 deepseek 路由上显式拒绝(静默忽略会让用户误以为生效)。 */
|
|
45
|
+
const PI_ONLY_FIELDS = [
|
|
46
|
+
'api',
|
|
47
|
+
'compat',
|
|
48
|
+
'headers',
|
|
49
|
+
'transport',
|
|
50
|
+
'websocketConnectTimeoutMs',
|
|
51
|
+
'thinkingBudgets',
|
|
52
|
+
'cacheRetention',
|
|
53
|
+
'reasoning',
|
|
54
|
+
'defaultInput',
|
|
55
|
+
'maxRequestImageBytes',
|
|
56
|
+
'timeoutMs',
|
|
57
|
+
] as const
|
|
58
|
+
|
|
59
|
+
/** schema 解析会把 dict 字段物化为 {}(schemastery 行为);空字典视为未配置。 */
|
|
60
|
+
function nonEmptyDict(value: unknown): boolean {
|
|
61
|
+
return typeof value === 'object' && value !== null && Object.keys(value).length > 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** defaultInput 的 schema 物化默认(['text']);显式写同值无害,同样视为未配置。 */
|
|
65
|
+
function isMaterializedDefaultInput(value: unknown): boolean {
|
|
66
|
+
return Array.isArray(value) && value.length === 1 && value[0] === 'text'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rejectPiOnlyFields(route: string, profile: ProviderProfileConfig): void {
|
|
70
|
+
for (const field of PI_ONLY_FIELDS) {
|
|
71
|
+
const value = profile[field]
|
|
72
|
+
if (value === undefined) continue
|
|
73
|
+
if (field === 'compat' || field === 'headers' || field === 'thinkingBudgets') {
|
|
74
|
+
if (!nonEmptyDict(value)) continue
|
|
75
|
+
} else if (field === 'defaultInput' && isMaterializedDefaultInput(value)) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
invalid(route, `的 ${field} 仅 adapter: pi 可用;adapter: deepseek 的 route 请移除该字段`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 解析模型条目的官方目录继承 base;extends 仅允许裸 id 或 "deepseek/id"。 */
|
|
83
|
+
function resolveOfficialBase(
|
|
84
|
+
route: string,
|
|
85
|
+
entry: ModelEntryConfig,
|
|
86
|
+
deps: BuildDeepseekDeps,
|
|
87
|
+
): OfficialModelBase {
|
|
88
|
+
const ref = entry.extends
|
|
89
|
+
if (ref !== undefined) {
|
|
90
|
+
const slash = ref.indexOf('/')
|
|
91
|
+
const source = slash < 0 ? 'deepseek' : ref.slice(0, slash)
|
|
92
|
+
const id = slash < 0 ? ref : ref.slice(slash + 1)
|
|
93
|
+
if (source !== 'deepseek' || id.length === 0 || id.includes('/')) {
|
|
94
|
+
invalid(
|
|
95
|
+
route,
|
|
96
|
+
`model "${entry.id}" 的 extends 引用 ${JSON.stringify(ref)} 非法:` +
|
|
97
|
+
'adapter: deepseek 的路由仅支持 "deepseek/model" 或裸 model id(官方目录)',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
const base = officialModelBase(deps.kit, id)
|
|
101
|
+
if (base === undefined) {
|
|
102
|
+
if (deps.lenient) {
|
|
103
|
+
deps.warn?.(
|
|
104
|
+
`llm-pi: provider "${route}" model "${entry.id}" 的 extends 引用 "${ref}" 在当前官方目录` +
|
|
105
|
+
'未命中;已降级为手写条目',
|
|
106
|
+
)
|
|
107
|
+
return {}
|
|
108
|
+
}
|
|
109
|
+
invalid(route, `model "${entry.id}" 的 extends 引用 "${ref}" 在官方目录中不存在`)
|
|
110
|
+
}
|
|
111
|
+
return base
|
|
112
|
+
}
|
|
113
|
+
// 缺省:同名继承官方目录(未命中即手写条目,缺省值由官方 schema 兜底)
|
|
114
|
+
return officialModelBase(deps.kit, entry.id) ?? {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 条目的声明模态;schema 物化的空数组视为"无答案",交官方继承 base(同 profiles.ts declaredInput)。 */
|
|
118
|
+
function declaredModalities(
|
|
119
|
+
configured: readonly ('text' | 'image')[] | undefined,
|
|
120
|
+
): ('text' | 'image')[] | undefined {
|
|
121
|
+
return configured === undefined || configured.length === 0 ? undefined : [...configured]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 物化单个模型目录条目:官方 base 在下,条目显式字段逐字段覆盖。 */
|
|
125
|
+
function materializeModel(
|
|
126
|
+
route: string,
|
|
127
|
+
entry: ModelEntryConfig,
|
|
128
|
+
deps: BuildDeepseekDeps,
|
|
129
|
+
): Record<string, unknown> {
|
|
130
|
+
if (entry.reasoningEfforts !== undefined || nonEmptyDict(entry.compat)) {
|
|
131
|
+
invalid(route, `model "${entry.id}" 的 reasoningEfforts/compat 仅 adapter: pi 可用`)
|
|
132
|
+
}
|
|
133
|
+
const base = resolveOfficialBase(route, entry, deps)
|
|
134
|
+
const input = declaredModalities(entry.input) ?? base.input
|
|
135
|
+
const imageFields = {
|
|
136
|
+
imagePixelBudget: entry.imagePixelBudget ?? base.imagePixelBudget,
|
|
137
|
+
imageMaxBytes: entry.imageMaxBytes ?? base.imageMaxBytes,
|
|
138
|
+
imageDetail: entry.imageDetail ?? base.imageDetail,
|
|
139
|
+
}
|
|
140
|
+
const acceptsImage = input?.includes('image') ?? false
|
|
141
|
+
if (!acceptsImage && Object.values(imageFields).some((value) => value !== undefined)) {
|
|
142
|
+
invalid(
|
|
143
|
+
route,
|
|
144
|
+
`model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes/imageDetail`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: entry.id,
|
|
149
|
+
...((entry.name ?? base.name) === undefined ? {} : { name: entry.name ?? base.name }),
|
|
150
|
+
...((entry.contextWindow ?? base.contextWindow) === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { contextWindow: entry.contextWindow ?? base.contextWindow }),
|
|
153
|
+
...((entry.maxTokens ?? base.maxTokens) === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { maxTokens: entry.maxTokens ?? base.maxTokens }),
|
|
156
|
+
...(input === undefined ? {} : { inputModalities: [...input] }),
|
|
157
|
+
...(Object.fromEntries(
|
|
158
|
+
Object.entries(imageFields).filter(([, value]) => value !== undefined),
|
|
159
|
+
) as Record<string, unknown>),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 物化一个 route 的模型目录;models 缺省时以官方目录全量继承(要求 extends: 'deepseek')。 */
|
|
164
|
+
function materializeModels(
|
|
165
|
+
route: string,
|
|
166
|
+
profile: ProviderProfileConfig,
|
|
167
|
+
deps: BuildDeepseekDeps,
|
|
168
|
+
): Record<string, unknown>[] {
|
|
169
|
+
if (profile.models !== undefined && profile.models.length > 0) {
|
|
170
|
+
const seen = new Set<string>()
|
|
171
|
+
return profile.models.map((entry) => {
|
|
172
|
+
if (entry.id.length === 0) invalid(route, '存在空 id 的模型条目')
|
|
173
|
+
if (seen.has(entry.id)) invalid(route, `模型 "${entry.id}" 重复列出`)
|
|
174
|
+
seen.add(entry.id)
|
|
175
|
+
return materializeModel(route, entry, deps)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
if (profile.extends === 'deepseek') {
|
|
179
|
+
return officialModelIds(deps.kit).map((id) => materializeModel(route, { id }, deps))
|
|
180
|
+
}
|
|
181
|
+
invalid(
|
|
182
|
+
route,
|
|
183
|
+
'未配置 models;adapter: deepseek 的 route 可配 extends: deepseek 全量继承官方目录',
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 组装官方 resolveAdapterOptions 的输入:仅放入显式配置的键,其余走官方默认值。 */
|
|
188
|
+
function rawAdapterConfig(
|
|
189
|
+
profile: ProviderProfileConfig,
|
|
190
|
+
baseURL: string,
|
|
191
|
+
models: Record<string, unknown>[],
|
|
192
|
+
): Record<string, unknown> {
|
|
193
|
+
const passthrough = [
|
|
194
|
+
'apiKeyEnv',
|
|
195
|
+
'thinking',
|
|
196
|
+
'reasoningEffort',
|
|
197
|
+
'streamIdleTimeoutMs',
|
|
198
|
+
'maxRequestFilesBytes',
|
|
199
|
+
'maxInlineRequestImageBytes',
|
|
200
|
+
'maxImagesPerRequest',
|
|
201
|
+
'imageOffloadByteQuantum',
|
|
202
|
+
'inlineImageOffloadByteQuantum',
|
|
203
|
+
'imageOffloadCountQuantum',
|
|
204
|
+
'filesApiTimeoutMs',
|
|
205
|
+
'fileExpiresAfterSeconds',
|
|
206
|
+
'fileRefreshMarginSeconds',
|
|
207
|
+
'retryPolicy',
|
|
208
|
+
] as const
|
|
209
|
+
const raw: Record<string, unknown> = { baseURL, models }
|
|
210
|
+
for (const key of passthrough) {
|
|
211
|
+
if (profile[key] !== undefined) raw[key] = profile[key]
|
|
212
|
+
}
|
|
213
|
+
if (profile.defaultMaxTokens !== undefined) raw['maxTokens'] = profile.defaultMaxTokens
|
|
214
|
+
if (profile.defaultContextWindow !== undefined) {
|
|
215
|
+
raw['defaultContextWindow'] = profile.defaultContextWindow
|
|
216
|
+
}
|
|
217
|
+
return raw
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 物化单个 deepseek route;lenient 下失败告警并返回 null(调用方跳过注册)。 */
|
|
221
|
+
function buildRoute(
|
|
222
|
+
route: string,
|
|
223
|
+
profile: ProviderProfileConfig,
|
|
224
|
+
deps: BuildDeepseekDeps,
|
|
225
|
+
): ResolvedDeepseekRoute | null {
|
|
226
|
+
try {
|
|
227
|
+
if (deps.kit.deepseek === undefined) {
|
|
228
|
+
invalid(
|
|
229
|
+
route,
|
|
230
|
+
'使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(需 dsh ≥ 0.1.1-rc.2)',
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
rejectPiOnlyFields(route, profile)
|
|
234
|
+
if (profile.extends !== undefined && profile.extends !== 'deepseek') {
|
|
235
|
+
invalid(
|
|
236
|
+
route,
|
|
237
|
+
`的 extends ${JSON.stringify(profile.extends)} 非法:adapter: deepseek 仅支持 "deepseek"(官方目录)`,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
if (profile.apiKeyEnv === undefined || profile.apiKeyEnv.length === 0) {
|
|
241
|
+
invalid(route, '需要 apiKeyEnv:DeepSeekAdapter 无环境自发现,凭据引用必须显式配置')
|
|
242
|
+
}
|
|
243
|
+
const baseURL =
|
|
244
|
+
profile.baseURL ?? (profile.extends === 'deepseek' ? officialBaseUrl(deps.kit) : undefined)
|
|
245
|
+
if (baseURL === undefined || baseURL.length === 0) {
|
|
246
|
+
invalid(route, '需要 baseURL(或 extends: deepseek 继承官方端点)')
|
|
247
|
+
}
|
|
248
|
+
const models = materializeModels(route, profile, deps)
|
|
249
|
+
const connection = deps.kit.deepseek.resolveAdapterOptions(
|
|
250
|
+
rawAdapterConfig(profile, baseURL, models) as never,
|
|
251
|
+
undefined,
|
|
252
|
+
)
|
|
253
|
+
return { route, displayName: profile.displayName ?? route, connection }
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (!deps.lenient) throw error
|
|
256
|
+
deps.warn?.(
|
|
257
|
+
`llm-pi: deepseek route "${route}" 不可服务(${error instanceof Error ? error.message : String(error)}),已跳过注册`,
|
|
258
|
+
)
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 校验并物化全部 adapter: deepseek 的 route。严格模式任一 route 失败即整体
|
|
265
|
+
* 抛错(settings 写入校验),lenient 模式跳过坏 route(运行期热更新)。
|
|
266
|
+
*/
|
|
267
|
+
export function buildDeepseekRoutes(
|
|
268
|
+
providers: Record<string, ProviderProfileConfig> | undefined,
|
|
269
|
+
deps: BuildDeepseekDeps,
|
|
270
|
+
): Map<string, ResolvedDeepseekRoute> {
|
|
271
|
+
const resolved = new Map<string, ResolvedDeepseekRoute>()
|
|
272
|
+
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
273
|
+
if ((profile.adapter ?? 'pi') !== 'deepseek') continue
|
|
274
|
+
if (route.length === 0) throw new Error('llm-pi: provider 名不能为空')
|
|
275
|
+
if (profile.displayName !== undefined && profile.displayName.length === 0) {
|
|
276
|
+
invalid(route, 'displayName 为空')
|
|
277
|
+
}
|
|
278
|
+
const built = buildRoute(route, profile, deps)
|
|
279
|
+
if (built !== null) resolved.set(route, built)
|
|
280
|
+
}
|
|
281
|
+
return resolved
|
|
282
|
+
}
|
package/src/profiles.ts
CHANGED
|
@@ -14,20 +14,25 @@
|
|
|
14
14
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
15
15
|
import type { ResolvedPiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
builtinProviderBaseUrl,
|
|
19
|
+
inheritedCatalogEntries,
|
|
20
|
+
type ModelBase,
|
|
21
|
+
} from './catalog/builtin.ts'
|
|
18
22
|
import type { ModelsDevSource } from './catalog/models-dev.ts'
|
|
19
23
|
import { mergeCompat, validateCompat } from './compat.ts'
|
|
20
24
|
import {
|
|
21
25
|
DEFAULT_CONTEXT_WINDOW,
|
|
22
|
-
DEFAULT_MAX_TOKENS,
|
|
23
26
|
DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
27
|
+
DEFAULT_MAX_TOKENS,
|
|
24
28
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
25
29
|
MAX_TIMER_DELAY_MS,
|
|
26
|
-
THINKING_LEVELS,
|
|
27
30
|
type ModelEntryConfig,
|
|
28
31
|
type ProviderProfileConfig,
|
|
32
|
+
THINKING_LEVELS,
|
|
29
33
|
} from './config.ts'
|
|
30
34
|
import { ExtendsError, resolveModelBase } from './inherit.ts'
|
|
35
|
+
import { buildDeepseekRoutes } from './profiles-deepseek.ts'
|
|
31
36
|
import type { DshKit } from './resolve-dsh.ts'
|
|
32
37
|
|
|
33
38
|
/** 内置目录未描述时的零价目(harness 不消费 cost 元数据,同官方 NO_COST)。 */
|
|
@@ -52,7 +57,9 @@ function invalid(provider: string, detail: string): never {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/** 条目的声明模态;缺省/空数组都视为"无答案",交下一级(同官方 declaredInput)。 */
|
|
55
|
-
function declaredInput(
|
|
60
|
+
function declaredInput(
|
|
61
|
+
configured: readonly ('text' | 'image')[] | undefined,
|
|
62
|
+
): ('text' | 'image')[] | undefined {
|
|
56
63
|
return configured === undefined || configured.length === 0 ? undefined : [...configured]
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -73,7 +80,9 @@ function resolveModelReasoning(
|
|
|
73
80
|
reasoning: base.reasoning,
|
|
74
81
|
...(base.thinkingLevelMap === undefined
|
|
75
82
|
? {}
|
|
76
|
-
: {
|
|
83
|
+
: {
|
|
84
|
+
thinkingLevelMap: base.thinkingLevelMap as Record<string, string | null>,
|
|
85
|
+
}),
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
if (efforts === false) return { reasoning: false }
|
|
@@ -84,12 +93,15 @@ function resolveModelReasoning(
|
|
|
84
93
|
const wire = (efforts as Record<string, string | null | undefined>)[level]
|
|
85
94
|
if (wire === undefined) continue
|
|
86
95
|
if (wire === null) {
|
|
87
|
-
if (level !== 'off')
|
|
96
|
+
if (level !== 'off')
|
|
97
|
+
invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 需要线值;仅 off 可留空`)
|
|
88
98
|
} else if (wire.length === 0) {
|
|
89
99
|
invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 不能为空字符串`)
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
const declared = THINKING_LEVELS.filter(
|
|
102
|
+
const declared = THINKING_LEVELS.filter(
|
|
103
|
+
(level) => (efforts as Record<string, unknown>)[level] !== undefined,
|
|
104
|
+
)
|
|
93
105
|
if (!declared.some((level) => level !== 'off')) {
|
|
94
106
|
invalid(provider, `model "${entry.id}" reasoningEfforts 只有 off;声明思考档位或置 false`)
|
|
95
107
|
}
|
|
@@ -107,7 +119,10 @@ function harnessApiKeyAuth(name: string) {
|
|
|
107
119
|
return {
|
|
108
120
|
name,
|
|
109
121
|
resolve: ({ credential }: { credential?: { key?: string } }) =>
|
|
110
|
-
Promise.resolve({
|
|
122
|
+
Promise.resolve({
|
|
123
|
+
auth: credential?.key === undefined ? {} : { apiKey: credential.key },
|
|
124
|
+
source: name,
|
|
125
|
+
}),
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|
|
@@ -142,7 +157,9 @@ function materializeModel(
|
|
|
142
157
|
const api = profile.api ?? base.api ?? routeApi
|
|
143
158
|
if (api === undefined) {
|
|
144
159
|
if (deps.lenient) {
|
|
145
|
-
deps.warn?.(
|
|
160
|
+
deps.warn?.(
|
|
161
|
+
`llm-pi: provider "${route}" model "${entry.id}" 无法获得 api(继承源缺失),已跳过该模型`,
|
|
162
|
+
)
|
|
146
163
|
return null
|
|
147
164
|
}
|
|
148
165
|
invalid(route, `model "${entry.id}" 需要 api:继承源未提供,请在 route 上设置 api`)
|
|
@@ -150,16 +167,27 @@ function materializeModel(
|
|
|
150
167
|
const baseUrl = profile.baseURL ?? base.baseUrl ?? providerBaseUrl
|
|
151
168
|
if (baseUrl === undefined) {
|
|
152
169
|
if (deps.lenient) {
|
|
153
|
-
deps.warn?.(
|
|
170
|
+
deps.warn?.(
|
|
171
|
+
`llm-pi: provider "${route}" model "${entry.id}" 无法获得 baseURL(继承源缺失),已跳过该模型`,
|
|
172
|
+
)
|
|
154
173
|
return null
|
|
155
174
|
}
|
|
156
175
|
invalid(route, `model "${entry.id}" 需要 baseURL:继承源未提供,请在 route 上设置 baseURL`)
|
|
157
176
|
}
|
|
158
|
-
const contextWindow =
|
|
159
|
-
|
|
177
|
+
const contextWindow =
|
|
178
|
+
entry.contextWindow ??
|
|
179
|
+
base.contextWindow ??
|
|
180
|
+
profile.defaultContextWindow ??
|
|
181
|
+
DEFAULT_CONTEXT_WINDOW
|
|
182
|
+
const maxTokens =
|
|
183
|
+
entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
|
|
160
184
|
if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
|
|
161
185
|
validateCompat(api, profile.compat as Record<string, unknown> | undefined, `provider "${route}"`)
|
|
162
|
-
validateCompat(
|
|
186
|
+
validateCompat(
|
|
187
|
+
api,
|
|
188
|
+
entry.compat as Record<string, unknown> | undefined,
|
|
189
|
+
`provider "${route}" model "${entry.id}"`,
|
|
190
|
+
)
|
|
163
191
|
const compat = mergeCompat(
|
|
164
192
|
base.api === api ? base.compat : undefined,
|
|
165
193
|
profile.compat as Record<string, unknown> | undefined,
|
|
@@ -188,7 +216,10 @@ function materializeRouteModels(
|
|
|
188
216
|
profile: ProviderProfileConfig,
|
|
189
217
|
deps: BuildDeps,
|
|
190
218
|
defaultInput: ('text' | 'image')[],
|
|
191
|
-
): {
|
|
219
|
+
): {
|
|
220
|
+
models: MaterializedModel[]
|
|
221
|
+
configuredMaxTokens: Map<string, number>
|
|
222
|
+
} | null {
|
|
192
223
|
const configuredMaxTokens = new Map<string, number>()
|
|
193
224
|
const providerBaseUrl =
|
|
194
225
|
profile.extends === undefined ? undefined : builtinProviderBaseUrl(deps.kit, profile.extends)
|
|
@@ -247,14 +278,19 @@ function materializeRouteModels(
|
|
|
247
278
|
.filter((model): model is MaterializedModel => model !== null)
|
|
248
279
|
if (models.length === 0) {
|
|
249
280
|
if (deps.lenient) {
|
|
250
|
-
deps.warn?.(
|
|
281
|
+
deps.warn?.(
|
|
282
|
+
`llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`,
|
|
283
|
+
)
|
|
251
284
|
return null
|
|
252
285
|
}
|
|
253
286
|
invalid(route, 'route 内没有可服务的模型')
|
|
254
287
|
}
|
|
255
288
|
const finalApis = new Set(models.map((m) => m.api))
|
|
256
289
|
if (finalApis.size > 1) {
|
|
257
|
-
invalid(
|
|
290
|
+
invalid(
|
|
291
|
+
route,
|
|
292
|
+
`route 内模型协议不一致(${[...finalApis].join(', ')});一个 route 只能服务一种协议`,
|
|
293
|
+
)
|
|
258
294
|
}
|
|
259
295
|
return { models, configuredMaxTokens }
|
|
260
296
|
}
|
|
@@ -270,12 +306,16 @@ export function buildProfiles(
|
|
|
270
306
|
): Map<string, ResolvedPiAiProviderProfile> {
|
|
271
307
|
const resolved = new Map<string, ResolvedPiAiProviderProfile>()
|
|
272
308
|
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
309
|
+
// adapter: deepseek 的 route 由 profiles-deepseek.ts 物化(官方 DeepSeekAdapter 链路)
|
|
310
|
+
if ((profile.adapter ?? 'pi') === 'deepseek') continue
|
|
273
311
|
if (route.length === 0) throw new Error('llm-pi: provider 名不能为空')
|
|
274
312
|
// 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
|
|
275
313
|
// route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
|
|
276
314
|
// 暴露(DUPLICATE_ADAPTER),由 service 层逐个 route 注册降级处理。
|
|
277
|
-
if (profile.baseURL !== undefined && profile.baseURL.length === 0)
|
|
278
|
-
|
|
315
|
+
if (profile.baseURL !== undefined && profile.baseURL.length === 0)
|
|
316
|
+
invalid(route, 'baseURL 为空')
|
|
317
|
+
if (profile.displayName !== undefined && profile.displayName.length === 0)
|
|
318
|
+
invalid(route, 'displayName 为空')
|
|
279
319
|
const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
280
320
|
if (
|
|
281
321
|
!Number.isFinite(streamIdleTimeoutMs) ||
|
|
@@ -290,9 +330,15 @@ export function buildProfiles(
|
|
|
290
330
|
const catalog = materializeRouteModels(route, profile, deps, defaultInput)
|
|
291
331
|
if (catalog === null) continue // lenient 下 route 无模型可服务,跳过注册
|
|
292
332
|
const api = catalog.models[0]?.api
|
|
293
|
-
const factory =
|
|
333
|
+
const factory =
|
|
334
|
+
api === undefined
|
|
335
|
+
? undefined
|
|
336
|
+
: deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
|
|
294
337
|
if (factory === undefined) {
|
|
295
|
-
invalid(
|
|
338
|
+
invalid(
|
|
339
|
+
route,
|
|
340
|
+
`api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`,
|
|
341
|
+
)
|
|
296
342
|
}
|
|
297
343
|
const piProvider = deps.kit.createProvider({
|
|
298
344
|
id: route,
|
|
@@ -319,7 +365,9 @@ export function buildProfiles(
|
|
|
319
365
|
),
|
|
320
366
|
...(profile.headers === undefined ? {} : { headers: { ...profile.headers } }),
|
|
321
367
|
...(profile.reasoning === undefined ? {} : { reasoning: profile.reasoning }),
|
|
322
|
-
...(profile.thinkingBudgets === undefined
|
|
368
|
+
...(profile.thinkingBudgets === undefined
|
|
369
|
+
? {}
|
|
370
|
+
: { thinkingBudgets: { ...profile.thinkingBudgets } }),
|
|
323
371
|
...(profile.cacheRetention === undefined ? {} : { cacheRetention: profile.cacheRetention }),
|
|
324
372
|
...(profile.transport === undefined ? {} : { transport: profile.transport }),
|
|
325
373
|
...(profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs }),
|
|
@@ -339,4 +387,6 @@ export function assertServiceable(
|
|
|
339
387
|
deps: BuildDeps,
|
|
340
388
|
): void {
|
|
341
389
|
buildProfiles(config.providers, deps)
|
|
390
|
+
// deepseek 路由同样整体验证(严格模式):写入处拒绝非法配置
|
|
391
|
+
buildDeepseekRoutes(config.providers, deps)
|
|
342
392
|
}
|
package/src/resolve-dsh.ts
CHANGED
|
@@ -16,21 +16,38 @@
|
|
|
16
16
|
import { existsSync, realpathSync } from 'node:fs'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
18
|
import { pathToFileURL } from 'node:url'
|
|
19
|
-
|
|
20
|
-
import
|
|
19
|
+
import type { getOrCreateAnonymousUserId as getAnonIdType } from '@deepseek-ai/dsh-anonymous-user-id'
|
|
20
|
+
import * as vendoredAnonId from '@deepseek-ai/dsh-anonymous-user-id'
|
|
21
21
|
import type * as DshLlm from '@deepseek-ai/dsh-llm'
|
|
22
|
+
import * as vendoredLlm from '@deepseek-ai/dsh-llm'
|
|
23
|
+
import type {
|
|
24
|
+
DeepSeekAdapter as DeepSeekAdapterType,
|
|
25
|
+
resolveAdapterOptions as resolveDeepSeekOptionsType,
|
|
26
|
+
} from '@deepseek-ai/dsh-llm-deepseek'
|
|
27
|
+
import * as vendoredDeepseek from '@deepseek-ai/dsh-llm-deepseek'
|
|
28
|
+
import type { PiAiAdapter as PiAiAdapterType } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
29
|
+
import * as vendoredPiAiAdapter from '@deepseek-ai/dsh-llm-pi-ai'
|
|
22
30
|
import type * as PiAi from '@earendil-works/pi-ai'
|
|
23
|
-
|
|
24
31
|
import * as vendoredPiAi from '@earendil-works/pi-ai'
|
|
25
|
-
import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
|
|
26
32
|
import { anthropicMessagesApi as vendoredAnthropic } from '@earendil-works/pi-ai/api/anthropic-messages.lazy'
|
|
27
33
|
import { openAICompletionsApi as vendoredCompletions } from '@earendil-works/pi-ai/api/openai-completions.lazy'
|
|
28
34
|
import { openAIResponsesApi as vendoredResponses } from '@earendil-works/pi-ai/api/openai-responses.lazy'
|
|
29
|
-
import * as
|
|
30
|
-
import * as vendoredPiAiAdapter from '@deepseek-ai/dsh-llm-pi-ai'
|
|
35
|
+
import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
|
|
31
36
|
|
|
32
37
|
import type { ProtocolId } from './config.ts'
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* deepseek 适配器模块表面(可选):dsh-llm-deepseek 的官方 DeepSeekAdapter
|
|
41
|
+
* 与目录解析器 + 匿名用户 id。缺失(旧版 dsh 树或形状漂移)不拖垮核心
|
|
42
|
+
* 套件——deepseek 类 route 在构建期以明确错误拒绝,pi 路由不受影响。
|
|
43
|
+
* 与核心套件强制同源(杜绝跨源模块混用:brand/LlmError 恒等性敏感)。
|
|
44
|
+
*/
|
|
45
|
+
export interface DeepSeekKit {
|
|
46
|
+
DeepSeekAdapter: typeof DeepSeekAdapterType
|
|
47
|
+
resolveAdapterOptions: typeof resolveDeepSeekOptionsType
|
|
48
|
+
getOrCreateAnonymousUserId: typeof getAnonIdType
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/** 插件运行期所需的全部上游模块表面(单一来源,内部一致)。 */
|
|
35
52
|
export interface DshKit {
|
|
36
53
|
/** 解析来源:dsh 安装树 / 插件 vendored 兜底副本。 */
|
|
@@ -48,6 +65,8 @@ export interface DshKit {
|
|
|
48
65
|
getBuiltinModels: typeof vendoredCatalog.getBuiltinModels
|
|
49
66
|
/** 三协议的 pi-ai api 实现工厂(与官方 PROTOCOLS 表同来源)。 */
|
|
50
67
|
protocolFactories: Record<ProtocolId, () => unknown>
|
|
68
|
+
/** deepseek 文件通道路由所需模块;同源自检失败时为 undefined(见 diagnostics)。 */
|
|
69
|
+
deepseek?: DeepSeekKit
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
/** kit 必备形状清单:缺失即视为上游不兼容。 */
|
|
@@ -55,7 +74,13 @@ function assertKitShape(kit: DshKit, origin: string): void {
|
|
|
55
74
|
const problems: string[] = []
|
|
56
75
|
if (typeof kit.PiAiAdapter !== 'function') problems.push('PiAiAdapter 不是类')
|
|
57
76
|
else {
|
|
58
|
-
for (const method of [
|
|
77
|
+
for (const method of [
|
|
78
|
+
'current',
|
|
79
|
+
'stream',
|
|
80
|
+
'listModels',
|
|
81
|
+
'resolveModel',
|
|
82
|
+
'providerInfo',
|
|
83
|
+
] as const) {
|
|
59
84
|
if (typeof (kit.PiAiAdapter.prototype as Record<string, unknown>)[method] !== 'function') {
|
|
60
85
|
problems.push(`PiAiAdapter.prototype.${method} 缺失`)
|
|
61
86
|
}
|
|
@@ -74,6 +99,22 @@ function assertKitShape(kit: DshKit, origin: string): void {
|
|
|
74
99
|
}
|
|
75
100
|
}
|
|
76
101
|
|
|
102
|
+
/** deepseek 模块形状自检;返回问题清单(空 = 可用),由调用方决定降级。 */
|
|
103
|
+
function checkDeepseekShape(kit: DeepSeekKit): string[] {
|
|
104
|
+
const problems: string[] = []
|
|
105
|
+
if (typeof kit.DeepSeekAdapter !== 'function') problems.push('DeepSeekAdapter 不是类')
|
|
106
|
+
else if (
|
|
107
|
+
typeof (kit.DeepSeekAdapter.prototype as Record<string, unknown>)['stream'] !== 'function'
|
|
108
|
+
) {
|
|
109
|
+
problems.push('DeepSeekAdapter.prototype.stream 缺失')
|
|
110
|
+
}
|
|
111
|
+
if (typeof kit.resolveAdapterOptions !== 'function') problems.push('resolveAdapterOptions 缺失')
|
|
112
|
+
if (typeof kit.getOrCreateAnonymousUserId !== 'function') {
|
|
113
|
+
problems.push('getOrCreateAnonymousUserId 缺失')
|
|
114
|
+
}
|
|
115
|
+
return problems
|
|
116
|
+
}
|
|
117
|
+
|
|
77
118
|
/** 从 startDir 向上找同时含有 dsh-llm-pi-ai 与 pi-ai 的安装树根。 */
|
|
78
119
|
function findDshTreeRoot(startDir: string): string | undefined {
|
|
79
120
|
let dir = startDir
|
|
@@ -119,6 +160,33 @@ async function importTreeModules(root: string): Promise<TreeModules> {
|
|
|
119
160
|
return { piAiAdapter, llm, piAi, catalog, completions, responses, anthropic }
|
|
120
161
|
}
|
|
121
162
|
|
|
163
|
+
/** 从同一 dsh 安装树加载 deepseek 适配器模块(失败返回 undefined,不拖垮核心套件)。 */
|
|
164
|
+
async function importTreeDeepseek(root: string): Promise<{ kit?: DeepSeekKit; problem?: string }> {
|
|
165
|
+
const nm = join(root, 'node_modules')
|
|
166
|
+
const load = (absPath: string): Promise<Record<string, unknown>> =>
|
|
167
|
+
import(pathToFileURL(absPath).href) as Promise<Record<string, unknown>>
|
|
168
|
+
try {
|
|
169
|
+
const [deepseek, anonId] = await Promise.all([
|
|
170
|
+
load(join(nm, '@deepseek-ai', 'dsh-llm-deepseek', 'lib', 'index.js')),
|
|
171
|
+
load(join(nm, '@deepseek-ai', 'dsh-anonymous-user-id', 'lib', 'index.js')),
|
|
172
|
+
])
|
|
173
|
+
const kit: DeepSeekKit = {
|
|
174
|
+
DeepSeekAdapter: deepseek['DeepSeekAdapter'] as DeepSeekKit['DeepSeekAdapter'],
|
|
175
|
+
resolveAdapterOptions: deepseek[
|
|
176
|
+
'resolveAdapterOptions'
|
|
177
|
+
] as DeepSeekKit['resolveAdapterOptions'],
|
|
178
|
+
getOrCreateAnonymousUserId: anonId[
|
|
179
|
+
'getOrCreateAnonymousUserId'
|
|
180
|
+
] as DeepSeekKit['getOrCreateAnonymousUserId'],
|
|
181
|
+
}
|
|
182
|
+
const problems = checkDeepseekShape(kit)
|
|
183
|
+
if (problems.length > 0) return { problem: `形状不兼容:${problems.join(';')}` }
|
|
184
|
+
return { kit }
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return { problem: error instanceof Error ? error.message : String(error) }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
122
190
|
function kitFromTree(mods: TreeModules): DshKit {
|
|
123
191
|
return {
|
|
124
192
|
source: 'dsh-tree',
|
|
@@ -141,8 +209,19 @@ function kitFromTree(mods: TreeModules): DshKit {
|
|
|
141
209
|
}
|
|
142
210
|
}
|
|
143
211
|
|
|
212
|
+
/** vendored 兜底副本的 deepseek 模块(形状自检不过时返回 undefined)。 */
|
|
213
|
+
function loadVendoredDeepseek(): DeepSeekKit | undefined {
|
|
214
|
+
const kit: DeepSeekKit = {
|
|
215
|
+
DeepSeekAdapter: vendoredDeepseek.DeepSeekAdapter,
|
|
216
|
+
resolveAdapterOptions: vendoredDeepseek.resolveAdapterOptions,
|
|
217
|
+
getOrCreateAnonymousUserId: vendoredAnonId.getOrCreateAnonymousUserId,
|
|
218
|
+
}
|
|
219
|
+
return checkDeepseekShape(kit).length === 0 ? kit : undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
144
222
|
/** vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。 */
|
|
145
223
|
export function loadVendoredKit(): DshKit {
|
|
224
|
+
const deepseek = loadVendoredDeepseek()
|
|
146
225
|
const kit: DshKit = {
|
|
147
226
|
source: 'vendored',
|
|
148
227
|
PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
|
|
@@ -161,6 +240,7 @@ export function loadVendoredKit(): DshKit {
|
|
|
161
240
|
'openai-responses': vendoredResponses,
|
|
162
241
|
'anthropic-messages': vendoredAnthropic,
|
|
163
242
|
},
|
|
243
|
+
...(deepseek === undefined ? {} : { deepseek }),
|
|
164
244
|
}
|
|
165
245
|
assertKitShape(kit, 'vendored')
|
|
166
246
|
return kit
|
|
@@ -182,13 +262,24 @@ function dshTreeAnchor(): string | undefined {
|
|
|
182
262
|
* 两者都过不了形状自检时抛错(调用方应记日志并放弃注册 route)。
|
|
183
263
|
* 返回的 diagnostics 记录回退原因,供配置卡片与日志展示。
|
|
184
264
|
*/
|
|
185
|
-
export async function resolveDshKit(): Promise<{
|
|
265
|
+
export async function resolveDshKit(): Promise<{
|
|
266
|
+
kit: DshKit
|
|
267
|
+
diagnostics: string[]
|
|
268
|
+
}> {
|
|
186
269
|
const diagnostics: string[] = []
|
|
187
270
|
const anchor = dshTreeAnchor()
|
|
188
271
|
if (anchor !== undefined) {
|
|
189
272
|
try {
|
|
190
273
|
const kit = kitFromTree(await importTreeModules(anchor))
|
|
191
274
|
assertKitShape(kit, 'dsh-tree')
|
|
275
|
+
const tree = await importTreeDeepseek(anchor)
|
|
276
|
+
if (tree.kit !== undefined) {
|
|
277
|
+
return { kit: { ...kit, deepseek: tree.kit }, diagnostics }
|
|
278
|
+
}
|
|
279
|
+
diagnostics.push(
|
|
280
|
+
`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? '未知原因'});` +
|
|
281
|
+
'adapter: deepseek 的 route 不可用,pi 路由不受影响',
|
|
282
|
+
)
|
|
192
283
|
return { kit, diagnostics }
|
|
193
284
|
} catch (error) {
|
|
194
285
|
diagnostics.push(
|
|
@@ -199,5 +290,10 @@ export async function resolveDshKit(): Promise<{ kit: DshKit; diagnostics: strin
|
|
|
199
290
|
diagnostics.push('未能从 process.argv[1] 定位 dsh 安装树;回退 vendored 副本')
|
|
200
291
|
}
|
|
201
292
|
const kit = loadVendoredKit()
|
|
293
|
+
if (kit.deepseek === undefined) {
|
|
294
|
+
diagnostics.push(
|
|
295
|
+
'vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
202
298
|
return { kit, diagnostics }
|
|
203
299
|
}
|