@dsh-plus/llm-pi 0.1.7 → 0.1.9

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 CHANGED
@@ -230,7 +230,7 @@ window.__ModuleLoader__.load({
230
230
  compat: provider.compat,
231
231
  defaultContextWindow: toNum(provider.defaultContextWindow),
232
232
  defaultMaxTokens: toNum(provider.defaultMaxTokens),
233
- input: inputToWire(provider.input),
233
+ defaultInput: inputToWire(provider.input),
234
234
  reasoning: provider.reasoning,
235
235
  thinkingBudgets: budgetToWire(provider.thinkingBudgets),
236
236
  cacheRetention: provider.cacheRetention,
package/lib/index.js CHANGED
@@ -497,6 +497,63 @@ var DeepseekRouteRegistrar = class {
497
497
  }
498
498
  };
499
499
  //#endregion
500
+ //#region src/directory.ts
501
+ /**
502
+ * 可配置 provider 目录注册:条目组装与冲突降级。
503
+ *
504
+ * 官方 `registerConfigurableProviders`/`replace` 的拒绝语义是原子的——任一条目
505
+ * 与既有声明冲突(典型:route 名撞官方内置 provider 目录条目,如 anthropic)则
506
+ * 整批不落盘。此处按 registerAdapter 同款降级模式:逐个剔除冲突条目重试,
507
+ * 让其余 route 的目录条目照常生效。
508
+ * @module llm-pi/directory
509
+ */
510
+ /** 组装当前全部 route 的目录条目(pi + deepseek 两族)。 */
511
+ function buildDirectoryEntries(kit, settingsNs, piProfiles, deepseekRoutes, drafts) {
512
+ const piEntries = [...piProfiles.entries()].map(([provider, profile]) => ({
513
+ provider,
514
+ displayName: profile.displayName,
515
+ settingsNs,
516
+ settingsPath: ["providers", provider],
517
+ declared: !hasBuiltinProvider(kit, provider)
518
+ }));
519
+ const deepseekEntries = [...deepseekRoutes.values()].map((built) => ({
520
+ provider: built.route,
521
+ displayName: built.displayName,
522
+ settingsNs,
523
+ settingsPath: ["providers", built.route],
524
+ declared: true
525
+ }));
526
+ const draftEntries = drafts.map(({ route, displayName }) => ({
527
+ provider: route,
528
+ displayName,
529
+ settingsNs,
530
+ settingsPath: ["providers", route],
531
+ declared: true
532
+ }));
533
+ return [
534
+ ...piEntries,
535
+ ...deepseekEntries,
536
+ ...draftEntries
537
+ ];
538
+ }
539
+ /**
540
+ * 提交目录条目;整批被原子拒绝时逐个剔除冲突条目重试(每轮至多剔一个,
541
+ * 轮数不超过条目数,必然终止)。非冲突错误原样上抛。
542
+ */
543
+ function commitDirectory(register, entries, warn) {
544
+ let rest = entries;
545
+ for (let attempts = 0; attempts <= entries.length; attempts++) try {
546
+ register(rest);
547
+ return;
548
+ } catch (error) {
549
+ const match = /configurable provider "([^"]+)" is already declared/.exec(error instanceof Error ? error.message : String(error));
550
+ const next = match === null ? [] : rest.filter((entry) => entry.provider !== match[1]);
551
+ if (match === null || next.length === rest.length) throw error;
552
+ warn(`llm-pi: 目录条目 "${match[1]}" 与既有声明冲突(通常为官方内置 provider 目录),已跳过该条目`);
553
+ rest = next;
554
+ }
555
+ }
556
+ //#endregion
500
557
  //#region src/catalog/official.ts
501
558
  /** 按 kit 备忘(kit 实例在进程内唯一,目录随 dsh 树版本固定)。 */
502
559
  const cache = /* @__PURE__ */ new WeakMap();
@@ -1154,14 +1211,24 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
1154
1211
  };
1155
1212
  }
1156
1213
  /**
1214
+ * 草稿路由:adapter pi、无 models 且无 provider 级 extends——占位待补
1215
+ * (用户先建路由、后填模型的工作流)。草稿不注册进 adapter(无模型可服务),
1216
+ * 但仍出现在可配置 provider 目录里(Models 页/配置卡片可见可编辑)。
1217
+ */
1218
+ function isDraftRoute(profile) {
1219
+ return (profile.adapter ?? "pi") === "pi" && (profile.models === void 0 || profile.models.length === 0) && profile.extends === void 0;
1220
+ }
1221
+ /**
1157
1222
  * 校验并物化全部 route。任一 route 不可服务即整体抛错——
1158
1223
  * settings 写入校验与运行期 profiles 回调共用本函数,
1159
1224
  * 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
1225
+ * 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
1160
1226
  */
1161
1227
  function buildProfiles(providers, deps) {
1162
1228
  const resolved = /* @__PURE__ */ new Map();
1163
1229
  for (const [route, profile] of Object.entries(providers ?? {})) {
1164
1230
  if ((profile.adapter ?? "pi") === "deepseek") continue;
1231
+ if (isDraftRoute(profile)) continue;
1165
1232
  if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
1166
1233
  if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
1167
1234
  if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
@@ -1541,29 +1608,25 @@ async function startRuntime(ctx, rawConfig) {
1541
1608
  };
1542
1609
  let directory;
1543
1610
  let directoryFacts;
1544
- const ensureDirectory = () => {
1545
- const piEntries = [...profiles().entries()].map(([provider, profile]) => ({
1546
- provider,
1547
- displayName: profile.displayName,
1548
- settingsNs: SETTINGS_NS,
1549
- settingsPath: ["providers", provider],
1550
- declared: !hasBuiltinProvider(kit, provider)
1551
- }));
1552
- const deepseekEntries = [...deepseekRoutes().values()].map((built) => ({
1553
- provider: built.route,
1554
- displayName: built.displayName,
1555
- settingsNs: SETTINGS_NS,
1556
- settingsPath: ["providers", built.route],
1557
- declared: true
1611
+ /** 草稿路由(无模型占位)从原始配置直读——它们不进 adapter profiles。 */
1612
+ const draftRoutes = () => {
1613
+ const providers = current().providers ?? {};
1614
+ return Object.entries(providers).filter(([, profile]) => isDraftRoute(profile)).map(([route, profile]) => ({
1615
+ route,
1616
+ displayName: profile.displayName ?? route
1558
1617
  }));
1559
- const entries = [...piEntries, ...deepseekEntries];
1618
+ };
1619
+ const ensureDirectory = () => {
1620
+ const entries = buildDirectoryEntries(kit, SETTINGS_NS, profiles(), deepseekRoutes(), draftRoutes());
1560
1621
  if (deepEqualJson(entries, directoryFacts)) return;
1561
1622
  if (entries.length === 0) {
1562
1623
  directoryFacts = entries;
1563
1624
  return;
1564
1625
  }
1565
- if (directory === void 0) directory = ctx.llm.registerConfigurableProviders(entries);
1566
- else directory.replace(entries);
1626
+ commitDirectory((batch) => {
1627
+ if (directory === void 0) directory = ctx.llm.registerConfigurableProviders(batch);
1628
+ else directory.replace(batch);
1629
+ }, entries, (message) => logger.warn(message));
1567
1630
  directoryFacts = entries;
1568
1631
  };
1569
1632
  const deepseekRegistrar = new DeepseekRouteRegistrar({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -285,7 +285,7 @@ function providerToWire(provider: ProviderDraft): WireProvider {
285
285
  compat: provider.compat,
286
286
  defaultContextWindow: toNum(provider.defaultContextWindow),
287
287
  defaultMaxTokens: toNum(provider.defaultMaxTokens),
288
- input: inputToWire(provider.input),
288
+ defaultInput: inputToWire(provider.input),
289
289
  reasoning: provider.reasoning,
290
290
  thinkingBudgets: budgetToWire(provider.thinkingBudgets),
291
291
  cacheRetention: provider.cacheRetention,
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 可配置 provider 目录注册:条目组装与冲突降级。
3
+ *
4
+ * 官方 `registerConfigurableProviders`/`replace` 的拒绝语义是原子的——任一条目
5
+ * 与既有声明冲突(典型:route 名撞官方内置 provider 目录条目,如 anthropic)则
6
+ * 整批不落盘。此处按 registerAdapter 同款降级模式:逐个剔除冲突条目重试,
7
+ * 让其余 route 的目录条目照常生效。
8
+ * @module llm-pi/directory
9
+ */
10
+ import { hasBuiltinProvider } from './catalog/builtin.ts'
11
+ import type { ResolvedDeepseekRoute } from './profiles-deepseek.ts'
12
+ import type { DshKit } from './resolve-dsh.ts'
13
+
14
+ /** 一条可配置 provider 目录条目(registerConfigurableProviders 的入参形状)。 */
15
+ export interface DirectoryEntry {
16
+ provider: string
17
+ displayName: string
18
+ settingsNs: string
19
+ settingsPath: string[]
20
+ declared: boolean
21
+ }
22
+
23
+ /** 组装当前全部 route 的目录条目(pi + deepseek 两族)。 */
24
+ export function buildDirectoryEntries(
25
+ kit: DshKit,
26
+ settingsNs: string,
27
+ piProfiles: Map<string, { displayName: string }>,
28
+ deepseekRoutes: Map<string, ResolvedDeepseekRoute>,
29
+ drafts: ReadonlyArray<{ route: string; displayName: string }>,
30
+ ): DirectoryEntry[] {
31
+ const piEntries = [...piProfiles.entries()].map(([provider, profile]) => ({
32
+ provider,
33
+ displayName: profile.displayName,
34
+ settingsNs,
35
+ settingsPath: ['providers', provider],
36
+ declared: !hasBuiltinProvider(kit, provider),
37
+ }))
38
+ const deepseekEntries = [...deepseekRoutes.values()].map((built) => ({
39
+ provider: built.route,
40
+ displayName: built.displayName,
41
+ settingsNs,
42
+ settingsPath: ['providers', built.route],
43
+ declared: true,
44
+ }))
45
+ // 草稿路由不进 adapter,但要在目录里可见(Models 页/配置卡片入口)
46
+ const draftEntries = drafts.map(({ route, displayName }) => ({
47
+ provider: route,
48
+ displayName,
49
+ settingsNs,
50
+ settingsPath: ['providers', route],
51
+ declared: true,
52
+ }))
53
+ return [...piEntries, ...deepseekEntries, ...draftEntries]
54
+ }
55
+
56
+ /**
57
+ * 提交目录条目;整批被原子拒绝时逐个剔除冲突条目重试(每轮至多剔一个,
58
+ * 轮数不超过条目数,必然终止)。非冲突错误原样上抛。
59
+ */
60
+ export function commitDirectory(
61
+ register: (entries: DirectoryEntry[]) => void,
62
+ entries: DirectoryEntry[],
63
+ warn: (message: string) => void,
64
+ ): void {
65
+ let rest = entries
66
+ for (let attempts = 0; attempts <= entries.length; attempts++) {
67
+ try {
68
+ register(rest)
69
+ return
70
+ } catch (error) {
71
+ const match = /configurable provider "([^"]+)" is already declared/.exec(
72
+ error instanceof Error ? error.message : String(error),
73
+ )
74
+ const next = match === null ? [] : rest.filter((entry) => entry.provider !== match[1])
75
+ if (match === null || next.length === rest.length) throw error
76
+ warn(
77
+ `llm-pi: 目录条目 "${match[1]}" 与既有声明冲突(通常为官方内置 provider 目录),已跳过该条目`,
78
+ )
79
+ rest = next
80
+ }
81
+ }
82
+ }
package/src/profiles.ts CHANGED
@@ -295,10 +295,24 @@ function materializeRouteModels(
295
295
  return { models, configuredMaxTokens }
296
296
  }
297
297
 
298
+ /**
299
+ * 草稿路由:adapter pi、无 models 且无 provider 级 extends——占位待补
300
+ * (用户先建路由、后填模型的工作流)。草稿不注册进 adapter(无模型可服务),
301
+ * 但仍出现在可配置 provider 目录里(Models 页/配置卡片可见可编辑)。
302
+ */
303
+ export function isDraftRoute(profile: ProviderProfileConfig): boolean {
304
+ return (
305
+ (profile.adapter ?? 'pi') === 'pi' &&
306
+ (profile.models === undefined || profile.models.length === 0) &&
307
+ profile.extends === undefined
308
+ )
309
+ }
310
+
298
311
  /**
299
312
  * 校验并物化全部 route。任一 route 不可服务即整体抛错——
300
313
  * settings 写入校验与运行期 profiles 回调共用本函数,
301
314
  * 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
315
+ * 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
302
316
  */
303
317
  export function buildProfiles(
304
318
  providers: Record<string, ProviderProfileConfig> | undefined,
@@ -308,6 +322,7 @@ export function buildProfiles(
308
322
  for (const [route, profile] of Object.entries(providers ?? {})) {
309
323
  // adapter: deepseek 的 route 由 profiles-deepseek.ts 物化(官方 DeepSeekAdapter 链路)
310
324
  if ((profile.adapter ?? 'pi') === 'deepseek') continue
325
+ if (isDraftRoute(profile)) continue // 草稿路由:占位待补,不进 adapter
311
326
  if (route.length === 0) throw new Error('llm-pi: provider 名不能为空')
312
327
  // 注意:不做"route 名与内置 provider 名重名"的静态校验——内置名 ≠ 已注册
313
328
  // route(如官方 llm-pi-ai 配置清空后 anthropic 名可用)。真实冲突只在注册期
package/src/service.ts CHANGED
@@ -16,12 +16,12 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
16
16
  import type { ResolvedPiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai'
17
17
  import { deepEqualJson, installSettingsSection } from '@deepseek-ai/dsh-settings'
18
18
 
19
- import { hasBuiltinProvider } from './catalog/builtin.ts'
20
19
  import { ModelsDevSource } from './catalog/models-dev.ts'
21
20
  import { Config, type LlmPiConfig, SETTINGS_NS } from './config.ts'
22
21
  import { DeepseekRouteRegistrar } from './deepseek-routes.ts'
22
+ import { buildDirectoryEntries, commitDirectory, type DirectoryEntry } from './directory.ts'
23
23
  import { discoverModels } from './discovery.ts'
24
- import { assertServiceable, buildProfiles } from './profiles.ts'
24
+ import { assertServiceable, buildProfiles, isDraftRoute } from './profiles.ts'
25
25
  import { buildDeepseekRoutes, type ResolvedDeepseekRoute } from './profiles-deepseek.ts'
26
26
  import { type DshKit, resolveDshKit } from './resolve-dsh.ts'
27
27
 
@@ -212,30 +212,37 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
212
212
 
213
213
  let directory: { replace: (entries: unknown[]) => void } | undefined
214
214
  let directoryFacts: unknown
215
+ /** 草稿路由(无模型占位)从原始配置直读——它们不进 adapter profiles。 */
216
+ const draftRoutes = (): { route: string; displayName: string }[] => {
217
+ const providers = current().providers ?? {}
218
+ return Object.entries(providers)
219
+ .filter(([, profile]) => isDraftRoute(profile))
220
+ .map(([route, profile]) => ({ route, displayName: profile.displayName ?? route }))
221
+ }
215
222
  const ensureDirectory = (): void => {
216
- const piEntries = [...profiles().entries()].map(([provider, profile]) => ({
217
- provider,
218
- displayName: profile.displayName,
219
- settingsNs: SETTINGS_NS,
220
- settingsPath: ['providers', provider],
221
- declared: !hasBuiltinProvider(kit, provider),
222
- }))
223
- const deepseekEntries = [...deepseekRoutes().values()].map((built) => ({
224
- provider: built.route,
225
- displayName: built.displayName,
226
- settingsNs: SETTINGS_NS,
227
- settingsPath: ['providers', built.route],
228
- declared: true,
229
- }))
230
- const entries = [...piEntries, ...deepseekEntries]
223
+ const entries = buildDirectoryEntries(
224
+ kit,
225
+ SETTINGS_NS,
226
+ profiles(),
227
+ deepseekRoutes(),
228
+ draftRoutes(),
229
+ )
230
+ // 备忘键为目标全集(含被冲突跳过的条目):目标不变不重复尝试/告警
231
231
  if (deepEqualJson(entries, directoryFacts)) return
232
232
  if (entries.length === 0) {
233
233
  // 空目录不可注册(INVALID_DIRECTORY);等 settings 用户层供数后在 onChange 注册
234
234
  directoryFacts = entries
235
235
  return
236
236
  }
237
- if (directory === undefined) directory = ctx.llm.registerConfigurableProviders(entries as never)
238
- else directory.replace(entries)
237
+ commitDirectory(
238
+ (batch: DirectoryEntry[]) => {
239
+ if (directory === undefined)
240
+ directory = ctx.llm.registerConfigurableProviders(batch as never)
241
+ else directory.replace(batch)
242
+ },
243
+ entries,
244
+ (message) => logger.warn(message),
245
+ )
239
246
  directoryFacts = entries
240
247
  }
241
248