@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 +8 -3
- package/lib/index.d.ts +18 -6
- package/lib/index.js +14 -4
- package/package.json +2 -2
- package/src/catalog/builtin.ts +10 -3
- package/src/catalog/official.ts +2 -1
- package/src/client/card.tsx +3 -2
- package/src/client/client.ts +4 -24
- package/src/client/draft.ts +9 -5
- package/src/client/fields.tsx +2 -2
- package/src/client/i18n.ts +12 -6
- package/src/client/views/compat.tsx +4 -1
- package/src/client/views/models.tsx +5 -4
- package/src/client/views/provider-fields.tsx +2 -1
- package/src/client/views/providers.tsx +3 -2
- package/src/config-api.ts +1 -0
- package/src/config.ts +26 -3
- package/src/discovery.ts +6 -4
- package/src/profiles.ts +4 -1
- package/src/resolve-dsh.ts +9 -2
- package/src/service.ts +3 -3
package/lib/client.js
CHANGED
|
@@ -168,7 +168,10 @@ window.__ModuleLoader__.load({
|
|
|
168
168
|
confirm: "Confirm",
|
|
169
169
|
cancel: "Cancel"
|
|
170
170
|
};
|
|
171
|
-
/**
|
|
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:
|
|
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 [
|
|
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 [
|
|
11
|
-
declare const MODALITIES: readonly [
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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.
|
|
35
|
+
"@dsh-plus/shared": "0.1.16"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.2",
|
package/src/catalog/builtin.ts
CHANGED
|
@@ -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
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
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 选择器用)。 */
|
package/src/catalog/official.ts
CHANGED
|
@@ -15,7 +15,8 @@ export interface OfficialModelBase {
|
|
|
15
15
|
contextWindow?: number
|
|
16
16
|
maxTokens?: number
|
|
17
17
|
input?: ('text' | 'image')[]
|
|
18
|
-
|
|
18
|
+
/** 官方目录取值为像素数或 'low' 预设(DeepSeekCatalogModel 同域;透传给官方 schema 校验)。 */
|
|
19
|
+
imagePixelBudget?: number | 'low'
|
|
19
20
|
imageMaxBytes?: number
|
|
20
21
|
}
|
|
21
22
|
|
package/src/client/card.tsx
CHANGED
|
@@ -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
|
|
33
|
+
t: Translate
|
|
33
34
|
scope: Scope
|
|
34
35
|
api: NamespaceSettingsApi
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function modelsDevText(status: WireModelsDevStatus | null, t:
|
|
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')
|
package/src/client/client.ts
CHANGED
|
@@ -18,11 +18,11 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
18
18
|
import {
|
|
19
19
|
createNamespaceApi,
|
|
20
20
|
createSettingsScope,
|
|
21
|
-
type
|
|
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
|
-
|
|
34
|
-
|
|
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
|
package/src/client/draft.ts
CHANGED
|
@@ -206,8 +206,8 @@ const KNOWN_PROVIDER_KEYS = new Set([
|
|
|
206
206
|
'models',
|
|
207
207
|
])
|
|
208
208
|
|
|
209
|
-
/** 摘出 wire 对象里卡片未认识的字段(undefined
|
|
210
|
-
function extraOf(wire:
|
|
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
|
|
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
|
|
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
|
-
|
|
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)]),
|
package/src/client/fields.tsx
CHANGED
|
@@ -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:
|
|
23
|
+
children: ReactNode
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function CollapseSection(props: CollapseSectionProps): ReactElement {
|
package/src/client/i18n.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
175
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
56
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/src/resolve-dsh.ts
CHANGED
|
@@ -131,7 +131,12 @@ function assertKitShape(kit: DshKit, origin: string): void {
|
|
|
131
131
|
'prepareCall',
|
|
132
132
|
'providerRetryPolicy',
|
|
133
133
|
] as const) {
|
|
134
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|