@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.
@@ -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 { inheritedCatalogEntries, builtinProviderBaseUrl, type ModelBase } from './catalog/builtin.ts'
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(configured: readonly ('text' | 'image')[] | undefined): ('text' | 'image')[] | undefined {
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
- : { thinkingLevelMap: base.thinkingLevelMap as Record<string, string | null> }),
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') invalid(provider, `model "${entry.id}" reasoningEfforts.${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((level) => (efforts as Record<string, unknown>)[level] !== undefined)
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({ auth: credential?.key === undefined ? {} : { apiKey: credential.key }, source: name }),
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?.(`llm-pi: provider "${route}" model "${entry.id}" 无法获得 api(继承源缺失),已跳过该模型`)
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?.(`llm-pi: provider "${route}" model "${entry.id}" 无法获得 baseURL(继承源缺失),已跳过该模型`)
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 = entry.contextWindow ?? base.contextWindow ?? profile.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW
159
- const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
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(api, entry.compat as Record<string, unknown> | undefined, `provider "${route}" model "${entry.id}"`)
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
- ): { models: MaterializedModel[]; configuredMaxTokens: Map<string, number> } | null {
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?.(`llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`)
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(route, `route 内模型协议不一致(${[...finalApis].join(', ')});一个 route 只能服务一种协议`)
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) invalid(route, 'baseURL 为空')
278
- if (profile.displayName !== undefined && profile.displayName.length === 0) invalid(route, 'displayName 为空')
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 = api === undefined ? undefined : deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
333
+ const factory =
334
+ api === undefined
335
+ ? undefined
336
+ : deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
294
337
  if (factory === undefined) {
295
- invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`)
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 ? {} : { thinkingBudgets: { ...profile.thinkingBudgets } }),
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
  }
@@ -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 type { PiAiAdapter as PiAiAdapterType } from '@deepseek-ai/dsh-llm-pi-ai'
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 vendoredLlm from '@deepseek-ai/dsh-llm'
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 ['current', 'stream', 'listModels', 'resolveModel', 'providerInfo'] as const) {
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<{ kit: DshKit; diagnostics: string[] }> {
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
  }