@dsh-plus/llm-pi 0.1.4 → 0.1.6

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/src/profiles.ts CHANGED
@@ -14,18 +14,22 @@
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'
31
35
  import type { DshKit } from './resolve-dsh.ts'
@@ -52,7 +56,9 @@ function invalid(provider: string, detail: string): never {
52
56
  }
53
57
 
54
58
  /** 条目的声明模态;缺省/空数组都视为"无答案",交下一级(同官方 declaredInput)。 */
55
- function declaredInput(configured: readonly ('text' | 'image')[] | undefined): ('text' | 'image')[] | undefined {
59
+ function declaredInput(
60
+ configured: readonly ('text' | 'image')[] | undefined,
61
+ ): ('text' | 'image')[] | undefined {
56
62
  return configured === undefined || configured.length === 0 ? undefined : [...configured]
57
63
  }
58
64
 
@@ -73,7 +79,9 @@ function resolveModelReasoning(
73
79
  reasoning: base.reasoning,
74
80
  ...(base.thinkingLevelMap === undefined
75
81
  ? {}
76
- : { thinkingLevelMap: base.thinkingLevelMap as Record<string, string | null> }),
82
+ : {
83
+ thinkingLevelMap: base.thinkingLevelMap as Record<string, string | null>,
84
+ }),
77
85
  }
78
86
  }
79
87
  if (efforts === false) return { reasoning: false }
@@ -84,12 +92,15 @@ function resolveModelReasoning(
84
92
  const wire = (efforts as Record<string, string | null | undefined>)[level]
85
93
  if (wire === undefined) continue
86
94
  if (wire === null) {
87
- if (level !== 'off') invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 需要线值;仅 off 可留空`)
95
+ if (level !== 'off')
96
+ invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 需要线值;仅 off 可留空`)
88
97
  } else if (wire.length === 0) {
89
98
  invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 不能为空字符串`)
90
99
  }
91
100
  }
92
- const declared = THINKING_LEVELS.filter((level) => (efforts as Record<string, unknown>)[level] !== undefined)
101
+ const declared = THINKING_LEVELS.filter(
102
+ (level) => (efforts as Record<string, unknown>)[level] !== undefined,
103
+ )
93
104
  if (!declared.some((level) => level !== 'off')) {
94
105
  invalid(provider, `model "${entry.id}" reasoningEfforts 只有 off;声明思考档位或置 false`)
95
106
  }
@@ -107,7 +118,10 @@ function harnessApiKeyAuth(name: string) {
107
118
  return {
108
119
  name,
109
120
  resolve: ({ credential }: { credential?: { key?: string } }) =>
110
- Promise.resolve({ auth: credential?.key === undefined ? {} : { apiKey: credential.key }, source: name }),
121
+ Promise.resolve({
122
+ auth: credential?.key === undefined ? {} : { apiKey: credential.key },
123
+ source: name,
124
+ }),
111
125
  }
112
126
  }
113
127
 
@@ -142,7 +156,9 @@ function materializeModel(
142
156
  const api = profile.api ?? base.api ?? routeApi
143
157
  if (api === undefined) {
144
158
  if (deps.lenient) {
145
- deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 无法获得 api(继承源缺失),已跳过该模型`)
159
+ deps.warn?.(
160
+ `llm-pi: provider "${route}" model "${entry.id}" 无法获得 api(继承源缺失),已跳过该模型`,
161
+ )
146
162
  return null
147
163
  }
148
164
  invalid(route, `model "${entry.id}" 需要 api:继承源未提供,请在 route 上设置 api`)
@@ -150,16 +166,27 @@ function materializeModel(
150
166
  const baseUrl = profile.baseURL ?? base.baseUrl ?? providerBaseUrl
151
167
  if (baseUrl === undefined) {
152
168
  if (deps.lenient) {
153
- deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 无法获得 baseURL(继承源缺失),已跳过该模型`)
169
+ deps.warn?.(
170
+ `llm-pi: provider "${route}" model "${entry.id}" 无法获得 baseURL(继承源缺失),已跳过该模型`,
171
+ )
154
172
  return null
155
173
  }
156
174
  invalid(route, `model "${entry.id}" 需要 baseURL:继承源未提供,请在 route 上设置 baseURL`)
157
175
  }
158
- const contextWindow = entry.contextWindow ?? base.contextWindow ?? profile.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW
159
- const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
176
+ const contextWindow =
177
+ entry.contextWindow ??
178
+ base.contextWindow ??
179
+ profile.defaultContextWindow ??
180
+ DEFAULT_CONTEXT_WINDOW
181
+ const maxTokens =
182
+ entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS
160
183
  if (entry.maxTokens !== undefined) configuredMaxTokens.set(entry.id, entry.maxTokens)
161
184
  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}"`)
185
+ validateCompat(
186
+ api,
187
+ entry.compat as Record<string, unknown> | undefined,
188
+ `provider "${route}" model "${entry.id}"`,
189
+ )
163
190
  const compat = mergeCompat(
164
191
  base.api === api ? base.compat : undefined,
165
192
  profile.compat as Record<string, unknown> | undefined,
@@ -188,7 +215,10 @@ function materializeRouteModels(
188
215
  profile: ProviderProfileConfig,
189
216
  deps: BuildDeps,
190
217
  defaultInput: ('text' | 'image')[],
191
- ): { models: MaterializedModel[]; configuredMaxTokens: Map<string, number> } | null {
218
+ ): {
219
+ models: MaterializedModel[]
220
+ configuredMaxTokens: Map<string, number>
221
+ } | null {
192
222
  const configuredMaxTokens = new Map<string, number>()
193
223
  const providerBaseUrl =
194
224
  profile.extends === undefined ? undefined : builtinProviderBaseUrl(deps.kit, profile.extends)
@@ -247,14 +277,19 @@ function materializeRouteModels(
247
277
  .filter((model): model is MaterializedModel => model !== null)
248
278
  if (models.length === 0) {
249
279
  if (deps.lenient) {
250
- deps.warn?.(`llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`)
280
+ deps.warn?.(
281
+ `llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`,
282
+ )
251
283
  return null
252
284
  }
253
285
  invalid(route, 'route 内没有可服务的模型')
254
286
  }
255
287
  const finalApis = new Set(models.map((m) => m.api))
256
288
  if (finalApis.size > 1) {
257
- invalid(route, `route 内模型协议不一致(${[...finalApis].join(', ')});一个 route 只能服务一种协议`)
289
+ invalid(
290
+ route,
291
+ `route 内模型协议不一致(${[...finalApis].join(', ')});一个 route 只能服务一种协议`,
292
+ )
258
293
  }
259
294
  return { models, configuredMaxTokens }
260
295
  }
@@ -274,8 +309,10 @@ export function buildProfiles(
274
309
  // 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
275
310
  // route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
276
311
  // 暴露(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 为空')
312
+ if (profile.baseURL !== undefined && profile.baseURL.length === 0)
313
+ invalid(route, 'baseURL 为空')
314
+ if (profile.displayName !== undefined && profile.displayName.length === 0)
315
+ invalid(route, 'displayName 为空')
279
316
  const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
280
317
  if (
281
318
  !Number.isFinite(streamIdleTimeoutMs) ||
@@ -290,9 +327,15 @@ export function buildProfiles(
290
327
  const catalog = materializeRouteModels(route, profile, deps, defaultInput)
291
328
  if (catalog === null) continue // lenient 下 route 无模型可服务,跳过注册
292
329
  const api = catalog.models[0]?.api
293
- const factory = api === undefined ? undefined : deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
330
+ const factory =
331
+ api === undefined
332
+ ? undefined
333
+ : deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
294
334
  if (factory === undefined) {
295
- invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`)
335
+ invalid(
336
+ route,
337
+ `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`,
338
+ )
296
339
  }
297
340
  const piProvider = deps.kit.createProvider({
298
341
  id: route,
@@ -319,7 +362,9 @@ export function buildProfiles(
319
362
  ),
320
363
  ...(profile.headers === undefined ? {} : { headers: { ...profile.headers } }),
321
364
  ...(profile.reasoning === undefined ? {} : { reasoning: profile.reasoning }),
322
- ...(profile.thinkingBudgets === undefined ? {} : { thinkingBudgets: { ...profile.thinkingBudgets } }),
365
+ ...(profile.thinkingBudgets === undefined
366
+ ? {}
367
+ : { thinkingBudgets: { ...profile.thinkingBudgets } }),
323
368
  ...(profile.cacheRetention === undefined ? {} : { cacheRetention: profile.cacheRetention }),
324
369
  ...(profile.transport === undefined ? {} : { transport: profile.transport }),
325
370
  ...(profile.timeoutMs === undefined ? {} : { timeoutMs: profile.timeoutMs }),
@@ -16,18 +16,16 @@
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'
21
19
  import type * as DshLlm from '@deepseek-ai/dsh-llm'
20
+ import * as vendoredLlm from '@deepseek-ai/dsh-llm'
21
+ import type { PiAiAdapter as PiAiAdapterType } from '@deepseek-ai/dsh-llm-pi-ai'
22
+ import * as vendoredPiAiAdapter from '@deepseek-ai/dsh-llm-pi-ai'
22
23
  import type * as PiAi from '@earendil-works/pi-ai'
23
-
24
24
  import * as vendoredPiAi from '@earendil-works/pi-ai'
25
- import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
26
25
  import { anthropicMessagesApi as vendoredAnthropic } from '@earendil-works/pi-ai/api/anthropic-messages.lazy'
27
26
  import { openAICompletionsApi as vendoredCompletions } from '@earendil-works/pi-ai/api/openai-completions.lazy'
28
27
  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'
28
+ import * as vendoredCatalog from '@earendil-works/pi-ai/providers/all'
31
29
 
32
30
  import type { ProtocolId } from './config.ts'
33
31
 
@@ -55,7 +53,13 @@ function assertKitShape(kit: DshKit, origin: string): void {
55
53
  const problems: string[] = []
56
54
  if (typeof kit.PiAiAdapter !== 'function') problems.push('PiAiAdapter 不是类')
57
55
  else {
58
- for (const method of ['current', 'stream', 'listModels', 'resolveModel', 'providerInfo'] as const) {
56
+ for (const method of [
57
+ 'current',
58
+ 'stream',
59
+ 'listModels',
60
+ 'resolveModel',
61
+ 'providerInfo',
62
+ ] as const) {
59
63
  if (typeof (kit.PiAiAdapter.prototype as Record<string, unknown>)[method] !== 'function') {
60
64
  problems.push(`PiAiAdapter.prototype.${method} 缺失`)
61
65
  }
@@ -182,7 +186,10 @@ function dshTreeAnchor(): string | undefined {
182
186
  * 两者都过不了形状自检时抛错(调用方应记日志并放弃注册 route)。
183
187
  * 返回的 diagnostics 记录回退原因,供配置卡片与日志展示。
184
188
  */
185
- export async function resolveDshKit(): Promise<{ kit: DshKit; diagnostics: string[] }> {
189
+ export async function resolveDshKit(): Promise<{
190
+ kit: DshKit
191
+ diagnostics: string[]
192
+ }> {
186
193
  const diagnostics: string[] = []
187
194
  const anchor = dshTreeAnchor()
188
195
  if (anchor !== undefined) {
package/src/service.ts CHANGED
@@ -18,10 +18,10 @@ import { deepEqualJson, installSettingsSection } from '@deepseek-ai/dsh-settings
18
18
 
19
19
  import { hasBuiltinProvider } from './catalog/builtin.ts'
20
20
  import { ModelsDevSource } from './catalog/models-dev.ts'
21
- import { Config, SETTINGS_NS, type LlmPiConfig } from './config.ts'
21
+ import { Config, type LlmPiConfig, SETTINGS_NS } from './config.ts'
22
22
  import { discoverModels } from './discovery.ts'
23
23
  import { assertServiceable, buildProfiles } from './profiles.ts'
24
- import { resolveDshKit, type DshKit } from './resolve-dsh.ts'
24
+ import { type DshKit, resolveDshKit } from './resolve-dsh.ts'
25
25
 
26
26
  export interface LlmPiRuntime {
27
27
  /** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */
@@ -46,7 +46,10 @@ function registrationFacts(profiles: ReadonlyMap<string, ResolvedPiAiProviderPro
46
46
 
47
47
  /** 凭据解析(逐行对齐官方 resolveApiKey):凭据服务优先,缺失时启动环境兜底。 */
48
48
  function makeResolveApiKey(ctx: Context, kit: DshKit) {
49
- return async (provider: string, profile: ResolvedPiAiProviderProfile): Promise<string | undefined> => {
49
+ return async (
50
+ provider: string,
51
+ profile: ResolvedPiAiProviderProfile,
52
+ ): Promise<string | undefined> => {
50
53
  const ref = profile.apiKeyEnv
51
54
  if (ref === undefined) return undefined
52
55
  const credentials = ctx.get('credentials')
@@ -54,7 +57,8 @@ function makeResolveApiKey(ctx: Context, kit: DshKit) {
54
57
  credentials !== undefined
55
58
  ? (await credentials.resolve(ref as CredentialRef))?.value
56
59
  : launchEnvironmentOf(ctx).get(ref as unknown as string)?.value
57
- if (hit !== undefined && hit.length > 0) return kit.assertUsableApiKey(hit, 'llm-pi', ref as unknown as string)
60
+ if (hit !== undefined && hit.length > 0)
61
+ return kit.assertUsableApiKey(hit, 'llm-pi', ref as unknown as string)
58
62
  throw new kit.LlmError(
59
63
  `llm-pi: provider route "${provider}" 的凭据引用 ${String(ref)} 未解析到值——` +
60
64
  '请经凭据服务(web Models 页)存放或导出环境变量;仅当该 provider 应使用 pi-ai 自有环境发现时才移除 apiKeyEnv',
@@ -92,7 +96,11 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
92
96
  const raw = current()
93
97
  if (raw === lastRaw && memoized !== undefined) return memoized
94
98
  const next = raw.enabled
95
- ? buildProfiles(raw.providers, { ...deps, lenient: true, warn: (message) => logger.warn(message) })
99
+ ? buildProfiles(raw.providers, {
100
+ ...deps,
101
+ lenient: true,
102
+ warn: (message) => logger.warn(message),
103
+ })
96
104
  : new Map()
97
105
  lastRaw = raw
98
106
  memoized = next
@@ -132,7 +140,10 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
132
140
  let registrations: RegistrationGroup[] | undefined
133
141
  let registeredFacts: unknown
134
142
 
135
- const registerGroup = (routes: string[], fallback: (error: unknown) => void): RegistrationGroup[] => {
143
+ const registerGroup = (
144
+ routes: string[],
145
+ fallback: (error: unknown) => void,
146
+ ): RegistrationGroup[] => {
136
147
  try {
137
148
  const handle = ctx.llm.registerAdapter(routes, adapter as never)
138
149
  return [{ routes, handle }]
@@ -144,7 +155,9 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
144
155
  const handle = ctx.llm.registerAdapter([route], adapter as never)
145
156
  groups.push({ routes: [route], handle })
146
157
  } catch (routeError) {
147
- logger.error(`llm-pi: route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`)
158
+ logger.error(
159
+ `llm-pi: route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`,
160
+ )
148
161
  logger.error(routeError)
149
162
  }
150
163
  }
@@ -168,9 +181,11 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
168
181
  })
169
182
  } else {
170
183
  try {
171
- registrations[0]!.handle.replace(routes)
172
- for (let i = 1; i < registrations.length; i += 1) registrations[i]!.handle.replace([])
173
- registrations = [{ routes, handle: registrations[0]!.handle }]
184
+ const [first, ...rest] = registrations
185
+ if (first === undefined) throw new Error('registrations 为空(此前整批注册全部失败)')
186
+ first.handle.replace(routes)
187
+ for (const group of rest) group.handle.replace([])
188
+ registrations = [{ routes, handle: first.handle }]
174
189
  } catch (error) {
175
190
  // 原子 replace 被拒(含冲突):保留此前注册(官方同款护栏)
176
191
  logger.error('llm-pi: 更新被拒,保留此前注册的 route')
@@ -204,11 +219,9 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
204
219
  ensureRegistration()
205
220
  ensureDirectory()
206
221
 
207
- let settingsBound = false
208
222
  installSettingsSection(ctx, SETTINGS_NS, Config, config, {
209
223
  validate: (cfg: LlmPiConfig) => assertServiceable(cfg, deps),
210
224
  setSource: (source: () => LlmPiConfig) => {
211
- settingsBound = true
212
225
  current = source
213
226
  },
214
227
  onChange: () => {