@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/lib/client.js +6 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +11 -9
- package/package.json +16 -5
- package/src/catalog/builtin.ts +17 -4
- package/src/catalog/models-dev.ts +13 -6
- package/src/client/api.ts +17 -4
- package/src/client/card.tsx +57 -20
- package/src/client/client.ts +20 -9
- package/src/client/constants.ts +5 -1
- package/src/client/draft.ts +20 -6
- package/src/client/fields.tsx +3 -1
- package/src/client/i18n.ts +4 -2
- package/src/client/scope.ts +22 -3
- package/src/client/views/compat.tsx +50 -44
- package/src/client/views/models.tsx +26 -7
- package/src/client/views/provider-fields.tsx +15 -3
- package/src/client/views/providers.tsx +31 -26
- package/src/compat.ts +18 -4
- package/src/config.ts +42 -11
- package/src/discovery.ts +30 -9
- package/src/index.ts +6 -3
- package/src/inherit.ts +11 -3
- package/src/profiles.ts +66 -21
- package/src/resolve-dsh.ts +15 -8
- package/src/service.ts +25 -12
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 {
|
|
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(
|
|
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
|
-
: {
|
|
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')
|
|
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(
|
|
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({
|
|
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?.(
|
|
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?.(
|
|
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 =
|
|
159
|
-
|
|
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(
|
|
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
|
-
): {
|
|
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?.(
|
|
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(
|
|
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)
|
|
278
|
-
|
|
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 =
|
|
330
|
+
const factory =
|
|
331
|
+
api === undefined
|
|
332
|
+
? undefined
|
|
333
|
+
: deps.kit.protocolFactories[api as keyof DshKit['protocolFactories']]
|
|
294
334
|
if (factory === undefined) {
|
|
295
|
-
invalid(
|
|
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
|
|
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 }),
|
package/src/resolve-dsh.ts
CHANGED
|
@@ -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
|
|
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 [
|
|
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<{
|
|
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,
|
|
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 {
|
|
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 (
|
|
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)
|
|
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, {
|
|
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 = (
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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: () => {
|