@dsh-plus/llm-pi 0.1.13 → 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/client.js +48 -50
- package/lib/index.d.ts +3 -2
- package/lib/index.js +634 -222
- package/package.json +28 -24
- package/src/auth-inline.ts +177 -0
- package/src/catalog/official.ts +0 -2
- package/src/client/api.ts +4 -3
- package/src/client/card.tsx +5 -13
- package/src/client/client.ts +16 -18
- package/src/client/constants.ts +11 -16
- package/src/compat.ts +136 -62
- package/src/config-api.ts +3 -3
- package/src/config.ts +25 -4
- package/src/deepseek-routes.ts +25 -0
- package/src/ns.ts +1 -1
- package/src/profiles-deepseek.ts +12 -6
- package/src/profiles.ts +439 -171
- package/src/resolve-dsh.ts +170 -13
- package/src/service.ts +26 -0
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("
|
|
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(
|
|
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
|
|
764
|
+
const GATES_BY_PROTOCOL = {
|
|
753
765
|
"openai-completions": {
|
|
754
|
-
supportsStore: "
|
|
755
|
-
supportsDeveloperRole: "
|
|
756
|
-
supportsReasoningEffort: "
|
|
757
|
-
supportsUsageInStreaming: "
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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: "
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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: "
|
|
806
|
-
supportsLongCacheRetention: "
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
supportsToolReferences: "
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
834
|
-
if (
|
|
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
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
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
|
|
1086
|
+
if (deps.kit.deepseek === void 0) invalid$1(route, "使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(0.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
|
|
1123
|
+
* profile 构建器(0.1.2-alpha.1 适配版):插件 Config → 官方 schema 形状的
|
|
1124
|
+
* 「前置数据源归一化」→ 官方 resolveProfiles(或插件等价兜底)→
|
|
1125
|
+
* ResolvedPiAiProviderProfile。
|
|
1070
1126
|
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
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
|
-
*
|
|
1077
|
-
*
|
|
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
|
-
|
|
1098
|
-
|
|
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 "${
|
|
1117
|
-
} else if (wire.length === 0) invalid(provider, `model "${
|
|
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 "${
|
|
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
|
-
/**
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
/**
|
|
1142
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1181
|
-
*
|
|
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
|
|
1216
|
-
if (
|
|
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
|
|
1224
|
-
if (
|
|
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
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1322
|
+
* 前置数据源归一化:一个 route → 官方 schema 形状的全显式 profile。
|
|
1323
|
+
* 不可服务的 route(deepseek 路由 / 草稿 / lenient 下无模型)返回 undefined。
|
|
1324
|
+
* 校验规则对齐官方 resolveProfiles(baseURL/displayName 非空、idle 超时边界、
|
|
1325
|
+
* 图片限额正整数、defaultInput 非空)。
|
|
1243
1326
|
*/
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
...profile.
|
|
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:
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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)
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
kit
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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;
|