@dsh-plus/llm-pi 0.1.6 → 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 +1880 -1844
- package/lib/index.d.ts +21 -4
- package/lib/index.js +562 -126
- package/package.json +16 -11
- package/src/catalog/official.ts +71 -0
- package/src/client/draft.ts +90 -32
- package/src/config.ts +105 -0
- package/src/deepseek-routes.ts +139 -0
- package/src/discovery.ts +33 -2
- package/src/profiles-deepseek.ts +282 -0
- package/src/profiles.ts +5 -0
- package/src/resolve-dsh.ts +89 -0
- package/src/service.ts +39 -1
|
@@ -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
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
THINKING_LEVELS,
|
|
33
33
|
} from './config.ts'
|
|
34
34
|
import { ExtendsError, resolveModelBase } from './inherit.ts'
|
|
35
|
+
import { buildDeepseekRoutes } from './profiles-deepseek.ts'
|
|
35
36
|
import type { DshKit } from './resolve-dsh.ts'
|
|
36
37
|
|
|
37
38
|
/** 内置目录未描述时的零价目(harness 不消费 cost 元数据,同官方 NO_COST)。 */
|
|
@@ -305,6 +306,8 @@ export function buildProfiles(
|
|
|
305
306
|
): Map<string, ResolvedPiAiProviderProfile> {
|
|
306
307
|
const resolved = new Map<string, ResolvedPiAiProviderProfile>()
|
|
307
308
|
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
309
|
+
// adapter: deepseek 的 route 由 profiles-deepseek.ts 物化(官方 DeepSeekAdapter 链路)
|
|
310
|
+
if ((profile.adapter ?? 'pi') === 'deepseek') continue
|
|
308
311
|
if (route.length === 0) throw new Error('llm-pi: provider 名不能为空')
|
|
309
312
|
// 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
|
|
310
313
|
// route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
|
|
@@ -384,4 +387,6 @@ export function assertServiceable(
|
|
|
384
387
|
deps: BuildDeps,
|
|
385
388
|
): void {
|
|
386
389
|
buildProfiles(config.providers, deps)
|
|
390
|
+
// deepseek 路由同样整体验证(严格模式):写入处拒绝非法配置
|
|
391
|
+
buildDeepseekRoutes(config.providers, deps)
|
|
387
392
|
}
|
package/src/resolve-dsh.ts
CHANGED
|
@@ -16,8 +16,15 @@
|
|
|
16
16
|
import { existsSync, realpathSync } from 'node:fs'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
18
|
import { pathToFileURL } from 'node:url'
|
|
19
|
+
import type { getOrCreateAnonymousUserId as getAnonIdType } from '@deepseek-ai/dsh-anonymous-user-id'
|
|
20
|
+
import * as vendoredAnonId from '@deepseek-ai/dsh-anonymous-user-id'
|
|
19
21
|
import type * as DshLlm from '@deepseek-ai/dsh-llm'
|
|
20
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'
|
|
21
28
|
import type { PiAiAdapter as PiAiAdapterType } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
22
29
|
import * as vendoredPiAiAdapter from '@deepseek-ai/dsh-llm-pi-ai'
|
|
23
30
|
import type * as PiAi from '@earendil-works/pi-ai'
|
|
@@ -29,6 +36,18 @@ import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
|
|
|
29
36
|
|
|
30
37
|
import type { ProtocolId } from './config.ts'
|
|
31
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
|
+
|
|
32
51
|
/** 插件运行期所需的全部上游模块表面(单一来源,内部一致)。 */
|
|
33
52
|
export interface DshKit {
|
|
34
53
|
/** 解析来源:dsh 安装树 / 插件 vendored 兜底副本。 */
|
|
@@ -46,6 +65,8 @@ export interface DshKit {
|
|
|
46
65
|
getBuiltinModels: typeof vendoredCatalog.getBuiltinModels
|
|
47
66
|
/** 三协议的 pi-ai api 实现工厂(与官方 PROTOCOLS 表同来源)。 */
|
|
48
67
|
protocolFactories: Record<ProtocolId, () => unknown>
|
|
68
|
+
/** deepseek 文件通道路由所需模块;同源自检失败时为 undefined(见 diagnostics)。 */
|
|
69
|
+
deepseek?: DeepSeekKit
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
/** kit 必备形状清单:缺失即视为上游不兼容。 */
|
|
@@ -78,6 +99,22 @@ function assertKitShape(kit: DshKit, origin: string): void {
|
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
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
|
+
|
|
81
118
|
/** 从 startDir 向上找同时含有 dsh-llm-pi-ai 与 pi-ai 的安装树根。 */
|
|
82
119
|
function findDshTreeRoot(startDir: string): string | undefined {
|
|
83
120
|
let dir = startDir
|
|
@@ -123,6 +160,33 @@ async function importTreeModules(root: string): Promise<TreeModules> {
|
|
|
123
160
|
return { piAiAdapter, llm, piAi, catalog, completions, responses, anthropic }
|
|
124
161
|
}
|
|
125
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
|
+
|
|
126
190
|
function kitFromTree(mods: TreeModules): DshKit {
|
|
127
191
|
return {
|
|
128
192
|
source: 'dsh-tree',
|
|
@@ -145,8 +209,19 @@ function kitFromTree(mods: TreeModules): DshKit {
|
|
|
145
209
|
}
|
|
146
210
|
}
|
|
147
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
|
+
|
|
148
222
|
/** vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。 */
|
|
149
223
|
export function loadVendoredKit(): DshKit {
|
|
224
|
+
const deepseek = loadVendoredDeepseek()
|
|
150
225
|
const kit: DshKit = {
|
|
151
226
|
source: 'vendored',
|
|
152
227
|
PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
|
|
@@ -165,6 +240,7 @@ export function loadVendoredKit(): DshKit {
|
|
|
165
240
|
'openai-responses': vendoredResponses,
|
|
166
241
|
'anthropic-messages': vendoredAnthropic,
|
|
167
242
|
},
|
|
243
|
+
...(deepseek === undefined ? {} : { deepseek }),
|
|
168
244
|
}
|
|
169
245
|
assertKitShape(kit, 'vendored')
|
|
170
246
|
return kit
|
|
@@ -196,6 +272,14 @@ export async function resolveDshKit(): Promise<{
|
|
|
196
272
|
try {
|
|
197
273
|
const kit = kitFromTree(await importTreeModules(anchor))
|
|
198
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
|
+
)
|
|
199
283
|
return { kit, diagnostics }
|
|
200
284
|
} catch (error) {
|
|
201
285
|
diagnostics.push(
|
|
@@ -206,5 +290,10 @@ export async function resolveDshKit(): Promise<{
|
|
|
206
290
|
diagnostics.push('未能从 process.argv[1] 定位 dsh 安装树;回退 vendored 副本')
|
|
207
291
|
}
|
|
208
292
|
const kit = loadVendoredKit()
|
|
293
|
+
if (kit.deepseek === undefined) {
|
|
294
|
+
diagnostics.push(
|
|
295
|
+
'vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
209
298
|
return { kit, diagnostics }
|
|
210
299
|
}
|
package/src/service.ts
CHANGED
|
@@ -19,8 +19,10 @@ import { deepEqualJson, installSettingsSection } from '@deepseek-ai/dsh-settings
|
|
|
19
19
|
import { hasBuiltinProvider } from './catalog/builtin.ts'
|
|
20
20
|
import { ModelsDevSource } from './catalog/models-dev.ts'
|
|
21
21
|
import { Config, type LlmPiConfig, SETTINGS_NS } from './config.ts'
|
|
22
|
+
import { DeepseekRouteRegistrar } from './deepseek-routes.ts'
|
|
22
23
|
import { discoverModels } from './discovery.ts'
|
|
23
24
|
import { assertServiceable, buildProfiles } from './profiles.ts'
|
|
25
|
+
import { buildDeepseekRoutes, type ResolvedDeepseekRoute } from './profiles-deepseek.ts'
|
|
24
26
|
import { type DshKit, resolveDshKit } from './resolve-dsh.ts'
|
|
25
27
|
|
|
26
28
|
export interface LlmPiRuntime {
|
|
@@ -89,6 +91,7 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
|
|
|
89
91
|
let current: () => LlmPiConfig = () => config
|
|
90
92
|
let lastRaw: LlmPiConfig | undefined
|
|
91
93
|
let memoized: Map<string, ResolvedPiAiProviderProfile> | undefined
|
|
94
|
+
let memoizedDeepseek: Map<string, ResolvedDeepseekRoute> | undefined
|
|
92
95
|
const deps = { kit, modelsDev }
|
|
93
96
|
/** 当前已解析 profiles,按原始 config identity 备忘(官方同款模式)。
|
|
94
97
|
* 运行期走 lenient:数据源漂移时降级/跳过并告警,而非抛错弄挂整个 route。 */
|
|
@@ -102,10 +105,22 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
|
|
|
102
105
|
warn: (message) => logger.warn(message),
|
|
103
106
|
})
|
|
104
107
|
: new Map()
|
|
108
|
+
memoizedDeepseek = raw.enabled
|
|
109
|
+
? buildDeepseekRoutes(raw.providers, {
|
|
110
|
+
kit,
|
|
111
|
+
lenient: true,
|
|
112
|
+
warn: (message) => logger.warn(message),
|
|
113
|
+
})
|
|
114
|
+
: new Map()
|
|
105
115
|
lastRaw = raw
|
|
106
116
|
memoized = next
|
|
107
117
|
return next
|
|
108
118
|
}
|
|
119
|
+
/** deepseek 路由物化表(与 profiles 同一次备忘窗口)。 */
|
|
120
|
+
const deepseekRoutes = (): Map<string, ResolvedDeepseekRoute> => {
|
|
121
|
+
profiles()
|
|
122
|
+
return memoizedDeepseek ?? new Map()
|
|
123
|
+
}
|
|
109
124
|
profiles() // 行级 config 不可服务则启动即失败(官方同款 fail-fast)
|
|
110
125
|
|
|
111
126
|
const adapter = new kit.PiAiAdapter({
|
|
@@ -198,13 +213,21 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
|
|
|
198
213
|
let directory: { replace: (entries: unknown[]) => void } | undefined
|
|
199
214
|
let directoryFacts: unknown
|
|
200
215
|
const ensureDirectory = (): void => {
|
|
201
|
-
const
|
|
216
|
+
const piEntries = [...profiles().entries()].map(([provider, profile]) => ({
|
|
202
217
|
provider,
|
|
203
218
|
displayName: profile.displayName,
|
|
204
219
|
settingsNs: SETTINGS_NS,
|
|
205
220
|
settingsPath: ['providers', provider],
|
|
206
221
|
declared: !hasBuiltinProvider(kit, provider),
|
|
207
222
|
}))
|
|
223
|
+
const deepseekEntries = [...deepseekRoutes().values()].map((built) => ({
|
|
224
|
+
provider: built.route,
|
|
225
|
+
displayName: built.displayName,
|
|
226
|
+
settingsNs: SETTINGS_NS,
|
|
227
|
+
settingsPath: ['providers', built.route],
|
|
228
|
+
declared: true,
|
|
229
|
+
}))
|
|
230
|
+
const entries = [...piEntries, ...deepseekEntries]
|
|
208
231
|
if (deepEqualJson(entries, directoryFacts)) return
|
|
209
232
|
if (entries.length === 0) {
|
|
210
233
|
// 空目录不可注册(INVALID_DIRECTORY);等 settings 用户层供数后在 onChange 注册
|
|
@@ -216,7 +239,16 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
|
|
|
216
239
|
directoryFacts = entries
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
const deepseekRegistrar = new DeepseekRouteRegistrar({
|
|
243
|
+
ctx,
|
|
244
|
+
kit,
|
|
245
|
+
logger: { warn: (m) => logger.warn(m), error: (m) => logger.error(m) },
|
|
246
|
+
routes: deepseekRoutes,
|
|
247
|
+
})
|
|
248
|
+
const ensureDeepseek = (): void => deepseekRegistrar.sync(deepseekRoutes())
|
|
249
|
+
|
|
219
250
|
ensureRegistration()
|
|
251
|
+
ensureDeepseek()
|
|
220
252
|
ensureDirectory()
|
|
221
253
|
|
|
222
254
|
installSettingsSection(ctx, SETTINGS_NS, Config, config, {
|
|
@@ -231,6 +263,12 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
|
|
|
231
263
|
logger.error('llm-pi: 更新被拒,保留此前注册的 route')
|
|
232
264
|
logger.error(error)
|
|
233
265
|
}
|
|
266
|
+
try {
|
|
267
|
+
ensureDeepseek()
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.error('llm-pi: deepseek route 更新失败,保留此前注册')
|
|
270
|
+
logger.error(error)
|
|
271
|
+
}
|
|
234
272
|
try {
|
|
235
273
|
ensureDirectory()
|
|
236
274
|
} catch (error) {
|