@dsh-plus/llm-pi 0.1.25 → 0.1.27

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
@@ -168,7 +168,10 @@ window.__ModuleLoader__.load({
168
168
  confirm: "Confirm",
169
169
  cancel: "Cancel"
170
170
  };
171
- /** own 覆盖同键合并(返回新对象,入参不可变)。 */
171
+ /**
172
+ * own 覆盖同键合并(返回新对象,入参不可变)。
173
+ * 返回类型保留两侧键集合,调用方据此派生 DictKey。
174
+ */
172
175
  function mergeDict(base, own) {
173
176
  return {
174
177
  ...base,
@@ -516,7 +519,7 @@ ${extra}
516
519
  "retryPolicy",
517
520
  "models"
518
521
  ]);
519
- /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留)。 */
522
+ /** 摘出 wire 对象里卡片未认识的字段(undefined 值不保留;入参只要求是普通对象)。 */
520
523
  function extraOf(wire, known) {
521
524
  const out = {};
522
525
  for (const [key, value] of Object.entries(wire)) if (!known.has(key) && value !== void 0) out[key] = value;
@@ -666,10 +669,11 @@ ${extra}
666
669
  }
667
670
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
668
671
  function toPatch(draft) {
672
+ const refreshHours = toNum(draft.catalogRefreshHours);
669
673
  return {
670
674
  enabled: draft.enabled,
671
675
  catalogUrl: draft.catalogUrl.trim(),
672
- catalogRefreshHours: toNum(draft.catalogRefreshHours),
676
+ catalogRefreshHours: refreshHours,
673
677
  catalogProxy: draft.catalogProxy.trim(),
674
678
  providers: Object.fromEntries(Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]))
675
679
  };
@@ -1029,6 +1033,7 @@ ${extra}
1029
1033
  className: "lpc-grid",
1030
1034
  children: fields.map((field) => {
1031
1035
  const spec = compatFieldSpec(effective, field);
1036
+ if (spec === void 0) return null;
1032
1037
  if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1033
1038
  id: `${props.idPrefix}-${field}`,
1034
1039
  label: field,
package/lib/index.d.ts CHANGED
@@ -4,13 +4,13 @@ import { Context } from "@deepseek-ai/cordis";
4
4
  /** settings 命名空间(字面量即合法命名空间,0.1.2-alpha.2 起编译期校验;webui 配置卡片与插件运行期读取同一份)。 */
5
5
  declare const SETTINGS_NS = "dsh-plus-llm-pi";
6
6
  /** 本插件可为手写 route 提供的协议实现(与官方 PROTOCOLS 表一致)。 */
7
- declare const PROTOCOL_IDS: readonly ["openai-completions", "openai-responses", "anthropic-messages"];
7
+ declare const PROTOCOL_IDS: readonly ['openai-completions', 'openai-responses', 'anthropic-messages'];
8
8
  type ProtocolId = (typeof PROTOCOL_IDS)[number];
9
9
  /** pi-ai 思考档位,升级序。 */
10
- declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
11
- declare const MODALITIES: readonly ["text", "image"];
10
+ declare const THINKING_LEVELS: readonly ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
11
+ declare const MODALITIES: readonly ['text', 'image'];
12
12
  /** 路由适配器种类:pi = PiAiAdapter(默认);deepseek = 官方 DeepSeekAdapter(文件通道)。 */
13
- declare const ADAPTER_KINDS: readonly ["pi", "deepseek"];
13
+ declare const ADAPTER_KINDS: readonly ['pi', 'deepseek'];
14
14
  type AdapterKind = (typeof ADAPTER_KINDS)[number];
15
15
  type ThinkingLevel = (typeof THINKING_LEVELS)[number];
16
16
  type Modality = (typeof MODALITIES)[number];
@@ -35,6 +35,13 @@ interface ProviderProfileConfig {
35
35
  extends?: string;
36
36
  displayName?: string;
37
37
  api?: ProtocolId;
38
+ /**
39
+ * 端点;缺省继承 extends 源 provider 的端点。
40
+ *
41
+ * 不并 `| undefined`:schemastery 的输入契约用 null 表示「显式置空」,省略即未设置;
42
+ * 消费方按 `baseURL ?? 默认值` 处理。并上 undefined 会与 ObjectS 的 `string | null`
43
+ * 冲突(exactOptionalPropertyTypes 下二者不可互赋)。
44
+ */
38
45
  baseURL?: string;
39
46
  apiKeyEnv?: string;
40
47
  headers?: Record<string, string>;
@@ -80,11 +87,16 @@ interface LlmPiConfig {
80
87
  catalogProxy: string;
81
88
  providers: Record<string, ProviderProfileConfig>;
82
89
  }
83
- declare const Config: z<LlmPiConfig>;
90
+ /**
91
+ * schema 的宽松输入面:cordis 行级 config 与 settings 用户层都可能只给部分键
92
+ * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
93
+ */
94
+ type LlmPiConfigInput = Partial<LlmPiConfig>;
95
+ declare const Config: z<LlmPiConfigInput, LlmPiConfig>;
84
96
  //#endregion
85
97
  //#region src/index.d.ts
86
98
  declare const name = "dsh-plus-llm-pi";
87
- declare const inject: readonly ["credentials", "llm"];
99
+ declare const inject: readonly ['credentials', 'llm'];
88
100
  declare function apply(ctx: Context, config: LlmPiConfig): Promise<void>;
89
101
  //#endregion
90
102
  export { Config, type LlmPiConfig, type ModelEntryConfig, type ProviderProfileConfig, SETTINGS_NS, apply, inject, name };
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.27",
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.16"
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
 
@@ -26,15 +26,16 @@ import {
26
26
  type ProviderDraft,
27
27
  toPatch,
28
28
  } from './draft.ts'
29
+ import type { Translate } from './i18n.ts'
29
30
  import { ProvidersSection } from './views/providers.tsx'
30
31
 
31
32
  export interface CardProps {
32
- t(key: string): string
33
+ t: Translate
33
34
  scope: Scope
34
35
  api: NamespaceSettingsApi
35
36
  }
36
37
 
37
- function modelsDevText(status: WireModelsDevStatus | null, t: (key: string) => string): string {
38
+ function modelsDevText(status: WireModelsDevStatus | null, t: Translate): string {
38
39
  if (status === null) return t('modelsDevEmpty')
39
40
  if (status.error !== null) return `${t('modelsDevError')}${status.error}`
40
41
  if (status.fetchedAt === null) return t('modelsDevEmpty')
@@ -18,11 +18,11 @@ import type { Context } from '@deepseek-ai/cordis'
18
18
  import {
19
19
  createNamespaceApi,
20
20
  createSettingsScope,
21
- type SettingsRemoteFace,
21
+ type PluginClientContext,
22
22
  } from '@dsh-plus/shared/client'
23
23
  import { SETTINGS_NS } from '../ns.ts'
24
24
  import { LlmPiCard } from './card.tsx'
25
- import { en, NS, zh } from './i18n.ts'
25
+ import { type DictKey, en, NS, zh } from './i18n.ts'
26
26
  import { injectStyle } from './styles.ts'
27
27
 
28
28
  export const name = 'dsh-plus-llm-pi'
@@ -30,28 +30,8 @@ export const name = 'dsh-plus-llm-pi'
30
30
  /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
31
31
  export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
32
32
 
33
- interface SlotsLike {
34
- inject(key: string, callback: () => unknown): unknown
35
- register(options: Record<string, unknown>, component: unknown): () => void
36
- }
37
-
38
- interface LocaleLike {
39
- register(ns: string, dict: { zh: Record<string, string>; en: Record<string, string> }): () => void
40
- bind(ns: string): (key: string) => string
41
- }
42
-
43
- interface RemoteLike {
44
- settings: SettingsRemoteFace
45
- $on(event: string, listener: (payload?: unknown) => void): () => void
46
- }
47
-
48
- interface ClientContext {
49
- slots: SlotsLike
50
- locale: LocaleLike
51
- get(key: 'remote'): RemoteLike
52
- on(event: string, listener: () => void): () => void
53
- effect(execute: () => () => void, label?: string): unknown
54
- }
33
+ /** 宿主窄面收编在 @dsh-plus/shared/client;TKey 传入本包 DictKey 使键名受检。 */
34
+ type ClientContext = PluginClientContext<DictKey>
55
35
 
56
36
  export function apply(ctx: Context): void {
57
37
  const c = ctx as unknown as ClientContext
@@ -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 {
@@ -7,7 +7,7 @@ import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
7
7
 
8
8
  export const NS = 'dsh-plus-llm-pi'
9
9
 
10
- const ownZh: Record<string, string> = {
10
+ const ownZh = {
11
11
  title: 'LLM 路由(llm-pi)',
12
12
  description: '自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。',
13
13
  enabled: '启用插件',
@@ -86,9 +86,9 @@ const ownZh: Record<string, string> = {
86
86
  catalogFailed: '目录加载失败。',
87
87
  saveOk: '已保存。',
88
88
  saveFailed: '保存失败:',
89
- }
89
+ } as const
90
90
 
91
- const ownEn: Record<string, string> = {
91
+ const ownEn = {
92
92
  title: 'LLM routes (llm-pi)',
93
93
  description: 'Custom LLM routes: protocol, compat, model catalog and models.dev fallback.',
94
94
  enabled: 'Enable plugin',
@@ -169,7 +169,13 @@ const ownEn: Record<string, string> = {
169
169
  catalogFailed: 'Failed to load catalog.',
170
170
  saveOk: 'Saved.',
171
171
  saveFailed: 'Save failed: ',
172
- }
172
+ } as const
173
173
 
174
- export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
175
- export const en: Record<string, string> = mergeDict(commonEn, ownEn)
174
+ export type DictKey = keyof typeof commonZh | keyof typeof ownZh
175
+
176
+ /** 卡片/面板共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
177
+ export type Translate = (key: DictKey) => string
178
+
179
+ // 标注为 Record<DictKey, string>:en 缺任一键即编译期报错(中英强制对齐)。
180
+ export const zh: Record<DictKey, string> = mergeDict(commonZh, ownZh)
181
+ export const en: Record<DictKey, string> = mergeDict(commonEn, ownEn)
@@ -9,6 +9,7 @@ import type { ReactElement } from 'react'
9
9
 
10
10
  import { COMPAT_FALLBACK_API, compatFieldSpec, compatFieldsOf } from '../constants.ts'
11
11
  import { CollapseSection, JsonField, SelectField } from '../fields.tsx'
12
+ import type { Translate } from '../i18n.ts'
12
13
 
13
14
  /** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
14
15
  export function pruneCompatForApi(
@@ -31,7 +32,7 @@ export interface CompatEditorProps {
31
32
  epoch: number
32
33
  disabled?: boolean
33
34
  wide?: boolean
34
- t(key: string): string
35
+ t: Translate
35
36
  onEdit(next: Record<string, unknown>): void
36
37
  }
37
38
 
@@ -56,6 +57,8 @@ export function CompatEditor(props: CompatEditorProps): ReactElement {
56
57
  <div className="lpc-grid">
57
58
  {fields.map((field) => {
58
59
  const spec = compatFieldSpec(effective, field)
60
+ // 键取自同一张表(compatFieldsOf),spec 不可能为 undefined;此处仅收窄类型
61
+ if (spec === undefined) return null
59
62
  if (spec === 'boolean') {
60
63
  return (
61
64
  <SelectField
@@ -11,11 +11,12 @@ import { fetchCatalog, type WireModelsDevStatus } from '../api.ts'
11
11
  import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
12
12
  import { emptyModelDraft, type ModelDraft, type ReasoningDraft } from '../draft.ts'
13
13
  import { TextField } from '../fields.tsx'
14
+ import type { Translate } from '../i18n.ts'
14
15
  import { CompatEditor } from './compat.tsx'
15
16
 
16
17
  type CatalogSource = 'builtin' | 'models-dev'
17
18
 
18
- function catalogNote(status: WireModelsDevStatus | undefined, t: (key: string) => string): string {
19
+ function catalogNote(status: WireModelsDevStatus | undefined, t: Translate): string {
19
20
  if (status === undefined) return ''
20
21
  if (status.error !== null) return `${t('modelsDevError')}${status.error}`
21
22
  return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? '-'}`
@@ -29,7 +30,7 @@ export interface ModelsTableProps {
29
30
  models: ModelDraft[]
30
31
  epoch: number
31
32
  disabled?: boolean
32
- t(key: string): string
33
+ t: Translate
33
34
  onModels(next: ModelDraft[]): void
34
35
  }
35
36
 
@@ -158,7 +159,7 @@ export interface ModelRowProps {
158
159
  listId: string
159
160
  epoch: number
160
161
  disabled?: boolean
161
- t(key: string): string
162
+ t: Translate
162
163
  onPatch(patch: Partial<ModelDraft>): void
163
164
  onRemove(): void
164
165
  }
@@ -268,7 +269,7 @@ export interface ReasoningEditorProps {
268
269
  idPrefix: string
269
270
  value: ReasoningDraft
270
271
  disabled?: boolean
271
- t(key: string): string
272
+ t: Translate
272
273
  onEdit(next: ReasoningDraft): void
273
274
  }
274
275
 
@@ -17,13 +17,14 @@ import {
17
17
  } from '../constants.ts'
18
18
  import type { ProviderDraft } from '../draft.ts'
19
19
  import { SelectField, TextField } from '../fields.tsx'
20
+ import type { Translate } from '../i18n.ts'
20
21
  import { pruneCompatForApi } from './compat.tsx'
21
22
 
22
23
  export interface ProviderScalarFieldsProps {
23
24
  id: string
24
25
  draft: ProviderDraft
25
26
  disabled?: boolean
26
- t(key: string): string
27
+ t: Translate
27
28
  onPatch(patch: Partial<ProviderDraft>): void
28
29
  }
29
30
 
@@ -10,6 +10,7 @@ import { type ReactElement, useState } from 'react'
10
10
 
11
11
  import type { ProviderDraft } from '../draft.ts'
12
12
  import { CollapseSection, JsonField, KeyValueEditor } from '../fields.tsx'
13
+ import type { Translate } from '../i18n.ts'
13
14
  import { CompatEditor } from './compat.tsx'
14
15
  import { ModelsTable } from './models.tsx'
15
16
  import { ProviderScalarFields, ProviderSelectFields } from './provider-fields.tsx'
@@ -18,7 +19,7 @@ export interface ProvidersSectionProps {
18
19
  providers: Record<string, ProviderDraft>
19
20
  epoch: number
20
21
  disabled?: boolean
21
- t(key: string): string
22
+ t: Translate
22
23
  onAddRoute(key: string): void
23
24
  onRemoveRoute(route: string): void
24
25
  onPatchProvider(route: string, patch: Partial<ProviderDraft>): void
@@ -86,7 +87,7 @@ export interface ProviderSectionProps {
86
87
  draft: ProviderDraft
87
88
  epoch: number
88
89
  disabled?: boolean
89
- t(key: string): string
90
+ t: Translate
90
91
  onRemove(): void
91
92
  onPatch(patch: Partial<ProviderDraft>): void
92
93
  }
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]
@@ -80,6 +90,13 @@ export interface ProviderProfileConfig {
80
90
  extends?: string
81
91
  displayName?: string
82
92
  api?: ProtocolId
93
+ /**
94
+ * 端点;缺省继承 extends 源 provider 的端点。
95
+ *
96
+ * 不并 `| undefined`:schemastery 的输入契约用 null 表示「显式置空」,省略即未设置;
97
+ * 消费方按 `baseURL ?? 默认值` 处理。并上 undefined 会与 ObjectS 的 `string | null`
98
+ * 冲突(exactOptionalPropertyTypes 下二者不可互赋)。
99
+ */
83
100
  baseURL?: string
84
101
  apiKeyEnv?: string
85
102
  headers?: Record<string, string>
@@ -127,6 +144,12 @@ export interface LlmPiConfig {
127
144
  providers: Record<string, ProviderProfileConfig>
128
145
  }
129
146
 
147
+ /**
148
+ * schema 的宽松输入面:cordis 行级 config 与 settings 用户层都可能只给部分键
149
+ * (5 个根字段均有 schema 默认值),解析输出仍是完整的 {@link LlmPiConfig}。
150
+ */
151
+ export type LlmPiConfigInput = Partial<LlmPiConfig>
152
+
130
153
  const thinkingBudgets = z.object({
131
154
  minimal: z.number(),
132
155
  low: z.number(),
@@ -302,7 +325,7 @@ const providerProfile = z.object({
302
325
  .description('本 route 的模型目录;缺省且 provider 有 extends 时继承该源全部模型'),
303
326
  })
304
327
 
305
- export const Config: z<LlmPiConfig> = z.object({
328
+ export const Config: z<LlmPiConfigInput, LlmPiConfig> = z.object({
306
329
  enabled: z.boolean().description('总开关(关闭则不注册任何 route)').default(true),
307
330
  catalogUrl: z
308
331
  .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,