@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/lib/index.js
CHANGED
|
@@ -9,29 +9,29 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import HttpsProxyAgentModule from "https-proxy-agent";
|
|
10
10
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
+
import * as vendoredAnonId from "@deepseek-ai/dsh-anonymous-user-id";
|
|
12
13
|
import * as vendoredLlm from "@deepseek-ai/dsh-llm";
|
|
14
|
+
import * as vendoredDeepseek from "@deepseek-ai/dsh-llm-deepseek";
|
|
13
15
|
import * as vendoredPiAiAdapter from "@deepseek-ai/dsh-llm-pi-ai";
|
|
14
16
|
import * as vendoredPiAi from "@earendil-works/pi-ai";
|
|
15
17
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
16
18
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
17
19
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
18
20
|
import * as vendoredCatalog from "@earendil-works/pi-ai/providers/all";
|
|
19
|
-
|
|
20
|
-
//#region src/ns.ts
|
|
21
|
-
/**
|
|
22
|
-
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
23
|
-
* 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
|
|
24
|
-
* settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
|
|
25
|
-
* 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
|
|
26
|
-
* 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
|
|
27
|
-
* @module llm-pi/ns
|
|
28
|
-
*/
|
|
29
|
-
const SETTINGS_NS$1 = "dsh-plus-llm-pi";
|
|
30
|
-
|
|
31
21
|
//#endregion
|
|
32
22
|
//#region src/config.ts
|
|
23
|
+
/**
|
|
24
|
+
* 配置单一事实源:cordis 行级 Config(组合默认值)与 settings namespace
|
|
25
|
+
*(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)共用同一
|
|
26
|
+
* schemastery schema。无密钥字段(apiKeyEnv 是凭据引用名而非密钥本身)。
|
|
27
|
+
*
|
|
28
|
+
* 与官方 llm-pi-ai 的差异:
|
|
29
|
+
* - compat 是开放 dict,物化时按协议校验(见 compat.ts),写入即拒绝未知键;
|
|
30
|
+
* - provider/model 均支持 extends 继承(见 inherit.ts)。
|
|
31
|
+
* @module llm-pi/config
|
|
32
|
+
*/
|
|
33
33
|
/** settings 命名空间;webui 配置卡片与插件运行期读取同一份(字面量见 ./ns.ts)。 */
|
|
34
|
-
const SETTINGS_NS = settingsNamespace(
|
|
34
|
+
const SETTINGS_NS = settingsNamespace("dsh-plus-llm-pi");
|
|
35
35
|
/** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
|
|
36
36
|
const PROTOCOL_IDS = [
|
|
37
37
|
"openai-completions",
|
|
@@ -49,12 +49,17 @@ const THINKING_LEVELS = [
|
|
|
49
49
|
"max"
|
|
50
50
|
];
|
|
51
51
|
const MODALITIES = ["text", "image"];
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/** 路由适配器种类:pi = PiAiAdapter(默认);deepseek = 官方 DeepSeekAdapter(文件通道)。 */
|
|
53
|
+
const ADAPTER_KINDS = ["pi", "deepseek"];
|
|
54
|
+
/** deepseek 路由的思考/推理档位(线协议枚举,对齐官方 llm-deepseek)。 */
|
|
55
|
+
const DEEPSEEK_THINKING = ["enabled", "disabled"];
|
|
56
|
+
const DEEPSEEK_REASONING_EFFORTS = [
|
|
57
|
+
"off",
|
|
58
|
+
"low",
|
|
59
|
+
"high",
|
|
60
|
+
"max"
|
|
61
|
+
];
|
|
62
|
+
const DEEPSEEK_IMAGE_DETAILS = ["auto", "low"];
|
|
58
63
|
/** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
|
|
59
64
|
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
60
65
|
/** 键为可选档位,值为线协议拼写;仅 off 可留空(支持但不发送参数)。 */
|
|
@@ -79,9 +84,13 @@ const modelEntry = z.object({
|
|
|
79
84
|
maxTokens: z.number().step(1).min(1).description("输出能力上限;显式配置同时成为无 cap 请求的默认 cap"),
|
|
80
85
|
input: z.array(z.union(MODALITIES)).description("请求模态;缺省继承内置目录,再退化 route defaultInput"),
|
|
81
86
|
reasoningEfforts: z.union([z.const(false), reasoningEfforts]).description("可选 reasoning 档位:false=非推理模型;dict=档位→线值映射;缺省继承内置目录能力"),
|
|
82
|
-
compat: compatDict.description("模型级 compat(字段级合并,压过 route 级与继承值)")
|
|
87
|
+
compat: compatDict.description("模型级 compat(字段级合并,压过 route 级与继承值)"),
|
|
88
|
+
imagePixelBudget: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的总像素预算;缺省继承官方目录"),
|
|
89
|
+
imageMaxBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录"),
|
|
90
|
+
imageDetail: z.union(DEEPSEEK_IMAGE_DETAILS).description("(仅 adapter: deepseek)图片细节档位;low 使用 512x512 像素预算")
|
|
83
91
|
});
|
|
84
92
|
const providerProfile = z.object({
|
|
93
|
+
adapter: z.union(ADAPTER_KINDS).description("路由适配器:pi=PiAiAdapter(默认,三协议);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)").default("pi"),
|
|
85
94
|
extends: z.string().description("provider 级继承:内置 provider id,提供 api/baseURL 默认值与模型 extends 的缺省查找源"),
|
|
86
95
|
displayName: z.string().description("选择器显示名;缺省为 route 键"),
|
|
87
96
|
api: z.union(PROTOCOL_IDS).description("线协议;缺省逐模型取继承值的 api,全部一致时作为 route 协议"),
|
|
@@ -110,6 +119,17 @@ const providerProfile = z.object({
|
|
|
110
119
|
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).description("单支流式读取的最大空闲间隔毫秒"),
|
|
111
120
|
maxRequestImageBytes: z.natural().description("单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)"),
|
|
112
121
|
retryPolicy: z.any().description("provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)"),
|
|
122
|
+
thinking: z.union(DEEPSEEK_THINKING).description("(仅 adapter: deepseek)思考模式开关;缺省用提供商默认"),
|
|
123
|
+
reasoningEffort: z.union(DEEPSEEK_REASONING_EFFORTS).description("(仅 adapter: deepseek)推理档位;缺省为 high"),
|
|
124
|
+
maxRequestFilesBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)单请求文件引用图片累积上限字节;缺省 128MiB"),
|
|
125
|
+
maxInlineRequestImageBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)文件通道降级后 base64 图片载荷上限字节;缺省 20MiB"),
|
|
126
|
+
maxImagesPerRequest: z.number().step(1).min(1).description("(仅 adapter: deepseek)单请求图片数量上限;缺省 600(调低时需同步调低 imageOffloadCountQuantum)"),
|
|
127
|
+
imageOffloadByteQuantum: z.number().step(1).min(1).description("(仅 adapter: deepseek)文件引用超限后的原始字节削减步长;缺省 64MiB,不可超过 maxRequestFilesBytes"),
|
|
128
|
+
inlineImageOffloadByteQuantum: z.number().step(1).min(1).description("(仅 adapter: deepseek)base64 降级超限后的削减步长;缺省 10MiB,不可超过 maxInlineRequestImageBytes"),
|
|
129
|
+
imageOffloadCountQuantum: z.number().step(1).min(1).description("(仅 adapter: deepseek)图片数量超限后的削减步长;缺省 20,不可超过 maxImagesPerRequest"),
|
|
130
|
+
filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).description("(仅 adapter: deepseek)单次 Files API 图片解析超时毫秒;缺省 60s"),
|
|
131
|
+
fileExpiresAfterSeconds: z.number().step(1).min(3600).max(2592e3).description("(仅 adapter: deepseek)上传文件的远端存活秒数;缺省 7 天"),
|
|
132
|
+
fileRefreshMarginSeconds: z.number().step(1).min(0).description("(仅 adapter: deepseek)文件过期前提前重传的余量秒数;缺省 3600"),
|
|
113
133
|
models: z.array(modelEntry).description("本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型")
|
|
114
134
|
});
|
|
115
135
|
const Config = z.object({
|
|
@@ -119,7 +139,6 @@ const Config = z.object({
|
|
|
119
139
|
catalogProxy: z.string().description("拉取 models.dev 目录时的 HTTP 代理地址(如 http://127.0.0.1:7890);留空直连").default(""),
|
|
120
140
|
providers: z.dict(providerProfile).description("provider 路由表,键即 route 名").default({})
|
|
121
141
|
});
|
|
122
|
-
|
|
123
142
|
//#endregion
|
|
124
143
|
//#region src/catalog/builtin.ts
|
|
125
144
|
/** 内置 provider 的端点(provider 级 extends 的 baseURL 缺省值)。 */
|
|
@@ -165,7 +184,6 @@ function inheritedCatalogEntries(kit, provider) {
|
|
|
165
184
|
base: builtinModelBase(kit, provider, model.id) ?? {}
|
|
166
185
|
}));
|
|
167
186
|
}
|
|
168
|
-
|
|
169
187
|
//#endregion
|
|
170
188
|
//#region src/config-api.ts
|
|
171
189
|
const ROUTE_CATALOG = "/dsh-plus/llm-pi/catalog";
|
|
@@ -220,14 +238,30 @@ function registerCatalogApi(ctx, runtime) {
|
|
|
220
238
|
handler: guard((req, res) => handleCatalog(runtime, req, res))
|
|
221
239
|
});
|
|
222
240
|
}
|
|
223
|
-
|
|
224
241
|
//#endregion
|
|
225
242
|
//#region src/catalog/models-dev.ts
|
|
243
|
+
/**
|
|
244
|
+
* models.dev 兜底目录源:继承解析的第二级数据源。
|
|
245
|
+
*
|
|
246
|
+
* 只用于内置目录尚未收录的新模型/新供应商。数据是公开快照
|
|
247
|
+
*(默认 https://models.dev/api.json),**默认不自动拉取**(catalogRefreshHours=0);
|
|
248
|
+
* 拉取方式二选一:
|
|
249
|
+
* - 配置 catalogRefreshHours > 0:启动/过期后后台自动刷新;
|
|
250
|
+
* - 配置卡片「手动拉取」或 POST /catalog/refresh:立即拉取。
|
|
251
|
+
* 拉取可经 catalogProxy 代理(HTTP 代理,如 http://127.0.0.1:7890)。
|
|
252
|
+
* 成功落盘缓存(storages/dsh-plus-llm-pi/models-dev.json);任何失败都退化为
|
|
253
|
+
* "仅内置目录",绝不阻塞 route 注册。
|
|
254
|
+
*
|
|
255
|
+
* 保守原则:models.dev 数据未经 pi 官方校正(无 compat/thinkingLevelMap),
|
|
256
|
+
* 且模态声明不被采信——继承自本源的模型 input 一律走 text-only 兜底,
|
|
257
|
+
* 视觉等模态须用户在条目上显式声明(防 over-claiming 导致会话重复失败请求)。
|
|
258
|
+
* @module llm-pi/catalog/models-dev
|
|
259
|
+
*/
|
|
226
260
|
/** 仅 https 目标走代理(http 目标直连;代理通常只提供 CONNECT 隧道)。 */
|
|
227
261
|
const { HttpsProxyAgent } = HttpsProxyAgentModule;
|
|
228
262
|
const FETCH_TIMEOUT_MS = 2e4;
|
|
229
263
|
/** 目录文档体上限(api.json 全量约 1-2MB,放宽到 20MB 防未来膨胀)。 */
|
|
230
|
-
const MAX_RESPONSE_BYTES$1 =
|
|
264
|
+
const MAX_RESPONSE_BYTES$1 = 20971520;
|
|
231
265
|
/**
|
|
232
266
|
* 极简 JSON GET(node:http(s) 实现):支持 HTTP 代理(仅 https 目标)与超时。
|
|
233
267
|
* 不跟随重定向(models.dev 直链无重定向;自定义端点需自行保证可直达)。
|
|
@@ -383,11 +417,136 @@ var ModelsDevSource = class {
|
|
|
383
417
|
}
|
|
384
418
|
}
|
|
385
419
|
};
|
|
386
|
-
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/deepseek-routes.ts
|
|
422
|
+
/** 凭据解析(逐行对齐官方 llm-deepseek resolveApiKey):凭据服务优先,启动环境兜底。 */
|
|
423
|
+
function makeResolveApiKey$1(ctx, kit) {
|
|
424
|
+
return async (connection) => {
|
|
425
|
+
const ref = connection.apiKeyEnv;
|
|
426
|
+
const credentials = ctx.get("credentials");
|
|
427
|
+
const hit = credentials !== void 0 ? (await credentials.resolve(ref))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
428
|
+
if (hit !== void 0 && hit.length > 0) return kit.assertUsableApiKey(hit, "llm-pi", ref);
|
|
429
|
+
throw new kit.LlmError(`llm-pi: deepseek route 的凭据引用 ${String(ref)} 未解析到值——请经凭据服务(web Models 页)存放或导出环境变量`, "MISSING_CREDENTIAL");
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
var DeepseekRouteRegistrar = class {
|
|
433
|
+
deps;
|
|
434
|
+
registrations = /* @__PURE__ */ new Map();
|
|
435
|
+
resolveApiKey;
|
|
436
|
+
userId;
|
|
437
|
+
constructor(deps) {
|
|
438
|
+
this.deps = deps;
|
|
439
|
+
this.resolveApiKey = makeResolveApiKey$1(deps.ctx, deps.kit);
|
|
440
|
+
}
|
|
441
|
+
/** 同步注册表与目标 route 集:新增注册、移除 dispose、retryPolicy 变化原地 replace。 */
|
|
442
|
+
sync(target) {
|
|
443
|
+
for (const [route, registration] of this.registrations) {
|
|
444
|
+
if (target.has(route)) continue;
|
|
445
|
+
registration.handle();
|
|
446
|
+
this.registrations.delete(route);
|
|
447
|
+
this.deps.logger.warn(`llm-pi: deepseek route "${route}" 已随配置移除而注销`);
|
|
448
|
+
}
|
|
449
|
+
for (const [route, built] of target) {
|
|
450
|
+
const existing = this.registrations.get(route);
|
|
451
|
+
if (existing !== void 0) {
|
|
452
|
+
this.refreshPolicy(route, built, existing);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
this.register(route, built);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
refreshPolicy(route, built, registration) {
|
|
459
|
+
const policy = built.connection.retryPolicy;
|
|
460
|
+
if (deepEqualJson(policy, registration.retryPolicy)) return;
|
|
461
|
+
try {
|
|
462
|
+
registration.handle.replace([route]);
|
|
463
|
+
registration.retryPolicy = policy;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.deps.logger.error(`llm-pi: deepseek route "${route}" 重试策略更新被拒,保留此前注册`);
|
|
466
|
+
this.deps.logger.error(error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/** 匿名用户 id(首次调用时生成并备忘;register 前已确认 kit.deepseek 存在)。 */
|
|
470
|
+
resolveUserId() {
|
|
471
|
+
if (this.userId === void 0) this.userId = this.deps.kit.deepseek?.getOrCreateAnonymousUserId();
|
|
472
|
+
return this.userId;
|
|
473
|
+
}
|
|
474
|
+
register(route, built) {
|
|
475
|
+
const { ctx, kit, logger, routes } = this.deps;
|
|
476
|
+
if (kit.deepseek === void 0) return;
|
|
477
|
+
const adapter = new kit.deepseek.DeepSeekAdapter({
|
|
478
|
+
options: () => {
|
|
479
|
+
const current = routes().get(route);
|
|
480
|
+
if (current === void 0) throw new kit.LlmError(`llm-pi: deepseek route "${route}" 已注销`, "INVALID_REQUEST");
|
|
481
|
+
return current.connection;
|
|
482
|
+
},
|
|
483
|
+
resolveApiKey: this.resolveApiKey,
|
|
484
|
+
resolveUserId: () => this.resolveUserId(),
|
|
485
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
486
|
+
});
|
|
487
|
+
try {
|
|
488
|
+
const handle = ctx.llm.registerAdapter([route], adapter);
|
|
489
|
+
this.registrations.set(route, {
|
|
490
|
+
handle,
|
|
491
|
+
retryPolicy: built.connection.retryPolicy
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logger.error(`llm-pi: deepseek route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`);
|
|
495
|
+
logger.error(error);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/catalog/official.ts
|
|
501
|
+
/** 按 kit 备忘(kit 实例在进程内唯一,目录随 dsh 树版本固定)。 */
|
|
502
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
503
|
+
function catalogOf(kit) {
|
|
504
|
+
const hit = cache.get(kit);
|
|
505
|
+
if (hit !== void 0) return hit;
|
|
506
|
+
if (kit.deepseek === void 0) throw new Error("llm-pi: 官方模型目录需要 dsh-llm-deepseek(当前运行时套件不含)");
|
|
507
|
+
const resolved = kit.deepseek.resolveAdapterOptions({}, void 0);
|
|
508
|
+
const models = /* @__PURE__ */ new Map();
|
|
509
|
+
for (const model of resolved.models) models.set(model.id, {
|
|
510
|
+
...model.name === void 0 ? {} : { name: model.name },
|
|
511
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
512
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
513
|
+
...model.inputModalities === void 0 ? {} : { input: [...model.inputModalities] },
|
|
514
|
+
...model.imagePixelBudget === void 0 ? {} : { imagePixelBudget: model.imagePixelBudget },
|
|
515
|
+
...model.imageMaxBytes === void 0 ? {} : { imageMaxBytes: model.imageMaxBytes },
|
|
516
|
+
...model.imageDetail === void 0 ? {} : { imageDetail: model.imageDetail }
|
|
517
|
+
});
|
|
518
|
+
const catalog = {
|
|
519
|
+
baseUrl: resolved.baseURL,
|
|
520
|
+
models
|
|
521
|
+
};
|
|
522
|
+
cache.set(kit, catalog);
|
|
523
|
+
return catalog;
|
|
524
|
+
}
|
|
525
|
+
/** 官方端点(route 配 extends: 'deepseek' 且未显式 baseURL 时的缺省值)。 */
|
|
526
|
+
function officialBaseUrl(kit) {
|
|
527
|
+
return catalogOf(kit).baseUrl;
|
|
528
|
+
}
|
|
529
|
+
/** 查单个官方模型为继承 base;未命中返回 undefined。 */
|
|
530
|
+
function officialModelBase(kit, modelId) {
|
|
531
|
+
const base = catalogOf(kit).models.get(modelId);
|
|
532
|
+
return base === void 0 ? void 0 : { ...base };
|
|
533
|
+
}
|
|
534
|
+
/** 官方目录的全部模型 id(route 级 extends: 'deepseek' 的全量继承与诊断用)。 */
|
|
535
|
+
function officialModelIds(kit) {
|
|
536
|
+
return [...catalogOf(kit).models.keys()];
|
|
537
|
+
}
|
|
387
538
|
//#endregion
|
|
388
539
|
//#region src/discovery.ts
|
|
389
|
-
|
|
390
|
-
|
|
540
|
+
/**
|
|
541
|
+
* 模型发现:配置面"拉取可用模型"动作的后端(对齐官方 discoverModels 语义)。
|
|
542
|
+
*
|
|
543
|
+
* 与官方的差异:本插件 route 不是内置目录 id,目录直答改为"provider 级
|
|
544
|
+
* extends 的内置源目录直答";其余(手写 route 仅 openai 系协议走
|
|
545
|
+
* GET {baseURL}/models、4MB 上限、署名头)与官方一致。结果不落盘。
|
|
546
|
+
* @module llm-pi/discovery
|
|
547
|
+
*/
|
|
548
|
+
const LISTABLE_PROTOCOLS = /* @__PURE__ */ new Set(["openai-completions", "openai-responses"]);
|
|
549
|
+
const MAX_RESPONSE_BYTES = 4194304;
|
|
391
550
|
/** 读取有界响应体:声明超长或累计超长都拒绝(对齐官方 readBounded)。 */
|
|
392
551
|
async function readBounded(kit, response, url) {
|
|
393
552
|
const oversized = () => new kit.LlmError(`${url} 响应超过 ${MAX_RESPONSE_BYTES} 字节`, "DISCOVERY_FAILED");
|
|
@@ -428,8 +587,8 @@ function readListing(kit, body) {
|
|
|
428
587
|
const entry = raw;
|
|
429
588
|
if (typeof entry?.["id"] !== "string" || entry["id"].length === 0) continue;
|
|
430
589
|
const out = { id: entry["id"] };
|
|
431
|
-
const name
|
|
432
|
-
if (typeof name
|
|
590
|
+
const name = entry["name"] ?? entry["display_name"];
|
|
591
|
+
if (typeof name === "string" && name.length > 0) out.name = name;
|
|
433
592
|
for (const [key, field] of [
|
|
434
593
|
["context_window", "contextWindow"],
|
|
435
594
|
["context_length", "contextWindow"],
|
|
@@ -443,6 +602,19 @@ function readListing(kit, body) {
|
|
|
443
602
|
}
|
|
444
603
|
return models;
|
|
445
604
|
}
|
|
605
|
+
/** deepseek 路由直答:extends 'deepseek' 给官方目录全量;否则给 route 自配模型。 */
|
|
606
|
+
function deepseekCatalogAnswer(kit, route) {
|
|
607
|
+
if (kit.deepseek === void 0) throw new kit.LlmError("当前运行时套件不含 dsh-llm-deepseek,无法读取官方目录", "DISCOVERY_FAILED");
|
|
608
|
+
if (route.extends === "deepseek") return officialModelIds(kit).map((id) => ({ id }));
|
|
609
|
+
const models = (route.models ?? []).map((entry) => ({
|
|
610
|
+
id: entry.id,
|
|
611
|
+
...entry.name === void 0 ? {} : { name: entry.name },
|
|
612
|
+
...entry.contextWindow === void 0 ? {} : { contextWindow: entry.contextWindow },
|
|
613
|
+
...entry.maxTokens === void 0 ? {} : { maxTokens: entry.maxTokens }
|
|
614
|
+
}));
|
|
615
|
+
if (models.length === 0) throw new kit.LlmError("该 route 未配置 models 且未 extends deepseek;请手工录入模型", "DISCOVERY_FAILED");
|
|
616
|
+
return models;
|
|
617
|
+
}
|
|
446
618
|
/** 内置目录直答(route 配了 provider 级 extends 时)。 */
|
|
447
619
|
function catalogAnswer(kit, source) {
|
|
448
620
|
return builtinModelIds(kit, source).map((id) => {
|
|
@@ -456,19 +628,21 @@ function catalogAnswer(kit, source) {
|
|
|
456
628
|
});
|
|
457
629
|
}
|
|
458
630
|
/**
|
|
459
|
-
* 回答"该 provider 可服务哪些模型":
|
|
460
|
-
*
|
|
631
|
+
* 回答"该 provider 可服务哪些模型":deepseek 路由读官方目录/自有配置直答;
|
|
632
|
+
* extends 内置源零网络直答;否则仅 openai 系协议走 GET {baseURL}/models;
|
|
633
|
+
* 其余协议明确不支持。
|
|
461
634
|
*/
|
|
462
|
-
async function discoverModels(request
|
|
635
|
+
async function discoverModels(request, deps) {
|
|
463
636
|
const { kit } = deps;
|
|
464
|
-
const route = request
|
|
637
|
+
const route = request.provider === void 0 ? void 0 : deps.configProviders()[request.provider];
|
|
638
|
+
if ((route?.adapter ?? "pi") === "deepseek") return deepseekCatalogAnswer(kit, route);
|
|
465
639
|
if (route?.extends !== void 0 && hasBuiltinProvider(kit, route.extends)) return catalogAnswer(kit, route.extends);
|
|
466
|
-
const baseURL = request
|
|
467
|
-
if (baseURL === void 0 || baseURL.length === 0) throw new kit.LlmError(`route ${JSON.stringify(request
|
|
468
|
-
const api = request
|
|
640
|
+
const baseURL = request.baseURL ?? route?.baseURL;
|
|
641
|
+
if (baseURL === void 0 || baseURL.length === 0) throw new kit.LlmError(`route ${JSON.stringify(request.provider ?? "")} 未配 baseURL 且 extends 源无内置目录;无法探测模型清单`, "DISCOVERY_FAILED");
|
|
642
|
+
const api = request.api ?? route?.api ?? "openai-completions";
|
|
469
643
|
if (!LISTABLE_PROTOCOLS.has(api)) throw new kit.LlmError(`协议 "${api}" 无可读取的模型清单端点;请手工录入模型`, "DISCOVERY_UNSUPPORTED");
|
|
470
644
|
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
471
|
-
const supplied = request
|
|
645
|
+
const supplied = request.apiKey ?? await deps.storedApiKey(request.provider);
|
|
472
646
|
let authorization;
|
|
473
647
|
if (supplied !== void 0) {
|
|
474
648
|
const checked = kit.normalizeApiKey(supplied);
|
|
@@ -484,10 +658,10 @@ async function discoverModels(request$2, deps) {
|
|
|
484
658
|
...authorization === void 0 ? {} : { authorization },
|
|
485
659
|
...kit.attributionHeaders()
|
|
486
660
|
},
|
|
487
|
-
...request
|
|
661
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
488
662
|
});
|
|
489
663
|
} catch (error) {
|
|
490
|
-
if (request
|
|
664
|
+
if (request.signal?.aborted) throw new kit.LlmError("模型发现被调用方中止", "ABORTED", { cause: error });
|
|
491
665
|
throw new kit.LlmError(`无法连接 ${url}`, "DISCOVERY_FAILED", { cause: error });
|
|
492
666
|
}
|
|
493
667
|
if (!response.ok) throw new kit.LlmError(`${url} 返回 ${response.status}${response.status === 401 || response.status === 403 ? ";请检查 API key" : ""}`, "DISCOVERY_FAILED");
|
|
@@ -499,78 +673,71 @@ async function discoverModels(request$2, deps) {
|
|
|
499
673
|
throw new kit.LlmError(`${url} 未返回 JSON`, "DISCOVERY_FAILED", { cause: error });
|
|
500
674
|
}
|
|
501
675
|
}
|
|
502
|
-
|
|
503
676
|
//#endregion
|
|
504
677
|
//#region src/compat.ts
|
|
505
|
-
/** openai-completions 的 21 个字段(pi-ai OpenAICompletionsCompat)。 */
|
|
506
|
-
const COMPLETIONS_FIELDS = {
|
|
507
|
-
supportsStore: "boolean",
|
|
508
|
-
supportsDeveloperRole: "boolean",
|
|
509
|
-
supportsReasoningEffort: "boolean",
|
|
510
|
-
supportsUsageInStreaming: "boolean",
|
|
511
|
-
maxTokensField: ["max_completion_tokens", "max_tokens"],
|
|
512
|
-
requiresToolResultName: "boolean",
|
|
513
|
-
requiresAssistantAfterToolResult: "boolean",
|
|
514
|
-
requiresThinkingAsText: "boolean",
|
|
515
|
-
requiresReasoningContentOnAssistantMessages: "boolean",
|
|
516
|
-
thinkingFormat: [
|
|
517
|
-
"openai",
|
|
518
|
-
"openrouter",
|
|
519
|
-
"deepseek",
|
|
520
|
-
"together",
|
|
521
|
-
"zai",
|
|
522
|
-
"qwen",
|
|
523
|
-
"chat-template",
|
|
524
|
-
"qwen-chat-template",
|
|
525
|
-
"string-thinking",
|
|
526
|
-
"ant-ling"
|
|
527
|
-
],
|
|
528
|
-
chatTemplateKwargs: "object",
|
|
529
|
-
openRouterRouting: "object",
|
|
530
|
-
vercelGatewayRouting: "object",
|
|
531
|
-
zaiToolStream: "boolean",
|
|
532
|
-
supportsOpenAIGrammarTools: "boolean",
|
|
533
|
-
supportsStrictMode: "boolean",
|
|
534
|
-
cacheControlFormat: ["anthropic"],
|
|
535
|
-
sendSessionAffinityHeaders: "boolean",
|
|
536
|
-
deferredToolsMode: ["kimi"],
|
|
537
|
-
sessionAffinityFormat: [
|
|
538
|
-
"openai",
|
|
539
|
-
"openai-nosession",
|
|
540
|
-
"openrouter"
|
|
541
|
-
],
|
|
542
|
-
supportsLongCacheRetention: "boolean"
|
|
543
|
-
};
|
|
544
|
-
/** openai-responses 的 7 个字段(pi-ai OpenAIResponsesCompat)。 */
|
|
545
|
-
const RESPONSES_FIELDS = {
|
|
546
|
-
supportsDeveloperRole: "boolean",
|
|
547
|
-
sessionAffinityFormat: [
|
|
548
|
-
"openai",
|
|
549
|
-
"openai-nosession",
|
|
550
|
-
"openrouter"
|
|
551
|
-
],
|
|
552
|
-
supportsLongCacheRetention: "boolean",
|
|
553
|
-
supportsStrictMode: "boolean",
|
|
554
|
-
supportsOpenAIGrammarTools: "boolean",
|
|
555
|
-
supportsToolSearch: "boolean",
|
|
556
|
-
supportsExplicitPromptCacheMode: "boolean"
|
|
557
|
-
};
|
|
558
|
-
/** anthropic-messages 的 9 个字段(pi-ai AnthropicMessagesCompat)。 */
|
|
559
|
-
const ANTHROPIC_FIELDS = {
|
|
560
|
-
supportsEagerToolInputStreaming: "boolean",
|
|
561
|
-
supportsLongCacheRetention: "boolean",
|
|
562
|
-
sendSessionAffinityHeaders: "boolean",
|
|
563
|
-
supportsCacheControlOnTools: "boolean",
|
|
564
|
-
supportsTemperature: "boolean",
|
|
565
|
-
forceAdaptiveThinking: "boolean",
|
|
566
|
-
allowEmptySignature: "boolean",
|
|
567
|
-
supportsStrictTools: "boolean",
|
|
568
|
-
supportsToolReferences: "boolean"
|
|
569
|
-
};
|
|
570
678
|
const FIELDS_BY_PROTOCOL = {
|
|
571
|
-
"openai-completions":
|
|
572
|
-
|
|
573
|
-
|
|
679
|
+
"openai-completions": {
|
|
680
|
+
supportsStore: "boolean",
|
|
681
|
+
supportsDeveloperRole: "boolean",
|
|
682
|
+
supportsReasoningEffort: "boolean",
|
|
683
|
+
supportsUsageInStreaming: "boolean",
|
|
684
|
+
maxTokensField: ["max_completion_tokens", "max_tokens"],
|
|
685
|
+
requiresToolResultName: "boolean",
|
|
686
|
+
requiresAssistantAfterToolResult: "boolean",
|
|
687
|
+
requiresThinkingAsText: "boolean",
|
|
688
|
+
requiresReasoningContentOnAssistantMessages: "boolean",
|
|
689
|
+
thinkingFormat: [
|
|
690
|
+
"openai",
|
|
691
|
+
"openrouter",
|
|
692
|
+
"deepseek",
|
|
693
|
+
"together",
|
|
694
|
+
"zai",
|
|
695
|
+
"qwen",
|
|
696
|
+
"chat-template",
|
|
697
|
+
"qwen-chat-template",
|
|
698
|
+
"string-thinking",
|
|
699
|
+
"ant-ling"
|
|
700
|
+
],
|
|
701
|
+
chatTemplateKwargs: "object",
|
|
702
|
+
openRouterRouting: "object",
|
|
703
|
+
vercelGatewayRouting: "object",
|
|
704
|
+
zaiToolStream: "boolean",
|
|
705
|
+
supportsOpenAIGrammarTools: "boolean",
|
|
706
|
+
supportsStrictMode: "boolean",
|
|
707
|
+
cacheControlFormat: ["anthropic"],
|
|
708
|
+
sendSessionAffinityHeaders: "boolean",
|
|
709
|
+
deferredToolsMode: ["kimi"],
|
|
710
|
+
sessionAffinityFormat: [
|
|
711
|
+
"openai",
|
|
712
|
+
"openai-nosession",
|
|
713
|
+
"openrouter"
|
|
714
|
+
],
|
|
715
|
+
supportsLongCacheRetention: "boolean"
|
|
716
|
+
},
|
|
717
|
+
"openai-responses": {
|
|
718
|
+
supportsDeveloperRole: "boolean",
|
|
719
|
+
sessionAffinityFormat: [
|
|
720
|
+
"openai",
|
|
721
|
+
"openai-nosession",
|
|
722
|
+
"openrouter"
|
|
723
|
+
],
|
|
724
|
+
supportsLongCacheRetention: "boolean",
|
|
725
|
+
supportsStrictMode: "boolean",
|
|
726
|
+
supportsOpenAIGrammarTools: "boolean",
|
|
727
|
+
supportsToolSearch: "boolean",
|
|
728
|
+
supportsExplicitPromptCacheMode: "boolean"
|
|
729
|
+
},
|
|
730
|
+
"anthropic-messages": {
|
|
731
|
+
supportsEagerToolInputStreaming: "boolean",
|
|
732
|
+
supportsLongCacheRetention: "boolean",
|
|
733
|
+
sendSessionAffinityHeaders: "boolean",
|
|
734
|
+
supportsCacheControlOnTools: "boolean",
|
|
735
|
+
supportsTemperature: "boolean",
|
|
736
|
+
forceAdaptiveThinking: "boolean",
|
|
737
|
+
allowEmptySignature: "boolean",
|
|
738
|
+
supportsStrictTools: "boolean",
|
|
739
|
+
supportsToolReferences: "boolean"
|
|
740
|
+
}
|
|
574
741
|
};
|
|
575
742
|
function checkValue(_api, field, spec, value, where) {
|
|
576
743
|
if (spec === "boolean") {
|
|
@@ -610,7 +777,6 @@ function mergeCompat(...layers) {
|
|
|
610
777
|
}
|
|
611
778
|
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
612
779
|
}
|
|
613
|
-
|
|
614
780
|
//#endregion
|
|
615
781
|
//#region src/inherit.ts
|
|
616
782
|
var ExtendsError = class extends Error {};
|
|
@@ -663,9 +829,181 @@ function resolveModelBase(route, profile, entry, kit, modelsDev) {
|
|
|
663
829
|
if (hit === void 0) throw new ExtendsError(`${where}: extends 引用 "${provider}/${ref.model}" 在内置目录与 models.dev 快照中都不存在`);
|
|
664
830
|
return hit;
|
|
665
831
|
}
|
|
666
|
-
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/profiles-deepseek.ts
|
|
834
|
+
/** 报告不可服务的 route,命名出错配置键(与 profiles.ts 的 invalid 同格式)。 */
|
|
835
|
+
function invalid$1(provider, detail) {
|
|
836
|
+
throw new Error(`llm-pi: provider "${provider}" ${detail}`);
|
|
837
|
+
}
|
|
838
|
+
/** pi 专有字段在 deepseek 路由上显式拒绝(静默忽略会让用户误以为生效)。 */
|
|
839
|
+
const PI_ONLY_FIELDS = [
|
|
840
|
+
"api",
|
|
841
|
+
"compat",
|
|
842
|
+
"headers",
|
|
843
|
+
"transport",
|
|
844
|
+
"websocketConnectTimeoutMs",
|
|
845
|
+
"thinkingBudgets",
|
|
846
|
+
"cacheRetention",
|
|
847
|
+
"reasoning",
|
|
848
|
+
"defaultInput",
|
|
849
|
+
"maxRequestImageBytes",
|
|
850
|
+
"timeoutMs"
|
|
851
|
+
];
|
|
852
|
+
/** schema 解析会把 dict 字段物化为 {}(schemastery 行为);空字典视为未配置。 */
|
|
853
|
+
function nonEmptyDict(value) {
|
|
854
|
+
return typeof value === "object" && value !== null && Object.keys(value).length > 0;
|
|
855
|
+
}
|
|
856
|
+
/** defaultInput 的 schema 物化默认(['text']);显式写同值无害,同样视为未配置。 */
|
|
857
|
+
function isMaterializedDefaultInput(value) {
|
|
858
|
+
return Array.isArray(value) && value.length === 1 && value[0] === "text";
|
|
859
|
+
}
|
|
860
|
+
function rejectPiOnlyFields(route, profile) {
|
|
861
|
+
for (const field of PI_ONLY_FIELDS) {
|
|
862
|
+
const value = profile[field];
|
|
863
|
+
if (value === void 0) continue;
|
|
864
|
+
if (field === "compat" || field === "headers" || field === "thinkingBudgets") {
|
|
865
|
+
if (!nonEmptyDict(value)) continue;
|
|
866
|
+
} else if (field === "defaultInput" && isMaterializedDefaultInput(value)) continue;
|
|
867
|
+
invalid$1(route, `的 ${field} 仅 adapter: pi 可用;adapter: deepseek 的 route 请移除该字段`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/** 解析模型条目的官方目录继承 base;extends 仅允许裸 id 或 "deepseek/id"。 */
|
|
871
|
+
function resolveOfficialBase(route, entry, deps) {
|
|
872
|
+
const ref = entry.extends;
|
|
873
|
+
if (ref !== void 0) {
|
|
874
|
+
const slash = ref.indexOf("/");
|
|
875
|
+
const source = slash < 0 ? "deepseek" : ref.slice(0, slash);
|
|
876
|
+
const id = slash < 0 ? ref : ref.slice(slash + 1);
|
|
877
|
+
if (source !== "deepseek" || id.length === 0 || id.includes("/")) invalid$1(route, `model "${entry.id}" 的 extends 引用 ${JSON.stringify(ref)} 非法:adapter: deepseek 的路由仅支持 "deepseek/model" 或裸 model id(官方目录)`);
|
|
878
|
+
const base = officialModelBase(deps.kit, id);
|
|
879
|
+
if (base === void 0) {
|
|
880
|
+
if (deps.lenient) {
|
|
881
|
+
deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 的 extends 引用 "${ref}" 在当前官方目录未命中;已降级为手写条目`);
|
|
882
|
+
return {};
|
|
883
|
+
}
|
|
884
|
+
invalid$1(route, `model "${entry.id}" 的 extends 引用 "${ref}" 在官方目录中不存在`);
|
|
885
|
+
}
|
|
886
|
+
return base;
|
|
887
|
+
}
|
|
888
|
+
return officialModelBase(deps.kit, entry.id) ?? {};
|
|
889
|
+
}
|
|
890
|
+
/** 条目的声明模态;schema 物化的空数组视为"无答案",交官方继承 base(同 profiles.ts declaredInput)。 */
|
|
891
|
+
function declaredModalities(configured) {
|
|
892
|
+
return configured === void 0 || configured.length === 0 ? void 0 : [...configured];
|
|
893
|
+
}
|
|
894
|
+
/** 物化单个模型目录条目:官方 base 在下,条目显式字段逐字段覆盖。 */
|
|
895
|
+
function materializeModel$1(route, entry, deps) {
|
|
896
|
+
if (entry.reasoningEfforts !== void 0 || nonEmptyDict(entry.compat)) invalid$1(route, `model "${entry.id}" 的 reasoningEfforts/compat 仅 adapter: pi 可用`);
|
|
897
|
+
const base = resolveOfficialBase(route, entry, deps);
|
|
898
|
+
const input = declaredModalities(entry.input) ?? base.input;
|
|
899
|
+
const imageFields = {
|
|
900
|
+
imagePixelBudget: entry.imagePixelBudget ?? base.imagePixelBudget,
|
|
901
|
+
imageMaxBytes: entry.imageMaxBytes ?? base.imageMaxBytes,
|
|
902
|
+
imageDetail: entry.imageDetail ?? base.imageDetail
|
|
903
|
+
};
|
|
904
|
+
if (!(input?.includes("image") ?? false) && Object.values(imageFields).some((value) => value !== void 0)) invalid$1(route, `model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes/imageDetail`);
|
|
905
|
+
return {
|
|
906
|
+
id: entry.id,
|
|
907
|
+
...(entry.name ?? base.name) === void 0 ? {} : { name: entry.name ?? base.name },
|
|
908
|
+
...(entry.contextWindow ?? base.contextWindow) === void 0 ? {} : { contextWindow: entry.contextWindow ?? base.contextWindow },
|
|
909
|
+
...(entry.maxTokens ?? base.maxTokens) === void 0 ? {} : { maxTokens: entry.maxTokens ?? base.maxTokens },
|
|
910
|
+
...input === void 0 ? {} : { inputModalities: [...input] },
|
|
911
|
+
...Object.fromEntries(Object.entries(imageFields).filter(([, value]) => value !== void 0))
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/** 物化一个 route 的模型目录;models 缺省时以官方目录全量继承(要求 extends: 'deepseek')。 */
|
|
915
|
+
function materializeModels(route, profile, deps) {
|
|
916
|
+
if (profile.models !== void 0 && profile.models.length > 0) {
|
|
917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
918
|
+
return profile.models.map((entry) => {
|
|
919
|
+
if (entry.id.length === 0) invalid$1(route, "存在空 id 的模型条目");
|
|
920
|
+
if (seen.has(entry.id)) invalid$1(route, `模型 "${entry.id}" 重复列出`);
|
|
921
|
+
seen.add(entry.id);
|
|
922
|
+
return materializeModel$1(route, entry, deps);
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (profile.extends === "deepseek") return officialModelIds(deps.kit).map((id) => materializeModel$1(route, { id }, deps));
|
|
926
|
+
invalid$1(route, "未配置 models;adapter: deepseek 的 route 可配 extends: deepseek 全量继承官方目录");
|
|
927
|
+
}
|
|
928
|
+
/** 组装官方 resolveAdapterOptions 的输入:仅放入显式配置的键,其余走官方默认值。 */
|
|
929
|
+
function rawAdapterConfig(profile, baseURL, models) {
|
|
930
|
+
const passthrough = [
|
|
931
|
+
"apiKeyEnv",
|
|
932
|
+
"thinking",
|
|
933
|
+
"reasoningEffort",
|
|
934
|
+
"streamIdleTimeoutMs",
|
|
935
|
+
"maxRequestFilesBytes",
|
|
936
|
+
"maxInlineRequestImageBytes",
|
|
937
|
+
"maxImagesPerRequest",
|
|
938
|
+
"imageOffloadByteQuantum",
|
|
939
|
+
"inlineImageOffloadByteQuantum",
|
|
940
|
+
"imageOffloadCountQuantum",
|
|
941
|
+
"filesApiTimeoutMs",
|
|
942
|
+
"fileExpiresAfterSeconds",
|
|
943
|
+
"fileRefreshMarginSeconds",
|
|
944
|
+
"retryPolicy"
|
|
945
|
+
];
|
|
946
|
+
const raw = {
|
|
947
|
+
baseURL,
|
|
948
|
+
models
|
|
949
|
+
};
|
|
950
|
+
for (const key of passthrough) if (profile[key] !== void 0) raw[key] = profile[key];
|
|
951
|
+
if (profile.defaultMaxTokens !== void 0) raw["maxTokens"] = profile.defaultMaxTokens;
|
|
952
|
+
if (profile.defaultContextWindow !== void 0) raw["defaultContextWindow"] = profile.defaultContextWindow;
|
|
953
|
+
return raw;
|
|
954
|
+
}
|
|
955
|
+
/** 物化单个 deepseek route;lenient 下失败告警并返回 null(调用方跳过注册)。 */
|
|
956
|
+
function buildRoute(route, profile, deps) {
|
|
957
|
+
try {
|
|
958
|
+
if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(需 dsh ≥ 0.1.1-rc.2)");
|
|
959
|
+
rejectPiOnlyFields(route, profile);
|
|
960
|
+
if (profile.extends !== void 0 && profile.extends !== "deepseek") invalid$1(route, `的 extends ${JSON.stringify(profile.extends)} 非法:adapter: deepseek 仅支持 "deepseek"(官方目录)`);
|
|
961
|
+
if (profile.apiKeyEnv === void 0 || profile.apiKeyEnv.length === 0) invalid$1(route, "需要 apiKeyEnv:DeepSeekAdapter 无环境自发现,凭据引用必须显式配置");
|
|
962
|
+
const baseURL = profile.baseURL ?? (profile.extends === "deepseek" ? officialBaseUrl(deps.kit) : void 0);
|
|
963
|
+
if (baseURL === void 0 || baseURL.length === 0) invalid$1(route, "需要 baseURL(或 extends: deepseek 继承官方端点)");
|
|
964
|
+
const models = materializeModels(route, profile, deps);
|
|
965
|
+
const connection = deps.kit.deepseek.resolveAdapterOptions(rawAdapterConfig(profile, baseURL, models), void 0);
|
|
966
|
+
return {
|
|
967
|
+
route,
|
|
968
|
+
displayName: profile.displayName ?? route,
|
|
969
|
+
connection
|
|
970
|
+
};
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (!deps.lenient) throw error;
|
|
973
|
+
deps.warn?.(`llm-pi: deepseek route "${route}" 不可服务(${error instanceof Error ? error.message : String(error)}),已跳过注册`);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* 校验并物化全部 adapter: deepseek 的 route。严格模式任一 route 失败即整体
|
|
979
|
+
* 抛错(settings 写入校验),lenient 模式跳过坏 route(运行期热更新)。
|
|
980
|
+
*/
|
|
981
|
+
function buildDeepseekRoutes(providers, deps) {
|
|
982
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
984
|
+
if ((profile.adapter ?? "pi") !== "deepseek") continue;
|
|
985
|
+
if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
|
|
986
|
+
if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid$1(route, "displayName 为空");
|
|
987
|
+
const built = buildRoute(route, profile, deps);
|
|
988
|
+
if (built !== null) resolved.set(route, built);
|
|
989
|
+
}
|
|
990
|
+
return resolved;
|
|
991
|
+
}
|
|
667
992
|
//#endregion
|
|
668
993
|
//#region src/profiles.ts
|
|
994
|
+
/**
|
|
995
|
+
* profile 构建器:插件 Config → ResolvedPiAiProviderProfile 映射。
|
|
996
|
+
*
|
|
997
|
+
* 语义与官方 dsh-llm-pi-ai 的 resolveProfiles 链逐点对齐(index.js:1068-1471),
|
|
998
|
+
* 差异仅在两点扩展:
|
|
999
|
+
* - base 来源从"route 同名内置目录"换为 extends 三级数据源(inherit.ts);
|
|
1000
|
+
* - compat 从 2 字段枚举扩展为逐协议全量字段(compat.ts),写入即校验。
|
|
1001
|
+
*
|
|
1002
|
+
* 产物经 `new PiAiAdapter({ profiles })` 进入官方消息链路;
|
|
1003
|
+
* 任何不可服务的配置在此处抛错(命名 route/model),配合 settings 写入校验,
|
|
1004
|
+
* 非法配置在写入时被拒绝、运行期保留上一份好配置。
|
|
1005
|
+
* @module llm-pi/profiles
|
|
1006
|
+
*/
|
|
669
1007
|
/** 内置目录未描述时的零价目(harness 不消费 cost 元数据,同官方 NO_COST)。 */
|
|
670
1008
|
const NO_COST = {
|
|
671
1009
|
input: 0,
|
|
@@ -717,12 +1055,12 @@ function resolveModelReasoning(provider, entry, base) {
|
|
|
717
1055
|
};
|
|
718
1056
|
}
|
|
719
1057
|
/** Harness 自管凭据的 api-key auth(对齐官方 harnessApiKeyAuth,index.js:1215)。 */
|
|
720
|
-
function harnessApiKeyAuth(name
|
|
1058
|
+
function harnessApiKeyAuth(name) {
|
|
721
1059
|
return {
|
|
722
|
-
name
|
|
1060
|
+
name,
|
|
723
1061
|
resolve: ({ credential }) => Promise.resolve({
|
|
724
1062
|
auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
|
|
725
|
-
source: name
|
|
1063
|
+
source: name
|
|
726
1064
|
})
|
|
727
1065
|
};
|
|
728
1066
|
}
|
|
@@ -744,8 +1082,8 @@ function materializeModel(route, profile, entry, base, routeApi, providerBaseUrl
|
|
|
744
1082
|
}
|
|
745
1083
|
invalid(route, `model "${entry.id}" 需要 baseURL:继承源未提供,请在 route 上设置 baseURL`);
|
|
746
1084
|
}
|
|
747
|
-
const contextWindow = entry.contextWindow ?? base.contextWindow ?? profile.defaultContextWindow ??
|
|
748
|
-
const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ??
|
|
1085
|
+
const contextWindow = entry.contextWindow ?? base.contextWindow ?? profile.defaultContextWindow ?? 262144;
|
|
1086
|
+
const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? 32768;
|
|
749
1087
|
if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
|
|
750
1088
|
validateCompat(api, profile.compat, `provider "${route}"`);
|
|
751
1089
|
validateCompat(api, entry.compat, `provider "${route}" model "${entry.id}"`);
|
|
@@ -823,10 +1161,11 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
|
|
|
823
1161
|
function buildProfiles(providers, deps) {
|
|
824
1162
|
const resolved = /* @__PURE__ */ new Map();
|
|
825
1163
|
for (const [route, profile] of Object.entries(providers ?? {})) {
|
|
1164
|
+
if ((profile.adapter ?? "pi") === "deepseek") continue;
|
|
826
1165
|
if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
|
|
827
1166
|
if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
|
|
828
1167
|
if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
|
|
829
|
-
const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ??
|
|
1168
|
+
const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? 3e5;
|
|
830
1169
|
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) invalid(route, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`);
|
|
831
1170
|
const defaultInput = [...profile.defaultInput ?? ["text"]];
|
|
832
1171
|
if (defaultInput.length === 0) invalid(route, "defaultInput 至少要声明一种模态");
|
|
@@ -850,7 +1189,7 @@ function buildProfiles(providers, deps) {
|
|
|
850
1189
|
displayName,
|
|
851
1190
|
...profile.apiKeyEnv === void 0 ? {} : { apiKeyEnv: credentialRef(profile.apiKeyEnv) },
|
|
852
1191
|
streamIdleTimeoutMs,
|
|
853
|
-
maxRequestImageBytes: profile.maxRequestImageBytes ??
|
|
1192
|
+
maxRequestImageBytes: profile.maxRequestImageBytes ?? 20971520,
|
|
854
1193
|
retryPolicy: deps.kit.resolveRetryPolicy(profile.retryPolicy, `llm-pi: provider "${route}" retryPolicy`),
|
|
855
1194
|
...profile.headers === void 0 ? {} : { headers: { ...profile.headers } },
|
|
856
1195
|
...profile.reasoning === void 0 ? {} : { reasoning: profile.reasoning },
|
|
@@ -868,10 +1207,25 @@ function buildProfiles(providers, deps) {
|
|
|
868
1207
|
/** settings 写入校验钩子:完整试跑解析,非法配置在写入处拒绝。 */
|
|
869
1208
|
function assertServiceable(config, deps) {
|
|
870
1209
|
buildProfiles(config.providers, deps);
|
|
1210
|
+
buildDeepseekRoutes(config.providers, deps);
|
|
871
1211
|
}
|
|
872
|
-
|
|
873
1212
|
//#endregion
|
|
874
1213
|
//#region src/resolve-dsh.ts
|
|
1214
|
+
/**
|
|
1215
|
+
* dsh 运行时套件解析:让插件跑在 dsh 当前安装树的同一份
|
|
1216
|
+
* dsh-llm-pi-ai / dsh-llm / pi-ai 之上,从而 dsh 升级即自动跟随上游。
|
|
1217
|
+
*
|
|
1218
|
+
* 解析策略(全部模块同源地整体成功或整体回退,杜绝跨源混用):
|
|
1219
|
+
* 1. dsh-tree:从 process.argv[1](systemd/CLI 启动的 dsh 即 bin.js shim,
|
|
1220
|
+
* realpath 后落在 dsh 安装树内)向上找到同时含有
|
|
1221
|
+
* node_modules/@deepseek-ai/dsh-llm-pi-ai 与 node_modules/@earendil-works/pi-ai
|
|
1222
|
+
* 的目录,按文件路径动态 import——与 dsh 官方插件共享同一模块实例;
|
|
1223
|
+
* 2. vendored:回退到本插件 dependencies 里的固定版本副本(裸 import)。
|
|
1224
|
+
*
|
|
1225
|
+
* 两条路径产物都过形状自检(assertKitShape):上游 rc 重构导致形状漂移时
|
|
1226
|
+
* 抛错,由调用方决定回退或放弃注册——绝不让坏套件进入消息链路。
|
|
1227
|
+
* @module llm-pi/resolve-dsh
|
|
1228
|
+
*/
|
|
875
1229
|
/** kit 必备形状清单:缺失即视为上游不兼容。 */
|
|
876
1230
|
function assertKitShape(kit, origin) {
|
|
877
1231
|
const problems = [];
|
|
@@ -891,6 +1245,15 @@ function assertKitShape(kit, origin) {
|
|
|
891
1245
|
if (typeof kit.resolveRetryPolicy !== "function") problems.push("dsh-llm resolveRetryPolicy 缺失");
|
|
892
1246
|
if (problems.length > 0) throw new Error(`llm-pi: ${origin} 来源的运行时套件形状不兼容:${problems.join(";")}`);
|
|
893
1247
|
}
|
|
1248
|
+
/** deepseek 模块形状自检;返回问题清单(空 = 可用),由调用方决定降级。 */
|
|
1249
|
+
function checkDeepseekShape(kit) {
|
|
1250
|
+
const problems = [];
|
|
1251
|
+
if (typeof kit.DeepSeekAdapter !== "function") problems.push("DeepSeekAdapter 不是类");
|
|
1252
|
+
else if (typeof kit.DeepSeekAdapter.prototype["stream"] !== "function") problems.push("DeepSeekAdapter.prototype.stream 缺失");
|
|
1253
|
+
if (typeof kit.resolveAdapterOptions !== "function") problems.push("resolveAdapterOptions 缺失");
|
|
1254
|
+
if (typeof kit.getOrCreateAnonymousUserId !== "function") problems.push("getOrCreateAnonymousUserId 缺失");
|
|
1255
|
+
return problems;
|
|
1256
|
+
}
|
|
894
1257
|
/** 从 startDir 向上找同时含有 dsh-llm-pi-ai 与 pi-ai 的安装树根。 */
|
|
895
1258
|
function findDshTreeRoot(startDir) {
|
|
896
1259
|
let dir = startDir;
|
|
@@ -925,6 +1288,24 @@ async function importTreeModules(root) {
|
|
|
925
1288
|
anthropic
|
|
926
1289
|
};
|
|
927
1290
|
}
|
|
1291
|
+
/** 从同一 dsh 安装树加载 deepseek 适配器模块(失败返回 undefined,不拖垮核心套件)。 */
|
|
1292
|
+
async function importTreeDeepseek(root) {
|
|
1293
|
+
const nm = join(root, "node_modules");
|
|
1294
|
+
const load = (absPath) => import(pathToFileURL(absPath).href);
|
|
1295
|
+
try {
|
|
1296
|
+
const [deepseek, anonId] = await Promise.all([load(join(nm, "@deepseek-ai", "dsh-llm-deepseek", "lib", "index.js")), load(join(nm, "@deepseek-ai", "dsh-anonymous-user-id", "lib", "index.js"))]);
|
|
1297
|
+
const kit = {
|
|
1298
|
+
DeepSeekAdapter: deepseek["DeepSeekAdapter"],
|
|
1299
|
+
resolveAdapterOptions: deepseek["resolveAdapterOptions"],
|
|
1300
|
+
getOrCreateAnonymousUserId: anonId["getOrCreateAnonymousUserId"]
|
|
1301
|
+
};
|
|
1302
|
+
const problems = checkDeepseekShape(kit);
|
|
1303
|
+
if (problems.length > 0) return { problem: `形状不兼容:${problems.join(";")}` };
|
|
1304
|
+
return { kit };
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
return { problem: error instanceof Error ? error.message : String(error) };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
928
1309
|
function kitFromTree(mods) {
|
|
929
1310
|
return {
|
|
930
1311
|
source: "dsh-tree",
|
|
@@ -946,8 +1327,18 @@ function kitFromTree(mods) {
|
|
|
946
1327
|
}
|
|
947
1328
|
};
|
|
948
1329
|
}
|
|
1330
|
+
/** vendored 兜底副本的 deepseek 模块(形状自检不过时返回 undefined)。 */
|
|
1331
|
+
function loadVendoredDeepseek() {
|
|
1332
|
+
const kit = {
|
|
1333
|
+
DeepSeekAdapter: vendoredDeepseek.DeepSeekAdapter,
|
|
1334
|
+
resolveAdapterOptions: vendoredDeepseek.resolveAdapterOptions,
|
|
1335
|
+
getOrCreateAnonymousUserId: vendoredAnonId.getOrCreateAnonymousUserId
|
|
1336
|
+
};
|
|
1337
|
+
return checkDeepseekShape(kit).length === 0 ? kit : void 0;
|
|
1338
|
+
}
|
|
949
1339
|
/** vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。 */
|
|
950
1340
|
function loadVendoredKit() {
|
|
1341
|
+
const deepseek = loadVendoredDeepseek();
|
|
951
1342
|
const kit = {
|
|
952
1343
|
source: "vendored",
|
|
953
1344
|
PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
|
|
@@ -965,7 +1356,8 @@ function loadVendoredKit() {
|
|
|
965
1356
|
"openai-completions": openAICompletionsApi,
|
|
966
1357
|
"openai-responses": openAIResponsesApi,
|
|
967
1358
|
"anthropic-messages": anthropicMessagesApi
|
|
968
|
-
}
|
|
1359
|
+
},
|
|
1360
|
+
...deepseek === void 0 ? {} : { deepseek }
|
|
969
1361
|
};
|
|
970
1362
|
assertKitShape(kit, "vendored");
|
|
971
1363
|
return kit;
|
|
@@ -991,6 +1383,15 @@ async function resolveDshKit() {
|
|
|
991
1383
|
if (anchor !== void 0) try {
|
|
992
1384
|
const kit = kitFromTree(await importTreeModules(anchor));
|
|
993
1385
|
assertKitShape(kit, "dsh-tree");
|
|
1386
|
+
const tree = await importTreeDeepseek(anchor);
|
|
1387
|
+
if (tree.kit !== void 0) return {
|
|
1388
|
+
kit: {
|
|
1389
|
+
...kit,
|
|
1390
|
+
deepseek: tree.kit
|
|
1391
|
+
},
|
|
1392
|
+
diagnostics
|
|
1393
|
+
};
|
|
1394
|
+
diagnostics.push(`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? "未知原因"});adapter: deepseek 的 route 不可用,pi 路由不受影响`);
|
|
994
1395
|
return {
|
|
995
1396
|
kit,
|
|
996
1397
|
diagnostics
|
|
@@ -999,12 +1400,13 @@ async function resolveDshKit() {
|
|
|
999
1400
|
diagnostics.push(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`);
|
|
1000
1401
|
}
|
|
1001
1402
|
else diagnostics.push("未能从 process.argv[1] 定位 dsh 安装树;回退 vendored 副本");
|
|
1403
|
+
const kit = loadVendoredKit();
|
|
1404
|
+
if (kit.deepseek === void 0) diagnostics.push("vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用");
|
|
1002
1405
|
return {
|
|
1003
|
-
kit
|
|
1406
|
+
kit,
|
|
1004
1407
|
diagnostics
|
|
1005
1408
|
};
|
|
1006
1409
|
}
|
|
1007
|
-
|
|
1008
1410
|
//#endregion
|
|
1009
1411
|
//#region src/service.ts
|
|
1010
1412
|
/** 注册时捕获的事实表;变化才重注册(按 provider 排序,免序误报)。 */
|
|
@@ -1038,6 +1440,7 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1038
1440
|
let current = () => config;
|
|
1039
1441
|
let lastRaw;
|
|
1040
1442
|
let memoized;
|
|
1443
|
+
let memoizedDeepseek;
|
|
1041
1444
|
const deps = {
|
|
1042
1445
|
kit,
|
|
1043
1446
|
modelsDev
|
|
@@ -1052,10 +1455,20 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1052
1455
|
lenient: true,
|
|
1053
1456
|
warn: (message) => logger.warn(message)
|
|
1054
1457
|
}) : /* @__PURE__ */ new Map();
|
|
1458
|
+
memoizedDeepseek = raw.enabled ? buildDeepseekRoutes(raw.providers, {
|
|
1459
|
+
kit,
|
|
1460
|
+
lenient: true,
|
|
1461
|
+
warn: (message) => logger.warn(message)
|
|
1462
|
+
}) : /* @__PURE__ */ new Map();
|
|
1055
1463
|
lastRaw = raw;
|
|
1056
1464
|
memoized = next;
|
|
1057
1465
|
return next;
|
|
1058
1466
|
};
|
|
1467
|
+
/** deepseek 路由物化表(与 profiles 同一次备忘窗口)。 */
|
|
1468
|
+
const deepseekRoutes = () => {
|
|
1469
|
+
profiles();
|
|
1470
|
+
return memoizedDeepseek ?? /* @__PURE__ */ new Map();
|
|
1471
|
+
};
|
|
1059
1472
|
profiles();
|
|
1060
1473
|
const adapter = new kit.PiAiAdapter({
|
|
1061
1474
|
profiles,
|
|
@@ -1068,7 +1481,7 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1068
1481
|
if (profile === void 0) return void 0;
|
|
1069
1482
|
return makeResolveApiKey(ctx, kit)(provider, profile);
|
|
1070
1483
|
};
|
|
1071
|
-
ctx.llm.registerModelDiscovery(SETTINGS_NS, (request
|
|
1484
|
+
ctx.llm.registerModelDiscovery(SETTINGS_NS, (request) => discoverModels(request, {
|
|
1072
1485
|
kit,
|
|
1073
1486
|
configProviders: () => current().providers ?? {},
|
|
1074
1487
|
storedApiKey
|
|
@@ -1129,13 +1542,21 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1129
1542
|
let directory;
|
|
1130
1543
|
let directoryFacts;
|
|
1131
1544
|
const ensureDirectory = () => {
|
|
1132
|
-
const
|
|
1545
|
+
const piEntries = [...profiles().entries()].map(([provider, profile]) => ({
|
|
1133
1546
|
provider,
|
|
1134
1547
|
displayName: profile.displayName,
|
|
1135
1548
|
settingsNs: SETTINGS_NS,
|
|
1136
1549
|
settingsPath: ["providers", provider],
|
|
1137
1550
|
declared: !hasBuiltinProvider(kit, provider)
|
|
1138
1551
|
}));
|
|
1552
|
+
const deepseekEntries = [...deepseekRoutes().values()].map((built) => ({
|
|
1553
|
+
provider: built.route,
|
|
1554
|
+
displayName: built.displayName,
|
|
1555
|
+
settingsNs: SETTINGS_NS,
|
|
1556
|
+
settingsPath: ["providers", built.route],
|
|
1557
|
+
declared: true
|
|
1558
|
+
}));
|
|
1559
|
+
const entries = [...piEntries, ...deepseekEntries];
|
|
1139
1560
|
if (deepEqualJson(entries, directoryFacts)) return;
|
|
1140
1561
|
if (entries.length === 0) {
|
|
1141
1562
|
directoryFacts = entries;
|
|
@@ -1145,7 +1566,18 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1145
1566
|
else directory.replace(entries);
|
|
1146
1567
|
directoryFacts = entries;
|
|
1147
1568
|
};
|
|
1569
|
+
const deepseekRegistrar = new DeepseekRouteRegistrar({
|
|
1570
|
+
ctx,
|
|
1571
|
+
kit,
|
|
1572
|
+
logger: {
|
|
1573
|
+
warn: (m) => logger.warn(m),
|
|
1574
|
+
error: (m) => logger.error(m)
|
|
1575
|
+
},
|
|
1576
|
+
routes: deepseekRoutes
|
|
1577
|
+
});
|
|
1578
|
+
const ensureDeepseek = () => deepseekRegistrar.sync(deepseekRoutes());
|
|
1148
1579
|
ensureRegistration();
|
|
1580
|
+
ensureDeepseek();
|
|
1149
1581
|
ensureDirectory();
|
|
1150
1582
|
installSettingsSection(ctx, SETTINGS_NS, Config, config, {
|
|
1151
1583
|
validate: (cfg) => assertServiceable(cfg, deps),
|
|
@@ -1159,6 +1591,12 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1159
1591
|
logger.error("llm-pi: 更新被拒,保留此前注册的 route");
|
|
1160
1592
|
logger.error(error);
|
|
1161
1593
|
}
|
|
1594
|
+
try {
|
|
1595
|
+
ensureDeepseek();
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
logger.error("llm-pi: deepseek route 更新失败,保留此前注册");
|
|
1598
|
+
logger.error(error);
|
|
1599
|
+
}
|
|
1162
1600
|
try {
|
|
1163
1601
|
ensureDirectory();
|
|
1164
1602
|
} catch (error) {
|
|
@@ -1179,7 +1617,6 @@ async function startRuntime(ctx, rawConfig) {
|
|
|
1179
1617
|
kit
|
|
1180
1618
|
};
|
|
1181
1619
|
}
|
|
1182
|
-
|
|
1183
1620
|
//#endregion
|
|
1184
1621
|
//#region src/index.ts
|
|
1185
1622
|
const name = "dsh-plus-llm-pi";
|
|
@@ -1190,6 +1627,5 @@ async function apply(ctx, config) {
|
|
|
1190
1627
|
registerCatalogApi(webCtx, runtime);
|
|
1191
1628
|
});
|
|
1192
1629
|
}
|
|
1193
|
-
|
|
1194
1630
|
//#endregion
|
|
1195
|
-
export { Config, SETTINGS_NS, apply, inject, name };
|
|
1631
|
+
export { Config, SETTINGS_NS, apply, inject, name };
|