@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.
- package/lib/client.js +1881 -1845
- package/lib/index.d.ts +21 -4
- package/lib/index.js +573 -135
- package/package.json +16 -11
- package/src/catalog/builtin.ts +17 -4
- package/src/catalog/models-dev.ts +13 -6
- package/src/catalog/official.ts +71 -0
- 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 +110 -38
- 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 +147 -11
- package/src/deepseek-routes.ts +139 -0
- package/src/discovery.ts +63 -11
- package/src/index.ts +6 -3
- package/src/inherit.ts +11 -3
- package/src/profiles-deepseek.ts +282 -0
- package/src/profiles.ts +71 -21
- package/src/resolve-dsh.ts +104 -8
- package/src/service.ts +64 -13
package/src/compat.ts
CHANGED
|
@@ -90,7 +90,13 @@ export function compatFieldSpec(api: ProtocolId, field: string): CompatValue | u
|
|
|
90
90
|
return FIELDS_BY_PROTOCOL[api][field]
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function checkValue(
|
|
93
|
+
function checkValue(
|
|
94
|
+
_api: ProtocolId,
|
|
95
|
+
field: string,
|
|
96
|
+
spec: CompatValue,
|
|
97
|
+
value: unknown,
|
|
98
|
+
where: string,
|
|
99
|
+
): void {
|
|
94
100
|
if (spec === 'boolean') {
|
|
95
101
|
if (typeof value !== 'boolean') throw new Error(`${where}: compat.${field} 必须是布尔值`)
|
|
96
102
|
return
|
|
@@ -102,7 +108,9 @@ function checkValue(api: ProtocolId, field: string, spec: CompatValue, value: un
|
|
|
102
108
|
return
|
|
103
109
|
}
|
|
104
110
|
if (typeof value !== 'string' || !spec.includes(value)) {
|
|
105
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
`${where}: compat.${field} 必须是 ${spec.map((v) => JSON.stringify(v)).join(' | ')} 之一`,
|
|
113
|
+
)
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -110,11 +118,17 @@ function checkValue(api: ProtocolId, field: string, spec: CompatValue, value: un
|
|
|
110
118
|
* 校验一份 compat 字典对指定协议合法:未知协议/未知键拒绝(对比官方的静默丢弃),
|
|
111
119
|
* 已知键校验值类型/枚举。undefined 值视为未设置,跳过(语义同 pi-ai 的 ??)。
|
|
112
120
|
*/
|
|
113
|
-
export function validateCompat(
|
|
121
|
+
export function validateCompat(
|
|
122
|
+
api: string,
|
|
123
|
+
compat: Record<string, unknown> | undefined,
|
|
124
|
+
where: string,
|
|
125
|
+
): void {
|
|
114
126
|
if (compat === undefined) return
|
|
115
127
|
const fields = FIELDS_BY_PROTOCOL[api as ProtocolId]
|
|
116
128
|
if (fields === undefined) {
|
|
117
|
-
throw new Error(
|
|
129
|
+
throw new Error(
|
|
130
|
+
`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(FIELDS_BY_PROTOCOL).join(', ')})`,
|
|
131
|
+
)
|
|
118
132
|
}
|
|
119
133
|
for (const [key, value] of Object.entries(compat)) {
|
|
120
134
|
const spec = fields[key]
|
package/src/config.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - provider/model 均支持 extends 继承(见 inherit.ts)。
|
|
9
9
|
* @module llm-pi/config
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
|
+
import z from '@deepseek-ai/schemastery'
|
|
13
14
|
|
|
14
15
|
import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
15
16
|
|
|
@@ -17,7 +18,11 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
|
17
18
|
export const SETTINGS_NS = settingsNamespace(NS_LITERAL)
|
|
18
19
|
|
|
19
20
|
/** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
|
|
20
|
-
export const PROTOCOL_IDS = [
|
|
21
|
+
export const PROTOCOL_IDS = [
|
|
22
|
+
'openai-completions',
|
|
23
|
+
'openai-responses',
|
|
24
|
+
'anthropic-messages',
|
|
25
|
+
] as const
|
|
21
26
|
export type ProtocolId = (typeof PROTOCOL_IDS)[number]
|
|
22
27
|
|
|
23
28
|
/** pi-ai 思考档位,升级序。 */
|
|
@@ -25,6 +30,15 @@ export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhig
|
|
|
25
30
|
|
|
26
31
|
export const MODALITIES = ['text', 'image'] as const
|
|
27
32
|
|
|
33
|
+
/** 路由适配器种类:pi = PiAiAdapter(默认);deepseek = 官方 DeepSeekAdapter(文件通道)。 */
|
|
34
|
+
export const ADAPTER_KINDS = ['pi', 'deepseek'] as const
|
|
35
|
+
export type AdapterKind = (typeof ADAPTER_KINDS)[number]
|
|
36
|
+
|
|
37
|
+
/** deepseek 路由的思考/推理档位(线协议枚举,对齐官方 llm-deepseek)。 */
|
|
38
|
+
export const DEEPSEEK_THINKING = ['enabled', 'disabled'] as const
|
|
39
|
+
export const DEEPSEEK_REASONING_EFFORTS = ['off', 'low', 'high', 'max'] as const
|
|
40
|
+
export const DEEPSEEK_IMAGE_DETAILS = ['auto', 'low'] as const
|
|
41
|
+
|
|
28
42
|
export const DEFAULT_CONTEXT_WINDOW = 262144
|
|
29
43
|
export const DEFAULT_MAX_TOKENS = 32768
|
|
30
44
|
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000
|
|
@@ -51,10 +65,15 @@ export interface ModelEntryConfig {
|
|
|
51
65
|
input?: Modality[]
|
|
52
66
|
reasoningEfforts?: false | ReasoningEfforts
|
|
53
67
|
compat?: Record<string, unknown>
|
|
68
|
+
/** 以下仅 adapter: deepseek 的 route 有效(官方 llm-deepseek 目录字段)。 */
|
|
69
|
+
imagePixelBudget?: number
|
|
70
|
+
imageMaxBytes?: number
|
|
71
|
+
imageDetail?: 'auto' | 'low'
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
/** 单个 provider route 配置(providers 字典的值)。 */
|
|
57
75
|
export interface ProviderProfileConfig {
|
|
76
|
+
adapter?: AdapterKind
|
|
58
77
|
extends?: string
|
|
59
78
|
displayName?: string
|
|
60
79
|
api?: ProtocolId
|
|
@@ -66,7 +85,12 @@ export interface ProviderProfileConfig {
|
|
|
66
85
|
defaultMaxTokens?: number
|
|
67
86
|
defaultInput?: Modality[]
|
|
68
87
|
reasoning?: ThinkingLevel
|
|
69
|
-
thinkingBudgets?: {
|
|
88
|
+
thinkingBudgets?: {
|
|
89
|
+
minimal: number
|
|
90
|
+
low: number
|
|
91
|
+
medium: number
|
|
92
|
+
high: number
|
|
93
|
+
}
|
|
70
94
|
cacheRetention?: 'none' | 'short' | 'long'
|
|
71
95
|
transport?: 'sse' | 'websocket' | 'websocket-cached' | 'auto'
|
|
72
96
|
timeoutMs?: number
|
|
@@ -75,6 +99,18 @@ export interface ProviderProfileConfig {
|
|
|
75
99
|
maxRequestImageBytes?: number
|
|
76
100
|
retryPolicy?: unknown
|
|
77
101
|
models?: ModelEntryConfig[]
|
|
102
|
+
/** 以下仅 adapter: deepseek 的 route 有效(透传官方 llm-deepseek 同名策略)。 */
|
|
103
|
+
thinking?: 'enabled' | 'disabled'
|
|
104
|
+
reasoningEffort?: 'off' | 'low' | 'high' | 'max'
|
|
105
|
+
maxRequestFilesBytes?: number
|
|
106
|
+
maxInlineRequestImageBytes?: number
|
|
107
|
+
maxImagesPerRequest?: number
|
|
108
|
+
imageOffloadByteQuantum?: number
|
|
109
|
+
inlineImageOffloadByteQuantum?: number
|
|
110
|
+
imageOffloadCountQuantum?: number
|
|
111
|
+
filesApiTimeoutMs?: number
|
|
112
|
+
fileExpiresAfterSeconds?: number
|
|
113
|
+
fileRefreshMarginSeconds?: number
|
|
78
114
|
}
|
|
79
115
|
|
|
80
116
|
/** 插件配置根。 */
|
|
@@ -104,28 +140,63 @@ const modelEntry = z.object({
|
|
|
104
140
|
id: z.string().required().description('模型 id(发送给 provider 的标识)'),
|
|
105
141
|
extends: z
|
|
106
142
|
.string()
|
|
107
|
-
.description(
|
|
143
|
+
.description(
|
|
144
|
+
'继承源:"provider/model" 或裸 model id(随 provider 级 extends 源);缺省先查内置目录同名模型',
|
|
145
|
+
),
|
|
108
146
|
name: z.string().description('选择器显示名;缺省继承内置目录名,再退化为 id'),
|
|
109
147
|
contextWindow: z.number().step(1).min(1).description('上下文容量(覆盖继承值)'),
|
|
110
|
-
maxTokens: z
|
|
111
|
-
|
|
148
|
+
maxTokens: z
|
|
149
|
+
.number()
|
|
150
|
+
.step(1)
|
|
151
|
+
.min(1)
|
|
152
|
+
.description('输出能力上限;显式配置同时成为无 cap 请求的默认 cap'),
|
|
153
|
+
input: z
|
|
154
|
+
.array(z.union(MODALITIES))
|
|
155
|
+
.description('请求模态;缺省继承内置目录,再退化 route defaultInput'),
|
|
112
156
|
reasoningEfforts: z
|
|
113
157
|
.union([z.const(false), reasoningEfforts])
|
|
114
158
|
.description('可选 reasoning 档位:false=非推理模型;dict=档位→线值映射;缺省继承内置目录能力'),
|
|
115
159
|
compat: compatDict.description('模型级 compat(字段级合并,压过 route 级与继承值)'),
|
|
160
|
+
imagePixelBudget: z
|
|
161
|
+
.number()
|
|
162
|
+
.step(1)
|
|
163
|
+
.min(1)
|
|
164
|
+
.description('(仅 adapter: deepseek)单张请求图片的总像素预算;缺省继承官方目录'),
|
|
165
|
+
imageMaxBytes: z
|
|
166
|
+
.number()
|
|
167
|
+
.step(1)
|
|
168
|
+
.min(1)
|
|
169
|
+
.description('(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录'),
|
|
170
|
+
imageDetail: z
|
|
171
|
+
.union(DEEPSEEK_IMAGE_DETAILS)
|
|
172
|
+
.description('(仅 adapter: deepseek)图片细节档位;low 使用 512x512 像素预算'),
|
|
116
173
|
})
|
|
117
174
|
|
|
118
175
|
const providerProfile = z.object({
|
|
176
|
+
adapter: z
|
|
177
|
+
.union(ADAPTER_KINDS)
|
|
178
|
+
.description(
|
|
179
|
+
'路由适配器:pi=PiAiAdapter(默认,三协议);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)',
|
|
180
|
+
)
|
|
181
|
+
.default('pi'),
|
|
119
182
|
extends: z
|
|
120
183
|
.string()
|
|
121
|
-
.description(
|
|
184
|
+
.description(
|
|
185
|
+
'provider 级继承:内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源',
|
|
186
|
+
),
|
|
122
187
|
displayName: z.string().description('选择器显示名;缺省为 route 键'),
|
|
123
|
-
api: z
|
|
188
|
+
api: z
|
|
189
|
+
.union(PROTOCOL_IDS)
|
|
190
|
+
.description('线协议;缺省逐模型取继承值的 api,全部一致时作为 route 协议'),
|
|
124
191
|
baseURL: z.string().description('端点;缺省继承 extends 源 provider 的端点'),
|
|
125
192
|
apiKeyEnv: z.string().role('credential-ref').description('凭据引用名(凭据服务/环境变量)'),
|
|
126
193
|
headers: z.dict(z.string()).description('provider 请求头(Harness 署名头保留名优先)'),
|
|
127
194
|
compat: compatDict.description('route 级 compat 默认(逐模型按字段生效)'),
|
|
128
|
-
defaultContextWindow: z
|
|
195
|
+
defaultContextWindow: z
|
|
196
|
+
.number()
|
|
197
|
+
.step(1)
|
|
198
|
+
.min(1)
|
|
199
|
+
.description('模型与继承源都未标注时的上下文容量兜底'),
|
|
129
200
|
defaultMaxTokens: z.number().step(1).min(1).description('模型与继承源都未标注时的输出能力兜底'),
|
|
130
201
|
defaultInput: z
|
|
131
202
|
.array(z.union(MODALITIES))
|
|
@@ -146,12 +217,77 @@ const providerProfile = z.object({
|
|
|
146
217
|
.natural()
|
|
147
218
|
.description('单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)'),
|
|
148
219
|
retryPolicy: z.any().description('provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)'),
|
|
149
|
-
|
|
220
|
+
thinking: z
|
|
221
|
+
.union(DEEPSEEK_THINKING)
|
|
222
|
+
.description('(仅 adapter: deepseek)思考模式开关;缺省用提供商默认'),
|
|
223
|
+
reasoningEffort: z
|
|
224
|
+
.union(DEEPSEEK_REASONING_EFFORTS)
|
|
225
|
+
.description('(仅 adapter: deepseek)推理档位;缺省为 high'),
|
|
226
|
+
maxRequestFilesBytes: z
|
|
227
|
+
.number()
|
|
228
|
+
.step(1)
|
|
229
|
+
.min(1)
|
|
230
|
+
.description('(仅 adapter: deepseek)单请求文件引用图片累积上限字节;缺省 128MiB'),
|
|
231
|
+
maxInlineRequestImageBytes: z
|
|
232
|
+
.number()
|
|
233
|
+
.step(1)
|
|
234
|
+
.min(1)
|
|
235
|
+
.description('(仅 adapter: deepseek)文件通道降级后 base64 图片载荷上限字节;缺省 20MiB'),
|
|
236
|
+
maxImagesPerRequest: z
|
|
237
|
+
.number()
|
|
238
|
+
.step(1)
|
|
239
|
+
.min(1)
|
|
240
|
+
.description(
|
|
241
|
+
'(仅 adapter: deepseek)单请求图片数量上限;缺省 600(调低时需同步调低 imageOffloadCountQuantum)',
|
|
242
|
+
),
|
|
243
|
+
imageOffloadByteQuantum: z
|
|
244
|
+
.number()
|
|
245
|
+
.step(1)
|
|
246
|
+
.min(1)
|
|
247
|
+
.description(
|
|
248
|
+
'(仅 adapter: deepseek)文件引用超限后的原始字节削减步长;缺省 64MiB,不可超过 maxRequestFilesBytes',
|
|
249
|
+
),
|
|
250
|
+
inlineImageOffloadByteQuantum: z
|
|
251
|
+
.number()
|
|
252
|
+
.step(1)
|
|
253
|
+
.min(1)
|
|
254
|
+
.description(
|
|
255
|
+
'(仅 adapter: deepseek)base64 降级超限后的削减步长;缺省 10MiB,不可超过 maxInlineRequestImageBytes',
|
|
256
|
+
),
|
|
257
|
+
imageOffloadCountQuantum: z
|
|
258
|
+
.number()
|
|
259
|
+
.step(1)
|
|
260
|
+
.min(1)
|
|
261
|
+
.description(
|
|
262
|
+
'(仅 adapter: deepseek)图片数量超限后的削减步长;缺省 20,不可超过 maxImagesPerRequest',
|
|
263
|
+
),
|
|
264
|
+
filesApiTimeoutMs: z
|
|
265
|
+
.number()
|
|
266
|
+
.min(Number.MIN_VALUE)
|
|
267
|
+
.max(MAX_TIMER_DELAY_MS)
|
|
268
|
+
.description('(仅 adapter: deepseek)单次 Files API 图片解析超时毫秒;缺省 60s'),
|
|
269
|
+
fileExpiresAfterSeconds: z
|
|
270
|
+
.number()
|
|
271
|
+
.step(1)
|
|
272
|
+
.min(3600)
|
|
273
|
+
.max(2592000)
|
|
274
|
+
.description('(仅 adapter: deepseek)上传文件的远端存活秒数;缺省 7 天'),
|
|
275
|
+
fileRefreshMarginSeconds: z
|
|
276
|
+
.number()
|
|
277
|
+
.step(1)
|
|
278
|
+
.min(0)
|
|
279
|
+
.description('(仅 adapter: deepseek)文件过期前提前重传的余量秒数;缺省 3600'),
|
|
280
|
+
models: z
|
|
281
|
+
.array(modelEntry)
|
|
282
|
+
.description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
|
|
150
283
|
})
|
|
151
284
|
|
|
152
285
|
export const Config: z<LlmPiConfig> = z.object({
|
|
153
286
|
enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true),
|
|
154
|
-
catalogUrl: z
|
|
287
|
+
catalogUrl: z
|
|
288
|
+
.string()
|
|
289
|
+
.description('models.dev 目录数据端点')
|
|
290
|
+
.default('https://models.dev/api.json'),
|
|
155
291
|
catalogRefreshHours: z
|
|
156
292
|
.number()
|
|
157
293
|
.description('models.dev 自动拉取间隔小时数;0 = 不自动拉取(可手动拉取或读已有缓存)')
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deepseek 路由注册器:每条 adapter: deepseek 的 route 一个官方
|
|
3
|
+
* DeepSeekAdapter 实例(baseURL 各异,无法共享实例),注册到独立路由名。
|
|
4
|
+
*
|
|
5
|
+
* 与官方 deepseek-official 渠道的隔离:
|
|
6
|
+
* - 路由名独立(重复名注册冲突时跳过并告警,不互相覆盖);
|
|
7
|
+
* - 文件索引作用域 = sha256(baseURL + apiKey)(官方实现),中转与官方天然分池;
|
|
8
|
+
* - 适配器实例、连接事实、重试策略各自独立。
|
|
9
|
+
*
|
|
10
|
+
* 热更新:适配器 options thunk 每次操作重读当前物化产物(连接事实变化下一
|
|
11
|
+
* 请求生效);retryPolicy 是注册期捕获事实,变化时 handle.replace 原地重注册
|
|
12
|
+
* (官方同款模式);route 移除即 dispose 释放路由名。
|
|
13
|
+
* @module llm-pi/deepseek-routes
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
|
|
17
|
+
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
18
|
+
import type { AdapterRegistrationHandle } from '@deepseek-ai/dsh-llm'
|
|
19
|
+
import { deepEqualJson } from '@deepseek-ai/dsh-settings'
|
|
20
|
+
|
|
21
|
+
import type { ResolvedDeepseekRoute } from './profiles-deepseek.ts'
|
|
22
|
+
import type { DshKit } from './resolve-dsh.ts'
|
|
23
|
+
|
|
24
|
+
interface RouteRegistration {
|
|
25
|
+
handle: AdapterRegistrationHandle
|
|
26
|
+
/** 注册期捕获的重试策略(变化才 replace)。 */
|
|
27
|
+
retryPolicy: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DeepseekRegistrarDeps {
|
|
31
|
+
ctx: Context
|
|
32
|
+
kit: DshKit
|
|
33
|
+
logger: {
|
|
34
|
+
warn(message: string): void
|
|
35
|
+
error(message: string | unknown): void
|
|
36
|
+
}
|
|
37
|
+
/** 当前物化产物表(service 层按 config identity 备忘)。 */
|
|
38
|
+
routes: () => Map<string, ResolvedDeepseekRoute>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 凭据解析(逐行对齐官方 llm-deepseek resolveApiKey):凭据服务优先,启动环境兜底。 */
|
|
42
|
+
function makeResolveApiKey(ctx: Context, kit: DshKit) {
|
|
43
|
+
return async (connection: { apiKeyEnv: unknown }): Promise<string> => {
|
|
44
|
+
const ref = connection.apiKeyEnv as CredentialRef
|
|
45
|
+
const credentials = ctx.get('credentials')
|
|
46
|
+
const hit =
|
|
47
|
+
credentials !== undefined
|
|
48
|
+
? (await credentials.resolve(ref))?.value
|
|
49
|
+
: launchEnvironmentOf(ctx).get(ref as unknown as string)?.value
|
|
50
|
+
if (hit !== undefined && hit.length > 0) {
|
|
51
|
+
return kit.assertUsableApiKey(hit, 'llm-pi', ref as unknown as string)
|
|
52
|
+
}
|
|
53
|
+
throw new kit.LlmError(
|
|
54
|
+
`llm-pi: deepseek route 的凭据引用 ${String(ref)} 未解析到值——` +
|
|
55
|
+
'请经凭据服务(web Models 页)存放或导出环境变量',
|
|
56
|
+
'MISSING_CREDENTIAL',
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class DeepseekRouteRegistrar {
|
|
62
|
+
private readonly deps: DeepseekRegistrarDeps
|
|
63
|
+
private readonly registrations = new Map<string, RouteRegistration>()
|
|
64
|
+
private readonly resolveApiKey: (connection: { apiKeyEnv: unknown }) => Promise<string>
|
|
65
|
+
private userId: unknown
|
|
66
|
+
|
|
67
|
+
constructor(deps: DeepseekRegistrarDeps) {
|
|
68
|
+
this.deps = deps
|
|
69
|
+
this.resolveApiKey = makeResolveApiKey(deps.ctx, deps.kit)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 同步注册表与目标 route 集:新增注册、移除 dispose、retryPolicy 变化原地 replace。 */
|
|
73
|
+
sync(target: Map<string, ResolvedDeepseekRoute>): void {
|
|
74
|
+
for (const [route, registration] of this.registrations) {
|
|
75
|
+
if (target.has(route)) continue
|
|
76
|
+
registration.handle()
|
|
77
|
+
this.registrations.delete(route)
|
|
78
|
+
this.deps.logger.warn(`llm-pi: deepseek route "${route}" 已随配置移除而注销`)
|
|
79
|
+
}
|
|
80
|
+
for (const [route, built] of target) {
|
|
81
|
+
const existing = this.registrations.get(route)
|
|
82
|
+
if (existing !== undefined) {
|
|
83
|
+
this.refreshPolicy(route, built, existing)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
this.register(route, built)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private refreshPolicy(
|
|
91
|
+
route: string,
|
|
92
|
+
built: ResolvedDeepseekRoute,
|
|
93
|
+
registration: RouteRegistration,
|
|
94
|
+
): void {
|
|
95
|
+
const policy = built.connection.retryPolicy
|
|
96
|
+
if (deepEqualJson(policy, registration.retryPolicy)) return
|
|
97
|
+
try {
|
|
98
|
+
registration.handle.replace([route])
|
|
99
|
+
registration.retryPolicy = policy
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.deps.logger.error(`llm-pi: deepseek route "${route}" 重试策略更新被拒,保留此前注册`)
|
|
102
|
+
this.deps.logger.error(error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 匿名用户 id(首次调用时生成并备忘;register 前已确认 kit.deepseek 存在)。 */
|
|
107
|
+
private resolveUserId(): unknown {
|
|
108
|
+
if (this.userId === undefined) {
|
|
109
|
+
this.userId = this.deps.kit.deepseek?.getOrCreateAnonymousUserId()
|
|
110
|
+
}
|
|
111
|
+
return this.userId
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private register(route: string, built: ResolvedDeepseekRoute): void {
|
|
115
|
+
const { ctx, kit, logger, routes } = this.deps
|
|
116
|
+
if (kit.deepseek === undefined) return // 构建期已拦截;此处只是类型护栏
|
|
117
|
+
const adapter = new kit.deepseek.DeepSeekAdapter({
|
|
118
|
+
options: () => {
|
|
119
|
+
const current = routes().get(route)
|
|
120
|
+
if (current === undefined) {
|
|
121
|
+
throw new kit.LlmError(`llm-pi: deepseek route "${route}" 已注销`, 'INVALID_REQUEST')
|
|
122
|
+
}
|
|
123
|
+
return current.connection
|
|
124
|
+
},
|
|
125
|
+
resolveApiKey: this.resolveApiKey as never,
|
|
126
|
+
resolveUserId: () => this.resolveUserId() as never,
|
|
127
|
+
resolveAttachments: () => ctx.get('attachments') as never,
|
|
128
|
+
})
|
|
129
|
+
try {
|
|
130
|
+
const handle = ctx.llm.registerAdapter([route], adapter as never)
|
|
131
|
+
this.registrations.set(route, { handle, retryPolicy: built.connection.retryPolicy })
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error(
|
|
134
|
+
`llm-pi: deepseek route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`,
|
|
135
|
+
)
|
|
136
|
+
logger.error(error)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/discovery.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* GET {baseURL}/models、4MB 上限、署名头)与官方一致。结果不落盘。
|
|
7
7
|
* @module llm-pi/discovery
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { builtinModelIds, hasBuiltinProvider } from './catalog/builtin.ts'
|
|
10
|
+
import { officialModelIds } from './catalog/official.ts'
|
|
10
11
|
import type { ProviderProfileConfig } from './config.ts'
|
|
11
12
|
import type { DshKit } from './resolve-dsh.ts'
|
|
12
13
|
|
|
@@ -37,7 +38,8 @@ export interface DiscoveryDeps {
|
|
|
37
38
|
|
|
38
39
|
/** 读取有界响应体:声明超长或累计超长都拒绝(对齐官方 readBounded)。 */
|
|
39
40
|
async function readBounded(kit: DshKit, response: Response, url: string): Promise<string> {
|
|
40
|
-
const oversized = () =>
|
|
41
|
+
const oversized = () =>
|
|
42
|
+
new kit.LlmError(`${url} 响应超过 ${MAX_RESPONSE_BYTES} 字节`, 'DISCOVERY_FAILED')
|
|
41
43
|
const declared = Number(response.headers.get('content-length') ?? NaN)
|
|
42
44
|
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
43
45
|
await response.body?.cancel()
|
|
@@ -87,7 +89,12 @@ function readListing(kit: DshKit, body: unknown): DiscoveryEntry[] {
|
|
|
87
89
|
['max_tokens', 'maxTokens'],
|
|
88
90
|
] as const) {
|
|
89
91
|
const value = entry[key]
|
|
90
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
typeof value === 'number' &&
|
|
94
|
+
Number.isInteger(value) &&
|
|
95
|
+
value > 0 &&
|
|
96
|
+
out[field] === undefined
|
|
97
|
+
) {
|
|
91
98
|
out[field] = value
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -96,6 +103,32 @@ function readListing(kit: DshKit, body: unknown): DiscoveryEntry[] {
|
|
|
96
103
|
return models
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
/** deepseek 路由直答:extends 'deepseek' 给官方目录全量;否则给 route 自配模型。 */
|
|
107
|
+
function deepseekCatalogAnswer(kit: DshKit, route: ProviderProfileConfig): DiscoveryEntry[] {
|
|
108
|
+
if (kit.deepseek === undefined) {
|
|
109
|
+
throw new kit.LlmError(
|
|
110
|
+
'当前运行时套件不含 dsh-llm-deepseek,无法读取官方目录',
|
|
111
|
+
'DISCOVERY_FAILED',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
if (route.extends === 'deepseek') {
|
|
115
|
+
return officialModelIds(kit).map((id) => ({ id }))
|
|
116
|
+
}
|
|
117
|
+
const models = (route.models ?? []).map((entry) => ({
|
|
118
|
+
id: entry.id,
|
|
119
|
+
...(entry.name === undefined ? {} : { name: entry.name }),
|
|
120
|
+
...(entry.contextWindow === undefined ? {} : { contextWindow: entry.contextWindow }),
|
|
121
|
+
...(entry.maxTokens === undefined ? {} : { maxTokens: entry.maxTokens }),
|
|
122
|
+
}))
|
|
123
|
+
if (models.length === 0) {
|
|
124
|
+
throw new kit.LlmError(
|
|
125
|
+
'该 route 未配置 models 且未 extends deepseek;请手工录入模型',
|
|
126
|
+
'DISCOVERY_FAILED',
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return models
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
/** 内置目录直答(route 配了 provider 级 extends 时)。 */
|
|
100
133
|
function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
|
|
101
134
|
return builtinModelIds(kit, source).map((id) => {
|
|
@@ -111,13 +144,20 @@ function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
|
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
/**
|
|
114
|
-
* 回答"该 provider 可服务哪些模型":
|
|
115
|
-
*
|
|
147
|
+
* 回答"该 provider 可服务哪些模型":deepseek 路由读官方目录/自有配置直答;
|
|
148
|
+
* extends 内置源零网络直答;否则仅 openai 系协议走 GET {baseURL}/models;
|
|
149
|
+
* 其余协议明确不支持。
|
|
116
150
|
*/
|
|
117
|
-
export async function discoverModels(
|
|
151
|
+
export async function discoverModels(
|
|
152
|
+
request: DiscoveryRequest,
|
|
153
|
+
deps: DiscoveryDeps,
|
|
154
|
+
): Promise<DiscoveryEntry[]> {
|
|
118
155
|
const { kit } = deps
|
|
119
156
|
const route: ProviderProfileConfig | undefined =
|
|
120
157
|
request.provider === undefined ? undefined : deps.configProviders()[request.provider]
|
|
158
|
+
if ((route?.adapter ?? 'pi') === 'deepseek') {
|
|
159
|
+
return deepseekCatalogAnswer(kit, route)
|
|
160
|
+
}
|
|
121
161
|
if (route?.extends !== undefined && hasBuiltinProvider(kit, route.extends)) {
|
|
122
162
|
return catalogAnswer(kit, route.extends)
|
|
123
163
|
}
|
|
@@ -130,7 +170,10 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
130
170
|
}
|
|
131
171
|
const api = request.api ?? route?.api ?? 'openai-completions'
|
|
132
172
|
if (!LISTABLE_PROTOCOLS.has(api)) {
|
|
133
|
-
throw new kit.LlmError(
|
|
173
|
+
throw new kit.LlmError(
|
|
174
|
+
`协议 "${api}" 无可读取的模型清单端点;请手工录入模型`,
|
|
175
|
+
'DISCOVERY_UNSUPPORTED',
|
|
176
|
+
)
|
|
134
177
|
}
|
|
135
178
|
const url = `${baseURL.replace(/\/+$/, '')}/models`
|
|
136
179
|
const supplied = request.apiKey ?? (await deps.storedApiKey(request.provider))
|
|
@@ -139,7 +182,9 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
139
182
|
const checked = kit.normalizeApiKey(supplied)
|
|
140
183
|
if (!checked.ok) {
|
|
141
184
|
throw new kit.LlmError(
|
|
142
|
-
checked.reason === 'empty'
|
|
185
|
+
checked.reason === 'empty'
|
|
186
|
+
? 'API key 为空;请在 Models 页配置或留空以匿名探测'
|
|
187
|
+
: 'API key 含有 HTTP 头无法携带的字符',
|
|
143
188
|
kit.INVALID_CREDENTIAL_CODE,
|
|
144
189
|
)
|
|
145
190
|
}
|
|
@@ -157,8 +202,13 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
157
202
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
158
203
|
})
|
|
159
204
|
} catch (error) {
|
|
160
|
-
if (request.signal?.aborted)
|
|
161
|
-
|
|
205
|
+
if (request.signal?.aborted)
|
|
206
|
+
throw new kit.LlmError('模型发现被调用方中止', 'ABORTED', {
|
|
207
|
+
cause: error,
|
|
208
|
+
})
|
|
209
|
+
throw new kit.LlmError(`无法连接 ${url}`, 'DISCOVERY_FAILED', {
|
|
210
|
+
cause: error,
|
|
211
|
+
})
|
|
162
212
|
}
|
|
163
213
|
if (!response.ok) {
|
|
164
214
|
throw new kit.LlmError(
|
|
@@ -171,6 +221,8 @@ export async function discoverModels(request: DiscoveryRequest, deps: DiscoveryD
|
|
|
171
221
|
return readListing(kit, JSON.parse(text))
|
|
172
222
|
} catch (error) {
|
|
173
223
|
if (error instanceof kit.LlmError) throw error
|
|
174
|
-
throw new kit.LlmError(`${url} 未返回 JSON`, 'DISCOVERY_FAILED', {
|
|
224
|
+
throw new kit.LlmError(`${url} 未返回 JSON`, 'DISCOVERY_FAILED', {
|
|
225
|
+
cause: error,
|
|
226
|
+
})
|
|
175
227
|
}
|
|
176
228
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,10 +20,13 @@ export const name = 'dsh-plus-llm-pi'
|
|
|
20
20
|
|
|
21
21
|
export const inject = ['llm'] as const
|
|
22
22
|
|
|
23
|
-
export {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export type {
|
|
24
|
+
LlmPiConfig,
|
|
25
|
+
ModelEntryConfig,
|
|
26
|
+
ProviderProfileConfig,
|
|
27
|
+
} from './config.ts'
|
|
26
28
|
export { SETTINGS_NS } from './config.ts'
|
|
29
|
+
export { Config }
|
|
27
30
|
|
|
28
31
|
export async function apply(ctx: Context, config: LlmPiConfig): Promise<void> {
|
|
29
32
|
const runtime = await startRuntime(ctx, config)
|
package/src/inherit.ts
CHANGED
|
@@ -26,13 +26,18 @@ export interface BaseResolution {
|
|
|
26
26
|
export class ExtendsError extends Error {}
|
|
27
27
|
|
|
28
28
|
/** 解析 "provider/model" 或裸 "model" 引用。 */
|
|
29
|
-
export function parseExtendsRef(raw: string): {
|
|
29
|
+
export function parseExtendsRef(raw: string): {
|
|
30
|
+
provider?: string
|
|
31
|
+
model: string
|
|
32
|
+
} {
|
|
30
33
|
const slash = raw.indexOf('/')
|
|
31
34
|
if (slash < 0) return { model: raw }
|
|
32
35
|
const provider = raw.slice(0, slash)
|
|
33
36
|
const model = raw.slice(slash + 1)
|
|
34
37
|
if (provider.length === 0 || model.length === 0 || model.includes('/')) {
|
|
35
|
-
throw new ExtendsError(
|
|
38
|
+
throw new ExtendsError(
|
|
39
|
+
`extends 引用 ${JSON.stringify(raw)} 非法:应为 "provider/model" 或 "model"`,
|
|
40
|
+
)
|
|
36
41
|
}
|
|
37
42
|
return { provider, model }
|
|
38
43
|
}
|
|
@@ -65,7 +70,10 @@ export function resolveModelBase(
|
|
|
65
70
|
if (entry.extends === undefined) {
|
|
66
71
|
if (profile.extends === undefined) return { base: {}, source: 'none' }
|
|
67
72
|
return (
|
|
68
|
-
lookup(kit, modelsDev, profile.extends, entry.id) ?? {
|
|
73
|
+
lookup(kit, modelsDev, profile.extends, entry.id) ?? {
|
|
74
|
+
base: {},
|
|
75
|
+
source: 'none',
|
|
76
|
+
}
|
|
69
77
|
)
|
|
70
78
|
}
|
|
71
79
|
const ref = parseExtendsRef(entry.extends)
|