@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.
- package/lib/client.js +1880 -1844
- package/lib/index.d.ts +21 -4
- package/lib/index.js +562 -126
- package/package.json +16 -11
- package/src/catalog/official.ts +71 -0
- package/src/client/draft.ts +90 -32
- package/src/config.ts +105 -0
- package/src/deepseek-routes.ts +139 -0
- package/src/discovery.ts +33 -2
- package/src/profiles-deepseek.ts +282 -0
- package/src/profiles.ts +5 -0
- package/src/resolve-dsh.ts +89 -0
- package/src/service.ts +39 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/llm-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,25 +36,30 @@
|
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
-
"@deepseek-ai/
|
|
40
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
|
|
41
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
|
|
39
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.1-rc.2",
|
|
42
40
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
|
|
43
41
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.8",
|
|
42
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
|
|
44
43
|
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
|
|
45
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.1-rc.2",
|
|
45
46
|
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.0-rc.8",
|
|
47
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
|
|
48
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
46
49
|
"@earendil-works/pi-ai": "^0.82.1"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@deepseek-ai/cordis": "4.0.1",
|
|
53
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.1.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.1-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.1.1-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2",
|
|
50
62
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
51
|
-
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
52
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
53
|
-
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
|
54
|
-
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
|
|
55
|
-
"@deepseek-ai/dsh-launch-environment": "0.1.0-rc.8",
|
|
56
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.8",
|
|
57
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
|
|
58
63
|
"@earendil-works/pi-ai": "0.82.1",
|
|
59
64
|
"@types/react": "~18.3.1",
|
|
60
65
|
"react": "^18.2.0"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 官方模型目录数据源:adapter: deepseek 的 route 的继承 base。
|
|
3
|
+
*
|
|
4
|
+
* 目录取自 dsh-llm-deepseek 的 resolveAdapterOptions({}, undefined)——
|
|
5
|
+
* 即官方 DEFAULT_MODELS 经 schema 默认值补全后的形态(含视觉模型的
|
|
6
|
+
* inputModalities / imagePixelBudget / imageMaxBytes),跟随 dsh 树升级
|
|
7
|
+
* 自动更新,不在本仓库复制一份硬编码副本。
|
|
8
|
+
* @module llm-pi/catalog/official
|
|
9
|
+
*/
|
|
10
|
+
import type { DshKit } from '../resolve-dsh.ts'
|
|
11
|
+
|
|
12
|
+
/** 官方目录可提供的可继承模型字段(DeepSeekCatalogModel 的可继承子集)。 */
|
|
13
|
+
export interface OfficialModelBase {
|
|
14
|
+
name?: string
|
|
15
|
+
contextWindow?: number
|
|
16
|
+
maxTokens?: number
|
|
17
|
+
input?: ('text' | 'image')[]
|
|
18
|
+
imagePixelBudget?: number
|
|
19
|
+
imageMaxBytes?: number
|
|
20
|
+
imageDetail?: 'auto' | 'low'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface OfficialCatalog {
|
|
24
|
+
baseUrl: string
|
|
25
|
+
models: Map<string, OfficialModelBase>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 按 kit 备忘(kit 实例在进程内唯一,目录随 dsh 树版本固定)。 */
|
|
29
|
+
const cache = new WeakMap<DshKit, OfficialCatalog>()
|
|
30
|
+
|
|
31
|
+
function catalogOf(kit: DshKit): OfficialCatalog {
|
|
32
|
+
const hit = cache.get(kit)
|
|
33
|
+
if (hit !== undefined) return hit
|
|
34
|
+
if (kit.deepseek === undefined) {
|
|
35
|
+
throw new Error('llm-pi: 官方模型目录需要 dsh-llm-deepseek(当前运行时套件不含)')
|
|
36
|
+
}
|
|
37
|
+
const resolved = kit.deepseek.resolveAdapterOptions({} as never, undefined)
|
|
38
|
+
const models = new Map<string, OfficialModelBase>()
|
|
39
|
+
for (const model of resolved.models) {
|
|
40
|
+
models.set(model.id, {
|
|
41
|
+
...(model.name === undefined ? {} : { name: model.name }),
|
|
42
|
+
...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }),
|
|
43
|
+
...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
|
|
44
|
+
...(model.inputModalities === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { input: [...model.inputModalities] as ('text' | 'image')[] }),
|
|
47
|
+
...(model.imagePixelBudget === undefined ? {} : { imagePixelBudget: model.imagePixelBudget }),
|
|
48
|
+
...(model.imageMaxBytes === undefined ? {} : { imageMaxBytes: model.imageMaxBytes }),
|
|
49
|
+
...(model.imageDetail === undefined ? {} : { imageDetail: model.imageDetail }),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
const catalog: OfficialCatalog = { baseUrl: resolved.baseURL, models }
|
|
53
|
+
cache.set(kit, catalog)
|
|
54
|
+
return catalog
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 官方端点(route 配 extends: 'deepseek' 且未显式 baseURL 时的缺省值)。 */
|
|
58
|
+
export function officialBaseUrl(kit: DshKit): string {
|
|
59
|
+
return catalogOf(kit).baseUrl
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 查单个官方模型为继承 base;未命中返回 undefined。 */
|
|
63
|
+
export function officialModelBase(kit: DshKit, modelId: string): OfficialModelBase | undefined {
|
|
64
|
+
const base = catalogOf(kit).models.get(modelId)
|
|
65
|
+
return base === undefined ? undefined : { ...base }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 官方目录的全部模型 id(route 级 extends: 'deepseek' 的全量继承与诊断用)。 */
|
|
69
|
+
export function officialModelIds(kit: DshKit): string[] {
|
|
70
|
+
return [...catalogOf(kit).models.keys()]
|
|
71
|
+
}
|
package/src/client/draft.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface ModelDraft {
|
|
|
38
38
|
input: InputDraft
|
|
39
39
|
reasoningEfforts: ReasoningDraft
|
|
40
40
|
compat: Record<string, unknown>
|
|
41
|
+
/** 卡片未认识的字段原样往返(如 adapter: deepseek 的 imagePixelBudget 等)。 */
|
|
42
|
+
extra: Record<string, unknown>
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface ProviderDraft {
|
|
@@ -61,6 +63,8 @@ export interface ProviderDraft {
|
|
|
61
63
|
maxRequestImageBytes: string
|
|
62
64
|
retryPolicy: unknown
|
|
63
65
|
models: ModelDraft[]
|
|
66
|
+
/** 卡片未认识的字段原样往返(如 adapter/thinking/filesApiTimeoutMs 等)。 */
|
|
67
|
+
extra: Record<string, unknown>
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
export interface Draft {
|
|
@@ -167,6 +171,50 @@ function omitEmpty(obj: Record<string, unknown>): Record<string, unknown> {
|
|
|
167
171
|
return out
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
/** 卡片已认识的 wire 字段(其余进 extra 原样往返)。 */
|
|
175
|
+
const KNOWN_MODEL_KEYS = new Set([
|
|
176
|
+
'id',
|
|
177
|
+
'extends',
|
|
178
|
+
'name',
|
|
179
|
+
'contextWindow',
|
|
180
|
+
'maxTokens',
|
|
181
|
+
'input',
|
|
182
|
+
'reasoningEfforts',
|
|
183
|
+
'compat',
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
const KNOWN_PROVIDER_KEYS = new Set([
|
|
187
|
+
'extends',
|
|
188
|
+
'displayName',
|
|
189
|
+
'api',
|
|
190
|
+
'baseURL',
|
|
191
|
+
'apiKeyEnv',
|
|
192
|
+
'headers',
|
|
193
|
+
'compat',
|
|
194
|
+
'defaultContextWindow',
|
|
195
|
+
'defaultMaxTokens',
|
|
196
|
+
'defaultInput',
|
|
197
|
+
'reasoning',
|
|
198
|
+
'thinkingBudgets',
|
|
199
|
+
'cacheRetention',
|
|
200
|
+
'transport',
|
|
201
|
+
'timeoutMs',
|
|
202
|
+
'websocketConnectTimeoutMs',
|
|
203
|
+
'streamIdleTimeoutMs',
|
|
204
|
+
'maxRequestImageBytes',
|
|
205
|
+
'retryPolicy',
|
|
206
|
+
'models',
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
/** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留)。 */
|
|
210
|
+
function extraOf(wire: Record<string, unknown>, known: Set<string>): Record<string, unknown> {
|
|
211
|
+
const out: Record<string, unknown> = {}
|
|
212
|
+
for (const [key, value] of Object.entries(wire)) {
|
|
213
|
+
if (!known.has(key) && value !== undefined) out[key] = value
|
|
214
|
+
}
|
|
215
|
+
return out
|
|
216
|
+
}
|
|
217
|
+
|
|
170
218
|
function modelDraftFromWire(model: WireModel): ModelDraft {
|
|
171
219
|
return {
|
|
172
220
|
id: model.id,
|
|
@@ -177,21 +225,25 @@ function modelDraftFromWire(model: WireModel): ModelDraft {
|
|
|
177
225
|
input: inputFromWire(model.input),
|
|
178
226
|
reasoningEfforts: reasoningFromWire(model.reasoningEfforts),
|
|
179
227
|
compat: { ...(model.compat ?? {}) },
|
|
228
|
+
extra: extraOf(model as Record<string, unknown>, KNOWN_MODEL_KEYS),
|
|
180
229
|
}
|
|
181
230
|
}
|
|
182
231
|
|
|
183
232
|
function modelToWire(model: ModelDraft): WireModel {
|
|
184
233
|
const reasoningEfforts = reasoningToWire(model.reasoningEfforts)
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
234
|
+
return {
|
|
235
|
+
...model.extra,
|
|
236
|
+
...(omitEmpty({
|
|
237
|
+
id: model.id.trim(),
|
|
238
|
+
extends: model.extends.trim(),
|
|
239
|
+
name: model.name.trim(),
|
|
240
|
+
contextWindow: toNum(model.contextWindow),
|
|
241
|
+
maxTokens: toNum(model.maxTokens),
|
|
242
|
+
input: inputToWire(model.input),
|
|
243
|
+
...(reasoningEfforts === undefined ? {} : { reasoningEfforts }),
|
|
244
|
+
compat: model.compat,
|
|
245
|
+
}) as Record<string, unknown>),
|
|
246
|
+
} as unknown as WireModel
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
function providerDraftFromWire(provider: WireProvider): ProviderDraft {
|
|
@@ -216,32 +268,36 @@ function providerDraftFromWire(provider: WireProvider): ProviderDraft {
|
|
|
216
268
|
maxRequestImageBytes: numToText(provider.maxRequestImageBytes),
|
|
217
269
|
retryPolicy: provider.retryPolicy,
|
|
218
270
|
models: (provider.models ?? []).map(modelDraftFromWire),
|
|
271
|
+
extra: extraOf(provider as Record<string, unknown>, KNOWN_PROVIDER_KEYS),
|
|
219
272
|
}
|
|
220
273
|
}
|
|
221
274
|
|
|
222
275
|
function providerToWire(provider: ProviderDraft): WireProvider {
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
276
|
+
return {
|
|
277
|
+
...provider.extra,
|
|
278
|
+
...(omitEmpty({
|
|
279
|
+
extends: provider.extends.trim(),
|
|
280
|
+
displayName: provider.displayName.trim(),
|
|
281
|
+
api: provider.api,
|
|
282
|
+
baseURL: provider.baseURL.trim(),
|
|
283
|
+
apiKeyEnv: provider.apiKeyEnv.trim(),
|
|
284
|
+
headers: pairsToHeaders(provider.headers),
|
|
285
|
+
compat: provider.compat,
|
|
286
|
+
defaultContextWindow: toNum(provider.defaultContextWindow),
|
|
287
|
+
defaultMaxTokens: toNum(provider.defaultMaxTokens),
|
|
288
|
+
input: inputToWire(provider.input),
|
|
289
|
+
reasoning: provider.reasoning,
|
|
290
|
+
thinkingBudgets: budgetToWire(provider.thinkingBudgets),
|
|
291
|
+
cacheRetention: provider.cacheRetention,
|
|
292
|
+
transport: provider.transport,
|
|
293
|
+
timeoutMs: toNum(provider.timeoutMs),
|
|
294
|
+
websocketConnectTimeoutMs: toNum(provider.websocketConnectTimeoutMs),
|
|
295
|
+
streamIdleTimeoutMs: toNum(provider.streamIdleTimeoutMs),
|
|
296
|
+
maxRequestImageBytes: toNum(provider.maxRequestImageBytes),
|
|
297
|
+
retryPolicy: provider.retryPolicy,
|
|
298
|
+
models: provider.models.map(modelToWire),
|
|
299
|
+
}) as Record<string, unknown>),
|
|
300
|
+
} as unknown as WireProvider
|
|
245
301
|
}
|
|
246
302
|
|
|
247
303
|
export function emptyProviderDraft(): ProviderDraft {
|
|
@@ -266,6 +322,7 @@ export function emptyProviderDraft(): ProviderDraft {
|
|
|
266
322
|
maxRequestImageBytes: '',
|
|
267
323
|
retryPolicy: undefined,
|
|
268
324
|
models: [],
|
|
325
|
+
extra: {},
|
|
269
326
|
}
|
|
270
327
|
}
|
|
271
328
|
|
|
@@ -279,6 +336,7 @@ export function emptyModelDraft(): ModelDraft {
|
|
|
279
336
|
input: { text: false, image: false },
|
|
280
337
|
reasoningEfforts: { nonReasoning: false, levels: {} },
|
|
281
338
|
compat: {},
|
|
339
|
+
extra: {},
|
|
282
340
|
}
|
|
283
341
|
}
|
|
284
342
|
|
package/src/config.ts
CHANGED
|
@@ -30,6 +30,15 @@ export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhig
|
|
|
30
30
|
|
|
31
31
|
export const MODALITIES = ['text', 'image'] as const
|
|
32
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
|
+
|
|
33
42
|
export const DEFAULT_CONTEXT_WINDOW = 262144
|
|
34
43
|
export const DEFAULT_MAX_TOKENS = 32768
|
|
35
44
|
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000
|
|
@@ -56,10 +65,15 @@ export interface ModelEntryConfig {
|
|
|
56
65
|
input?: Modality[]
|
|
57
66
|
reasoningEfforts?: false | ReasoningEfforts
|
|
58
67
|
compat?: Record<string, unknown>
|
|
68
|
+
/** 以下仅 adapter: deepseek 的 route 有效(官方 llm-deepseek 目录字段)。 */
|
|
69
|
+
imagePixelBudget?: number
|
|
70
|
+
imageMaxBytes?: number
|
|
71
|
+
imageDetail?: 'auto' | 'low'
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/** 单个 provider route 配置(providers 字典的值)。 */
|
|
62
75
|
export interface ProviderProfileConfig {
|
|
76
|
+
adapter?: AdapterKind
|
|
63
77
|
extends?: string
|
|
64
78
|
displayName?: string
|
|
65
79
|
api?: ProtocolId
|
|
@@ -85,6 +99,18 @@ export interface ProviderProfileConfig {
|
|
|
85
99
|
maxRequestImageBytes?: number
|
|
86
100
|
retryPolicy?: unknown
|
|
87
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
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
/** 插件配置根。 */
|
|
@@ -131,9 +157,28 @@ const modelEntry = z.object({
|
|
|
131
157
|
.union([z.const(false), reasoningEfforts])
|
|
132
158
|
.description('可选 reasoning 档位:false=非推理模型;dict=档位→线值映射;缺省继承内置目录能力'),
|
|
133
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 像素预算'),
|
|
134
173
|
})
|
|
135
174
|
|
|
136
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'),
|
|
137
182
|
extends: z
|
|
138
183
|
.string()
|
|
139
184
|
.description(
|
|
@@ -172,6 +217,66 @@ const providerProfile = z.object({
|
|
|
172
217
|
.natural()
|
|
173
218
|
.description('单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)'),
|
|
174
219
|
retryPolicy: z.any().description('provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)'),
|
|
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'),
|
|
175
280
|
models: z
|
|
176
281
|
.array(modelEntry)
|
|
177
282
|
.description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
|
|
@@ -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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module llm-pi/discovery
|
|
8
8
|
*/
|
|
9
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
|
|
|
@@ -102,6 +103,32 @@ function readListing(kit: DshKit, body: unknown): DiscoveryEntry[] {
|
|
|
102
103
|
return models
|
|
103
104
|
}
|
|
104
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
|
+
|
|
105
132
|
/** 内置目录直答(route 配了 provider 级 extends 时)。 */
|
|
106
133
|
function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
|
|
107
134
|
return builtinModelIds(kit, source).map((id) => {
|
|
@@ -117,8 +144,9 @@ function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
|
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
/**
|
|
120
|
-
* 回答"该 provider 可服务哪些模型":
|
|
121
|
-
*
|
|
147
|
+
* 回答"该 provider 可服务哪些模型":deepseek 路由读官方目录/自有配置直答;
|
|
148
|
+
* extends 内置源零网络直答;否则仅 openai 系协议走 GET {baseURL}/models;
|
|
149
|
+
* 其余协议明确不支持。
|
|
122
150
|
*/
|
|
123
151
|
export async function discoverModels(
|
|
124
152
|
request: DiscoveryRequest,
|
|
@@ -127,6 +155,9 @@ export async function discoverModels(
|
|
|
127
155
|
const { kit } = deps
|
|
128
156
|
const route: ProviderProfileConfig | undefined =
|
|
129
157
|
request.provider === undefined ? undefined : deps.configProviders()[request.provider]
|
|
158
|
+
if ((route?.adapter ?? 'pi') === 'deepseek') {
|
|
159
|
+
return deepseekCatalogAnswer(kit, route)
|
|
160
|
+
}
|
|
130
161
|
if (route?.extends !== undefined && hasBuiltinProvider(kit, route.extends)) {
|
|
131
162
|
return catalogAnswer(kit, route.extends)
|
|
132
163
|
}
|