@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.
@@ -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
  }
@@ -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 entries = [...profiles().entries()].map(([provider, profile]) => ({
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) {