@dsh-plus/llm-pi 0.1.42 → 0.1.43
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 +1489 -636
- package/lib/index.d.ts +4 -8
- package/lib/index.js +943 -391
- package/package.json +35 -36
- package/src/catalog/browse.ts +297 -0
- package/src/catalog/builtin.ts +183 -35
- package/src/client/api.ts +96 -33
- package/src/client/card.tsx +180 -120
- package/src/client/client.ts +2 -2
- package/src/client/constants.ts +25 -5
- package/src/client/draft.ts +34 -13
- package/src/client/fields.tsx +18 -3
- package/src/client/i18n.ts +153 -49
- package/src/client/root-fields.tsx +64 -52
- package/src/client/route-fit.ts +117 -0
- package/src/client/styles.ts +40 -10
- package/src/client/views/catalog-browser.tsx +376 -0
- package/src/client/views/catalog-row.tsx +147 -0
- package/src/client/views/compat.tsx +31 -3
- package/src/client/views/models.tsx +189 -193
- package/src/client/views/provider-fields.tsx +2 -2
- package/src/client/views/providers.tsx +51 -11
- package/src/compat.ts +43 -19
- package/src/config-api.ts +65 -37
- package/src/config.ts +20 -28
- package/src/inherit.ts +15 -31
- package/src/kit-meta.ts +156 -0
- package/src/{compat-gates.ts → official-surface.ts} +60 -16
- package/src/profiles.ts +52 -13
- package/src/resolve-dsh.ts +291 -72
- package/src/service.ts +209 -90
- package/src/catalog/models-dev.ts +0 -261
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/llm-pi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、
|
|
3
|
+
"version": "0.1.43",
|
|
4
|
+
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、pi-ai 内置目录;协议/compat/字段集现场推导,自动跟随已装 dsh)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -31,44 +31,43 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"@dsh-plus/shared": "0.1.32"
|
|
34
|
+
"@dsh-plus/shared": "0.1.33"
|
|
36
35
|
},
|
|
37
36
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
39
|
-
"@deepseek-ai/dsh-anonymous-user-id": "^0.2.
|
|
40
|
-
"@deepseek-ai/dsh-credentials": "^0.2.
|
|
41
|
-
"@deepseek-ai/dsh-home-paths": "^0.2.
|
|
42
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.
|
|
43
|
-
"@deepseek-ai/dsh-launch-environment": "^0.2.
|
|
44
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
45
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.2.
|
|
46
|
-
"@deepseek-ai/dsh-llm-pi-ai": "^0.2.
|
|
47
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
48
|
-
"@deepseek-ai/dsh-util-values": "^0.2.
|
|
49
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
50
|
-
"@earendil-works/pi-ai": "
|
|
37
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.1",
|
|
38
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.2.1-alpha.1",
|
|
39
|
+
"@deepseek-ai/dsh-credentials": "^0.2.1-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.1",
|
|
41
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-alpha.1",
|
|
42
|
+
"@deepseek-ai/dsh-launch-environment": "^0.2.1-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.2.1-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.2.1-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-util-values": "^0.2.1-alpha.1",
|
|
48
|
+
"@deepseek-ai/schemastery": "^3.18.5-alpha.1",
|
|
49
|
+
"@earendil-works/pi-ai": ">=0.85.1"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "4.0.
|
|
54
|
-
"@deepseek-ai/dsh-anonymous-user-id": "0.2.
|
|
55
|
-
"@deepseek-ai/dsh-app-boot": "0.2.
|
|
56
|
-
"@deepseek-ai/dsh-atomic-write": "0.2.
|
|
57
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
58
|
-
"@deepseek-ai/dsh-credentials": "0.2.
|
|
59
|
-
"@deepseek-ai/dsh-home-paths": "0.2.
|
|
60
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.
|
|
61
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.
|
|
62
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
63
|
-
"@deepseek-ai/dsh-llm-deepseek": "0.2.
|
|
64
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.2.
|
|
65
|
-
"@deepseek-ai/dsh-settings": "0.2.
|
|
66
|
-
"@deepseek-ai/dsh-timeout": "0.2.
|
|
67
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
68
|
-
"@deepseek-ai/dsh-util-crypto": "0.2.
|
|
69
|
-
"@deepseek-ai/dsh-util-values": "0.2.
|
|
70
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
71
|
-
"@earendil-works/pi-ai": "0.
|
|
52
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.2.1-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-app-boot": "0.2.1-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.1-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.2.1-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-settings": "0.2.1-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "0.2.1-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
|
|
67
|
+
"@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.1",
|
|
68
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.1",
|
|
69
|
+
"@deepseek-ai/schemastery": "3.18.5-alpha.1",
|
|
70
|
+
"@earendil-works/pi-ai": "0.87.1",
|
|
72
71
|
"@types/react": "~18.3.31",
|
|
73
72
|
"react": "^18.2.0"
|
|
74
73
|
},
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ai 内置目录的浏览/搜索后端(纯函数,node 侧单测直测)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么在服务端搜:浏览器半拿不到 pi-ai(只能读本插件的 HTTP 端点),且
|
|
5
|
+
* 匹配/排序逻辑放服务端才能进 `node --test`(本仓无 DOM 测试设施)。
|
|
6
|
+
* 目录规模(40+ provider / 1500 模型)用按 kit 备忘的全量快照 + 内存过滤完全够用,
|
|
7
|
+
* 每次搜索只把当前页(默认 40 条)序列化给浏览器。
|
|
8
|
+
*
|
|
9
|
+
* 字段不钉死:模型事实由 `builtinModelFacts` 现场读取(见 catalog/builtin.ts),
|
|
10
|
+
* 官方 pi-ai 新增参数字段会自动出现在 `rest` 里,界面按原样展示。
|
|
11
|
+
* @module llm-pi/catalog/browse
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { DshKit } from '../resolve-dsh.ts'
|
|
15
|
+
import {
|
|
16
|
+
type BuiltinModelFacts,
|
|
17
|
+
type BuiltinProviderEntry,
|
|
18
|
+
builtinModelFacts,
|
|
19
|
+
builtinProviderEntries,
|
|
20
|
+
} from './builtin.ts'
|
|
21
|
+
|
|
22
|
+
/** 浏览器可消费的模型条目(纯 JSON;函数面一律不进)。 */
|
|
23
|
+
export interface BuiltinModelInfo {
|
|
24
|
+
provider: string
|
|
25
|
+
/** 发送给 provider 的请求 id。 */
|
|
26
|
+
id: string
|
|
27
|
+
/** `provider/id` 路径 id(extends 引用与复制用)。 */
|
|
28
|
+
path: string
|
|
29
|
+
/** 显示名(pi-ai 目录给的 name)。 */
|
|
30
|
+
name: string
|
|
31
|
+
/** 请求协议。 */
|
|
32
|
+
api: string
|
|
33
|
+
baseUrl: string
|
|
34
|
+
contextWindow: number
|
|
35
|
+
maxTokens: number
|
|
36
|
+
input: ('text' | 'image')[]
|
|
37
|
+
reasoning: boolean
|
|
38
|
+
thinkingLevelMap?: Record<string, string | null>
|
|
39
|
+
compat?: Record<string, unknown>
|
|
40
|
+
/** 目录给出但未单列的其余字段(cost / inputLimits / headers 等),原样展示。 */
|
|
41
|
+
rest: Record<string, unknown>
|
|
42
|
+
/** 当前生效协议集合能否服务该模型(false = 不能加进任何 pi route)。 */
|
|
43
|
+
servable: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 浏览筛选条件(全部可选;缺省 = 不加该维度过滤)。 */
|
|
47
|
+
export interface BrowseQuery {
|
|
48
|
+
/** 模糊搜索串(空白分隔多词,全部词都要命中)。 */
|
|
49
|
+
q?: string
|
|
50
|
+
/** 供应商(pi-ai provider id)精确匹配。 */
|
|
51
|
+
provider?: string
|
|
52
|
+
/** 请求协议精确匹配。 */
|
|
53
|
+
api?: string
|
|
54
|
+
/** 只要推理模型。 */
|
|
55
|
+
reasoning?: boolean
|
|
56
|
+
/** 只要支持图片输入的模型。 */
|
|
57
|
+
image?: boolean
|
|
58
|
+
/** 只列本插件可服务的模型;缺省 true。 */
|
|
59
|
+
servableOnly?: boolean
|
|
60
|
+
/** 起始下标(缺省 0)。 */
|
|
61
|
+
offset?: number
|
|
62
|
+
/** 每页条数(缺省 {@link BROWSE_DEFAULT_LIMIT},上限 {@link BROWSE_MAX_LIMIT})。 */
|
|
63
|
+
limit?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BrowseResult {
|
|
67
|
+
/** 命中总数(应用 servableOnly 之后)。 */
|
|
68
|
+
total: number
|
|
69
|
+
/** 仅因协议不可服务而被隐藏的条数(servableOnly 打开时才有意义)。 */
|
|
70
|
+
servableHidden: number
|
|
71
|
+
offset: number
|
|
72
|
+
limit: number
|
|
73
|
+
items: BuiltinModelInfo[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const BROWSE_DEFAULT_LIMIT = 40
|
|
77
|
+
export const BROWSE_MAX_LIMIT = 100
|
|
78
|
+
|
|
79
|
+
/** 全量快照按 kit 备忘(同一 kit 实例的目录在进程内不变)。 */
|
|
80
|
+
const snapshotCache = new WeakMap<DshKit, BuiltinModelInfo[]>()
|
|
81
|
+
|
|
82
|
+
/** 归一化:小写 + 去掉分隔符(`/ - _ . 空格`),用于子序列匹配。 */
|
|
83
|
+
function compact(text: string): string {
|
|
84
|
+
return text.toLowerCase().replace(/[\s/\-_.]+/g, '')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 按分隔符切词(小写),用于词首命中判定。 */
|
|
88
|
+
function words(text: string): string[] {
|
|
89
|
+
return text
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.split(/[\s/\-_.]+/)
|
|
92
|
+
.filter((word) => word.length > 0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 模糊匹配得分:越小越相关;不匹配返回 undefined。
|
|
97
|
+
* 分级:完全相等 0 < 整体前缀 1 < 词首前缀 2 < 子串 3 < 归一化相等 4 <
|
|
98
|
+
* 归一化前缀 5 < 首字母缩写 6+(同一档内以命中跨度加权,越紧凑越小)。
|
|
99
|
+
*
|
|
100
|
+
* 缩写匹配要求首字符落在**词首**(`deepseek-flash` 的 `d`、`flash` 的 `f`):
|
|
101
|
+
* 否则任何长路径都会命中任意短查询(实测 `kimi` 会命中
|
|
102
|
+
* `cloudflare-ai-gateway/workers-ai/@cf/ibm-granite/...`——wokers 里的 k 起头也能凑出
|
|
103
|
+
* 子序列),搜索质量崩坏。跳字过多(超过 needle 长度 4 倍)同样判不命中。
|
|
104
|
+
*/
|
|
105
|
+
export function fuzzyScore(query: string, target: string): number | undefined {
|
|
106
|
+
const needle = query.toLowerCase()
|
|
107
|
+
if (needle.length === 0) return 0
|
|
108
|
+
const haystack = target.toLowerCase()
|
|
109
|
+
if (haystack === needle) return 0
|
|
110
|
+
if (haystack.startsWith(needle)) return 1
|
|
111
|
+
if (words(target).some((word) => word.startsWith(needle))) return 2
|
|
112
|
+
if (haystack.includes(needle)) return 3
|
|
113
|
+
const compactNeedle = compact(query)
|
|
114
|
+
const compactTarget = compact(target)
|
|
115
|
+
if (compactNeedle.length === 0) return 0
|
|
116
|
+
if (compactTarget === compactNeedle) return 4
|
|
117
|
+
if (compactTarget.startsWith(compactNeedle)) return 5
|
|
118
|
+
if (compactNeedle.length === 1) return 6
|
|
119
|
+
// 首字母缩写:needle 首字符必须是某个词的首字符,其余按顺序命中;
|
|
120
|
+
// 命中跨度越紧凑越相关(同一档内以跨度/上限为小数权重,越紧越小)。
|
|
121
|
+
const first = needle[0] as string
|
|
122
|
+
const spanCap = compactNeedle.length * 4 + 4
|
|
123
|
+
let best: number | undefined
|
|
124
|
+
for (const start of wordStarts(target)) {
|
|
125
|
+
if (haystack[start] !== first) continue
|
|
126
|
+
let cursor = 1
|
|
127
|
+
let last = start
|
|
128
|
+
for (let i = start + 1; i < haystack.length && cursor < compactNeedle.length; i += 1) {
|
|
129
|
+
const ch = haystack[i] as string
|
|
130
|
+
if (/[\s/\-_.]/.test(ch)) continue
|
|
131
|
+
if (ch === compactNeedle[cursor]) {
|
|
132
|
+
cursor += 1
|
|
133
|
+
last = i
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (cursor < compactNeedle.length) continue
|
|
137
|
+
const span = last - start + 1
|
|
138
|
+
if (span > spanCap) continue
|
|
139
|
+
const score = 6 + span / spanCap
|
|
140
|
+
if (best === undefined || score < best) best = score
|
|
141
|
+
}
|
|
142
|
+
return best
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 词首下标(按分隔符切分的每个词的首字符位置)。 */
|
|
146
|
+
function wordStarts(text: string): number[] {
|
|
147
|
+
const out: number[] = []
|
|
148
|
+
let atStart = true
|
|
149
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
150
|
+
const ch = text[i] as string
|
|
151
|
+
if (/[\s/\-_.]/.test(ch)) {
|
|
152
|
+
atStart = true
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (atStart) {
|
|
156
|
+
out.push(i)
|
|
157
|
+
atStart = false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 单条记录的多字段得分:取最相关字段;全部字段都不匹配返回 undefined。 */
|
|
164
|
+
function scoreOf(query: string, info: BuiltinModelInfo): number | undefined {
|
|
165
|
+
let best: number | undefined
|
|
166
|
+
for (const field of [info.path, info.id, info.name, info.provider]) {
|
|
167
|
+
const score = fuzzyScore(query, field)
|
|
168
|
+
if (score === undefined) continue
|
|
169
|
+
// path 命中比 id/name 命中更相关(用户复制/extends 用的就是 path)
|
|
170
|
+
const weighted = field === info.path ? score : score + 2
|
|
171
|
+
if (best === undefined || weighted < best) best = weighted
|
|
172
|
+
}
|
|
173
|
+
return best
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 多词 AND:每个词都要有命中,得分取各词之和(词少者更相关由长度自然体现)。 */
|
|
177
|
+
function multiScore(query: string, info: BuiltinModelInfo): number | undefined {
|
|
178
|
+
const tokens = query.split(/\s+/).filter((token) => token.length > 0)
|
|
179
|
+
if (tokens.length === 0) return 0
|
|
180
|
+
let total = 0
|
|
181
|
+
for (const token of tokens) {
|
|
182
|
+
const score = scoreOf(token, info)
|
|
183
|
+
if (score === undefined) return undefined
|
|
184
|
+
total += score
|
|
185
|
+
}
|
|
186
|
+
return total
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toInfo(kit: DshKit, facts: BuiltinModelFacts): BuiltinModelInfo {
|
|
190
|
+
return {
|
|
191
|
+
provider: facts.provider,
|
|
192
|
+
id: facts.id,
|
|
193
|
+
path: facts.path,
|
|
194
|
+
name: facts.name,
|
|
195
|
+
api: facts.api,
|
|
196
|
+
baseUrl: facts.baseUrl,
|
|
197
|
+
contextWindow: facts.contextWindow,
|
|
198
|
+
maxTokens: facts.maxTokens,
|
|
199
|
+
input: facts.input,
|
|
200
|
+
reasoning: facts.reasoning,
|
|
201
|
+
...(facts.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: facts.thinkingLevelMap }),
|
|
202
|
+
...(facts.compat === undefined ? {} : { compat: facts.compat }),
|
|
203
|
+
rest: facts.rest,
|
|
204
|
+
servable: kit.protocols.includes(facts.api),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 全量模型条目(按目录顺序:provider 排序即 pi-ai 目录顺序,模型同)。 */
|
|
209
|
+
export function listModelInfos(kit: DshKit): BuiltinModelInfo[] {
|
|
210
|
+
const cached = snapshotCache.get(kit)
|
|
211
|
+
if (cached !== undefined) return cached
|
|
212
|
+
const out: BuiltinModelInfo[] = []
|
|
213
|
+
for (const provider of kit.getBuiltinProviders()) {
|
|
214
|
+
for (const model of kit.getBuiltinModels(provider)) {
|
|
215
|
+
const facts = builtinModelFacts(provider, model)
|
|
216
|
+
if (facts !== undefined) out.push(toInfo(kit, facts))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
snapshotCache.set(kit, out)
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 供应商条目(含模型数与显示名)。 */
|
|
224
|
+
export function listProviders(kit: DshKit): BuiltinProviderEntry[] {
|
|
225
|
+
return builtinProviderEntries(kit)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 紧凑协议索引 `{provider: {modelId: api}}`:浏览器半据此推断某个 route 当前
|
|
230
|
+
* 实际生效的协议(route 级 `api` 缺省时由模型继承值决定),用于一键添加前的冲突检查。
|
|
231
|
+
*/
|
|
232
|
+
export function buildApiIndex(
|
|
233
|
+
infos: readonly BuiltinModelInfo[],
|
|
234
|
+
): Record<string, Record<string, string>> {
|
|
235
|
+
const out: Record<string, Record<string, string>> = {}
|
|
236
|
+
for (const info of infos) {
|
|
237
|
+
const perProvider = out[info.provider] ?? {}
|
|
238
|
+
perProvider[info.id] = info.api
|
|
239
|
+
out[info.provider] = perProvider
|
|
240
|
+
}
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function matchesFilters(info: BuiltinModelInfo, query: BrowseQuery): boolean {
|
|
245
|
+
if (query.provider !== undefined && query.provider.length > 0 && info.provider !== query.provider)
|
|
246
|
+
return false
|
|
247
|
+
if (query.api !== undefined && query.api.length > 0 && info.api !== query.api) return false
|
|
248
|
+
if (query.reasoning === true && !info.reasoning) return false
|
|
249
|
+
if (query.image === true && !info.input.includes('image')) return false
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clampLimit(value: number | undefined): number {
|
|
254
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return BROWSE_DEFAULT_LIMIT
|
|
255
|
+
return Math.min(Math.floor(value), BROWSE_MAX_LIMIT)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 搜索 + 过滤 + 分页。无搜索串时按目录顺序(provider/id)返回;
|
|
260
|
+
* 有搜索串时按相关度排序,同分按路径字典序稳定。
|
|
261
|
+
*/
|
|
262
|
+
export function browseModels(kit: DshKit, query: BrowseQuery = {}): BrowseResult {
|
|
263
|
+
const all = listModelInfos(kit)
|
|
264
|
+
const servableOnly = query.servableOnly !== false
|
|
265
|
+
const limit = clampLimit(query.limit)
|
|
266
|
+
const offset = Math.max(0, Math.floor(query.offset ?? 0))
|
|
267
|
+
const q = (query.q ?? '').trim()
|
|
268
|
+
|
|
269
|
+
let servableHidden = 0
|
|
270
|
+
const matched: { info: BuiltinModelInfo; score: number }[] = []
|
|
271
|
+
for (const info of all) {
|
|
272
|
+
if (!info.servable) {
|
|
273
|
+
if (servableOnly) servableHidden += 1
|
|
274
|
+
if (servableOnly) continue
|
|
275
|
+
}
|
|
276
|
+
if (!matchesFilters(info, query)) continue
|
|
277
|
+
const score = q.length === 0 ? 0 : multiScore(q, info)
|
|
278
|
+
if (score === undefined) continue
|
|
279
|
+
matched.push({ info, score })
|
|
280
|
+
}
|
|
281
|
+
if (q.length > 0) {
|
|
282
|
+
// 同分优先更短的路径(更具体的命中),再按字典序稳定
|
|
283
|
+
matched.sort((left, right) =>
|
|
284
|
+
left.score === right.score
|
|
285
|
+
? left.info.path.length - right.info.path.length ||
|
|
286
|
+
left.info.path.localeCompare(right.info.path)
|
|
287
|
+
: left.score - right.score,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
total: matched.length,
|
|
292
|
+
servableHidden,
|
|
293
|
+
offset,
|
|
294
|
+
limit,
|
|
295
|
+
items: matched.slice(offset, offset + limit).map((entry) => entry.info),
|
|
296
|
+
}
|
|
297
|
+
}
|
package/src/catalog/builtin.ts
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-ai
|
|
3
|
-
*
|
|
2
|
+
* pi-ai 内置目录适配:extends 继承解析的唯一数据源,也是配置页「内置模型目录」
|
|
3
|
+
* 浏览器的数据来源。
|
|
4
|
+
*
|
|
5
|
+
* 两个「不钉死」:
|
|
6
|
+
* - 目录条目随已装 dsh 的 pi-ai 版本自动更新(本文件只读,不复制目录);
|
|
7
|
+
* - 可继承的模型级键集按已装官方适配器 Config schema 现场推导
|
|
8
|
+
* (`kit.officialModelFields`),官方新增模型级字段即自动透传;
|
|
9
|
+
* 目录里出现而官方 schema 不接受的字段(如 cost/headers/inputLimits)
|
|
10
|
+
* 仍会被摘进 facts.rest 供界面展示,但不参与继承。
|
|
11
|
+
*
|
|
12
|
+
* 运行时读取对未知/异形字段宽容(跨版本容忍):只把能确认形状的值纳入事实。
|
|
4
13
|
* @module llm-pi/catalog/builtin
|
|
5
14
|
*/
|
|
6
15
|
import type { DshKit } from '../resolve-dsh.ts'
|
|
7
16
|
|
|
17
|
+
/** 内置目录的 provider id 字面量联合(pi-ai 生成目录的键集,按 vendored 版本收窄)。 */
|
|
18
|
+
export type BuiltinProviderId = ReturnType<DshKit['getBuiltinProviders']>[number]
|
|
19
|
+
|
|
20
|
+
/** 浏览/状态用的 provider 条目(不含 pi-ai 的函数面,可安全序列化给浏览器半)。 */
|
|
21
|
+
export interface BuiltinProviderEntry {
|
|
22
|
+
id: string
|
|
23
|
+
name: string
|
|
24
|
+
baseUrl?: string
|
|
25
|
+
modelCount: number
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
29
|
+
* 单个内置模型的事实快照(与 pi-ai 类型解耦的运行期读取结果)。
|
|
30
|
+
* `rest` 是目录给出但本插件不认识的其余自有键(展示用,随上游字段增删自动变化)。
|
|
11
31
|
*/
|
|
32
|
+
export interface BuiltinModelFacts {
|
|
33
|
+
provider: string
|
|
34
|
+
id: string
|
|
35
|
+
/** `provider/id` 路径 id(extends 引用与「复制路径 id」用)。 */
|
|
36
|
+
path: string
|
|
37
|
+
name: string
|
|
38
|
+
api: string
|
|
39
|
+
baseUrl: string
|
|
40
|
+
reasoning: boolean
|
|
41
|
+
input: ('text' | 'image')[]
|
|
42
|
+
contextWindow: number
|
|
43
|
+
maxTokens: number
|
|
44
|
+
thinkingLevelMap?: Record<string, string | null>
|
|
45
|
+
compat?: Record<string, unknown>
|
|
46
|
+
rest: Record<string, unknown>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 继承源可提供的模型字段(pi-ai Model 的可继承子集)。 */
|
|
12
50
|
export interface ModelBase {
|
|
13
51
|
name?: string
|
|
14
52
|
api?: string
|
|
@@ -19,17 +57,94 @@ export interface ModelBase {
|
|
|
19
57
|
compat?: Record<string, unknown>
|
|
20
58
|
contextWindow?: number
|
|
21
59
|
maxTokens?: number
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
/** 官方 schema 接受、且目录给出的其余模型级字段(键集现场推导,浅拷贝)。 */
|
|
61
|
+
extra?: Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 已在 facts 显式取出的键:这些不再进 rest。 */
|
|
65
|
+
const FACT_KEYS = new Set([
|
|
66
|
+
'id',
|
|
67
|
+
'provider',
|
|
68
|
+
'name',
|
|
69
|
+
'api',
|
|
70
|
+
'baseUrl',
|
|
71
|
+
'reasoning',
|
|
72
|
+
'input',
|
|
73
|
+
'contextWindow',
|
|
74
|
+
'maxTokens',
|
|
75
|
+
'thinkingLevelMap',
|
|
76
|
+
'compat',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
/** JSON 值浅拷贝(对象/数组一层;条目要经 settings 序列化,不可留上游引用)。 */
|
|
80
|
+
export function cloneJsonValue(value: unknown): unknown {
|
|
81
|
+
if (Array.isArray(value)) return [...value]
|
|
82
|
+
if (typeof value === 'object' && value !== null) return { ...(value as Record<string, unknown>) }
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
87
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
88
|
+
? (value as Record<string, unknown>)
|
|
89
|
+
: undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 读模态数组:只保留 text/image 两种已知值,其余丢弃(防上游改成别的枚举)。 */
|
|
93
|
+
function readInput(value: unknown): ('text' | 'image')[] {
|
|
94
|
+
if (!Array.isArray(value)) return ['text']
|
|
95
|
+
const out = value.filter((m): m is 'text' | 'image' => m === 'text' || m === 'image')
|
|
96
|
+
return out.length > 0 ? out : ['text']
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 读 thinkingLevelMap:值为字符串或 null,其余(含 undefined 键)丢弃。 */
|
|
100
|
+
function readThinkingLevelMap(value: unknown): Record<string, string | null> | undefined {
|
|
101
|
+
const record = asRecord(value)
|
|
102
|
+
if (record === undefined) return undefined
|
|
103
|
+
const out: Record<string, string | null> = {}
|
|
104
|
+
for (const [level, wire] of Object.entries(record)) {
|
|
105
|
+
if (typeof wire === 'string') out[level] = wire
|
|
106
|
+
else if (wire === null) out[level] = null
|
|
27
107
|
}
|
|
28
|
-
|
|
108
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
29
109
|
}
|
|
30
110
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
111
|
+
/**
|
|
112
|
+
* 把运行期模型对象读成事实快照。入参用 unknown:pi-ai Model 的类型随版本变化,
|
|
113
|
+
* 继承/浏览都只依赖此处确认过的字段,未确认的一律进 rest 原样展示。
|
|
114
|
+
*/
|
|
115
|
+
export function builtinModelFacts(provider: string, raw: unknown): BuiltinModelFacts | undefined {
|
|
116
|
+
const model = asRecord(raw)
|
|
117
|
+
if (model === undefined) return undefined
|
|
118
|
+
const id = model['id']
|
|
119
|
+
if (typeof id !== 'string' || id.length === 0) return undefined
|
|
120
|
+
const rest: Record<string, unknown> = {}
|
|
121
|
+
for (const [key, value] of Object.entries(model)) {
|
|
122
|
+
if (FACT_KEYS.has(key) || value === undefined) continue
|
|
123
|
+
rest[key] = cloneJsonValue(value)
|
|
124
|
+
}
|
|
125
|
+
const name = model['name']
|
|
126
|
+
const api = model['api']
|
|
127
|
+
const baseUrl = model['baseUrl']
|
|
128
|
+
const contextWindow = model['contextWindow']
|
|
129
|
+
const maxTokens = model['maxTokens']
|
|
130
|
+
const thinkingLevelMap = readThinkingLevelMap(model['thinkingLevelMap'])
|
|
131
|
+
const compat = asRecord(model['compat'])
|
|
132
|
+
return {
|
|
133
|
+
provider,
|
|
134
|
+
id,
|
|
135
|
+
path: `${provider}/${id}`,
|
|
136
|
+
name: typeof name === 'string' && name.length > 0 ? name : id,
|
|
137
|
+
api: typeof api === 'string' ? api : '',
|
|
138
|
+
baseUrl: typeof baseUrl === 'string' ? baseUrl : '',
|
|
139
|
+
reasoning: model['reasoning'] === true,
|
|
140
|
+
input: readInput(model['input']),
|
|
141
|
+
contextWindow: typeof contextWindow === 'number' ? contextWindow : 0,
|
|
142
|
+
maxTokens: typeof maxTokens === 'number' ? maxTokens : 0,
|
|
143
|
+
...(thinkingLevelMap === undefined ? {} : { thinkingLevelMap }),
|
|
144
|
+
...(compat === undefined ? {} : { compat: { ...compat } }),
|
|
145
|
+
rest,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
33
148
|
|
|
34
149
|
/** 内置 provider 的端点(provider 级 extends 的 baseURL 缺省值)。 */
|
|
35
150
|
export function builtinProviderBaseUrl(kit: DshKit, provider: string): string | undefined {
|
|
@@ -45,40 +160,73 @@ export function hasBuiltinProvider(kit: DshKit, provider: string): provider is B
|
|
|
45
160
|
return ids.includes(provider)
|
|
46
161
|
}
|
|
47
162
|
|
|
48
|
-
/** 内置 provider 的全部模型 id(UI
|
|
163
|
+
/** 内置 provider 的全部模型 id(UI 选择器/浏览用)。 */
|
|
49
164
|
export function builtinModelIds(kit: DshKit, provider: string): string[] {
|
|
50
165
|
if (!hasBuiltinProvider(kit, provider)) return []
|
|
51
166
|
return kit.getBuiltinModels(provider).map((m) => m.id)
|
|
52
167
|
}
|
|
53
168
|
|
|
54
|
-
/**
|
|
55
|
-
export function
|
|
169
|
+
/** 内置目录的全部 provider 条目(含 pi-ai 给的显示名与端点)。 */
|
|
170
|
+
export function builtinProviderEntries(kit: DshKit): BuiltinProviderEntry[] {
|
|
171
|
+
const catalog = kit.builtinProviders()
|
|
172
|
+
const byId = new Map(catalog.map((p) => [p.id as string, p]))
|
|
173
|
+
return kit.getBuiltinProviders().map((id) => {
|
|
174
|
+
const provider = byId.get(id)
|
|
175
|
+
const baseUrl = provider?.baseUrl
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
name: provider?.name ?? id,
|
|
179
|
+
...(typeof baseUrl === 'string' && baseUrl.length > 0 ? { baseUrl } : {}),
|
|
180
|
+
modelCount: kit.getBuiltinModels(id).length,
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 取某 provider 下某模型的事实快照;provider/模型不存在返回 undefined。 */
|
|
186
|
+
export function builtinModelFactsOf(
|
|
56
187
|
kit: DshKit,
|
|
57
188
|
provider: string,
|
|
58
189
|
modelId: string,
|
|
59
|
-
):
|
|
190
|
+
): BuiltinModelFacts | undefined {
|
|
60
191
|
if (!hasBuiltinProvider(kit, provider)) return undefined
|
|
61
192
|
const model = kit.getBuiltinModels(provider).find((m) => m.id === modelId)
|
|
62
|
-
|
|
193
|
+
return model === undefined ? undefined : builtinModelFacts(provider, model)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 事实快照 → 继承 base(`extra` 只包含官方 schema 接受的其余模型级键)。 */
|
|
197
|
+
export function modelBaseOf(kit: DshKit, facts: BuiltinModelFacts): ModelBase {
|
|
198
|
+
const extra: Record<string, unknown> = {}
|
|
199
|
+
for (const field of kit.officialModelFields) {
|
|
200
|
+
if (FACT_KEYS.has(field) || field === 'reasoningEfforts') continue
|
|
201
|
+
const value = facts.rest[field]
|
|
202
|
+
if (value !== undefined) extra[field] = cloneJsonValue(value)
|
|
203
|
+
}
|
|
63
204
|
return {
|
|
64
|
-
name:
|
|
65
|
-
api:
|
|
66
|
-
baseUrl:
|
|
67
|
-
input: [...
|
|
68
|
-
reasoning:
|
|
69
|
-
...(
|
|
70
|
-
? {}
|
|
71
|
-
: { thinkingLevelMap: { ...model.thinkingLevelMap } }),
|
|
72
|
-
...(model.compat === undefined
|
|
205
|
+
name: facts.name,
|
|
206
|
+
api: facts.api,
|
|
207
|
+
baseUrl: facts.baseUrl,
|
|
208
|
+
input: [...facts.input],
|
|
209
|
+
reasoning: facts.reasoning,
|
|
210
|
+
...(facts.thinkingLevelMap === undefined
|
|
73
211
|
? {}
|
|
74
|
-
: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
...(
|
|
212
|
+
: { thinkingLevelMap: { ...facts.thinkingLevelMap } }),
|
|
213
|
+
...(facts.compat === undefined ? {} : { compat: { ...facts.compat } }),
|
|
214
|
+
contextWindow: facts.contextWindow,
|
|
215
|
+
maxTokens: facts.maxTokens,
|
|
216
|
+
...(Object.keys(extra).length === 0 ? {} : { extra }),
|
|
79
217
|
}
|
|
80
218
|
}
|
|
81
219
|
|
|
220
|
+
/** 查单个内置模型为继承 base;未命中返回 undefined。 */
|
|
221
|
+
export function builtinModelBase(
|
|
222
|
+
kit: DshKit,
|
|
223
|
+
provider: string,
|
|
224
|
+
modelId: string,
|
|
225
|
+
): ModelBase | undefined {
|
|
226
|
+
const facts = builtinModelFactsOf(kit, provider, modelId)
|
|
227
|
+
return facts === undefined ? undefined : modelBaseOf(kit, facts)
|
|
228
|
+
}
|
|
229
|
+
|
|
82
230
|
/**
|
|
83
231
|
* provider 级 extends 的全量模型继承:route 不写 models 时,
|
|
84
232
|
* 以继承源 provider 的全部内置模型作为条目(每个条目 base 即其自身)。
|
|
@@ -88,8 +236,8 @@ export function inheritedCatalogEntries(
|
|
|
88
236
|
provider: string,
|
|
89
237
|
): { id: string; base: ModelBase }[] {
|
|
90
238
|
if (!hasBuiltinProvider(kit, provider)) return []
|
|
91
|
-
return kit.getBuiltinModels(provider).map((model) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
239
|
+
return kit.getBuiltinModels(provider).map((model) => {
|
|
240
|
+
const facts = builtinModelFacts(provider, model)
|
|
241
|
+
return { id: model.id, base: facts === undefined ? {} : modelBaseOf(kit, facts) }
|
|
242
|
+
})
|
|
95
243
|
}
|