@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/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(SETTINGS_NS$1);
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
- const DEFAULT_CONTEXT_WINDOW = 262144;
53
- const DEFAULT_MAX_TOKENS = 32768;
54
- const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
55
- /** 单请求 base64 图片载荷上限(rc8 起官方 ResolvedPiAiProviderProfile 必需字段;
56
- * 缺省取官方默认 20MiB,旧配置无需改动即自动生效)。 */
57
- const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
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 = 20 * 1024 * 1024;
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
- const LISTABLE_PROTOCOLS = new Set(["openai-completions", "openai-responses"]);
390
- const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
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$1 = entry["name"] ?? entry["display_name"];
432
- if (typeof name$1 === "string" && name$1.length > 0) out.name = name$1;
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 可服务哪些模型":extends 内置源零网络直答;
460
- * 否则仅 openai 系协议走 GET {baseURL}/models;其余协议明确不支持。
631
+ * 回答"该 provider 可服务哪些模型":deepseek 路由读官方目录/自有配置直答;
632
+ * extends 内置源零网络直答;否则仅 openai 系协议走 GET {baseURL}/models
633
+ * 其余协议明确不支持。
461
634
  */
462
- async function discoverModels(request$2, deps) {
635
+ async function discoverModels(request, deps) {
463
636
  const { kit } = deps;
464
- const route = request$2.provider === void 0 ? void 0 : deps.configProviders()[request$2.provider];
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$2.baseURL ?? route?.baseURL;
467
- if (baseURL === void 0 || baseURL.length === 0) throw new kit.LlmError(`route ${JSON.stringify(request$2.provider ?? "")} 未配 baseURL 且 extends 源无内置目录;无法探测模型清单`, "DISCOVERY_FAILED");
468
- const api = request$2.api ?? route?.api ?? "openai-completions";
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$2.apiKey ?? await deps.storedApiKey(request$2.provider);
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$2.signal === void 0 ? {} : { signal: request$2.signal }
661
+ ...request.signal === void 0 ? {} : { signal: request.signal }
488
662
  });
489
663
  } catch (error) {
490
- if (request$2.signal?.aborted) throw new kit.LlmError("模型发现被调用方中止", "ABORTED", { cause: error });
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": COMPLETIONS_FIELDS,
572
- "openai-responses": RESPONSES_FIELDS,
573
- "anthropic-messages": ANTHROPIC_FIELDS
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$1) {
1058
+ function harnessApiKeyAuth(name) {
721
1059
  return {
722
- name: name$1,
1060
+ name,
723
1061
  resolve: ({ credential }) => Promise.resolve({
724
1062
  auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
725
- source: name$1
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 ?? DEFAULT_CONTEXT_WINDOW;
748
- const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? DEFAULT_MAX_TOKENS;
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 ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
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 ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES,
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: loadVendoredKit(),
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$2) => discoverModels(request$2, {
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 entries = [...profiles().entries()].map(([provider, profile]) => ({
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 };