@dsh-plus/llm-pi 0.1.8 → 0.1.10

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
@@ -449,20 +449,23 @@ var DeepseekRouteRegistrar = class {
449
449
  for (const [route, built] of target) {
450
450
  const existing = this.registrations.get(route);
451
451
  if (existing !== void 0) {
452
- this.refreshPolicy(route, built, existing);
452
+ this.refreshRegistration(route, built, existing);
453
453
  continue;
454
454
  }
455
455
  this.register(route, built);
456
456
  }
457
457
  }
458
- refreshPolicy(route, built, registration) {
459
- const policy = built.connection.retryPolicy;
460
- if (deepEqualJson(policy, registration.retryPolicy)) return;
458
+ /** retryPolicy displayName 变化时原地 replace(两者均为注册期捕获事实)。 */
459
+ refreshRegistration(route, built, registration) {
460
+ const policyUnchanged = deepEqualJson(built.connection.retryPolicy, registration.retryPolicy);
461
+ const nameUnchanged = built.displayName === registration.displayName;
462
+ if (policyUnchanged && nameUnchanged) return;
461
463
  try {
462
464
  registration.handle.replace([route]);
463
- registration.retryPolicy = policy;
465
+ registration.retryPolicy = built.connection.retryPolicy;
466
+ registration.displayName = built.displayName;
464
467
  } catch (error) {
465
- this.deps.logger.error(`llm-pi: deepseek route "${route}" 重试策略更新被拒,保留此前注册`);
468
+ this.deps.logger.error(`llm-pi: deepseek route "${route}" 注册事实更新被拒,保留此前注册`);
466
469
  this.deps.logger.error(error);
467
470
  }
468
471
  }
@@ -484,11 +487,16 @@ var DeepseekRouteRegistrar = class {
484
487
  resolveUserId: () => this.resolveUserId(),
485
488
  resolveAttachments: () => ctx.get("attachments")
486
489
  });
490
+ adapter.providerInfo = (provider) => ({
491
+ id: provider,
492
+ name: routes().get(route)?.displayName ?? built.displayName
493
+ });
487
494
  try {
488
495
  const handle = ctx.llm.registerAdapter([route], adapter);
489
496
  this.registrations.set(route, {
490
497
  handle,
491
- retryPolicy: built.connection.retryPolicy
498
+ retryPolicy: built.connection.retryPolicy,
499
+ displayName: built.displayName
492
500
  });
493
501
  } catch (error) {
494
502
  logger.error(`llm-pi: deepseek route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`);
@@ -508,7 +516,7 @@ var DeepseekRouteRegistrar = class {
508
516
  * @module llm-pi/directory
509
517
  */
510
518
  /** 组装当前全部 route 的目录条目(pi + deepseek 两族)。 */
511
- function buildDirectoryEntries(kit, settingsNs, piProfiles, deepseekRoutes) {
519
+ function buildDirectoryEntries(kit, settingsNs, piProfiles, deepseekRoutes, drafts) {
512
520
  const piEntries = [...piProfiles.entries()].map(([provider, profile]) => ({
513
521
  provider,
514
522
  displayName: profile.displayName,
@@ -523,7 +531,18 @@ function buildDirectoryEntries(kit, settingsNs, piProfiles, deepseekRoutes) {
523
531
  settingsPath: ["providers", built.route],
524
532
  declared: true
525
533
  }));
526
- return [...piEntries, ...deepseekEntries];
534
+ const draftEntries = drafts.map(({ route, displayName }) => ({
535
+ provider: route,
536
+ displayName,
537
+ settingsNs,
538
+ settingsPath: ["providers", route],
539
+ declared: true
540
+ }));
541
+ return [
542
+ ...piEntries,
543
+ ...deepseekEntries,
544
+ ...draftEntries
545
+ ];
527
546
  }
528
547
  /**
529
548
  * 提交目录条目;整批被原子拒绝时逐个剔除冲突条目重试(每轮至多剔一个,
@@ -1200,14 +1219,24 @@ function materializeRouteModels(route, profile, deps, defaultInput) {
1200
1219
  };
1201
1220
  }
1202
1221
  /**
1222
+ * 草稿路由:adapter pi、无 models 且无 provider 级 extends——占位待补
1223
+ * (用户先建路由、后填模型的工作流)。草稿不注册进 adapter(无模型可服务),
1224
+ * 但仍出现在可配置 provider 目录里(Models 页/配置卡片可见可编辑)。
1225
+ */
1226
+ function isDraftRoute(profile) {
1227
+ return (profile.adapter ?? "pi") === "pi" && (profile.models === void 0 || profile.models.length === 0) && profile.extends === void 0;
1228
+ }
1229
+ /**
1203
1230
  * 校验并物化全部 route。任一 route 不可服务即整体抛错——
1204
1231
  * settings 写入校验与运行期 profiles 回调共用本函数,
1205
1232
  * 保证"写入时被拒"与"运行期不可能拿到坏配置"互为表里。
1233
+ * 草稿路由(isDraftRoute)跳过物化,不参与 adapter 注册。
1206
1234
  */
1207
1235
  function buildProfiles(providers, deps) {
1208
1236
  const resolved = /* @__PURE__ */ new Map();
1209
1237
  for (const [route, profile] of Object.entries(providers ?? {})) {
1210
1238
  if ((profile.adapter ?? "pi") === "deepseek") continue;
1239
+ if (isDraftRoute(profile)) continue;
1211
1240
  if (route.length === 0) throw new Error("llm-pi: provider 名不能为空");
1212
1241
  if (profile.baseURL !== void 0 && profile.baseURL.length === 0) invalid(route, "baseURL 为空");
1213
1242
  if (profile.displayName !== void 0 && profile.displayName.length === 0) invalid(route, "displayName 为空");
@@ -1587,8 +1616,16 @@ async function startRuntime(ctx, rawConfig) {
1587
1616
  };
1588
1617
  let directory;
1589
1618
  let directoryFacts;
1619
+ /** 草稿路由(无模型占位)从原始配置直读——它们不进 adapter profiles。 */
1620
+ const draftRoutes = () => {
1621
+ const providers = current().providers ?? {};
1622
+ return Object.entries(providers).filter(([, profile]) => isDraftRoute(profile)).map(([route, profile]) => ({
1623
+ route,
1624
+ displayName: profile.displayName ?? route
1625
+ }));
1626
+ };
1590
1627
  const ensureDirectory = () => {
1591
- const entries = buildDirectoryEntries(kit, SETTINGS_NS, profiles(), deepseekRoutes());
1628
+ const entries = buildDirectoryEntries(kit, SETTINGS_NS, profiles(), deepseekRoutes(), draftRoutes());
1592
1629
  if (deepEqualJson(entries, directoryFacts)) return;
1593
1630
  if (entries.length === 0) {
1594
1631
  directoryFacts = entries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -25,6 +25,8 @@ interface RouteRegistration {
25
25
  handle: AdapterRegistrationHandle
26
26
  /** 注册期捕获的重试策略(变化才 replace)。 */
27
27
  retryPolicy: unknown
28
+ /** 注册期捕获的展示名(providerInfo 是注册期捕获事实,变化才 replace)。 */
29
+ displayName: string
28
30
  }
29
31
 
30
32
  export interface DeepseekRegistrarDeps {
@@ -80,25 +82,28 @@ export class DeepseekRouteRegistrar {
80
82
  for (const [route, built] of target) {
81
83
  const existing = this.registrations.get(route)
82
84
  if (existing !== undefined) {
83
- this.refreshPolicy(route, built, existing)
85
+ this.refreshRegistration(route, built, existing)
84
86
  continue
85
87
  }
86
88
  this.register(route, built)
87
89
  }
88
90
  }
89
91
 
90
- private refreshPolicy(
92
+ /** retryPolicy 或 displayName 变化时原地 replace(两者均为注册期捕获事实)。 */
93
+ private refreshRegistration(
91
94
  route: string,
92
95
  built: ResolvedDeepseekRoute,
93
96
  registration: RouteRegistration,
94
97
  ): void {
95
- const policy = built.connection.retryPolicy
96
- if (deepEqualJson(policy, registration.retryPolicy)) return
98
+ const policyUnchanged = deepEqualJson(built.connection.retryPolicy, registration.retryPolicy)
99
+ const nameUnchanged = built.displayName === registration.displayName
100
+ if (policyUnchanged && nameUnchanged) return
97
101
  try {
98
102
  registration.handle.replace([route])
99
- registration.retryPolicy = policy
103
+ registration.retryPolicy = built.connection.retryPolicy
104
+ registration.displayName = built.displayName
100
105
  } catch (error) {
101
- this.deps.logger.error(`llm-pi: deepseek route "${route}" 重试策略更新被拒,保留此前注册`)
106
+ this.deps.logger.error(`llm-pi: deepseek route "${route}" 注册事实更新被拒,保留此前注册`)
102
107
  this.deps.logger.error(error)
103
108
  }
104
109
  }
@@ -126,9 +131,20 @@ export class DeepseekRouteRegistrar {
126
131
  resolveUserId: () => this.resolveUserId() as never,
127
132
  resolveAttachments: () => ctx.get('attachments') as never,
128
133
  })
134
+ // 官方 providerInfo 硬编码 name: "DeepSeek"——覆盖为按路由动态读取的
135
+ // displayName,否则自定义 deepseek 路由在模型选择器里与 deepseek-official
136
+ // 撞名(两个 "DeepSeek" 分组,无法区分)。
137
+ adapter.providerInfo = (provider: string) => ({
138
+ id: provider,
139
+ name: routes().get(route)?.displayName ?? built.displayName,
140
+ })
129
141
  try {
130
142
  const handle = ctx.llm.registerAdapter([route], adapter as never)
131
- this.registrations.set(route, { handle, retryPolicy: built.connection.retryPolicy })
143
+ this.registrations.set(route, {
144
+ handle,
145
+ retryPolicy: built.connection.retryPolicy,
146
+ displayName: built.displayName,
147
+ })
132
148
  } catch (error) {
133
149
  logger.error(
134
150
  `llm-pi: deepseek route "${route}" 注册失败(可能与其他 adapter 重名),该 route 不可用`,
package/src/directory.ts CHANGED
@@ -26,6 +26,7 @@ export function buildDirectoryEntries(
26
26
  settingsNs: string,
27
27
  piProfiles: Map<string, { displayName: string }>,
28
28
  deepseekRoutes: Map<string, ResolvedDeepseekRoute>,
29
+ drafts: ReadonlyArray<{ route: string; displayName: string }>,
29
30
  ): DirectoryEntry[] {
30
31
  const piEntries = [...piProfiles.entries()].map(([provider, profile]) => ({
31
32
  provider,
@@ -41,7 +42,15 @@ export function buildDirectoryEntries(
41
42
  settingsPath: ['providers', built.route],
42
43
  declared: true,
43
44
  }))
44
- return [...piEntries, ...deepseekEntries]
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]
45
54
  }
46
55
 
47
56
  /**
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
@@ -21,7 +21,7 @@ import { Config, type LlmPiConfig, SETTINGS_NS } from './config.ts'
21
21
  import { DeepseekRouteRegistrar } from './deepseek-routes.ts'
22
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,8 +212,21 @@ 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 entries = buildDirectoryEntries(kit, SETTINGS_NS, profiles(), deepseekRoutes())
223
+ const entries = buildDirectoryEntries(
224
+ kit,
225
+ SETTINGS_NS,
226
+ profiles(),
227
+ deepseekRoutes(),
228
+ draftRoutes(),
229
+ )
217
230
  // 备忘键为目标全集(含被冲突跳过的条目):目标不变不重复尝试/告警
218
231
  if (deepEqualJson(entries, directoryFacts)) return
219
232
  if (entries.length === 0) {