@dsh-plus/llm-pi 0.1.27 → 0.1.28
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 +126 -108
- package/lib/index.js +352 -147
- package/package.json +27 -27
- package/src/client/api.ts +8 -0
- package/src/client/card.tsx +3 -0
- package/src/client/constants.ts +12 -52
- package/src/client/fields.tsx +58 -0
- package/src/client/i18n.ts +2 -0
- package/src/client/views/compat.tsx +16 -1
- package/src/compat-gates.ts +265 -0
- package/src/compat.ts +70 -141
- package/src/config-api.ts +29 -0
- package/src/resolve-dsh.ts +54 -3
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { request as request$1 } from "node:https";
|
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
10
10
|
import { credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
|
|
11
|
-
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { fileURLToPath, 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
14
|
import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
@@ -201,8 +201,313 @@ function inheritedCatalogEntries(kit, provider) {
|
|
|
201
201
|
}));
|
|
202
202
|
}
|
|
203
203
|
//#endregion
|
|
204
|
+
//#region src/compat-gates.ts
|
|
205
|
+
/** 从 `{` 起做括号配对,返回内部文本;未配对返回 undefined。 */
|
|
206
|
+
function balancedBody(text, openIndex) {
|
|
207
|
+
const start = text.indexOf("{", openIndex);
|
|
208
|
+
if (start < 0) return void 0;
|
|
209
|
+
let depth = 0;
|
|
210
|
+
for (let i = start; i < text.length; i += 1) {
|
|
211
|
+
const ch = text[i];
|
|
212
|
+
if (ch === "{") depth += 1;
|
|
213
|
+
else if (ch === "}") {
|
|
214
|
+
depth -= 1;
|
|
215
|
+
if (depth === 0) return text.slice(start + 1, i);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** 解析 `field: "offer" | "withhold"` 键值对。 */
|
|
220
|
+
function parseGateFields(body) {
|
|
221
|
+
const out = {};
|
|
222
|
+
for (const m of body.matchAll(/(\w+):\s*"(offer|withhold)"/g)) out[m[1]] = m[2];
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** 取 `const NAME = { ... }` 的块体(兼容 `let`/`var` 与省略分号)。 */
|
|
226
|
+
function namedBlock(bundle, name) {
|
|
227
|
+
const m = new RegExp(`(?:const|let|var)\\s+${name}\\s*=\\s*`).exec(bundle);
|
|
228
|
+
return m === null ? void 0 : balancedBody(bundle, m.index + m[0].length);
|
|
229
|
+
}
|
|
230
|
+
/** 解析顶层 `"api": IDENT` 与 `"api": { ... }` 两种条目。 */
|
|
231
|
+
function splitGateEntries(body) {
|
|
232
|
+
const out = /* @__PURE__ */ new Map();
|
|
233
|
+
const re = /"([^"]+)"\s*:\s*/g;
|
|
234
|
+
let m = re.exec(body);
|
|
235
|
+
while (m !== null) {
|
|
236
|
+
const api = m[1];
|
|
237
|
+
const rest = body.slice(m.index + m[0].length);
|
|
238
|
+
if (rest.startsWith("{")) {
|
|
239
|
+
const inner = balancedBody(rest, 0);
|
|
240
|
+
if (inner !== void 0) out.set(api, parseGateFields(inner));
|
|
241
|
+
} else {
|
|
242
|
+
const ident = /^[A-Za-z_$][\w$]*/.exec(rest);
|
|
243
|
+
if (ident !== null) out.set(api, ident[0]);
|
|
244
|
+
}
|
|
245
|
+
m = re.exec(body);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 从运行期 bundle 推导「协议 → 门控」:解析 COMPAT_GATES 的每条目,
|
|
251
|
+
* 命名常量回查同名 `const` 块,内联对象直接取字段。
|
|
252
|
+
*/
|
|
253
|
+
function deriveGates(bundle) {
|
|
254
|
+
const gatesBody = namedBlock(bundle, "COMPAT_GATES");
|
|
255
|
+
if (gatesBody === void 0) return void 0;
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [api, ref] of splitGateEntries(gatesBody)) {
|
|
258
|
+
if (typeof ref !== "string") {
|
|
259
|
+
out[api] = ref;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const block = namedBlock(bundle, ref);
|
|
263
|
+
if (block === void 0) continue;
|
|
264
|
+
const fields = parseGateFields(block);
|
|
265
|
+
if (Object.keys(fields).length > 0) out[api] = fields;
|
|
266
|
+
}
|
|
267
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
268
|
+
}
|
|
269
|
+
/** 单个 schema 节点 → 取值约束;无法归类的返回 undefined(跳过该字段)。 */
|
|
270
|
+
function specOfNode(node) {
|
|
271
|
+
if (node === void 0) return void 0;
|
|
272
|
+
if (node.type === "boolean") return "boolean";
|
|
273
|
+
if (node.type === "number") return node.meta?.step === 1 ? "integer" : "number";
|
|
274
|
+
if (node.type === "dict") return "object";
|
|
275
|
+
if (node.type === "object") return "object";
|
|
276
|
+
if (node.type === "union" && Array.isArray(node.list) && node.list.length > 0) {
|
|
277
|
+
const values = [];
|
|
278
|
+
for (const member of node.list) if (member?.type === "const" && typeof member.value === "string") values.push(member.value);
|
|
279
|
+
else return void 0;
|
|
280
|
+
return values;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** 从官方 Config schema 推导字段取值约束(`providers.*.compat` 节点)。 */
|
|
284
|
+
function deriveSpecs(module) {
|
|
285
|
+
const fields = (module["Config"]?.dict?.["providers"]?.inner?.dict?.["compat"])?.dict;
|
|
286
|
+
if (fields === void 0) return {};
|
|
287
|
+
const out = {};
|
|
288
|
+
for (const [field, node] of Object.entries(fields)) {
|
|
289
|
+
const spec = specOfNode(node);
|
|
290
|
+
if (spec !== void 0) out[field] = spec;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
/** 官方门控表不可解析时的最后已知快照(0.1.5-rc.2 实测值)。 */
|
|
295
|
+
const FALLBACK_TABLE = {
|
|
296
|
+
source: "fallback",
|
|
297
|
+
gates: {
|
|
298
|
+
"openai-completions": {
|
|
299
|
+
supportsStore: "offer",
|
|
300
|
+
supportsDeveloperRole: "offer",
|
|
301
|
+
supportsReasoningEffort: "offer",
|
|
302
|
+
supportsUsageInStreaming: "offer",
|
|
303
|
+
supportsFinishReason: "offer",
|
|
304
|
+
maxTokensField: "offer",
|
|
305
|
+
requiresToolResultName: "offer",
|
|
306
|
+
requiresAssistantAfterToolResult: "offer",
|
|
307
|
+
requiresThinkingAsText: "offer",
|
|
308
|
+
requiresReasoningContentOnAssistantMessages: "offer",
|
|
309
|
+
thinkingFormat: "offer",
|
|
310
|
+
chatTemplateKwargs: "offer",
|
|
311
|
+
chatTemplateArgs: "offer",
|
|
312
|
+
supportsThinkingTokenBudget: "offer",
|
|
313
|
+
thinkingTokenBudgetField: "offer",
|
|
314
|
+
vllmPriority: "offer",
|
|
315
|
+
supportsStrictMode: "offer",
|
|
316
|
+
cacheControlFormat: "offer",
|
|
317
|
+
supportsLongCacheRetention: "offer",
|
|
318
|
+
openRouterRouting: "withhold",
|
|
319
|
+
vercelGatewayRouting: "withhold",
|
|
320
|
+
zaiToolStream: "withhold",
|
|
321
|
+
supportsOpenAIGrammarTools: "withhold",
|
|
322
|
+
sendSessionAffinityHeaders: "withhold",
|
|
323
|
+
deferredToolsMode: "withhold",
|
|
324
|
+
sessionAffinityFormat: "withhold"
|
|
325
|
+
},
|
|
326
|
+
"openai-responses": {
|
|
327
|
+
supportsDeveloperRole: "offer",
|
|
328
|
+
supportsMaxOutputTokens: "offer",
|
|
329
|
+
supportsStrictMode: "offer",
|
|
330
|
+
supportsLongCacheRetention: "offer",
|
|
331
|
+
sessionAffinityFormat: "withhold",
|
|
332
|
+
supportsOpenAIGrammarTools: "withhold",
|
|
333
|
+
supportsAdditionalTools: "withhold",
|
|
334
|
+
supportsToolSearch: "withhold",
|
|
335
|
+
supportsExplicitPromptCacheMode: "withhold"
|
|
336
|
+
},
|
|
337
|
+
"anthropic-messages": {
|
|
338
|
+
supportsEagerToolInputStreaming: "offer",
|
|
339
|
+
supportsLongCacheRetention: "offer",
|
|
340
|
+
supportsCacheControlOnTools: "offer",
|
|
341
|
+
supportsTemperature: "offer",
|
|
342
|
+
forceAdaptiveThinking: "offer",
|
|
343
|
+
allowEmptySignature: "offer",
|
|
344
|
+
supportsStrictTools: "offer",
|
|
345
|
+
sendSessionAffinityHeaders: "withhold",
|
|
346
|
+
supportsToolReferences: "withhold",
|
|
347
|
+
supportsMidConvoEffort: "withhold",
|
|
348
|
+
allowedFallbackModels: "withhold"
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
specs: {
|
|
352
|
+
supportsStore: "boolean",
|
|
353
|
+
supportsDeveloperRole: "boolean",
|
|
354
|
+
supportsReasoningEffort: "boolean",
|
|
355
|
+
supportsUsageInStreaming: "boolean",
|
|
356
|
+
supportsFinishReason: "boolean",
|
|
357
|
+
maxTokensField: ["max_completion_tokens", "max_tokens"],
|
|
358
|
+
requiresToolResultName: "boolean",
|
|
359
|
+
requiresAssistantAfterToolResult: "boolean",
|
|
360
|
+
requiresThinkingAsText: "boolean",
|
|
361
|
+
requiresReasoningContentOnAssistantMessages: "boolean",
|
|
362
|
+
thinkingFormat: [
|
|
363
|
+
"openai",
|
|
364
|
+
"deepseek",
|
|
365
|
+
"openrouter",
|
|
366
|
+
"together",
|
|
367
|
+
"baseten",
|
|
368
|
+
"zai",
|
|
369
|
+
"qwen",
|
|
370
|
+
"chat-template",
|
|
371
|
+
"qwen-chat-template",
|
|
372
|
+
"string-thinking",
|
|
373
|
+
"ant-ling"
|
|
374
|
+
],
|
|
375
|
+
chatTemplateKwargs: "object",
|
|
376
|
+
chatTemplateArgs: "object",
|
|
377
|
+
supportsThinkingTokenBudget: "boolean",
|
|
378
|
+
thinkingTokenBudgetField: [
|
|
379
|
+
"thinking_token_budget",
|
|
380
|
+
"thinking_budget",
|
|
381
|
+
"thinking_budget_tokens"
|
|
382
|
+
],
|
|
383
|
+
vllmPriority: "integer",
|
|
384
|
+
supportsMaxOutputTokens: "boolean",
|
|
385
|
+
supportsStrictMode: "boolean",
|
|
386
|
+
cacheControlFormat: ["anthropic"],
|
|
387
|
+
supportsLongCacheRetention: "boolean",
|
|
388
|
+
supportsEagerToolInputStreaming: "boolean",
|
|
389
|
+
supportsCacheControlOnTools: "boolean",
|
|
390
|
+
supportsTemperature: "boolean",
|
|
391
|
+
forceAdaptiveThinking: "boolean",
|
|
392
|
+
allowEmptySignature: "boolean",
|
|
393
|
+
supportsStrictTools: "boolean"
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/compat.ts
|
|
398
|
+
/**
|
|
399
|
+
* 生效门控表:由 resolve-dsh 的套件加载流程经 {@link installCompatTable} 注入
|
|
400
|
+
* (与 PiAiAdapter 同源——即 dsh 树或 vendored 副本里**正在运行**的那份官方代码)。
|
|
401
|
+
* 未注入时用 FALLBACK_TABLE(纯函数测试路径/极端启动顺序下的保守兜底)。
|
|
402
|
+
*/
|
|
403
|
+
let active = FALLBACK_TABLE;
|
|
404
|
+
/** 注入推导结果(幂等;resolve-dsh 在套件加载后调用一次)。 */
|
|
405
|
+
function installCompatTable(table) {
|
|
406
|
+
active = table;
|
|
407
|
+
}
|
|
408
|
+
/** 当前生效表的来源诊断(状态行/日志)。 */
|
|
409
|
+
function compatTableInfo() {
|
|
410
|
+
return active.problem === void 0 ? { source: active.source } : {
|
|
411
|
+
source: active.source,
|
|
412
|
+
problem: active.problem
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
|
|
416
|
+
function compatFieldsOf(api) {
|
|
417
|
+
const gate = active.gates[api];
|
|
418
|
+
if (gate === void 0) return [];
|
|
419
|
+
return Object.entries(gate).flatMap(([field, disposition]) => disposition === "offer" ? [field] : []);
|
|
420
|
+
}
|
|
421
|
+
/** 某协议某 offer 字段的取值约束(UI 渲染开关/下拉用)。 */
|
|
422
|
+
function compatFieldSpec(api, field) {
|
|
423
|
+
return active.gates[api]?.[field] === "offer" ? active.specs[field] : void 0;
|
|
424
|
+
}
|
|
425
|
+
/** 官方声明的全部可配置字段(未知键报错时列出,对齐官方 allOfferedCompatFields)。 */
|
|
426
|
+
function allOfferedFields() {
|
|
427
|
+
const out = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const gate of Object.values(active.gates)) for (const [field, disposition] of Object.entries(gate)) if (disposition === "offer") out.add(field);
|
|
429
|
+
return [...out];
|
|
430
|
+
}
|
|
431
|
+
function checkValue(field, spec, value, where) {
|
|
432
|
+
if (spec === "boolean") {
|
|
433
|
+
if (typeof value !== "boolean") throw new Error(`${where}: compat.${field} 必须是布尔值`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (spec === "integer") {
|
|
437
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${where}: compat.${field} 必须是整数`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (spec === "number") {
|
|
441
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${where}: compat.${field} 必须是数字`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (spec === "object") {
|
|
445
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${where}: compat.${field} 必须是对象`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (typeof value !== "string" || !spec.includes(value)) throw new Error(`${where}: compat.${field} 必须是 ${spec.map((v) => JSON.stringify(v)).join(" | ")} 之一`);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* 校验一份 compat 字典对指定协议合法(对齐官方门控语义 + schema 值约束):
|
|
452
|
+
* - 未知键/withhold 字段拒绝(官方写时拒绝,替代旧版静默丢弃);
|
|
453
|
+
* - 值类型/枚举按官方 schema 校验;
|
|
454
|
+
* - 无值键(null/undefined)拒绝(官方 assertOfferedCompatFields 同款)。
|
|
455
|
+
*/
|
|
456
|
+
function validateCompat(api, compat, where) {
|
|
457
|
+
if (compat === void 0) return;
|
|
458
|
+
const gate = active.gates[api];
|
|
459
|
+
if (gate === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(active.gates).join(", ")})`);
|
|
460
|
+
const offered = compatFieldsOf(api);
|
|
461
|
+
for (const [key, value] of Object.entries(compat)) {
|
|
462
|
+
const disposition = gate[key];
|
|
463
|
+
if (disposition !== "offer") {
|
|
464
|
+
if (disposition === "withhold") throw new Error(`${where}: compat.${key} 官方按协议 withhold(内置目录已为对应厂商设置该开关);请以目录 provider 名作为 route 名(继承目录值),或移除该字段`);
|
|
465
|
+
throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(", ")};官方全部可配字段:${allOfferedFields().join(", ")})`);
|
|
466
|
+
}
|
|
467
|
+
if (value === void 0 || value === null) throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`);
|
|
468
|
+
const spec = active.specs[key];
|
|
469
|
+
if (spec !== void 0) checkValue(key, spec, value, where);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined/null 值。
|
|
474
|
+
* 层序:继承源(仅同协议)→ route 级 → 模型级。
|
|
475
|
+
*/
|
|
476
|
+
function mergeCompat(...layers) {
|
|
477
|
+
const merged = {};
|
|
478
|
+
for (const layer of layers) {
|
|
479
|
+
if (layer === void 0) continue;
|
|
480
|
+
for (const [key, value] of Object.entries(layer)) if (value !== void 0 && value !== null) merged[key] = value;
|
|
481
|
+
}
|
|
482
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
204
485
|
//#region src/config-api.ts
|
|
205
486
|
const ROUTE_CATALOG = "/dsh-plus/llm-pi/catalog";
|
|
487
|
+
/**
|
|
488
|
+
* 服务端推导的 compat 字段表(协议 → 字段 → 取值约束)。浏览器半读不到官方包,
|
|
489
|
+
* 由本端点下发,UI 渲染与服务端校验同源(不再各存一份手抄镜像表)。
|
|
490
|
+
*/
|
|
491
|
+
function compatTablePayload() {
|
|
492
|
+
const fields = {};
|
|
493
|
+
for (const api of PROTOCOL_IDS) {
|
|
494
|
+
const perField = {};
|
|
495
|
+
for (const field of compatFieldsOf(api)) {
|
|
496
|
+
const spec = compatFieldSpec(api, field);
|
|
497
|
+
if (spec !== void 0) perField[field] = spec;
|
|
498
|
+
}
|
|
499
|
+
fields[api] = perField;
|
|
500
|
+
}
|
|
501
|
+
const info = compatTableInfo();
|
|
502
|
+
return info.problem === void 0 ? {
|
|
503
|
+
fields,
|
|
504
|
+
source: info.source
|
|
505
|
+
} : {
|
|
506
|
+
fields,
|
|
507
|
+
source: info.source,
|
|
508
|
+
problem: info.problem
|
|
509
|
+
};
|
|
510
|
+
}
|
|
206
511
|
function sendJson(res, status, body) {
|
|
207
512
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
208
513
|
res.end(JSON.stringify(body));
|
|
@@ -218,19 +523,23 @@ function handleCatalog(runtime, req, res) {
|
|
|
218
523
|
}
|
|
219
524
|
const url = new URL(req.url ?? "", "http://localhost");
|
|
220
525
|
const provider = url.searchParams.get("provider") ?? "";
|
|
221
|
-
|
|
526
|
+
const source = url.searchParams.get("source") ?? "builtin";
|
|
527
|
+
const compat = compatTablePayload();
|
|
528
|
+
if (source === "models-dev") {
|
|
222
529
|
sendJson(res, 200, {
|
|
223
530
|
providers: runtime.modelsDev.providerIds(),
|
|
224
531
|
models: provider.length > 0 ? runtime.modelsDev.modelIds(provider) : [],
|
|
225
532
|
status: runtime.modelsDev.status(),
|
|
226
|
-
kitSource
|
|
533
|
+
kitSource,
|
|
534
|
+
compat
|
|
227
535
|
});
|
|
228
536
|
return;
|
|
229
537
|
}
|
|
230
538
|
sendJson(res, 200, {
|
|
231
539
|
providers: runtime.kit.getBuiltinProviders(),
|
|
232
540
|
models: provider.length > 0 ? builtinModelIds(runtime.kit, provider) : [],
|
|
233
|
-
kitSource
|
|
541
|
+
kitSource,
|
|
542
|
+
compat
|
|
234
543
|
});
|
|
235
544
|
}
|
|
236
545
|
/** 注册目录路由(webServer 缺失时由调用方保证不调用)。 */
|
|
@@ -768,148 +1077,6 @@ async function discoverModels(request, deps) {
|
|
|
768
1077
|
}
|
|
769
1078
|
}
|
|
770
1079
|
//#endregion
|
|
771
|
-
//#region src/compat.ts
|
|
772
|
-
const GATES_BY_PROTOCOL = {
|
|
773
|
-
"openai-completions": {
|
|
774
|
-
supportsStore: "offer",
|
|
775
|
-
supportsDeveloperRole: "offer",
|
|
776
|
-
supportsReasoningEffort: "offer",
|
|
777
|
-
supportsUsageInStreaming: "offer",
|
|
778
|
-
supportsFinishReason: "offer",
|
|
779
|
-
maxTokensField: "offer",
|
|
780
|
-
requiresToolResultName: "offer",
|
|
781
|
-
requiresAssistantAfterToolResult: "offer",
|
|
782
|
-
requiresThinkingAsText: "offer",
|
|
783
|
-
requiresReasoningContentOnAssistantMessages: "offer",
|
|
784
|
-
thinkingFormat: "offer",
|
|
785
|
-
chatTemplateKwargs: "offer",
|
|
786
|
-
chatTemplateArgs: "offer",
|
|
787
|
-
supportsThinkingTokenBudget: "offer",
|
|
788
|
-
supportsStrictMode: "offer",
|
|
789
|
-
cacheControlFormat: "offer",
|
|
790
|
-
supportsLongCacheRetention: "offer",
|
|
791
|
-
openRouterRouting: "withhold",
|
|
792
|
-
vercelGatewayRouting: "withhold",
|
|
793
|
-
zaiToolStream: "withhold",
|
|
794
|
-
supportsOpenAIGrammarTools: "withhold",
|
|
795
|
-
sendSessionAffinityHeaders: "withhold",
|
|
796
|
-
deferredToolsMode: "withhold",
|
|
797
|
-
sessionAffinityFormat: "withhold"
|
|
798
|
-
},
|
|
799
|
-
"openai-responses": {
|
|
800
|
-
supportsDeveloperRole: "offer",
|
|
801
|
-
supportsStrictMode: "offer",
|
|
802
|
-
supportsLongCacheRetention: "offer",
|
|
803
|
-
sessionAffinityFormat: "withhold",
|
|
804
|
-
supportsOpenAIGrammarTools: "withhold",
|
|
805
|
-
supportsAdditionalTools: "withhold",
|
|
806
|
-
supportsToolSearch: "withhold",
|
|
807
|
-
supportsExplicitPromptCacheMode: "withhold"
|
|
808
|
-
},
|
|
809
|
-
"anthropic-messages": {
|
|
810
|
-
supportsEagerToolInputStreaming: "offer",
|
|
811
|
-
supportsLongCacheRetention: "offer",
|
|
812
|
-
supportsCacheControlOnTools: "offer",
|
|
813
|
-
supportsTemperature: "offer",
|
|
814
|
-
forceAdaptiveThinking: "offer",
|
|
815
|
-
allowEmptySignature: "offer",
|
|
816
|
-
supportsStrictTools: "offer",
|
|
817
|
-
sendSessionAffinityHeaders: "withhold",
|
|
818
|
-
supportsToolReferences: "withhold"
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
/**
|
|
822
|
-
* 字段取值约束(对齐官方 config.ts compatProfile schema):
|
|
823
|
-
* boolean 字段 → 'boolean';maxTokensField/thinkingFormat/cacheControlFormat
|
|
824
|
-
* → 枚举;chatTemplateKwargs/chatTemplateArgs → 对象。
|
|
825
|
-
*/
|
|
826
|
-
const VALUE_SPECS = {
|
|
827
|
-
supportsStore: "boolean",
|
|
828
|
-
supportsDeveloperRole: "boolean",
|
|
829
|
-
supportsReasoningEffort: "boolean",
|
|
830
|
-
supportsUsageInStreaming: "boolean",
|
|
831
|
-
supportsFinishReason: "boolean",
|
|
832
|
-
maxTokensField: ["max_completion_tokens", "max_tokens"],
|
|
833
|
-
requiresToolResultName: "boolean",
|
|
834
|
-
requiresAssistantAfterToolResult: "boolean",
|
|
835
|
-
requiresThinkingAsText: "boolean",
|
|
836
|
-
requiresReasoningContentOnAssistantMessages: "boolean",
|
|
837
|
-
thinkingFormat: [
|
|
838
|
-
"openai",
|
|
839
|
-
"deepseek",
|
|
840
|
-
"openrouter",
|
|
841
|
-
"together",
|
|
842
|
-
"baseten",
|
|
843
|
-
"zai",
|
|
844
|
-
"qwen",
|
|
845
|
-
"chat-template",
|
|
846
|
-
"qwen-chat-template",
|
|
847
|
-
"string-thinking",
|
|
848
|
-
"ant-ling"
|
|
849
|
-
],
|
|
850
|
-
chatTemplateKwargs: "object",
|
|
851
|
-
chatTemplateArgs: "object",
|
|
852
|
-
supportsThinkingTokenBudget: "boolean",
|
|
853
|
-
supportsStrictMode: "boolean",
|
|
854
|
-
cacheControlFormat: ["anthropic"],
|
|
855
|
-
supportsLongCacheRetention: "boolean",
|
|
856
|
-
supportsEagerToolInputStreaming: "boolean",
|
|
857
|
-
supportsCacheControlOnTools: "boolean",
|
|
858
|
-
supportsTemperature: "boolean",
|
|
859
|
-
forceAdaptiveThinking: "boolean",
|
|
860
|
-
allowEmptySignature: "boolean",
|
|
861
|
-
supportsStrictTools: "boolean"
|
|
862
|
-
};
|
|
863
|
-
/** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
|
|
864
|
-
function compatFieldsOf(api) {
|
|
865
|
-
return Object.entries(GATES_BY_PROTOCOL[api]).flatMap(([field, disposition]) => disposition === "offer" ? [field] : []);
|
|
866
|
-
}
|
|
867
|
-
function checkValue(field, spec, value, where) {
|
|
868
|
-
if (spec === "boolean") {
|
|
869
|
-
if (typeof value !== "boolean") throw new Error(`${where}: compat.${field} 必须是布尔值`);
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (spec === "object") {
|
|
873
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${where}: compat.${field} 必须是对象`);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (typeof value !== "string" || !spec.includes(value)) throw new Error(`${where}: compat.${field} 必须是 ${spec.map((v) => JSON.stringify(v)).join(" | ")} 之一`);
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* 校验一份 compat 字典对指定协议合法(对齐官方门控语义 + schema 值约束):
|
|
880
|
-
* - 未知键/withhold 字段拒绝(官方 0.1.2-alpha.1 写时拒绝,替代旧版静默丢弃);
|
|
881
|
-
* - 值类型/枚举按官方 schema 校验;
|
|
882
|
-
* - 无值键(null/undefined)拒绝(官方 assertOfferedCompatFields 同款:
|
|
883
|
-
* "写了但没生效" 的表面状态不允许)。
|
|
884
|
-
*/
|
|
885
|
-
function validateCompat(api, compat, where) {
|
|
886
|
-
if (compat === void 0) return;
|
|
887
|
-
const gate = GATES_BY_PROTOCOL[api];
|
|
888
|
-
if (gate === void 0) throw new Error(`${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(GATES_BY_PROTOCOL).join(", ")})`);
|
|
889
|
-
const offered = compatFieldsOf(api);
|
|
890
|
-
for (const [key, value] of Object.entries(compat)) {
|
|
891
|
-
const disposition = gate[key];
|
|
892
|
-
if (disposition !== "offer") {
|
|
893
|
-
if (disposition === "withhold") throw new Error(`${where}: compat.${key} 官方按协议 withhold(内置目录已为对应厂商设置该开关);请以目录 provider 名作为 route 名(继承目录值),或移除该字段`);
|
|
894
|
-
throw new Error(`${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(", ")})`);
|
|
895
|
-
}
|
|
896
|
-
if (value === void 0 || value === null) throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`);
|
|
897
|
-
checkValue(key, VALUE_SPECS[key], value, where);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined/null 值。
|
|
902
|
-
* 层序:继承源(仅同协议)→ route 级 → 模型级。
|
|
903
|
-
*/
|
|
904
|
-
function mergeCompat(...layers) {
|
|
905
|
-
const merged = {};
|
|
906
|
-
for (const layer of layers) {
|
|
907
|
-
if (layer === void 0) continue;
|
|
908
|
-
for (const [key, value] of Object.entries(layer)) if (value !== void 0 && value !== null) merged[key] = value;
|
|
909
|
-
}
|
|
910
|
-
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
911
|
-
}
|
|
912
|
-
//#endregion
|
|
913
1080
|
//#region src/inherit.ts
|
|
914
1081
|
var ExtendsError = class extends Error {};
|
|
915
1082
|
/** 解析 "provider/model" 或裸 "model" 引用。 */
|
|
@@ -1772,6 +1939,34 @@ function treeResolverDeps(mods, protocolFactories) {
|
|
|
1772
1939
|
resolveRetryPolicy: mods.llm["resolveRetryPolicy"]
|
|
1773
1940
|
};
|
|
1774
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* 从官方安装副本推导 compat 门控表(bundle 文本给分型、Config schema 给取值约束),
|
|
1944
|
+
* 并安装为插件生效表。任一环节失败回退 FALLBACK_TABLE 并给诊断——绝不因推导失败
|
|
1945
|
+
* 弄挂插件启动(宁可放宽校验,也不误拒官方可配字段)。
|
|
1946
|
+
*/
|
|
1947
|
+
function installOfficialCompatTable(bundlePath, module, diagnostics) {
|
|
1948
|
+
const label = "compat 门控表";
|
|
1949
|
+
try {
|
|
1950
|
+
const gates = deriveGates(readFileSync(bundlePath, "utf8"));
|
|
1951
|
+
if (gates === void 0) {
|
|
1952
|
+
diagnostics.push(`${label}未能从官方 bundle 解析 COMPAT_GATES(${bundlePath});使用内置快照(官方新增字段可能被误拒,请检查 dsh-llm-pi-ai 打包形态)`);
|
|
1953
|
+
installCompatTable(FALLBACK_TABLE);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
installCompatTable({
|
|
1957
|
+
gates,
|
|
1958
|
+
specs: {
|
|
1959
|
+
...FALLBACK_TABLE.specs,
|
|
1960
|
+
...deriveSpecs(module)
|
|
1961
|
+
},
|
|
1962
|
+
source: "official"
|
|
1963
|
+
});
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1966
|
+
diagnostics.push(`${label}推导失败(${reason});使用内置快照`);
|
|
1967
|
+
installCompatTable(FALLBACK_TABLE);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1775
1970
|
function kitFromTree(mods, auth, config) {
|
|
1776
1971
|
const protocolFactories = protocolFactoriesOf(mods);
|
|
1777
1972
|
const resolverDeps = treeResolverDeps(mods, protocolFactories);
|
|
@@ -1811,9 +2006,17 @@ function loadVendoredDeepseek() {
|
|
|
1811
2006
|
return checkDeepseekShape(kit).length === 0 ? kit : void 0;
|
|
1812
2007
|
}
|
|
1813
2008
|
/**
|
|
2009
|
+
* vendored `dsh-llm-pi-ai` 的 bundle 绝对路径(compat 门控推导用)。
|
|
2010
|
+
* 经包名解析拿到入口文件——npm/pnpm 各布局都适用,不硬编码 node_modules 结构。
|
|
2011
|
+
*/
|
|
2012
|
+
function vendoredAdapterBundlePath() {
|
|
2013
|
+
return fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-llm-pi-ai"));
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
1814
2016
|
* vendored 兜底副本套件(导出供单测直接使用,免走 dsh 树解析)。
|
|
1815
2017
|
* npm 发布形态不携带 src/、lib 仅 index.js/invariant.js——resolveProfiles 与
|
|
1816
|
-
* auth
|
|
2018
|
+
* auth 助手在此无条件走插件等价实现(内联,见文件头说明);compat 门控表则
|
|
2019
|
+
* 仍从 vendored 副本的 bundle + Config schema 现场推导(与 dsh 树同一推导链)。
|
|
1817
2020
|
*/
|
|
1818
2021
|
function loadVendoredKit() {
|
|
1819
2022
|
const deepseek = loadVendoredDeepseek();
|
|
@@ -1822,6 +2025,7 @@ function loadVendoredKit() {
|
|
|
1822
2025
|
"openai-responses": openAIResponsesApi,
|
|
1823
2026
|
"anthropic-messages": anthropicMessagesApi
|
|
1824
2027
|
};
|
|
2028
|
+
installOfficialCompatTable(vendoredAdapterBundlePath(), vendoredPiAiAdapter, []);
|
|
1825
2029
|
const kit = {
|
|
1826
2030
|
source: "vendored",
|
|
1827
2031
|
PiAiAdapter: vendoredPiAiAdapter.PiAiAdapter,
|
|
@@ -1876,6 +2080,7 @@ async function resolveDshKit() {
|
|
|
1876
2080
|
const [auth, config] = await Promise.all([probeSubmodule(treePkgDir, "auth"), probeSubmodule(treePkgDir, "config")]);
|
|
1877
2081
|
const kit = kitFromTree(mods, auth, config);
|
|
1878
2082
|
assertKitShape(kit, "dsh-tree");
|
|
2083
|
+
installOfficialCompatTable(join(treePkgDir, "lib", "index.js"), mods.piAiAdapter, diagnostics);
|
|
1879
2084
|
if (auth === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);认证助手使用插件内联等价实现");
|
|
1880
2085
|
if (config === void 0) diagnostics.push("dsh 树不含 dsh-llm-pi-ai/src(npm 发布形态);profile 解析使用插件等价实现(compat 门控对齐官方 catalog.ts)");
|
|
1881
2086
|
const tree = await importTreeDeepseek(anchor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/llm-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,40 +32,40 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"https-proxy-agent": "^9.1.0",
|
|
35
|
-
"@dsh-plus/shared": "0.1.
|
|
35
|
+
"@dsh-plus/shared": "0.1.17"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
39
|
-
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.5-rc.
|
|
40
|
-
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.
|
|
41
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.
|
|
42
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.
|
|
43
|
-
"@deepseek-ai/dsh-launch-environment": "^0.1.5-rc.
|
|
44
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.
|
|
45
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.1.5-rc.
|
|
46
|
-
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.
|
|
47
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-rc.
|
|
48
|
-
"@deepseek-ai/dsh-util-values": "^0.1.5-rc.
|
|
39
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.5-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.5-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.5-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-util-values": "^0.1.5-rc.2",
|
|
49
49
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
50
50
|
"@earendil-works/pi-ai": "^0.85.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@deepseek-ai/cordis": "4.0.2",
|
|
54
|
-
"@deepseek-ai/dsh-anonymous-user-id": "0.1.5-rc.
|
|
55
|
-
"@deepseek-ai/dsh-atomic-write": "0.1.5-rc.
|
|
56
|
-
"@deepseek-ai/dsh-attachment": "0.1.5-rc.
|
|
57
|
-
"@deepseek-ai/dsh-credentials": "0.1.5-rc.
|
|
58
|
-
"@deepseek-ai/dsh-home-paths": "0.1.5-rc.
|
|
59
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.
|
|
60
|
-
"@deepseek-ai/dsh-launch-environment": "0.1.5-rc.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "0.1.5-rc.
|
|
62
|
-
"@deepseek-ai/dsh-llm-deepseek": "0.1.5-rc.
|
|
63
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.
|
|
64
|
-
"@deepseek-ai/dsh-settings": "0.1.5-rc.
|
|
65
|
-
"@deepseek-ai/dsh-timeout": "0.1.5-rc.
|
|
66
|
-
"@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.
|
|
67
|
-
"@deepseek-ai/dsh-util-crypto": "0.1.5-rc.
|
|
68
|
-
"@deepseek-ai/dsh-util-values": "0.1.5-rc.
|
|
54
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.1.5-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-credentials": "0.1.5-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.5-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.1.5-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-util-crypto": "0.1.5-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-util-values": "0.1.5-rc.2",
|
|
69
69
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
70
70
|
"@earendil-works/pi-ai": "0.85.1",
|
|
71
71
|
"@types/react": "~18.3.31",
|
package/src/client/api.ts
CHANGED
|
@@ -63,12 +63,20 @@ export interface ConfigValue {
|
|
|
63
63
|
/** 保存提交形状:完整配置对象,providers 全量替换(settings.replace 语义)。 */
|
|
64
64
|
export type ConfigPatch = ConfigValue
|
|
65
65
|
|
|
66
|
+
/** compat 字段表(服务端从官方包推导后下发;浏览器半不手抄)。 */
|
|
67
|
+
export interface WireCompatTable {
|
|
68
|
+
fields: Record<string, Record<string, 'boolean' | 'integer' | 'number' | 'object' | string[]>>
|
|
69
|
+
source: string
|
|
70
|
+
problem?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
/** GET /catalog?provider=&source= 返回(kitSource 为运行期套件来源诊断)。 */
|
|
67
74
|
export interface CatalogResult {
|
|
68
75
|
providers: string[]
|
|
69
76
|
models: string[]
|
|
70
77
|
status?: WireModelsDevStatus
|
|
71
78
|
kitSource?: string
|
|
79
|
+
compat?: WireCompatTable
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const ROUTE_CATALOG = '/dsh-plus/llm-pi/catalog'
|