@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/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
- if ((url.searchParams.get("source") ?? "builtin") === "models-dev") {
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.27",
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.16"
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.1",
40
- "@deepseek-ai/dsh-credentials": "^0.1.5-rc.1",
41
- "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
42
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
43
- "@deepseek-ai/dsh-launch-environment": "^0.1.5-rc.1",
44
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
45
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.5-rc.1",
46
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
47
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
48
- "@deepseek-ai/dsh-util-values": "^0.1.5-rc.1",
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.1",
55
- "@deepseek-ai/dsh-atomic-write": "0.1.5-rc.1",
56
- "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
57
- "@deepseek-ai/dsh-credentials": "0.1.5-rc.1",
58
- "@deepseek-ai/dsh-home-paths": "0.1.5-rc.1",
59
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
60
- "@deepseek-ai/dsh-launch-environment": "0.1.5-rc.1",
61
- "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
62
- "@deepseek-ai/dsh-llm-deepseek": "0.1.5-rc.1",
63
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.1",
64
- "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
65
- "@deepseek-ai/dsh-timeout": "0.1.5-rc.1",
66
- "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1",
67
- "@deepseek-ai/dsh-util-crypto": "0.1.5-rc.1",
68
- "@deepseek-ai/dsh-util-values": "0.1.5-rc.1",
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'