@dsh-plus/llm-pi 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,15 +9,25 @@
9
9
  * 的目录,按文件路径动态 import——与 dsh 官方插件共享同一模块实例;
10
10
  * 2. vendored:回退到本插件 dependencies 里的固定版本副本(裸 import)。
11
11
  *
12
- * 两条路径产物都过形状自检(assertKitShape):上游 rc 重构导致形状漂移时
12
+ * 两条路径产物都过形状自检(assertKitShape):上游重构导致形状漂移时
13
13
  * 抛错,由调用方决定回退或放弃注册——绝不让坏套件进入消息链路。
14
+ *
15
+ * 0.1.2-alpha.1 起的新面(包根未导出、仅 src 子路径有):
16
+ * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
17
+ * - credentialStoreFrom/authContextFrom(auth.ts):PiAiAdapter 必需 auth 注入。
18
+ * npm 发布形态(lib/index.js 单 bundle + 无 src/)两者都拿不到:dsh 树优先
19
+ * 探测 src/*(含 lib/* 候选),未命中时 resolveProfiles 走插件等价实现
20
+ * (profiles.ts,门控表对齐官方 catalog.ts)、auth 走内联等价实现
21
+ * (auth-inline.ts,recordKeyFor 自包根导出同源注入)——形状自检兜底。
14
22
  * @module llm-pi/resolve-dsh
15
23
  */
16
24
  import { existsSync, realpathSync } from 'node:fs'
17
25
  import { dirname, join } from 'node:path'
18
26
  import { pathToFileURL } from 'node:url'
27
+ import type { Context } from '@deepseek-ai/cordis'
19
28
  import type { getOrCreateAnonymousUserId as getAnonIdType } from '@deepseek-ai/dsh-anonymous-user-id'
20
29
  import * as vendoredAnonId from '@deepseek-ai/dsh-anonymous-user-id'
30
+ import type { CredentialKey } from '@deepseek-ai/dsh-credentials'
21
31
  import type * as DshLlm from '@deepseek-ai/dsh-llm'
22
32
  import * as vendoredLlm from '@deepseek-ai/dsh-llm'
23
33
  import type {
@@ -25,16 +35,26 @@ import type {
25
35
  resolveAdapterOptions as resolveDeepSeekOptionsType,
26
36
  } from '@deepseek-ai/dsh-llm-deepseek'
27
37
  import * as vendoredDeepseek from '@deepseek-ai/dsh-llm-deepseek'
28
- import type { PiAiAdapter as PiAiAdapterType } from '@deepseek-ai/dsh-llm-pi-ai'
38
+ import type {
39
+ PiAiAdapter as PiAiAdapterType,
40
+ PiAiProviderProfile,
41
+ ResolvedPiAiProviderProfile,
42
+ } from '@deepseek-ai/dsh-llm-pi-ai'
29
43
  import * as vendoredPiAiAdapter from '@deepseek-ai/dsh-llm-pi-ai'
30
44
  import type * as PiAi from '@earendil-works/pi-ai'
45
+ import type { AuthContext, CredentialStore } from '@earendil-works/pi-ai'
31
46
  import * as vendoredPiAi from '@earendil-works/pi-ai'
32
47
  import { anthropicMessagesApi as vendoredAnthropic } from '@earendil-works/pi-ai/api/anthropic-messages.lazy'
33
48
  import { openAICompletionsApi as vendoredCompletions } from '@earendil-works/pi-ai/api/openai-completions.lazy'
34
49
  import { openAIResponsesApi as vendoredResponses } from '@earendil-works/pi-ai/api/openai-responses.lazy'
35
50
  import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
36
51
 
52
+ import {
53
+ authContextFrom as inlineAuthContextFrom,
54
+ credentialStoreFrom as inlineCredentialStoreFrom,
55
+ } from './auth-inline.ts'
37
56
  import type { ProtocolId } from './config.ts'
57
+ import { type ResolverDeps, resolveProfilesFallback } from './profiles.ts'
38
58
 
39
59
  /**
40
60
  * deepseek 适配器模块表面(可选):dsh-llm-deepseek 的官方 DeepSeekAdapter
@@ -48,6 +68,17 @@ export interface DeepSeekKit {
48
68
  getOrCreateAnonymousUserId: typeof getAnonIdType
49
69
  }
50
70
 
71
+ /** profile 解析面(官方 config.ts resolveProfiles 的签名;包根未导出,见文件头)。 */
72
+ export type ResolveProfiles = (
73
+ providers: Readonly<Record<string, PiAiProviderProfile>> | undefined,
74
+ ) => Map<string, ResolvedPiAiProviderProfile>
75
+
76
+ /** PiAiAdapter 必需 auth 注入的两个助手(官方 auth.ts 面;包根未导出)。 */
77
+ export interface AuthHelpers {
78
+ credentialStoreFrom(ctx: Context): CredentialStore
79
+ authContextFrom(ctx: Context): AuthContext
80
+ }
81
+
51
82
  /** 插件运行期所需的全部上游模块表面(单一来源,内部一致)。 */
52
83
  export interface DshKit {
53
84
  /** 解析来源:dsh 安装树 / 插件 vendored 兜底副本。 */
@@ -65,6 +96,19 @@ export interface DshKit {
65
96
  getBuiltinModels: typeof vendoredCatalog.getBuiltinModels
66
97
  /** 三协议的 pi-ai api 实现工厂(与官方 PROTOCOLS 表同来源)。 */
67
98
  protocolFactories: Record<ProtocolId, () => unknown>
99
+ /**
100
+ * profile 解析链:dsh 树 dev 布局(src/config.ts 存在)时为官方
101
+ * resolveProfiles;npm 形态(无 src)与 vendored 兜底时为插件等价实现。
102
+ */
103
+ resolveProfiles: ResolveProfiles
104
+ /**
105
+ * PiAiAdapter 必需 auth 注入:dsh 树 dev 布局(src/auth.ts 存在)时为官方
106
+ * credentialStoreFrom/authContextFrom;其余形态为内联等价实现
107
+ * (auth-inline.ts,与官方同语义:记录作用域 llm-pi-ai + recordKeyFor)。
108
+ */
109
+ auth: AuthHelpers
110
+ /** 附件引用 → 当前模型工具执行世界的只读路径桥(官方 dsh-llm 导出)。 */
111
+ resolveImageAttachmentAccess: typeof DshLlm.resolveImageAttachmentAccess
68
112
  /** deepseek 文件通道路由所需模块;同源自检失败时为 undefined(见 diagnostics)。 */
69
113
  deepseek?: DeepSeekKit
70
114
  }
@@ -74,12 +118,18 @@ function assertKitShape(kit: DshKit, origin: string): void {
74
118
  const problems: string[] = []
75
119
  if (typeof kit.PiAiAdapter !== 'function') problems.push('PiAiAdapter 不是类')
76
120
  else {
121
+ // 0.1.2-alpha.1 的 LlmAdapter 抽象面:prepareCall/providerRetryPolicy 为
122
+ // master 适配器必需 override;models.getModels/getModel 语义未变
123
+ // (pi-ai 0.84.2 Models.getModel(provider,id)/getModels(provider),
124
+ // 由同源 adapter 内部消费,本插件不直调)。
77
125
  for (const method of [
78
126
  'current',
79
127
  'stream',
80
128
  'listModels',
81
129
  'resolveModel',
82
130
  'providerInfo',
131
+ 'prepareCall',
132
+ 'providerRetryPolicy',
83
133
  ] as const) {
84
134
  if (typeof (kit.PiAiAdapter.prototype as Record<string, unknown>)[method] !== 'function') {
85
135
  problems.push(`PiAiAdapter.prototype.${method} 缺失`)
@@ -94,6 +144,16 @@ function assertKitShape(kit: DshKit, origin: string): void {
94
144
  }
95
145
  if (typeof kit.LlmError !== 'function') problems.push('dsh-llm LlmError 缺失')
96
146
  if (typeof kit.resolveRetryPolicy !== 'function') problems.push('dsh-llm resolveRetryPolicy 缺失')
147
+ if (typeof kit.resolveProfiles !== 'function') problems.push('resolveProfiles 缺失')
148
+ if (typeof kit.resolveImageAttachmentAccess !== 'function') {
149
+ problems.push('resolveImageAttachmentAccess 缺失')
150
+ }
151
+ if (
152
+ typeof kit.auth?.credentialStoreFrom !== 'function' ||
153
+ typeof kit.auth?.authContextFrom !== 'function'
154
+ ) {
155
+ problems.push('auth 助手(credentialStoreFrom/authContextFrom)缺失')
156
+ }
97
157
  if (problems.length > 0) {
98
158
  throw new Error(`llm-pi: ${origin} 来源的运行时套件形状不兼容:${problems.join(';')}`)
99
159
  }
@@ -160,6 +220,27 @@ async function importTreeModules(root: string): Promise<TreeModules> {
160
220
  return { piAiAdapter, llm, piAi, catalog, completions, responses, anthropic }
161
221
  }
162
222
 
223
+ /**
224
+ * 探测包目录下的子路径模块:src/<name>.ts(dsh 树 dev 布局 / 源码仓库)→
225
+ * src/<name>.js → lib/<name>.js(未来构建形态)。加载失败(依赖缺失等)
226
+ * 视为该候选不可用,继续下一候选;全部未命中返回 undefined(调用方兜底)。
227
+ */
228
+ async function probeSubmodule(
229
+ pkgDir: string,
230
+ name: string,
231
+ ): Promise<Record<string, unknown> | undefined> {
232
+ for (const candidate of [`src/${name}.ts`, `src/${name}.js`, `lib/${name}.js`]) {
233
+ const absPath = join(pkgDir, candidate)
234
+ if (!existsSync(absPath)) continue
235
+ try {
236
+ return (await import(pathToFileURL(absPath).href)) as Record<string, unknown>
237
+ } catch {
238
+ // 候选存在但不可加载:尝试下一候选
239
+ }
240
+ }
241
+ return undefined
242
+ }
243
+
163
244
  /** 从同一 dsh 安装树加载 deepseek 适配器模块(失败返回 undefined,不拖垮核心套件)。 */
164
245
  async function importTreeDeepseek(root: string): Promise<{ kit?: DeepSeekKit; problem?: string }> {
165
246
  const nm = join(root, 'node_modules')
@@ -187,7 +268,37 @@ async function importTreeDeepseek(root: string): Promise<{ kit?: DeepSeekKit; pr
187
268
  }
188
269
  }
189
270
 
190
- function kitFromTree(mods: TreeModules): DshKit {
271
+ function protocolFactoriesOf(mods: TreeModules): DshKit['protocolFactories'] {
272
+ return {
273
+ 'openai-completions': mods.completions['openAICompletionsApi'] as () => unknown,
274
+ 'openai-responses': mods.responses['openAIResponsesApi'] as () => unknown,
275
+ 'anthropic-messages': mods.anthropic['anthropicMessagesApi'] as () => unknown,
276
+ }
277
+ }
278
+
279
+ /** 插件等价解析链的套件依赖(与 PiAiAdapter 同源,杜绝跨源混用)。 */
280
+ function treeResolverDeps(
281
+ mods: TreeModules,
282
+ protocolFactories: DshKit['protocolFactories'],
283
+ ): ResolverDeps {
284
+ return {
285
+ createProvider: mods.piAi['createProvider'] as DshKit['createProvider'],
286
+ protocolFactories,
287
+ resolveRetryPolicy: mods.llm['resolveRetryPolicy'] as DshKit['resolveRetryPolicy'],
288
+ }
289
+ }
290
+
291
+ function kitFromTree(
292
+ mods: TreeModules,
293
+ auth: Record<string, unknown> | undefined,
294
+ config: Record<string, unknown> | undefined,
295
+ ): DshKit {
296
+ const protocolFactories = protocolFactoriesOf(mods)
297
+ const resolverDeps = treeResolverDeps(mods, protocolFactories)
298
+ const officialResolveProfiles = config?.['resolveProfiles']
299
+ const officialCredentialStoreFrom = auth?.['credentialStoreFrom']
300
+ const officialAuthContextFrom = auth?.['authContextFrom']
301
+ const recordKeyFor = mods.piAiAdapter['recordKeyFor'] as (providerId: string) => CredentialKey
191
302
  return {
192
303
  source: 'dsh-tree',
193
304
  PiAiAdapter: mods.piAiAdapter['PiAiAdapter'] as DshKit['PiAiAdapter'],
@@ -201,11 +312,24 @@ function kitFromTree(mods: TreeModules): DshKit {
201
312
  builtinProviders: mods.catalog['builtinProviders'] as DshKit['builtinProviders'],
202
313
  getBuiltinProviders: mods.catalog['getBuiltinProviders'] as DshKit['getBuiltinProviders'],
203
314
  getBuiltinModels: mods.catalog['getBuiltinModels'] as DshKit['getBuiltinModels'],
204
- protocolFactories: {
205
- 'openai-completions': mods.completions['openAICompletionsApi'] as () => unknown,
206
- 'openai-responses': mods.responses['openAIResponsesApi'] as () => unknown,
207
- 'anthropic-messages': mods.anthropic['anthropicMessagesApi'] as () => unknown,
315
+ protocolFactories,
316
+ resolveProfiles:
317
+ officialResolveProfiles !== undefined
318
+ ? (officialResolveProfiles as ResolveProfiles)
319
+ : (providers) => resolveProfilesFallback(providers, resolverDeps),
320
+ auth: {
321
+ credentialStoreFrom:
322
+ officialCredentialStoreFrom !== undefined
323
+ ? (officialCredentialStoreFrom as AuthHelpers['credentialStoreFrom'])
324
+ : (ctx) => inlineCredentialStoreFrom(ctx, recordKeyFor as never),
325
+ authContextFrom:
326
+ officialAuthContextFrom !== undefined
327
+ ? (officialAuthContextFrom as AuthHelpers['authContextFrom'])
328
+ : inlineAuthContextFrom,
208
329
  },
330
+ resolveImageAttachmentAccess: mods.llm[
331
+ 'resolveImageAttachmentAccess'
332
+ ] as DshKit['resolveImageAttachmentAccess'],
209
333
  }
210
334
  }
211
335
 
@@ -219,9 +343,18 @@ function loadVendoredDeepseek(): DeepSeekKit | undefined {
219
343
  return checkDeepseekShape(kit).length === 0 ? kit : undefined
220
344
  }
221
345
 
222
- /** vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。 */
346
+ /**
347
+ * vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。
348
+ * npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
349
+ * auth 助手在此无条件走插件等价实现(内联/门控表对齐,见文件头说明)。
350
+ */
223
351
  export function loadVendoredKit(): DshKit {
224
352
  const deepseek = loadVendoredDeepseek()
353
+ const protocolFactories = {
354
+ 'openai-completions': vendoredCompletions,
355
+ 'openai-responses': vendoredResponses,
356
+ 'anthropic-messages': vendoredAnthropic,
357
+ }
225
358
  const kit: DshKit = {
226
359
  source: 'vendored',
227
360
  PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
@@ -235,11 +368,19 @@ export function loadVendoredKit(): DshKit {
235
368
  builtinProviders: vendoredCatalog.builtinProviders,
236
369
  getBuiltinProviders: vendoredCatalog.getBuiltinProviders,
237
370
  getBuiltinModels: vendoredCatalog.getBuiltinModels,
238
- protocolFactories: {
239
- 'openai-completions': vendoredCompletions,
240
- 'openai-responses': vendoredResponses,
241
- 'anthropic-messages': vendoredAnthropic,
371
+ protocolFactories,
372
+ resolveProfiles: (providers) =>
373
+ resolveProfilesFallback(providers, {
374
+ createProvider: vendoredPiAi.createProvider,
375
+ protocolFactories,
376
+ resolveRetryPolicy: vendoredLlm.resolveRetryPolicy,
377
+ }),
378
+ auth: {
379
+ credentialStoreFrom: (ctx) =>
380
+ inlineCredentialStoreFrom(ctx, vendoredPiAiAdapter.recordKeyFor),
381
+ authContextFrom: inlineAuthContextFrom,
242
382
  },
383
+ resolveImageAttachmentAccess: vendoredLlm.resolveImageAttachmentAccess,
243
384
  ...(deepseek === undefined ? {} : { deepseek }),
244
385
  }
245
386
  assertKitShape(kit, 'vendored')
@@ -269,9 +410,25 @@ export async function resolveDshKit(): Promise<{
269
410
  const diagnostics: string[] = []
270
411
  const anchor = dshTreeAnchor()
271
412
  if (anchor !== undefined) {
413
+ const treePkgDir = join(anchor, 'node_modules', '@deepseek-ai', 'dsh-llm-pi-ai')
272
414
  try {
273
- const kit = kitFromTree(await importTreeModules(anchor))
415
+ const mods = await importTreeModules(anchor)
416
+ const [auth, config] = await Promise.all([
417
+ probeSubmodule(treePkgDir, 'auth'),
418
+ probeSubmodule(treePkgDir, 'config'),
419
+ ])
420
+ const kit = kitFromTree(mods, auth, config)
274
421
  assertKitShape(kit, 'dsh-tree')
422
+ if (auth === undefined) {
423
+ diagnostics.push(
424
+ 'dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现',
425
+ )
426
+ }
427
+ if (config === undefined) {
428
+ diagnostics.push(
429
+ 'dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)',
430
+ )
431
+ }
275
432
  const tree = await importTreeDeepseek(anchor)
276
433
  if (tree.kit !== undefined) {
277
434
  return { kit: { ...kit, deepseek: tree.kit }, diagnostics }
package/src/service.ts CHANGED
@@ -127,6 +127,32 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
127
127
  profiles,
128
128
  resolveApiKey: makeResolveApiKey(ctx, kit),
129
129
  resolveAttachments: () => ctx.get('attachments'),
130
+ // 0.1.2-alpha.1 必需:登录/OAuth 类 provider 与 pi-ai 自有凭据写入的落点。
131
+ // dsh 树 dev 布局(src/auth.ts 存在)时为官方助手,其余形态为内联等价
132
+ // 实现(resolve-dsh.ts 探测,见 auth-inline.ts)。
133
+ auth: {
134
+ credentials: kit.auth.credentialStoreFrom(ctx),
135
+ authContext: kit.auth.authContextFrom(ctx),
136
+ },
137
+ // 官方接线范式(llm-deepseek/src/index.ts:447-452):附件宿主路径 →
138
+ // 当前模型工具执行世界的只读路径桥(经 ctx.fs 的 host→执行世界映射)。
139
+ resolveImageAccess: (attachments, ref) =>
140
+ kit.resolveImageAttachmentAccess(
141
+ attachments,
142
+ (hostPath) =>
143
+ (
144
+ ctx.get('fs') as
145
+ | { processPathFromHostPath(hostPath: string): string | undefined }
146
+ | undefined
147
+ )?.processPathFromHostPath(hostPath),
148
+ ref,
149
+ ),
150
+ onReplayDegrade: ({ provider, model, reason }) => {
151
+ logger.warn(
152
+ `llm-pi: 助手历史中 "${provider}/${model}" 的回放状态不可用,` +
153
+ `该消息按 provider 中立内容发送(${reason})`,
154
+ )
155
+ },
130
156
  })
131
157
 
132
158
  const storedApiKey = async (provider: string | undefined): Promise<string | undefined> => {