@dsh-plus/llm-pi 0.1.12 → 0.1.14

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
@@ -5,12 +5,13 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
6
6
  import { request } from "node:http";
7
7
  import { request as request$1 } from "node:https";
8
- import { dirname, join } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
9
  import HttpsProxyAgentModule from "https-proxy-agent";
10
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
10
+ import { credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import * as vendoredAnonId from "@deepseek-ai/dsh-anonymous-user-id";
13
13
  import * as vendoredLlm from "@deepseek-ai/dsh-llm";
14
+ import { LlmError } from "@deepseek-ai/dsh-llm";
14
15
  import * as vendoredDeepseek from "@deepseek-ai/dsh-llm-deepseek";
15
16
  import * as vendoredPiAiAdapter from "@deepseek-ai/dsh-llm-pi-ai";
16
17
  import * as vendoredPiAi from "@earendil-works/pi-ai";
@@ -18,6 +19,8 @@ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messag
18
19
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
19
20
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
20
21
  import * as vendoredCatalog from "@earendil-works/pi-ai/providers/all";
22
+ import { access } from "node:fs/promises";
23
+ import { homedir } from "node:os";
21
24
  //#endregion
22
25
  //#region src/config.ts
23
26
  /**
@@ -59,6 +62,7 @@ const DEEPSEEK_REASONING_EFFORTS = [
59
62
  "high",
60
63
  "max"
61
64
  ];
65
+ /** 旧版 imageDetail 枚举(0.1.2-alpha.1 已移除该字段;仅用于 schema 透传陷阱的取值集)。 */
62
66
  const DEEPSEEK_IMAGE_DETAILS = ["auto", "low"];
63
67
  /** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
64
68
  const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
@@ -87,7 +91,7 @@ const modelEntry = z.object({
87
91
  compat: compatDict.description("模型级 compat(字段级合并,压过 route 级与继承值)"),
88
92
  imagePixelBudget: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的总像素预算;缺省继承官方目录"),
89
93
  imageMaxBytes: z.number().step(1).min(1).description("(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录"),
90
- imageDetail: z.union(DEEPSEEK_IMAGE_DETAILS).description("(仅 adapter: deepseek)图片细节档位;low 使用 512x512 像素预算")
94
+ imageDetail: z.union(DEEPSEEK_IMAGE_DETAILS).description("(已移除)图片细节档位;0.1.2-alpha.1 起请改用 imagePixelBudget/imageMaxBytes")
91
95
  });
92
96
  const providerProfile = z.object({
93
97
  adapter: z.union(ADAPTER_KINDS).description("路由适配器:pi=PiAiAdapter(默认,三协议);deepseek=官方 DeepSeekAdapter(图片走 Files API 文件通道,失败自动降级 base64)").default("pi"),
@@ -117,7 +121,9 @@ const providerProfile = z.object({
117
121
  timeoutMs: z.natural().description("HTTP/provider 超时毫秒"),
118
122
  websocketConnectTimeoutMs: z.natural().description("WebSocket 连接超时毫秒"),
119
123
  streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).description("单支流式读取的最大空闲间隔毫秒"),
120
- maxRequestImageBytes: z.natural().description("单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)"),
124
+ maxRequestImageBytes: z.natural().description("单请求 base64 图片载荷上限字节;缺省 20MiB(0.1.2-alpha.1 起官方必需字段,旧配置免改)"),
125
+ requestImagePixelBudget: z.number().step(1).min(1).description("单请求每个确定性内联图片版本的像素总预算;缺省 2048x2048(官方同值)"),
126
+ requestImageMaxBytes: z.number().step(1).min(1).description("内联图片版本编码字节目标;缺省 1MiB(官方同值)"),
121
127
  retryPolicy: z.any().description("provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)"),
122
128
  thinking: z.union(DEEPSEEK_THINKING).description("(仅 adapter: deepseek)思考模式开关;缺省用提供商默认"),
123
129
  reasoningEffort: z.union(DEEPSEEK_REASONING_EFFORTS).description("(仅 adapter: deepseek)推理档位;缺省为 high"),
@@ -494,7 +500,14 @@ var DeepseekRouteRegistrar = class {
494
500
  },
495
501
  resolveApiKey: this.resolveApiKey,
496
502
  resolveUserId: () => this.resolveUserId(),
497
- resolveAttachments: () => ctx.get("attachments")
503
+ resolveAttachments: () => ctx.get("attachments"),
504
+ resolveImageAccess: (attachments, ref) => kit.resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
505
+ prepareExtensions: (request) => {
506
+ return ctx.get("deepseekLlmApiExtensions")?.prepare(request) ?? Promise.resolve({
507
+ fields: {},
508
+ accept: () => Promise.resolve()
509
+ });
510
+ }
498
511
  });
499
512
  adapter.providerInfo = (provider) => ({
500
513
  id: provider,
@@ -586,8 +599,7 @@ function catalogOf(kit) {
586
599
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
587
600
  ...model.inputModalities === void 0 ? {} : { input: [...model.inputModalities] },
588
601
  ...model.imagePixelBudget === void 0 ? {} : { imagePixelBudget: model.imagePixelBudget },
589
- ...model.imageMaxBytes === void 0 ? {} : { imageMaxBytes: model.imageMaxBytes },
590
- ...model.imageDetail === void 0 ? {} : { imageDetail: model.imageDetail }
602
+ ...model.imageMaxBytes === void 0 ? {} : { imageMaxBytes: model.imageMaxBytes }
591
603
  });
592
604
  const catalog = {
593
605
  baseUrl: resolved.baseURL,
@@ -749,71 +761,106 @@ async function discoverModels(request, deps) {
749
761
  }
750
762
  //#endregion
751
763
  //#region src/compat.ts
752
- const FIELDS_BY_PROTOCOL = {
764
+ const GATES_BY_PROTOCOL = {
753
765
  "openai-completions": {
754
- supportsStore: "boolean",
755
- supportsDeveloperRole: "boolean",
756
- supportsReasoningEffort: "boolean",
757
- supportsUsageInStreaming: "boolean",
758
- maxTokensField: ["max_completion_tokens", "max_tokens"],
759
- requiresToolResultName: "boolean",
760
- requiresAssistantAfterToolResult: "boolean",
761
- requiresThinkingAsText: "boolean",
762
- requiresReasoningContentOnAssistantMessages: "boolean",
763
- thinkingFormat: [
764
- "openai",
765
- "openrouter",
766
- "deepseek",
767
- "together",
768
- "zai",
769
- "qwen",
770
- "chat-template",
771
- "qwen-chat-template",
772
- "string-thinking",
773
- "ant-ling"
774
- ],
775
- chatTemplateKwargs: "object",
776
- openRouterRouting: "object",
777
- vercelGatewayRouting: "object",
778
- zaiToolStream: "boolean",
779
- supportsOpenAIGrammarTools: "boolean",
780
- supportsStrictMode: "boolean",
781
- cacheControlFormat: ["anthropic"],
782
- sendSessionAffinityHeaders: "boolean",
783
- deferredToolsMode: ["kimi"],
784
- sessionAffinityFormat: [
785
- "openai",
786
- "openai-nosession",
787
- "openrouter"
788
- ],
789
- supportsLongCacheRetention: "boolean"
766
+ supportsStore: "offer",
767
+ supportsDeveloperRole: "offer",
768
+ supportsReasoningEffort: "offer",
769
+ supportsUsageInStreaming: "offer",
770
+ supportsFinishReason: "offer",
771
+ maxTokensField: "offer",
772
+ requiresToolResultName: "offer",
773
+ requiresAssistantAfterToolResult: "offer",
774
+ requiresThinkingAsText: "offer",
775
+ requiresReasoningContentOnAssistantMessages: "offer",
776
+ thinkingFormat: "offer",
777
+ chatTemplateKwargs: "offer",
778
+ chatTemplateArgs: "offer",
779
+ supportsThinkingTokenBudget: "offer",
780
+ supportsStrictMode: "offer",
781
+ cacheControlFormat: "offer",
782
+ supportsLongCacheRetention: "offer",
783
+ openRouterRouting: "withhold",
784
+ vercelGatewayRouting: "withhold",
785
+ zaiToolStream: "withhold",
786
+ supportsOpenAIGrammarTools: "withhold",
787
+ sendSessionAffinityHeaders: "withhold",
788
+ deferredToolsMode: "withhold",
789
+ sessionAffinityFormat: "withhold"
790
790
  },
791
791
  "openai-responses": {
792
- supportsDeveloperRole: "boolean",
793
- sessionAffinityFormat: [
794
- "openai",
795
- "openai-nosession",
796
- "openrouter"
797
- ],
798
- supportsLongCacheRetention: "boolean",
799
- supportsStrictMode: "boolean",
800
- supportsOpenAIGrammarTools: "boolean",
801
- supportsToolSearch: "boolean",
802
- supportsExplicitPromptCacheMode: "boolean"
792
+ supportsDeveloperRole: "offer",
793
+ supportsStrictMode: "offer",
794
+ supportsLongCacheRetention: "offer",
795
+ sessionAffinityFormat: "withhold",
796
+ supportsOpenAIGrammarTools: "withhold",
797
+ supportsAdditionalTools: "withhold",
798
+ supportsToolSearch: "withhold",
799
+ supportsExplicitPromptCacheMode: "withhold"
803
800
  },
804
801
  "anthropic-messages": {
805
- supportsEagerToolInputStreaming: "boolean",
806
- supportsLongCacheRetention: "boolean",
807
- sendSessionAffinityHeaders: "boolean",
808
- supportsCacheControlOnTools: "boolean",
809
- supportsTemperature: "boolean",
810
- forceAdaptiveThinking: "boolean",
811
- allowEmptySignature: "boolean",
812
- supportsStrictTools: "boolean",
813
- supportsToolReferences: "boolean"
802
+ supportsEagerToolInputStreaming: "offer",
803
+ supportsLongCacheRetention: "offer",
804
+ supportsCacheControlOnTools: "offer",
805
+ supportsTemperature: "offer",
806
+ forceAdaptiveThinking: "offer",
807
+ allowEmptySignature: "offer",
808
+ supportsStrictTools: "offer",
809
+ sendSessionAffinityHeaders: "withhold",
810
+ supportsToolReferences: "withhold"
814
811
  }
815
812
  };
816
- function checkValue(_api, field, spec, value, where) {
813
+ /** 某协议某字段的可配性('offer'/'withhold';未列出 = 无此字段)。 */
814
+ function compatDispositionOf(api, field) {
815
+ return GATES_BY_PROTOCOL[api][field];
816
+ }
817
+ /**
818
+ * 字段取值约束(对齐官方 config.ts compatProfile schema):
819
+ * boolean 字段 → 'boolean';maxTokensField/thinkingFormat/cacheControlFormat
820
+ * → 枚举;chatTemplateKwargs/chatTemplateArgs → 对象。
821
+ */
822
+ const VALUE_SPECS = {
823
+ supportsStore: "boolean",
824
+ supportsDeveloperRole: "boolean",
825
+ supportsReasoningEffort: "boolean",
826
+ supportsUsageInStreaming: "boolean",
827
+ supportsFinishReason: "boolean",
828
+ maxTokensField: ["max_completion_tokens", "max_tokens"],
829
+ requiresToolResultName: "boolean",
830
+ requiresAssistantAfterToolResult: "boolean",
831
+ requiresThinkingAsText: "boolean",
832
+ requiresReasoningContentOnAssistantMessages: "boolean",
833
+ thinkingFormat: [
834
+ "openai",
835
+ "deepseek",
836
+ "openrouter",
837
+ "together",
838
+ "baseten",
839
+ "zai",
840
+ "qwen",
841
+ "chat-template",
842
+ "qwen-chat-template",
843
+ "string-thinking",
844
+ "ant-ling"
845
+ ],
846
+ chatTemplateKwargs: "object",
847
+ chatTemplateArgs: "object",
848
+ supportsThinkingTokenBudget: "boolean",
849
+ supportsStrictMode: "boolean",
850
+ cacheControlFormat: ["anthropic"],
851
+ supportsLongCacheRetention: "boolean",
852
+ supportsEagerToolInputStreaming: "boolean",
853
+ supportsCacheControlOnTools: "boolean",
854
+ supportsTemperature: "boolean",
855
+ forceAdaptiveThinking: "boolean",
856
+ allowEmptySignature: "boolean",
857
+ supportsStrictTools: "boolean"
858
+ };
859
+ /** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
860
+ function compatFieldsOf(api) {
861
+ return Object.entries(GATES_BY_PROTOCOL[api]).flatMap(([field, disposition]) => disposition === "offer" ? [field] : []);
862
+ }
863
+ function checkValue(field, spec, value, where) {
817
864
  if (spec === "boolean") {
818
865
  if (typeof value !== "boolean") throw new Error(`${where}: compat.${field} 必须是布尔值`);
819
866
  return;
@@ -825,29 +872,36 @@ function checkValue(_api, field, spec, value, where) {
825
872
  if (typeof value !== "string" || !spec.includes(value)) throw new Error(`${where}: compat.${field} 必须是 ${spec.map((v) => JSON.stringify(v)).join(" | ")} 之一`);
826
873
  }
827
874
  /**
828
- * 校验一份 compat 字典对指定协议合法:未知协议/未知键拒绝(对比官方的静默丢弃),
829
- * 已知键校验值类型/枚举。undefined 值视为未设置,跳过(语义同 pi-ai 的 ??)。
875
+ * 校验一份 compat 字典对指定协议合法(对齐官方门控语义 + schema 值约束):
876
+ * - 未知键/withhold 字段拒绝(官方 0.1.2-alpha.1 写时拒绝,替代旧版静默丢弃);
877
+ * - 值类型/枚举按官方 schema 校验;
878
+ * - 无值键(null/undefined)拒绝(官方 assertOfferedCompatFields 同款:
879
+ * "写了但没生效" 的表面状态不允许)。
830
880
  */
831
881
  function validateCompat(api, compat, where) {
832
882
  if (compat === void 0) return;
833
- const fields = FIELDS_BY_PROTOCOL[api];
834
- if (fields === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(FIELDS_BY_PROTOCOL).join(", ")})`);
883
+ const gate = GATES_BY_PROTOCOL[api];
884
+ if (gate === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(GATES_BY_PROTOCOL).join(", ")})`);
885
+ const offered = compatFieldsOf(api);
835
886
  for (const [key, value] of Object.entries(compat)) {
836
- const spec = fields[key];
837
- if (spec === void 0) throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(合法字段:${Object.keys(fields).join(", ")})`);
838
- if (value === void 0) continue;
839
- checkValue(api, key, spec, value, where);
887
+ const disposition = gate[key];
888
+ if (disposition !== "offer") {
889
+ if (disposition === "withhold") throw new Error(`${where}: compat.${key} 官方按协议 withhold(内置目录已为对应厂商设置该开关);请以目录 provider 名作为 route 名(继承目录值),或移除该字段`);
890
+ throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(", ")})`);
891
+ }
892
+ if (value === void 0 || value === null) throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`);
893
+ checkValue(key, VALUE_SPECS[key], value, where);
840
894
  }
841
895
  }
842
896
  /**
843
- * 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined 值。
897
+ * 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined/null 值。
844
898
  * 层序:继承源(仅同协议)→ route 级 → 模型级。
845
899
  */
846
900
  function mergeCompat(...layers) {
847
901
  const merged = {};
848
902
  for (const layer of layers) {
849
903
  if (layer === void 0) continue;
850
- for (const [key, value] of Object.entries(layer)) if (value !== void 0) merged[key] = value;
904
+ for (const [key, value] of Object.entries(layer)) if (value !== void 0 && value !== null) merged[key] = value;
851
905
  }
852
906
  return Object.keys(merged).length > 0 ? merged : void 0;
853
907
  }
@@ -968,14 +1022,14 @@ function declaredModalities(configured) {
968
1022
  /** 物化单个模型目录条目:官方 base 在下,条目显式字段逐字段覆盖。 */
969
1023
  function materializeModel$1(route, entry, deps) {
970
1024
  if (entry.reasoningEfforts !== void 0 || nonEmptyDict(entry.compat)) invalid$1(route, `model "${entry.id}" 的 reasoningEfforts/compat 仅 adapter: pi 可用`);
1025
+ if (entry.imageDetail !== void 0) invalid$1(route, `model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;请改用 imagePixelBudget/imageMaxBytes(低细节可配更小的 imagePixelBudget)并删除 imageDetail`);
971
1026
  const base = resolveOfficialBase(route, entry, deps);
972
1027
  const input = declaredModalities(entry.input) ?? base.input;
973
1028
  const imageFields = {
974
1029
  imagePixelBudget: entry.imagePixelBudget ?? base.imagePixelBudget,
975
- imageMaxBytes: entry.imageMaxBytes ?? base.imageMaxBytes,
976
- imageDetail: entry.imageDetail ?? base.imageDetail
1030
+ imageMaxBytes: entry.imageMaxBytes ?? base.imageMaxBytes
977
1031
  };
978
- if (!(input?.includes("image") ?? false) && Object.values(imageFields).some((value) => value !== void 0)) invalid$1(route, `model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes/imageDetail`);
1032
+ if (!(input?.includes("image") ?? false) && Object.values(imageFields).some((value) => value !== void 0)) invalid$1(route, `model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes`);
979
1033
  return {
980
1034
  id: entry.id,
981
1035
  ...(entry.name ?? base.name) === void 0 ? {} : { name: entry.name ?? base.name },
@@ -1029,7 +1083,7 @@ function rawAdapterConfig(profile, baseURL, models) {
1029
1083
  /** 物化单个 deepseek route;lenient 下失败告警并返回 null(调用方跳过注册)。 */
1030
1084
  function buildRoute(route, profile, deps) {
1031
1085
  try {
1032
- if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(需 dsh ≥ 0.1.1-rc.2)");
1086
+ if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek0.1.2-alpha.1 基线)");
1033
1087
  rejectPiOnlyFields(route, profile);
1034
1088
  if (profile.extends !== void 0 && profile.extends !== "deepseek") invalid$1(route, `的 extends ${JSON.stringify(profile.extends)} 非法:adapter: deepseek 仅支持 "deepseek"(官方目录)`);
1035
1089
  if (profile.apiKeyEnv === void 0 || profile.apiKeyEnv.length === 0) invalid$1(route, "需要 apiKeyEnv:DeepSeekAdapter 无环境自发现,凭据引用必须显式配置");
@@ -1066,16 +1120,31 @@ function buildDeepseekRoutes(providers, deps) {
1066
1120
  //#endregion
1067
1121
  //#region src/profiles.ts
1068
1122
  /**
1069
- * profile 构建器:插件 Config → ResolvedPiAiProviderProfile 映射。
1123
+ * profile 构建器(0.1.2-alpha.1 适配版):插件 Config → 官方 schema 形状的
1124
+ * 「前置数据源归一化」→ 官方 resolveProfiles(或插件等价兜底)→
1125
+ * ResolvedPiAiProviderProfile。
1070
1126
  *
1071
- * 语义与官方 dsh-llm-pi-ai 的 resolveProfiles 链逐点对齐(index.js:1068-1471),
1072
- * 差异仅在两点扩展:
1073
- * - base 来源从"route 同名内置目录"换为 extends 三级数据源(inherit.ts);
1074
- * - compat 2 字段枚举扩展为逐协议全量字段(compat.ts),写入即校验。
1127
+ * 适配决策(resolveProfiles 复用评估,2026-08):
1128
+ * - 官方 resolveProfiles 仅在 @deepseek-ai/dsh-llm-pi-ai 的 src/config.ts 子路径
1129
+ * 导出,npm 发布形态不携带 src/(lib/index.js bundle),包根也不导出该
1130
+ * 符号——真实部署(npm 布局 dsh 树)与 vendored 兜底都无法引用官方解析链;
1131
+ * - 因此:dsh 树 dev 布局(src 存在)时经 src 子路径复用官方 resolveProfiles
1132
+ * (resolve-dsh.ts 探测),其余形态走本文件的 resolveProfilesFallback——
1133
+ * 语义逐行对齐官方 config.ts:resolveProfiles + catalog.ts:resolveRouteModels
1134
+ * (协议限定本插件三协议),compat 门控以官方 catalog.ts COMPAT_GATES 为
1135
+ * 唯一事实源(见 compat.ts);
1136
+ * - 两条路径的输入相同:插件独占的 extends/models.dev/草稿路由等语义在此
1137
+ * 「前置归一化」为全显式官方 schema 形状(模型条目字段全部落定),官方链
1138
+ * 不再需要目录查找;normalizeRoute 的产物经 kit.resolveProfiles 进入
1139
+ * 官方 adapter 消息链路。
1075
1140
  *
1076
- * 产物经 `new PiAiAdapter({ profiles })` 进入官方消息链路;
1077
- * 任何不可服务的配置在此处抛错(命名 route/model),配合 settings 写入校验,
1078
- * 非法配置在写入时被拒绝、运行期保留上一份好配置。
1141
+ * 与官方链的既有缝隙(记录在案):
1142
+ * - 模型级 maxTokens 在官方 schema 中兼作「能力值」与「显式配置的请求默认
1143
+ * cap」;归一化把继承值物化进条目后官方链会全部计入 configuredMaxTokens,
1144
+ * 此处按插件原语义回填为「仅用户显式配置」——请求默认 cap 行为不变;
1145
+ * - 官方 schema 无模型级 baseUrl/headers/cost:端点收敛为 route 级单一值
1146
+ * (模型间端点不一致写时拒绝),headers/cost 不继承(0.84.2 目录无模型级
1147
+ * headers;harness 不消费 cost 元数据,与官方 NO_COST 同口径)。
1079
1148
  * @module llm-pi/profiles
1080
1149
  */
1081
1150
  /** 内置目录未描述时的零价目(harness 不消费 cost 元数据,同官方 NO_COST)。 */
@@ -1093,53 +1162,60 @@ function invalid(provider, detail) {
1093
1162
  function declaredInput(configured) {
1094
1163
  return configured === void 0 || configured.length === 0 ? void 0 : [...configured];
1095
1164
  }
1096
- /**
1097
- * 单个模型的 reasoning 物化(逐行对齐官方 resolveModelReasoning):
1098
- * 显式 dict 全档位确定的 thinkingLevelMap(未声明档位置 null);
1099
- * false → 非推理模型;缺省 → 保留继承源的 reasoning 能力。
1100
- */
1101
- function resolveModelReasoning(provider, entry, base) {
1102
- const efforts = entry.reasoningEfforts;
1103
- if (efforts === void 0) {
1104
- if (base.reasoning === void 0) return { reasoning: false };
1105
- return {
1106
- reasoning: base.reasoning,
1107
- ...base.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: base.thinkingLevelMap }
1108
- };
1109
- }
1110
- if (efforts === false) return { reasoning: false };
1111
- if (Object.keys(efforts).length === 0) invalid(provider, `model "${entry.id}" 的 reasoningEfforts 为空:声明档位、置 false 或缺省继承`);
1165
+ /** 显式 reasoningEfforts 字典的合法性校验(官方 resolveModelReasoning 同款规则)。 */
1166
+ function validateReasoningEfforts(provider, entryId, efforts) {
1167
+ if (Object.keys(efforts).length === 0) invalid(provider, `model "${entryId}" 的 reasoningEfforts 为空:声明档位、置 false 或缺省继承`);
1112
1168
  for (const level of THINKING_LEVELS) {
1113
1169
  const wire = efforts[level];
1114
1170
  if (wire === void 0) continue;
1115
1171
  if (wire === null) {
1116
- if (level !== "off") invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 需要线值;仅 off 可留空`);
1117
- } else if (wire.length === 0) invalid(provider, `model "${entry.id}" reasoningEfforts.${level} 不能为空字符串`);
1172
+ if (level !== "off") invalid(provider, `model "${entryId}" reasoningEfforts.${level} 需要线值;仅 off 可留空`);
1173
+ } else if (wire.length === 0) invalid(provider, `model "${entryId}" reasoningEfforts.${level} 不能为空字符串`);
1118
1174
  }
1119
- if (!THINKING_LEVELS.filter((level) => efforts[level] !== void 0).some((level) => level !== "off")) invalid(provider, `model "${entry.id}" reasoningEfforts 只有 off;声明思考档位或置 false`);
1120
- const map = {};
1121
- for (const level of THINKING_LEVELS) {
1122
- const wire = efforts[level];
1123
- if (wire === void 0) map[level] = null;
1124
- else if (wire !== null) map[level] = wire;
1125
- }
1126
- return {
1127
- reasoning: true,
1128
- thinkingLevelMap: map
1129
- };
1175
+ if (!THINKING_LEVELS.filter((level) => efforts[level] !== void 0).some((level) => level !== "off")) invalid(provider, `model "${entryId}" reasoningEfforts 只有 off;声明思考档位或置 false`);
1130
1176
  }
1131
- /** Harness 自管凭据的 api-key auth(对齐官方 harnessApiKeyAuth,index.js:1215)。 */
1132
- function harnessApiKeyAuth(name) {
1133
- return {
1134
- name,
1135
- resolve: ({ credential }) => Promise.resolve({
1136
- auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
1137
- source: name
1138
- })
1177
+ /**
1178
+ * 继承源的 reasoning 能力 → 官方 reasoningEfforts 字典(全显式归一化的一环)。
1179
+ * - base 无推理能力 → false(与官方"手写模型缺省不推理"同语义);
1180
+ * - base 带 thinkingLevelMap → 逐档位翻译:线值照抄;null(不支持)不声明
1181
+ * (dict 未声明档位置 null = 不支持,语义一致);缺省档位(off/minimal/
1182
+ * low/medium/high)线值 = 档位名(pi-ai dispatch `map?.[level] ?? level`),
1183
+ * xhigh/max 缺省 = 不支持(pi-ai 对两档的缺省语义)——翻译后支持档位集合
1184
+ * 与线值与内置目录逐档位一致;
1185
+ * - base 只有 reasoning:true 无表(models.dev 保守源)→ 声明基档位为默认
1186
+ * 线值、xhigh/max 不支持——与 pi-ai 无表默认语义一致。
1187
+ */
1188
+ function reasoningEffortsFromBase(provider, entry, base) {
1189
+ if (entry.reasoningEfforts !== void 0) {
1190
+ if (entry.reasoningEfforts === false) return false;
1191
+ validateReasoningEfforts(provider, entry.id, entry.reasoningEfforts);
1192
+ return entry.reasoningEfforts;
1193
+ }
1194
+ if (base.reasoning !== true) return false;
1195
+ const map = base.thinkingLevelMap;
1196
+ if (map === void 0) return {
1197
+ off: null,
1198
+ minimal: "minimal",
1199
+ low: "low",
1200
+ medium: "medium",
1201
+ high: "high"
1139
1202
  };
1203
+ const dict = {};
1204
+ for (const level of THINKING_LEVELS) {
1205
+ const wire = map[level];
1206
+ if (wire === void 0) {
1207
+ if (level === "xhigh" || level === "max") continue;
1208
+ dict[level] = level;
1209
+ } else if (wire !== null) dict[level] = wire;
1210
+ }
1211
+ return dict;
1140
1212
  }
1141
- /** 物化单个模型:继承 base 在下,条目显式字段逐字段覆盖。lenient 下缺 api/baseURL 时跳过(返回 null)。 */
1142
- function materializeModel(route, profile, entry, base, routeApi, providerBaseUrl, defaultInput, deps, configuredMaxTokens) {
1213
+ /**
1214
+ * 物化单个模型为官方 schema 条目(全显式)。lenient 下缺 api/baseURL
1215
+ * 返回 null(调用方跳过该模型)。
1216
+ */
1217
+ function materializeModel(route, profile, entry, base, routeApi, defaultInput, deps) {
1218
+ if (entry.imageDetail !== void 0) invalid(route, `model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;请改用 imagePixelBudget/imageMaxBytes(仅 adapter: deepseek)并删除 imageDetail`);
1143
1219
  const api = profile.api ?? base.api ?? routeApi;
1144
1220
  if (api === void 0) {
1145
1221
  if (deps.lenient) {
@@ -1148,7 +1224,7 @@ function materializeModel(route, profile, entry, base, routeApi, providerBaseUrl
1148
1224
  }
1149
1225
  invalid(route, `model "${entry.id}" 需要 api:继承源未提供,请在 route 上设置 api`);
1150
1226
  }
1151
- const baseUrl = profile.baseURL ?? base.baseUrl ?? providerBaseUrl;
1227
+ const baseUrl = profile.baseURL ?? base.baseUrl ?? builtinProviderBaseUrl(deps.kit, profile.extends ?? "");
1152
1228
  if (baseUrl === void 0) {
1153
1229
  if (deps.lenient) {
1154
1230
  deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 无法获得 baseURL(继承源缺失),已跳过该模型`);
@@ -1156,32 +1232,34 @@ function materializeModel(route, profile, entry, base, routeApi, providerBaseUrl
1156
1232
  }
1157
1233
  invalid(route, `model "${entry.id}" 需要 baseURL:继承源未提供,请在 route 上设置 baseURL`);
1158
1234
  }
1235
+ const name = entry.name ?? base.name ?? entry.id;
1159
1236
  const contextWindow = entry.contextWindow ?? base.contextWindow ?? profile.defaultContextWindow ?? 262144;
1160
1237
  const maxTokens = entry.maxTokens ?? base.maxTokens ?? profile.defaultMaxTokens ?? 32768;
1161
- if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
1238
+ const input = declaredInput(entry.input) ?? base.input ?? [...defaultInput];
1162
1239
  validateCompat(api, profile.compat, `provider "${route}"`);
1163
1240
  validateCompat(api, entry.compat, `provider "${route}" model "${entry.id}"`);
1164
1241
  const compat = mergeCompat(base.api === api ? base.compat : void 0, profile.compat, entry.compat);
1242
+ if (compat !== void 0) validateCompat(api, compat, `provider "${route}" model "${entry.id}"`);
1165
1243
  return {
1166
- id: entry.id,
1167
- name: entry.name ?? base.name ?? entry.id,
1244
+ entry: {
1245
+ id: entry.id,
1246
+ ...name === entry.id ? {} : { name },
1247
+ ...contextWindow === 262144 ? {} : { contextWindow },
1248
+ ...maxTokens === 32768 ? {} : { maxTokens },
1249
+ ...input.length === 1 && input[0] === "text" ? {} : { input },
1250
+ reasoningEfforts: reasoningEffortsFromBase(route, entry, base),
1251
+ ...compat === void 0 ? {} : { compat }
1252
+ },
1168
1253
  api,
1169
- provider: route,
1170
- baseUrl,
1171
- ...resolveModelReasoning(route, entry, base),
1172
- input: declaredInput(entry.input) ?? base.input ?? [...defaultInput],
1173
- cost: base.cost ?? NO_COST,
1174
- contextWindow,
1175
- maxTokens,
1176
- ...base.headers === void 0 ? {} : { headers: { ...base.headers } },
1177
- ...compat === void 0 ? {} : { compat }
1254
+ baseUrl
1178
1255
  };
1179
1256
  }
1180
- /** 物化一个 route 的全部模型;返回模型列表与显式配置的请求 cap 表。
1181
- * lenient route 无任何可服务模型时返回 null(调用方跳过该 route)。 */
1257
+ /**
1258
+ * 物化一个 route 的全部模型为官方条目;返回 route 级唯一 api/baseURL 与
1259
+ * 用户显式配置的请求 cap 表。lenient 下 route 无任何可服务模型时返回 null。
1260
+ */
1182
1261
  function materializeRouteModels(route, profile, deps, defaultInput) {
1183
1262
  const configuredMaxTokens = /* @__PURE__ */ new Map();
1184
- const providerBaseUrl = profile.extends === void 0 ? void 0 : builtinProviderBaseUrl(deps.kit, profile.extends);
1185
1263
  const entries = [];
1186
1264
  if (profile.models !== void 0 && profile.models.length > 0) {
1187
1265
  const seen = /* @__PURE__ */ new Set();
@@ -1197,6 +1275,7 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
1197
1275
  deps.warn?.(`llm-pi: provider "${route}" model "${entry.id}" 的 extends 引用当前不可解析(${error.message});已降级为手写条目`);
1198
1276
  base = {};
1199
1277
  }
1278
+ if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
1200
1279
  entries.push({
1201
1280
  id: entry.id,
1202
1281
  entry,
@@ -1212,18 +1291,22 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
1212
1291
  } else invalid(route, "未配置 models 且未配置 provider 级 extends;本 route 无模型可服务");
1213
1292
  const apis = new Set(entries.map(({ base }) => profile.api ?? base.api).filter((api) => api !== void 0));
1214
1293
  const routeApi = apis.size === 1 ? [...apis][0] : void 0;
1215
- const models = entries.map(({ id, entry, base }) => materializeModel(route, profile, entry ?? { id }, base, routeApi, providerBaseUrl, defaultInput, deps, configuredMaxTokens)).filter((model) => model !== null);
1216
- if (models.length === 0) {
1294
+ const materialized = entries.map(({ id, entry, base }) => materializeModel(route, profile, entry ?? { id }, base, routeApi, defaultInput, deps)).filter((model) => model !== null);
1295
+ if (materialized.length === 0) {
1217
1296
  if (deps.lenient) {
1218
1297
  deps.warn?.(`llm-pi: provider "${route}" 当前没有可服务的模型(继承源漂移),已跳过该 route 的注册`);
1219
1298
  return null;
1220
1299
  }
1221
1300
  invalid(route, "route 内没有可服务的模型");
1222
1301
  }
1223
- const finalApis = new Set(models.map((m) => m.api));
1224
- if (finalApis.size > 1) invalid(route, `route 内模型协议不一致(${[...finalApis].join(", ")});一个 route 只能服务一种协议`);
1302
+ const apisFinal = new Set(materialized.map((m) => m.api));
1303
+ if (apisFinal.size > 1) invalid(route, `route 内模型协议不一致(${[...apisFinal].join(", ")});一个 route 只能服务一种协议`);
1304
+ const baseUrls = new Set(materialized.map((m) => m.baseUrl));
1305
+ if (baseUrls.size > 1) invalid(route, `route 内模型端点不一致(${[...baseUrls].join(", ")});官方 schema 无模型级端点,请以 route 级 baseURL 统一`);
1225
1306
  return {
1226
- models,
1307
+ models: materialized.map((m) => m.entry),
1308
+ api: materialized[0]?.api,
1309
+ baseUrl: materialized[0]?.baseUrl,
1227
1310
  configuredMaxTokens
1228
1311
  };
1229
1312
  }
@@ -1236,63 +1319,309 @@ function isDraftRoute(profile) {
1236
1319
  return (profile.adapter ?? "pi") === "pi" && (profile.models === void 0 || profile.models.length === 0) && profile.extends === void 0;
1237
1320
  }
1238
1321
  /**
1239
- * 校验并物化全部 route。任一 route 不可服务即整体抛错——
1240
- * settings 写入校验与运行期 profiles 回调共用本函数,
1241
- * 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
1242
- * 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
1322
+ * 前置数据源归一化:一个 route 官方 schema 形状的全显式 profile。
1323
+ * 不可服务的 route(deepseek 路由 / 草稿 / lenient 下无模型)返回 undefined。
1324
+ * 校验规则对齐官方 resolveProfiles(baseURL/displayName 非空、idle 超时边界、
1325
+ * 图片限额正整数、defaultInput 非空)。
1243
1326
  */
1244
- function buildProfiles(providers, deps) {
1245
- const resolved = /* @__PURE__ */ new Map();
1246
- for (const [route, profile] of Object.entries(providers ?? {})) {
1247
- if ((profile.adapter ?? "pi") === "deepseek") continue;
1248
- if (isDraftRoute(profile)) continue;
1249
- if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
1250
- if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
1251
- if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
1252
- const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? 3e5;
1253
- if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) invalid(route, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`);
1254
- const defaultInput = [...profile.defaultInput ?? ["text"]];
1255
- if (defaultInput.length === 0) invalid(route, "defaultInput 至少要声明一种模态");
1256
- const displayName = profile.displayName ?? route;
1257
- const catalog = materializeRouteModels(route, profile, deps, defaultInput);
1258
- if (catalog === null) continue;
1259
- const api = catalog.models[0]?.api;
1260
- const factory = api === void 0 ? void 0 : deps.kit.protocolFactories[api];
1261
- if (factory === void 0) invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`);
1262
- const piProvider = deps.kit.createProvider({
1263
- id: route,
1264
- name: displayName,
1265
- ...profile.baseURL === void 0 ? profile.extends === void 0 ? {} : { baseUrl: builtinProviderBaseUrl(deps.kit, profile.extends) } : { baseUrl: profile.baseURL },
1266
- ...profile.headers === void 0 ? {} : { headers: { ...profile.headers } },
1267
- auth: { apiKey: harnessApiKeyAuth(displayName) },
1268
- models: catalog.models,
1269
- api: factory()
1270
- });
1271
- resolved.set(route, {
1272
- provider: route,
1327
+ function normalizeRoute(route, profile, deps) {
1328
+ if ((profile.adapter ?? "pi") === "deepseek") return void 0;
1329
+ if (isDraftRoute(profile)) return void 0;
1330
+ if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
1331
+ if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
1332
+ if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
1333
+ const streamIdleTimeoutMs = profile.streamIdleTimeoutMs ?? 3e5;
1334
+ if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) invalid(route, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`);
1335
+ const maxRequestImageBytes = profile.maxRequestImageBytes ?? 20971520;
1336
+ if (!Number.isInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) invalid(route, "maxRequestImageBytes 必须是正整数");
1337
+ const requestImagePixelBudget = profile.requestImagePixelBudget ?? 4194304;
1338
+ if (!Number.isSafeInteger(requestImagePixelBudget) || requestImagePixelBudget <= 0) invalid(route, "requestImagePixelBudget 必须是正整数");
1339
+ const requestImageMaxBytes = profile.requestImageMaxBytes ?? 1048576;
1340
+ if (!Number.isSafeInteger(requestImageMaxBytes) || requestImageMaxBytes <= 0) invalid(route, "requestImageMaxBytes 必须是正整数");
1341
+ const defaultInput = [...profile.defaultInput ?? ["text"]];
1342
+ if (defaultInput.length === 0) invalid(route, "defaultInput 至少要声明一种模态");
1343
+ const materialized = materializeRouteModels(route, profile, deps, defaultInput);
1344
+ if (materialized === null) return void 0;
1345
+ const { api, baseUrl, models, configuredMaxTokens } = materialized;
1346
+ if (deps.kit.protocolFactories[api] === void 0) invalid(route, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`);
1347
+ const displayName = profile.displayName ?? route;
1348
+ return {
1349
+ profile: {
1350
+ ...profile.apiKeyEnv === void 0 ? {} : { apiKeyEnv: profile.apiKeyEnv },
1273
1351
  displayName,
1274
- ...profile.apiKeyEnv === void 0 ? {} : { apiKeyEnv: credentialRef(profile.apiKeyEnv) },
1275
- streamIdleTimeoutMs,
1276
- maxRequestImageBytes: profile.maxRequestImageBytes ?? 20971520,
1277
- retryPolicy: deps.kit.resolveRetryPolicy(profile.retryPolicy, `llm-pi: provider "${route}" retryPolicy`),
1278
- ...profile.headers === void 0 ? {} : { headers: { ...profile.headers } },
1352
+ api,
1353
+ ...baseUrl === void 0 ? {} : { baseURL: baseUrl },
1354
+ models,
1355
+ ...profile.defaultContextWindow === void 0 ? {} : { defaultContextWindow: profile.defaultContextWindow },
1356
+ ...profile.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: profile.defaultMaxTokens },
1357
+ defaultInput,
1358
+ ...profile.headers === void 0 ? {} : { headers: profile.headers },
1279
1359
  ...profile.reasoning === void 0 ? {} : { reasoning: profile.reasoning },
1280
- ...profile.thinkingBudgets === void 0 ? {} : { thinkingBudgets: { ...profile.thinkingBudgets } },
1360
+ ...profile.thinkingBudgets === void 0 ? {} : { thinkingBudgets: profile.thinkingBudgets },
1281
1361
  ...profile.cacheRetention === void 0 ? {} : { cacheRetention: profile.cacheRetention },
1282
1362
  ...profile.transport === void 0 ? {} : { transport: profile.transport },
1283
1363
  ...profile.timeoutMs === void 0 ? {} : { timeoutMs: profile.timeoutMs },
1284
1364
  ...profile.websocketConnectTimeoutMs === void 0 ? {} : { websocketConnectTimeoutMs: profile.websocketConnectTimeoutMs },
1285
- configuredMaxTokens: catalog.configuredMaxTokens,
1286
- piProvider
1365
+ ...profile.streamIdleTimeoutMs === void 0 ? {} : { streamIdleTimeoutMs: profile.streamIdleTimeoutMs },
1366
+ ...profile.maxRequestImageBytes === void 0 ? {} : { maxRequestImageBytes: profile.maxRequestImageBytes },
1367
+ ...profile.requestImagePixelBudget === void 0 ? {} : { requestImagePixelBudget: profile.requestImagePixelBudget },
1368
+ ...profile.requestImageMaxBytes === void 0 ? {} : { requestImageMaxBytes: profile.requestImageMaxBytes },
1369
+ ...profile.retryPolicy === void 0 ? {} : { retryPolicy: profile.retryPolicy }
1370
+ },
1371
+ configuredMaxTokens
1372
+ };
1373
+ }
1374
+ /** Harness 自管凭据的 api-key auth(对齐官方 harnessApiKeyAuth)。 */
1375
+ function harnessApiKeyAuth(name) {
1376
+ return {
1377
+ name,
1378
+ resolve: ({ credential }) => Promise.resolve({
1379
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
1380
+ source: name
1381
+ })
1382
+ };
1383
+ }
1384
+ /** 官方 resolveModelReasoning 的插件侧镜像:dict → {reasoning, thinkingLevelMap}。 */
1385
+ function resolveModelReasoning(entry) {
1386
+ const efforts = entry.reasoningEfforts;
1387
+ if (efforts === void 0 || efforts === false) return { reasoning: false };
1388
+ const map = {};
1389
+ for (const level of THINKING_LEVELS) {
1390
+ const wire = efforts[level];
1391
+ if (wire === void 0) map[level] = null;
1392
+ else if (wire !== null) map[level] = wire;
1393
+ }
1394
+ return {
1395
+ reasoning: true,
1396
+ thinkingLevelMap: map
1397
+ };
1398
+ }
1399
+ /** 官方 resolveModelCompat 的插件侧镜像:门控表过滤 + 模型级拒绝不适配字段。 */
1400
+ function resolveModelCompat(provider, entry, api) {
1401
+ const configured = {};
1402
+ for (const [field, value] of Object.entries(entry.compat ?? {})) {
1403
+ if (compatDispositionOf(api, field) !== "offer") {
1404
+ const offered = compatFieldsOf(api);
1405
+ invalid(provider, `model "${entry.id}" 的 compat.${field} 不适用于协议 ${api};该协议可配置字段:${offered.length === 0 ? "无" : offered.join(", ")}`);
1406
+ }
1407
+ if (value === void 0 || value === null) invalid(provider, `model "${entry.id}" 的 compat.${field} 未设置值;给出值或移除该键`);
1408
+ configured[field] = value;
1409
+ }
1410
+ if (Object.keys(configured).length === 0) return {};
1411
+ return { compat: configured };
1412
+ }
1413
+ /**
1414
+ * 官方 resolveProfiles 的插件等价实现(npm 形态无 src/config.ts 可导入时兜底)。
1415
+ * 输入为 normalizeRoute 的全显式产物(api/baseURL/条目字段全部落定),语义与
1416
+ * 官方 config.ts:resolveProfiles + catalog.ts:resolveRouteModels 逐行对齐:
1417
+ * 默认值(streamIdleTimeoutMs/图片限额/容量兜底)、retryPolicy 解析、
1418
+ * configuredMaxTokens(此处按全显式条目计入,buildProfiles 再回填为用户显式
1419
+ * 集)、createProvider 构建 piProvider(harness api-key auth)。
1420
+ */
1421
+ function resolveProfilesFallback(providers, deps) {
1422
+ const resolved = /* @__PURE__ */ new Map();
1423
+ for (const [provider, source] of Object.entries(providers ?? {})) {
1424
+ if (source.baseURL !== void 0 && source.baseURL.length === 0) invalid(provider, "baseURL 为空");
1425
+ if (source.displayName !== void 0 && source.displayName.length === 0) invalid(provider, "displayName 为空");
1426
+ const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? 3e5;
1427
+ if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) invalid(provider, `streamIdleTimeoutMs 必须是 (0, ${MAX_TIMER_DELAY_MS}] 内的有限数`);
1428
+ const maxRequestImageBytes = source.maxRequestImageBytes ?? 20971520;
1429
+ if (!Number.isInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) invalid(provider, "maxRequestImageBytes 必须是正整数");
1430
+ const requestImagePixelBudget = source.requestImagePixelBudget ?? 4194304;
1431
+ if (!Number.isSafeInteger(requestImagePixelBudget) || requestImagePixelBudget <= 0) invalid(provider, "requestImagePixelBudget 必须是正整数");
1432
+ const requestImageMaxBytes = source.requestImageMaxBytes ?? 1048576;
1433
+ if (!Number.isSafeInteger(requestImageMaxBytes) || requestImageMaxBytes <= 0) invalid(provider, "requestImageMaxBytes 必须是正整数");
1434
+ const defaultInput = [...source.defaultInput ?? ["text"]];
1435
+ if (defaultInput.length === 0) invalid(provider, "defaultInput 至少要声明一种模态");
1436
+ const displayName = source.displayName ?? provider;
1437
+ const api = source.api;
1438
+ if (api === void 0) invalid(provider, "需要 api:本插件仅支持 openai-completions/openai-responses/anthropic-messages");
1439
+ const factory = deps.protocolFactories[api];
1440
+ if (factory === void 0) invalid(provider, `api ${JSON.stringify(api)} 本插件无法服务(支持:openai-completions/openai-responses/anthropic-messages)`);
1441
+ if (source.baseURL === void 0) invalid(provider, "需要 baseURL:官方 schema 无模型级端点,端点只能 route 级表达");
1442
+ const configuredMaxTokens = /* @__PURE__ */ new Map();
1443
+ const seen = /* @__PURE__ */ new Set();
1444
+ const models = (source.models ?? []).map((entry) => {
1445
+ if (entry.id.length === 0) invalid(provider, "存在空 id 的模型条目");
1446
+ if (seen.has(entry.id)) invalid(provider, `模型 "${entry.id}" 重复列出`);
1447
+ seen.add(entry.id);
1448
+ const contextWindow = entry.contextWindow ?? source.defaultContextWindow ?? 262144;
1449
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) invalid(provider, `model "${entry.id}" contextWindow 必须是正整数`);
1450
+ const maxTokens = entry.maxTokens ?? source.defaultMaxTokens ?? 32768;
1451
+ if (!Number.isInteger(maxTokens) || maxTokens <= 0) invalid(provider, `model "${entry.id}" maxTokens 必须是正整数`);
1452
+ if (entry.maxTokens !== void 0) configuredMaxTokens.set(entry.id, entry.maxTokens);
1453
+ return {
1454
+ id: entry.id,
1455
+ name: entry.name ?? entry.id,
1456
+ api,
1457
+ provider,
1458
+ baseUrl: source.baseURL,
1459
+ input: [...entry.input ?? defaultInput],
1460
+ cost: NO_COST,
1461
+ contextWindow,
1462
+ maxTokens,
1463
+ ...resolveModelReasoning(entry),
1464
+ ...resolveModelCompat(provider, entry, api)
1465
+ };
1466
+ });
1467
+ if (models.length === 0) invalid(provider, "route 内没有可服务的模型");
1468
+ const { apiKeyEnv, retryPolicy, models: _models, displayName: _displayName, ...rest } = source;
1469
+ resolved.set(provider, {
1470
+ ...rest,
1471
+ provider,
1472
+ displayName,
1473
+ ...apiKeyEnv === void 0 ? {} : { apiKeyEnv: credentialRef(apiKeyEnv) },
1474
+ streamIdleTimeoutMs,
1475
+ maxRequestImageBytes,
1476
+ requestImagePixelBudget,
1477
+ requestImageMaxBytes,
1478
+ retryPolicy: deps.resolveRetryPolicy(retryPolicy, `llm-pi: provider "${provider}" retryPolicy`),
1479
+ ...rest.headers === void 0 ? {} : { headers: { ...rest.headers } },
1480
+ ...rest.thinkingBudgets === void 0 ? {} : { thinkingBudgets: { ...rest.thinkingBudgets } },
1481
+ configuredMaxTokens,
1482
+ piProvider: deps.createProvider({
1483
+ id: provider,
1484
+ name: displayName,
1485
+ ...source.baseURL === void 0 ? {} : { baseUrl: source.baseURL },
1486
+ auth: { apiKey: harnessApiKeyAuth(displayName) },
1487
+ models,
1488
+ api: factory()
1489
+ })
1287
1490
  });
1288
1491
  }
1289
1492
  return resolved;
1290
1493
  }
1494
+ /**
1495
+ * 校验并物化全部 pi route。任一 route 不可服务即整体抛错——
1496
+ * settings 写入校验与运行期 profiles 回调共用本函数,
1497
+ * 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
1498
+ * 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
1499
+ */
1500
+ function buildProfiles(providers, deps) {
1501
+ const resolved = /* @__PURE__ */ new Map();
1502
+ for (const [route, profile] of Object.entries(providers ?? {})) {
1503
+ let normalized;
1504
+ try {
1505
+ normalized = normalizeRoute(route, profile, deps);
1506
+ if (normalized === void 0) continue;
1507
+ const single = deps.kit.resolveProfiles({ [route]: normalized.profile });
1508
+ for (const [name, built] of single) resolved.set(name, {
1509
+ ...built,
1510
+ configuredMaxTokens: normalized.configuredMaxTokens
1511
+ });
1512
+ } catch (error) {
1513
+ if (!deps.lenient) throw error;
1514
+ deps.warn?.(`llm-pi: provider "${route}" 不可服务(${error instanceof Error ? error.message : String(error)}),已跳过注册`);
1515
+ }
1516
+ }
1517
+ return resolved;
1518
+ }
1291
1519
  /** settings 写入校验钩子:完整试跑解析,非法配置在写入处拒绝。 */
1292
1520
  function assertServiceable(config, deps) {
1293
1521
  buildProfiles(config.providers, deps);
1294
1522
  buildDeepseekRoutes(config.providers, deps);
1295
1523
  }
1524
+ /** 官方 jsonImage:pi-ai 凭据里显式 undefined 成员 JSON 序列化为缺省/数组 null。 */
1525
+ function jsonImage(value) {
1526
+ if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
1527
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1528
+ const image = {};
1529
+ for (const [key, member] of Object.entries(value)) if (member !== void 0) image[key] = jsonImage(member);
1530
+ return image;
1531
+ }
1532
+ return value;
1533
+ }
1534
+ /** 官方 toPiCredential:api-key 记录按字段重建;grant 载荷原样透传。 */
1535
+ function toPiCredential(record) {
1536
+ if (record === void 0) return void 0;
1537
+ if (record.kind === "api-key") return {
1538
+ type: "api_key",
1539
+ ...record.key === void 0 ? {} : { key: record.key },
1540
+ ...record.env === void 0 ? {} : { env: { ...record.env } }
1541
+ };
1542
+ return record.payload;
1543
+ }
1544
+ /** 官方 toRecord:pi-ai 凭据 → 可存储的记录联合。 */
1545
+ function toRecord(credential) {
1546
+ if (credential.type === "api_key") return {
1547
+ kind: "api-key",
1548
+ ...credential.key === void 0 ? {} : { key: credential.key },
1549
+ ...credential.env === void 0 ? {} : { env: { ...credential.env } }
1550
+ };
1551
+ return {
1552
+ kind: "grant",
1553
+ payload: jsonImage(credential)
1554
+ };
1555
+ }
1556
+ /** 官方 writableStore:写路径需要凭据服务,缺失时抛 NO_CREDENTIAL_STORE。 */
1557
+ function writableStore(ctx) {
1558
+ const credentials = ctx.get("credentials");
1559
+ if (credentials === void 0) throw new LlmError("llm-pi: 当前组合未挂载凭据服务(dsh-credentials-local),登录产生的凭据无处存储", "NO_CREDENTIAL_STORE");
1560
+ return credentials;
1561
+ }
1562
+ /**
1563
+ * pi-ai `CredentialStore`(官方 credentialStoreFrom 等价实现)。
1564
+ * 路由键任意而记录 id 受语法约束:语法外的 id 读答"未存储"、删除无事可做、
1565
+ * modify 拒绝(写不落地不能报成功)。
1566
+ */
1567
+ function credentialStoreFrom(ctx, recordKeyFor) {
1568
+ return {
1569
+ async read(providerId) {
1570
+ const credentials = ctx.get("credentials");
1571
+ if (credentials === void 0) return void 0;
1572
+ if (!isCredentialKeySegment(providerId)) return void 0;
1573
+ return toPiCredential(await credentials.readRecord(recordKeyFor(providerId)));
1574
+ },
1575
+ async list() {
1576
+ const stored = await ctx.get("credentials")?.listRecords() ?? [];
1577
+ const mine = [];
1578
+ for (const entry of stored) {
1579
+ if (credentialKeyScope(entry.key) !== "llm-pi-ai") continue;
1580
+ mine.push({
1581
+ providerId: credentialKeyId(entry.key),
1582
+ type: entry.kind === "api-key" ? "api_key" : "oauth"
1583
+ });
1584
+ }
1585
+ return mine;
1586
+ },
1587
+ async modify(providerId, mutate) {
1588
+ if (!isCredentialKeySegment(providerId)) throw new LlmError(`llm-pi: provider id "${providerId}" 无法寻址存储的凭据记录(记录 id 是小写连字符标识);该 route 请改用 apiKeyEnv 认证`, "UNSTORABLE_PROVIDER_ID");
1589
+ return toPiCredential(await writableStore(ctx).modifyRecord(recordKeyFor(providerId), async (current) => {
1590
+ const next = await mutate(toPiCredential(current));
1591
+ return next === void 0 ? void 0 : toRecord(next);
1592
+ }));
1593
+ },
1594
+ async delete(providerId) {
1595
+ if (!isCredentialKeySegment(providerId)) return;
1596
+ await writableStore(ctx).deleteRecord(recordKeyFor(providerId));
1597
+ }
1598
+ };
1599
+ }
1600
+ /**
1601
+ * pi-ai `AuthContext`(官方 authContextFrom 等价实现):
1602
+ * env() 先查凭据服务(存储的环境值对 provider 自发现可见),再回退启动环境;
1603
+ * fileExists() 问宿主进程文件系统(~/.aws/credentials 等是本进程所在机器的事实)。
1604
+ */
1605
+ function authContextFrom(ctx) {
1606
+ return {
1607
+ async env(name) {
1608
+ if (isCredentialRefName(name)) {
1609
+ const hit = await ctx.get("credentials")?.resolve(credentialRef(name));
1610
+ if (hit !== void 0) return hit.value;
1611
+ }
1612
+ return launchEnvironmentOf(ctx).get(name)?.value;
1613
+ },
1614
+ async fileExists(path) {
1615
+ const expanded = path.startsWith("~/") || path === "~" ? resolve(homedir(), path.slice(1).replace(/^\//, "")) : path;
1616
+ try {
1617
+ await access(expanded);
1618
+ return true;
1619
+ } catch {
1620
+ return false;
1621
+ }
1622
+ }
1623
+ };
1624
+ }
1296
1625
  //#endregion
1297
1626
  //#region src/resolve-dsh.ts
1298
1627
  /**
@@ -1306,8 +1635,16 @@ function assertServiceable(config, deps) {
1306
1635
  * 的目录,按文件路径动态 import——与 dsh 官方插件共享同一模块实例;
1307
1636
  * 2. vendored:回退到本插件 dependencies 里的固定版本副本(裸 import)。
1308
1637
  *
1309
- * 两条路径产物都过形状自检(assertKitShape):上游 rc 重构导致形状漂移时
1638
+ * 两条路径产物都过形状自检(assertKitShape):上游重构导致形状漂移时
1310
1639
  * 抛错,由调用方决定回退或放弃注册——绝不让坏套件进入消息链路。
1640
+ *
1641
+ * 0.1.2-alpha.1 起的新面(包根未导出、仅 src 子路径有):
1642
+ * - resolveProfiles(config.ts):官方解析链,dev 布局树可经 src 子路径复用;
1643
+ * - credentialStoreFrom/authContextFrom(auth.ts):PiAiAdapter 必需 auth 注入。
1644
+ * npm 发布形态(lib/index.js 单 bundle + 无 src/)两者都拿不到:dsh 树优先
1645
+ * 探测 src/*(含 lib/* 候选),未命中时 resolveProfiles 走插件等价实现
1646
+ * (profiles.ts,门控表对齐官方 catalog.ts)、auth 走内联等价实现
1647
+ * (auth-inline.ts,recordKeyFor 自包根导出同源注入)——形状自检兜底。
1311
1648
  * @module llm-pi/resolve-dsh
1312
1649
  */
1313
1650
  /** kit 必备形状清单:缺失即视为上游不兼容。 */
@@ -1319,7 +1656,9 @@ function assertKitShape(kit, origin) {
1319
1656
  "stream",
1320
1657
  "listModels",
1321
1658
  "resolveModel",
1322
- "providerInfo"
1659
+ "providerInfo",
1660
+ "prepareCall",
1661
+ "providerRetryPolicy"
1323
1662
  ]) if (typeof kit.PiAiAdapter.prototype[method] !== "function") problems.push(`PiAiAdapter.prototype.${method} 缺失`);
1324
1663
  if (typeof kit.createProvider !== "function") problems.push("pi-ai createProvider 缺失");
1325
1664
  if (typeof kit.getBuiltinModels !== "function") problems.push("pi-ai getBuiltinModels 缺失");
@@ -1327,6 +1666,9 @@ function assertKitShape(kit, origin) {
1327
1666
  for (const [api, factory] of Object.entries(kit.protocolFactories)) if (typeof factory !== "function") problems.push(`协议工厂 ${api} 缺失`);
1328
1667
  if (typeof kit.LlmError !== "function") problems.push("dsh-llm LlmError 缺失");
1329
1668
  if (typeof kit.resolveRetryPolicy !== "function") problems.push("dsh-llm resolveRetryPolicy 缺失");
1669
+ if (typeof kit.resolveProfiles !== "function") problems.push("resolveProfiles 缺失");
1670
+ if (typeof kit.resolveImageAttachmentAccess !== "function") problems.push("resolveImageAttachmentAccess 缺失");
1671
+ if (typeof kit.auth?.credentialStoreFrom !== "function" || typeof kit.auth?.authContextFrom !== "function") problems.push("auth 助手(credentialStoreFrom/authContextFrom)缺失");
1330
1672
  if (problems.length > 0) throw new Error(`llm-pi: ${origin} 来源的运行时套件形状不兼容:${problems.join(";")}`);
1331
1673
  }
1332
1674
  /** deepseek 模块形状自检;返回问题清单(空 = 可用),由调用方决定降级。 */
@@ -1372,6 +1714,24 @@ async function importTreeModules(root) {
1372
1714
  anthropic
1373
1715
  };
1374
1716
  }
1717
+ /**
1718
+ * 探测包目录下的子路径模块:src/<name>.ts(dsh 树 dev 布局 / 源码仓库)→
1719
+ * src/<name>.js → lib/<name>.js(未来构建形态)。加载失败(依赖缺失等)
1720
+ * 视为该候选不可用,继续下一候选;全部未命中返回 undefined(调用方兜底)。
1721
+ */
1722
+ async function probeSubmodule(pkgDir, name) {
1723
+ for (const candidate of [
1724
+ `src/${name}.ts`,
1725
+ `src/${name}.js`,
1726
+ `lib/${name}.js`
1727
+ ]) {
1728
+ const absPath = join(pkgDir, candidate);
1729
+ if (!existsSync(absPath)) continue;
1730
+ try {
1731
+ return await import(pathToFileURL(absPath).href);
1732
+ } catch {}
1733
+ }
1734
+ }
1375
1735
  /** 从同一 dsh 安装树加载 deepseek 适配器模块(失败返回 undefined,不拖垮核心套件)。 */
1376
1736
  async function importTreeDeepseek(root) {
1377
1737
  const nm = join(root, "node_modules");
@@ -1390,7 +1750,28 @@ async function importTreeDeepseek(root) {
1390
1750
  return { problem: error instanceof Error ? error.message : String(error) };
1391
1751
  }
1392
1752
  }
1393
- function kitFromTree(mods) {
1753
+ function protocolFactoriesOf(mods) {
1754
+ return {
1755
+ "openai-completions": mods.completions["openAICompletionsApi"],
1756
+ "openai-responses": mods.responses["openAIResponsesApi"],
1757
+ "anthropic-messages": mods.anthropic["anthropicMessagesApi"]
1758
+ };
1759
+ }
1760
+ /** 插件等价解析链的套件依赖(与 PiAiAdapter 同源,杜绝跨源混用)。 */
1761
+ function treeResolverDeps(mods, protocolFactories) {
1762
+ return {
1763
+ createProvider: mods.piAi["createProvider"],
1764
+ protocolFactories,
1765
+ resolveRetryPolicy: mods.llm["resolveRetryPolicy"]
1766
+ };
1767
+ }
1768
+ function kitFromTree(mods, auth, config) {
1769
+ const protocolFactories = protocolFactoriesOf(mods);
1770
+ const resolverDeps = treeResolverDeps(mods, protocolFactories);
1771
+ const officialResolveProfiles = config?.["resolveProfiles"];
1772
+ const officialCredentialStoreFrom = auth?.["credentialStoreFrom"];
1773
+ const officialAuthContextFrom = auth?.["authContextFrom"];
1774
+ const recordKeyFor = mods.piAiAdapter["recordKeyFor"];
1394
1775
  return {
1395
1776
  source: "dsh-tree",
1396
1777
  PiAiAdapter: mods.piAiAdapter["PiAiAdapter"],
@@ -1404,11 +1785,13 @@ function kitFromTree(mods) {
1404
1785
  builtinProviders: mods.catalog["builtinProviders"],
1405
1786
  getBuiltinProviders: mods.catalog["getBuiltinProviders"],
1406
1787
  getBuiltinModels: mods.catalog["getBuiltinModels"],
1407
- protocolFactories: {
1408
- "openai-completions": mods.completions["openAICompletionsApi"],
1409
- "openai-responses": mods.responses["openAIResponsesApi"],
1410
- "anthropic-messages": mods.anthropic["anthropicMessagesApi"]
1411
- }
1788
+ protocolFactories,
1789
+ resolveProfiles: officialResolveProfiles !== void 0 ? officialResolveProfiles : (providers) => resolveProfilesFallback(providers, resolverDeps),
1790
+ auth: {
1791
+ credentialStoreFrom: officialCredentialStoreFrom !== void 0 ? officialCredentialStoreFrom : (ctx) => credentialStoreFrom(ctx, recordKeyFor),
1792
+ authContextFrom: officialAuthContextFrom !== void 0 ? officialAuthContextFrom : authContextFrom
1793
+ },
1794
+ resolveImageAttachmentAccess: mods.llm["resolveImageAttachmentAccess"]
1412
1795
  };
1413
1796
  }
1414
1797
  /** vendored 兜底副本的 deepseek 模块(形状自检不过时返回 undefined)。 */
@@ -1420,9 +1803,18 @@ function loadVendoredDeepseek() {
1420
1803
  };
1421
1804
  return checkDeepseekShape(kit).length === 0 ? kit : void 0;
1422
1805
  }
1423
- /** vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。 */
1806
+ /**
1807
+ * vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。
1808
+ * npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
1809
+ * auth 助手在此无条件走插件等价实现(内联/门控表对齐,见文件头说明)。
1810
+ */
1424
1811
  function loadVendoredKit() {
1425
1812
  const deepseek = loadVendoredDeepseek();
1813
+ const protocolFactories = {
1814
+ "openai-completions": openAICompletionsApi,
1815
+ "openai-responses": openAIResponsesApi,
1816
+ "anthropic-messages": anthropicMessagesApi
1817
+ };
1426
1818
  const kit = {
1427
1819
  source: "vendored",
1428
1820
  PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
@@ -1436,11 +1828,17 @@ function loadVendoredKit() {
1436
1828
  builtinProviders: vendoredCatalog.builtinProviders,
1437
1829
  getBuiltinProviders: vendoredCatalog.getBuiltinProviders,
1438
1830
  getBuiltinModels: vendoredCatalog.getBuiltinModels,
1439
- protocolFactories: {
1440
- "openai-completions": openAICompletionsApi,
1441
- "openai-responses": openAIResponsesApi,
1442
- "anthropic-messages": anthropicMessagesApi
1831
+ protocolFactories,
1832
+ resolveProfiles: (providers) => resolveProfilesFallback(providers, {
1833
+ createProvider: vendoredPiAi.createProvider,
1834
+ protocolFactories,
1835
+ resolveRetryPolicy: vendoredLlm.resolveRetryPolicy
1836
+ }),
1837
+ auth: {
1838
+ credentialStoreFrom: (ctx) => credentialStoreFrom(ctx, vendoredPiAiAdapter.recordKeyFor),
1839
+ authContextFrom
1443
1840
  },
1841
+ resolveImageAttachmentAccess: vendoredLlm.resolveImageAttachmentAccess,
1444
1842
  ...deepseek === void 0 ? {} : { deepseek }
1445
1843
  };
1446
1844
  assertKitShape(kit, "vendored");
@@ -1464,26 +1862,32 @@ function dshTreeAnchor() {
1464
1862
  async function resolveDshKit() {
1465
1863
  const diagnostics = [];
1466
1864
  const anchor = dshTreeAnchor();
1467
- if (anchor !== void 0) try {
1468
- const kit = kitFromTree(await importTreeModules(anchor));
1469
- assertKitShape(kit, "dsh-tree");
1470
- const tree = await importTreeDeepseek(anchor);
1471
- if (tree.kit !== void 0) return {
1472
- kit: {
1473
- ...kit,
1474
- deepseek: tree.kit
1475
- },
1476
- diagnostics
1477
- };
1478
- diagnostics.push(`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? "未知原因"});adapter: deepseek 的 route 不可用,pi 路由不受影响`);
1479
- return {
1480
- kit,
1481
- diagnostics
1482
- };
1483
- } catch (error) {
1484
- diagnostics.push(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`);
1485
- }
1486
- else diagnostics.push("未能从 process.argv[1] 定位 dsh 安装树;回退 vendored 副本");
1865
+ if (anchor !== void 0) {
1866
+ const treePkgDir = join(anchor, "node_modules", "@deepseek-ai", "dsh-llm-pi-ai");
1867
+ try {
1868
+ const mods = await importTreeModules(anchor);
1869
+ const [auth, config] = await Promise.all([probeSubmodule(treePkgDir, "auth"), probeSubmodule(treePkgDir, "config")]);
1870
+ const kit = kitFromTree(mods, auth, config);
1871
+ assertKitShape(kit, "dsh-tree");
1872
+ if (auth === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现");
1873
+ if (config === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)");
1874
+ const tree = await importTreeDeepseek(anchor);
1875
+ if (tree.kit !== void 0) return {
1876
+ kit: {
1877
+ ...kit,
1878
+ deepseek: tree.kit
1879
+ },
1880
+ diagnostics
1881
+ };
1882
+ diagnostics.push(`dsh 安装树的 dsh-llm-deepseek 不可用(${tree.problem ?? "未知原因"});adapter: deepseek route 不可用,pi 路由不受影响`);
1883
+ return {
1884
+ kit,
1885
+ diagnostics
1886
+ };
1887
+ } catch (error) {
1888
+ diagnostics.push(`dsh 安装树套件不可用(${anchor}):${error instanceof Error ? error.message : String(error)};回退 vendored 副本`);
1889
+ }
1890
+ } else diagnostics.push("未能从 process.argv[1] 定位 dsh 安装树;回退 vendored 副本");
1487
1891
  const kit = loadVendoredKit();
1488
1892
  if (kit.deepseek === void 0) diagnostics.push("vendored 副本的 dsh-llm-deepseek 形状不兼容;adapter: deepseek 的 route 不可用");
1489
1893
  return {
@@ -1557,7 +1961,15 @@ async function startRuntime(ctx, rawConfig) {
1557
1961
  const adapter = new kit.PiAiAdapter({
1558
1962
  profiles,
1559
1963
  resolveApiKey: makeResolveApiKey(ctx, kit),
1560
- resolveAttachments: () => ctx.get("attachments")
1964
+ resolveAttachments: () => ctx.get("attachments"),
1965
+ auth: {
1966
+ credentials: kit.auth.credentialStoreFrom(ctx),
1967
+ authContext: kit.auth.authContextFrom(ctx)
1968
+ },
1969
+ resolveImageAccess: (attachments, ref) => kit.resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
1970
+ onReplayDegrade: ({ provider, model, reason }) => {
1971
+ logger.warn(`llm-pi: 助手历史中 "${provider}/${model}" 的回放状态不可用,该消息按 provider 中立内容发送(${reason})`);
1972
+ }
1561
1973
  });
1562
1974
  const storedApiKey = async (provider) => {
1563
1975
  if (provider === void 0) return void 0;