@dsh-plus/llm-pi 0.1.25 → 0.1.26

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
@@ -516,7 +516,7 @@ ${extra}
516
516
  "retryPolicy",
517
517
  "models"
518
518
  ]);
519
- /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留)。 */
519
+ /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留;入参只要求是普通对象)。 */
520
520
  function extraOf(wire, known) {
521
521
  const out = {};
522
522
  for (const [key, value] of Object.entries(wire)) if (!known.has(key) && value !== void 0) out[key] = value;
@@ -666,10 +666,11 @@ ${extra}
666
666
  }
667
667
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
668
668
  function toPatch(draft) {
669
+ const refreshHours = toNum(draft.catalogRefreshHours);
669
670
  return {
670
671
  enabled: draft.enabled,
671
672
  catalogUrl: draft.catalogUrl.trim(),
672
- catalogRefreshHours: toNum(draft.catalogRefreshHours),
673
+ catalogRefreshHours: refreshHours,
673
674
  catalogProxy: draft.catalogProxy.trim(),
674
675
  providers: Object.fromEntries(Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]))
675
676
  };
@@ -1029,6 +1030,7 @@ ${extra}
1029
1030
  className: "lpc-grid",
1030
1031
  children: fields.map((field) => {
1031
1032
  const spec = compatFieldSpec(effective, field);
1033
+ if (spec === void 0) return null;
1032
1034
  if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1033
1035
  id: `${props.idPrefix}-${field}`,
1034
1036
  label: field,
package/lib/index.d.ts CHANGED
@@ -80,7 +80,12 @@ interface LlmPiConfig {
80
80
  catalogProxy: string;
81
81
  providers: Record<string, ProviderProfileConfig>;
82
82
  }
83
- declare const Config: z<LlmPiConfig>;
83
+ /**
84
+ * schema 的宽松输入面:cordis 行级 config 与 settings 用户层都可能只给部分键
85
+ * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
86
+ */
87
+ type LlmPiConfigInput = Partial<LlmPiConfig>;
88
+ declare const Config: z<LlmPiConfigInput, LlmPiConfig>;
84
89
  //#endregion
85
90
  //#region src/index.d.ts
86
91
  declare const name = "dsh-plus-llm-pi";
package/lib/index.js CHANGED
@@ -66,7 +66,14 @@ const DEEPSEEK_REASONING_EFFORTS = [
66
66
  const DEEPSEEK_IMAGE_DETAILS = ["auto", "low"];
67
67
  /** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
68
68
  const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
69
- /** 键为可选档位,值为线协议拼写;仅 off 可留空(支持但不发送参数)。 */
69
+ /**
70
+ * 键为可选档位,值为线协议拼写;仅 off 可留空(支持但不发送参数)。
71
+ *
72
+ * 类型收窄说明:schemastery 的 z.dict 把输出推断为"全部键必填"(cosmokit 的
73
+ * Dict 是不带可选修饰的映射类型),而运行期语义是"键可选"——profiles.ts 的
74
+ * validateReasoningEfforts 按缺键 = 未声明处理,schema 也只校验给出的键。
75
+ * 故此处只把该节点的类型标注回真实契约,schema 校验行为不变。
76
+ */
70
77
  const reasoningEfforts = z.dict(z.union([z.string(), z.const(null)]), z.union(THINKING_LEVELS));
71
78
  const thinkingBudgets = z.object({
72
79
  minimal: z.number(),
@@ -151,7 +158,10 @@ const Config = z.object({
151
158
  function builtinProviderBaseUrl(kit, provider) {
152
159
  return kit.builtinProviders().find((p) => p.id === provider)?.baseUrl;
153
160
  }
154
- /** 内置目录是否存在该 provider。 */
161
+ /**
162
+ * 内置目录是否存在该 provider;同时把运行期配置里的 provider 字符串
163
+ * 收窄为目录键字面量(调用方随后可直调 getBuiltinModels)。
164
+ */
155
165
  function hasBuiltinProvider(kit, provider) {
156
166
  return kit.getBuiltinProviders().includes(provider);
157
167
  }
@@ -699,7 +709,7 @@ function deepseekCatalogAnswer(kit, route) {
699
709
  if (models.length === 0) throw new kit.LlmError("该 route 未配置 models 且未 extends deepseek;请手工录入模型", "DISCOVERY_FAILED");
700
710
  return models;
701
711
  }
702
- /** 内置目录直答(route 配了 provider 级 extends 时)。 */
712
+ /** 内置目录直答(route 配了 provider 级 extends 时;入参已由 hasBuiltinProvider 收窄)。 */
703
713
  function catalogAnswer(kit, source) {
704
714
  return builtinModelIds(kit, source).map((id) => {
705
715
  const model = kit.getBuiltinModels(source).find((m) => m.id === id);
@@ -719,7 +729,7 @@ function catalogAnswer(kit, source) {
719
729
  async function discoverModels(request, deps) {
720
730
  const { kit } = deps;
721
731
  const route = request.provider === void 0 ? void 0 : deps.configProviders()[request.provider];
722
- if ((route?.adapter ?? "pi") === "deepseek") return deepseekCatalogAnswer(kit, route);
732
+ if (route !== void 0 && (route.adapter ?? "pi") === "deepseek") return deepseekCatalogAnswer(kit, route);
723
733
  if (route?.extends !== void 0 && hasBuiltinProvider(kit, route.extends)) return catalogAnswer(kit, route.extends);
724
734
  const baseURL = request.baseURL ?? route?.baseURL;
725
735
  if (baseURL === void 0 || baseURL.length === 0) throw new kit.LlmError(`route ${JSON.stringify(request.provider ?? "")} 未配 baseURL 且 extends 源无内置目录;无法探测模型清单`, "DISCOVERY_FAILED");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/llm-pi",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "https-proxy-agent": "^9.1.0",
35
- "@dsh-plus/shared": "0.1.14"
35
+ "@dsh-plus/shared": "0.1.15"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.2",
@@ -28,14 +28,21 @@ export interface ModelBase {
28
28
  headers?: Record<string, string>
29
29
  }
30
30
 
31
+ /** 内置目录的 provider id 字面量联合(pi-ai 生成目录的键集)。 */
32
+ export type BuiltinProviderId = ReturnType<DshKit['getBuiltinProviders']>[number]
33
+
31
34
  /** 内置 provider 的端点(provider 级 extends 的 baseURL 缺省值)。 */
32
35
  export function builtinProviderBaseUrl(kit: DshKit, provider: string): string | undefined {
33
36
  return kit.builtinProviders().find((p) => p.id === provider)?.baseUrl
34
37
  }
35
38
 
36
- /** 内置目录是否存在该 provider。 */
37
- export function hasBuiltinProvider(kit: DshKit, provider: string): boolean {
38
- return kit.getBuiltinProviders().includes(provider)
39
+ /**
40
+ * 内置目录是否存在该 provider;同时把运行期配置里的 provider 字符串
41
+ * 收窄为目录键字面量(调用方随后可直调 getBuiltinModels)。
42
+ */
43
+ export function hasBuiltinProvider(kit: DshKit, provider: string): provider is BuiltinProviderId {
44
+ const ids: readonly string[] = kit.getBuiltinProviders()
45
+ return ids.includes(provider)
39
46
  }
40
47
 
41
48
  /** 内置 provider 的全部模型 id(UI extends 选择器用)。 */
@@ -15,7 +15,8 @@ export interface OfficialModelBase {
15
15
  contextWindow?: number
16
16
  maxTokens?: number
17
17
  input?: ('text' | 'image')[]
18
- imagePixelBudget?: number
18
+ /** 官方目录取值为像素数或 'low' 预设(DeepSeekCatalogModel 同域;透传给官方 schema 校验)。 */
19
+ imagePixelBudget?: number | 'low'
19
20
  imageMaxBytes?: number
20
21
  }
21
22
 
@@ -206,8 +206,8 @@ const KNOWN_PROVIDER_KEYS = new Set([
206
206
  'models',
207
207
  ])
208
208
 
209
- /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留)。 */
210
- function extraOf(wire: Record<string, unknown>, known: Set<string>): Record<string, unknown> {
209
+ /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留;入参只要求是普通对象)。 */
210
+ function extraOf(wire: object, known: Set<string>): Record<string, unknown> {
211
211
  const out: Record<string, unknown> = {}
212
212
  for (const [key, value] of Object.entries(wire)) {
213
213
  if (!known.has(key) && value !== undefined) out[key] = value
@@ -225,7 +225,7 @@ function modelDraftFromWire(model: WireModel): ModelDraft {
225
225
  input: inputFromWire(model.input),
226
226
  reasoningEfforts: reasoningFromWire(model.reasoningEfforts),
227
227
  compat: { ...(model.compat ?? {}) },
228
- extra: extraOf(model as Record<string, unknown>, KNOWN_MODEL_KEYS),
228
+ extra: extraOf(model, KNOWN_MODEL_KEYS),
229
229
  }
230
230
  }
231
231
 
@@ -268,7 +268,7 @@ function providerDraftFromWire(provider: WireProvider): ProviderDraft {
268
268
  maxRequestImageBytes: numToText(provider.maxRequestImageBytes),
269
269
  retryPolicy: provider.retryPolicy,
270
270
  models: (provider.models ?? []).map(modelDraftFromWire),
271
- extra: extraOf(provider as Record<string, unknown>, KNOWN_PROVIDER_KEYS),
271
+ extra: extraOf(provider, KNOWN_PROVIDER_KEYS),
272
272
  }
273
273
  }
274
274
 
@@ -357,10 +357,14 @@ export function draftFromValue(value: ConfigValue): Draft {
357
357
 
358
358
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
359
359
  export function toPatch(draft: Draft): ConfigPatch {
360
+ const refreshHours = toNum(draft.catalogRefreshHours)
360
361
  return {
361
362
  enabled: draft.enabled,
362
363
  catalogUrl: draft.catalogUrl.trim(),
363
- catalogRefreshHours: toNum(draft.catalogRefreshHours),
364
+ // 保存动作在文本非法时不可用(card.tsx 的 invalid 门控),故写入路径必为合法数字;
365
+ // dirty 比较路径可能拿到非法文本的 undefined(JSON.stringify 时同键被丢弃),
366
+ // 此处仅把"保存前已校验"的运行期事实带到类型层,取值不变。
367
+ catalogRefreshHours: refreshHours as unknown as number,
364
368
  catalogProxy: draft.catalogProxy.trim(),
365
369
  providers: Object.fromEntries(
366
370
  Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]),
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { ChevronDownIcon } from '@dsh-plus/shared/client'
9
- import { type ReactElement, useEffect, useState } from 'react'
9
+ import { type ReactElement, type ReactNode, useEffect, useState } from 'react'
10
10
 
11
11
  import type { HeaderPair } from './draft.ts'
12
12
 
@@ -20,7 +20,7 @@ export interface CollapseSectionProps {
20
20
  /** 默认展开态;复杂项传 false(默认折叠)。 */
21
21
  defaultOpen: boolean
22
22
  disabled?: boolean
23
- children: ReactElement | ReactElement[] | null
23
+ children: ReactNode
24
24
  }
25
25
 
26
26
  export function CollapseSection(props: CollapseSectionProps): ReactElement {
@@ -56,6 +56,8 @@ export function CompatEditor(props: CompatEditorProps): ReactElement {
56
56
  <div className="lpc-grid">
57
57
  {fields.map((field) => {
58
58
  const spec = compatFieldSpec(effective, field)
59
+ // 键取自同一张表(compatFieldsOf),spec 不可能为 undefined;此处仅收窄类型
60
+ if (spec === undefined) return null
59
61
  if (spec === 'boolean') {
60
62
  return (
61
63
  <SelectField
package/src/config-api.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { IncomingMessage, ServerResponse } from 'node:http'
9
9
 
10
10
  import type { Context } from '@deepseek-ai/cordis'
11
+ import type {} from '@deepseek-ai/dsh-host-webserver'
11
12
 
12
13
  import { builtinModelIds } from './catalog/builtin.ts'
13
14
  import type { LlmPiRuntime } from './service.ts'
package/src/config.ts CHANGED
@@ -52,8 +52,18 @@ export const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024
52
52
  /** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
53
53
  export const MAX_TIMER_DELAY_MS = 2 ** 31 - 1
54
54
 
55
- /** 键为可选档位,值为线协议拼写;仅 off 可留空(支持但不发送参数)。 */
56
- const reasoningEfforts = z.dict(z.union([z.string(), z.const(null)]), z.union(THINKING_LEVELS))
55
+ /**
56
+ * 键为可选档位,值为线协议拼写;仅 off 可留空(支持但不发送参数)。
57
+ *
58
+ * 类型收窄说明:schemastery 的 z.dict 把输出推断为"全部键必填"(cosmokit 的
59
+ * Dict 是不带可选修饰的映射类型),而运行期语义是"键可选"——profiles.ts 的
60
+ * validateReasoningEfforts 按缺键 = 未声明处理,schema 也只校验给出的键。
61
+ * 故此处只把该节点的类型标注回真实契约,schema 校验行为不变。
62
+ */
63
+ const reasoningEfforts = z.dict(
64
+ z.union([z.string(), z.const(null)]),
65
+ z.union(THINKING_LEVELS),
66
+ ) as unknown as z<ReasoningEfforts>
57
67
 
58
68
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number]
59
69
  export type Modality = (typeof MODALITIES)[number]
@@ -127,6 +137,12 @@ export interface LlmPiConfig {
127
137
  providers: Record<string, ProviderProfileConfig>
128
138
  }
129
139
 
140
+ /**
141
+ * schema 的宽松输入面:cordis 行级 config 与 settings 用户层都可能只给部分键
142
+ * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
143
+ */
144
+ export type LlmPiConfigInput = Partial<LlmPiConfig>
145
+
130
146
  const thinkingBudgets = z.object({
131
147
  minimal: z.number(),
132
148
  low: z.number(),
@@ -302,7 +318,7 @@ const providerProfile = z.object({
302
318
  .description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
303
319
  })
304
320
 
305
- export const Config: z<LlmPiConfig> = z.object({
321
+ export const Config: z<LlmPiConfigInput, LlmPiConfig> = z.object({
306
322
  enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true),
307
323
  catalogUrl: z
308
324
  .string()
package/src/discovery.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * GET {baseURL}/models、4MB 上限、署名头)与官方一致。结果不落盘。
7
7
  * @module llm-pi/discovery
8
8
  */
9
- import { builtinModelIds, hasBuiltinProvider } from './catalog/builtin.ts'
9
+ import { type BuiltinProviderId, builtinModelIds, hasBuiltinProvider } from './catalog/builtin.ts'
10
10
  import { officialModelIds } from './catalog/official.ts'
11
11
  import type { ProviderProfileConfig } from './config.ts'
12
12
  import type { DshKit } from './resolve-dsh.ts'
@@ -129,8 +129,8 @@ function deepseekCatalogAnswer(kit: DshKit, route: ProviderProfileConfig): Disco
129
129
  return models
130
130
  }
131
131
 
132
- /** 内置目录直答(route 配了 provider 级 extends 时)。 */
133
- function catalogAnswer(kit: DshKit, source: string): DiscoveryEntry[] {
132
+ /** 内置目录直答(route 配了 provider 级 extends 时;入参已由 hasBuiltinProvider 收窄)。 */
133
+ function catalogAnswer(kit: DshKit, source: BuiltinProviderId): DiscoveryEntry[] {
134
134
  return builtinModelIds(kit, source).map((id) => {
135
135
  const models = kit.getBuiltinModels(source)
136
136
  const model = models.find((m) => m.id === id)
@@ -155,7 +155,9 @@ export async function discoverModels(
155
155
  const { kit } = deps
156
156
  const route: ProviderProfileConfig | undefined =
157
157
  request.provider === undefined ? undefined : deps.configProviders()[request.provider]
158
- if ((route?.adapter ?? 'pi') === 'deepseek') {
158
+ // route 为空时 (route?.adapter ?? 'pi') 恒为 'pi',故显式判空不改变分支结果,
159
+ // 只用于把 route 收窄为已定义(供 deepseekCatalogAnswer 使用)。
160
+ if (route !== undefined && (route.adapter ?? 'pi') === 'deepseek') {
159
161
  return deepseekCatalogAnswer(kit, route)
160
162
  }
161
163
  if (route?.extends !== undefined && hasBuiltinProvider(kit, route.extends)) {
package/src/profiles.ts CHANGED
@@ -628,7 +628,10 @@ export function resolveProfilesFallback(
628
628
  name: displayName,
629
629
  ...(source.baseURL === undefined ? {} : { baseUrl: source.baseURL }),
630
630
  auth: { apiKey: harnessApiKeyAuth(displayName) },
631
- models,
631
+ // 物化条目的 compat 是开放字典(写时经 validateCompat 按 api 门控校验),
632
+ // 与 pi-ai Model 的逐协议 compat 类型不同构;运行期仅由 pi-ai 按字段读取,
633
+ // 形状与官方 resolveProfiles 产物一致,故此处按目标契约收窄。
634
+ models: models as unknown as Parameters<DshKit['createProvider']>[0]['models'],
632
635
  api: factory() as never,
633
636
  }),
634
637
  } as ResolvedPiAiProviderProfile)
@@ -131,7 +131,12 @@ function assertKitShape(kit: DshKit, origin: string): void {
131
131
  'prepareCall',
132
132
  'providerRetryPolicy',
133
133
  ] as const) {
134
- if (typeof (kit.PiAiAdapter.prototype as Record<string, unknown>)[method] !== 'function') {
134
+ // 原型方法存在性自检:类实例的原型在运行期是普通对象,按方法名索引读取安全
135
+ // (类类型本身无索引签名,故经 unknown 转字典视图;形状由本函数逐项断言)。
136
+ if (
137
+ typeof (kit.PiAiAdapter.prototype as unknown as Record<string, unknown>)[method] !==
138
+ 'function'
139
+ ) {
135
140
  problems.push(`PiAiAdapter.prototype.${method} 缺失`)
136
141
  }
137
142
  }
@@ -164,7 +169,9 @@ function checkDeepseekShape(kit: DeepSeekKit): string[] {
164
169
  const problems: string[] = []
165
170
  if (typeof kit.DeepSeekAdapter !== 'function') problems.push('DeepSeekAdapter 不是类')
166
171
  else if (
167
- typeof (kit.DeepSeekAdapter.prototype as Record<string, unknown>)['stream'] !== 'function'
172
+ // assertKitShape:原型方法存在性自检,经 unknown 转字典视图读取。
173
+ typeof (kit.DeepSeekAdapter.prototype as unknown as Record<string, unknown>)['stream'] !==
174
+ 'function'
168
175
  ) {
169
176
  problems.push('DeepSeekAdapter.prototype.stream 缺失')
170
177
  }
package/src/service.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import type { Context } from '@deepseek-ai/cordis'
13
13
  import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
14
14
  import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
15
+ import type { DirectoryRegistrationHandle } from '@deepseek-ai/dsh-llm'
15
16
  import type { ResolvedPiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai'
16
17
  import type {} from '@deepseek-ai/dsh-settings'
17
18
  import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
@@ -237,7 +238,7 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
237
238
  registeredFacts = facts
238
239
  }
239
240
 
240
- let directory: { replace: (entries: unknown[]) => void } | undefined
241
+ let directory: DirectoryRegistrationHandle | undefined
241
242
  let directoryFacts: unknown
242
243
  /** 草稿路由(无模型占位)从原始配置直读——它们不进 adapter profiles。 */
243
244
  const draftRoutes = (): { route: string; displayName: string }[] => {
@@ -263,8 +264,7 @@ export async function startRuntime(ctx: Context, rawConfig: LlmPiConfig): Promis
263
264
  }
264
265
  commitDirectory(
265
266
  (batch: DirectoryEntry[]) => {
266
- if (directory === undefined)
267
- directory = ctx.llm.registerConfigurableProviders(batch as never)
267
+ if (directory === undefined) directory = ctx.llm.registerConfigurableProviders(batch)
268
268
  else directory.replace(batch)
269
269
  },
270
270
  entries,