@dsh-plus/llm-pi 0.1.0
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/LICENSE +21 -0
- package/lib/client.js +1904 -0
- package/lib/index.d.ts +90 -0
- package/lib/index.js +1247 -0
- package/package.json +62 -0
- package/src/catalog/builtin.ts +75 -0
- package/src/catalog/models-dev.ts +248 -0
- package/src/client/api.ts +109 -0
- package/src/client/card.tsx +239 -0
- package/src/client/client.ts +63 -0
- package/src/client/constants.ts +97 -0
- package/src/client/draft.ts +293 -0
- package/src/client/fields.tsx +272 -0
- package/src/client/i18n.ts +184 -0
- package/src/client/styles.ts +92 -0
- package/src/client/views/compat.tsx +104 -0
- package/src/client/views/models.tsx +292 -0
- package/src/client/views/provider-fields.tsx +177 -0
- package/src/client/views/providers.tsx +176 -0
- package/src/compat.ts +146 -0
- package/src/config-api.ts +135 -0
- package/src/config.ts +210 -0
- package/src/discovery.ts +176 -0
- package/src/index.ts +33 -0
- package/src/inherit.ts +85 -0
- package/src/profiles.ts +340 -0
- package/src/resolve-dsh.ts +203 -0
- package/src/service.ts +238 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-plus/llm-pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"dsh": {
|
|
24
|
+
"client": {
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
27
|
+
"@deepseek-ai/dsh-client-locale"
|
|
28
|
+
],
|
|
29
|
+
"platform": "web"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
34
|
+
"@deepseek-ai/schemastery": "3.18.1",
|
|
35
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.6",
|
|
36
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
37
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
|
|
38
|
+
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.6",
|
|
39
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6",
|
|
40
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.6",
|
|
41
|
+
"@earendil-works/pi-ai": "0.82.1",
|
|
42
|
+
"https-proxy-agent": "^7.0.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
|
|
46
|
+
"@types/react": "~18.3.1",
|
|
47
|
+
"react": "^18.2.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/A-G-guy/dsh-plugins.git",
|
|
56
|
+
"directory": "packages/llm-pi"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"watch": "tsdown --watch"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-ai 内置目录适配:继承解析的第一级(最高优先级)数据源。
|
|
3
|
+
* 内置条目带 pi 官方校正(compat、thinkingLevelMap、能力过滤),最可信。
|
|
4
|
+
* @module llm-pi/catalog/builtin
|
|
5
|
+
*/
|
|
6
|
+
import type { DshKit } from '../resolve-dsh.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 继承源可提供的模型字段(pi-ai Model 的可继承子集)。
|
|
10
|
+
* 全部可选:models.dev 兜底源只给得出其中一部分。
|
|
11
|
+
*/
|
|
12
|
+
export interface ModelBase {
|
|
13
|
+
name?: string
|
|
14
|
+
api?: string
|
|
15
|
+
baseUrl?: string
|
|
16
|
+
input?: ('text' | 'image')[]
|
|
17
|
+
reasoning?: boolean
|
|
18
|
+
thinkingLevelMap?: Record<string, string | null | undefined>
|
|
19
|
+
compat?: Record<string, unknown>
|
|
20
|
+
contextWindow?: number
|
|
21
|
+
maxTokens?: number
|
|
22
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }
|
|
23
|
+
headers?: Record<string, string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 内置 provider 的端点(provider 级 extends 的 baseURL 缺省值)。 */
|
|
27
|
+
export function builtinProviderBaseUrl(kit: DshKit, provider: string): string | undefined {
|
|
28
|
+
return kit.builtinProviders().find((p) => p.id === provider)?.baseUrl
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 内置目录是否存在该 provider。 */
|
|
32
|
+
export function hasBuiltinProvider(kit: DshKit, provider: string): boolean {
|
|
33
|
+
return kit.getBuiltinProviders().includes(provider)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 内置 provider 的全部模型 id(UI extends 选择器用)。 */
|
|
37
|
+
export function builtinModelIds(kit: DshKit, provider: string): string[] {
|
|
38
|
+
if (!hasBuiltinProvider(kit, provider)) return []
|
|
39
|
+
return kit.getBuiltinModels(provider).map((m) => m.id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 查单个内置模型为继承 base;未命中返回 undefined。 */
|
|
43
|
+
export function builtinModelBase(kit: DshKit, provider: string, modelId: string): ModelBase | undefined {
|
|
44
|
+
if (!hasBuiltinProvider(kit, provider)) return undefined
|
|
45
|
+
const model = kit.getBuiltinModels(provider).find((m) => m.id === modelId)
|
|
46
|
+
if (model === undefined) return undefined
|
|
47
|
+
return {
|
|
48
|
+
name: model.name,
|
|
49
|
+
api: model.api,
|
|
50
|
+
baseUrl: model.baseUrl,
|
|
51
|
+
input: [...model.input],
|
|
52
|
+
reasoning: model.reasoning,
|
|
53
|
+
...(model.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: { ...model.thinkingLevelMap } }),
|
|
54
|
+
...(model.compat === undefined ? {} : { compat: { ...(model.compat as Record<string, unknown>) } }),
|
|
55
|
+
contextWindow: model.contextWindow,
|
|
56
|
+
maxTokens: model.maxTokens,
|
|
57
|
+
cost: { ...model.cost },
|
|
58
|
+
...(model.headers === undefined ? {} : { headers: { ...model.headers } }),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* provider 级 extends 的全量模型继承:route 不写 models 时,
|
|
64
|
+
* 以继承源 provider 的全部内置模型作为条目(每个条目 base 即其自身)。
|
|
65
|
+
*/
|
|
66
|
+
export function inheritedCatalogEntries(
|
|
67
|
+
kit: DshKit,
|
|
68
|
+
provider: string,
|
|
69
|
+
): { id: string; base: ModelBase }[] {
|
|
70
|
+
if (!hasBuiltinProvider(kit, provider)) return []
|
|
71
|
+
return kit.getBuiltinModels(provider).map((model) => ({
|
|
72
|
+
id: model.id,
|
|
73
|
+
base: builtinModelBase(kit, provider, model.id) ?? {},
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models.dev 兜底目录源:继承解析的第二级数据源。
|
|
3
|
+
*
|
|
4
|
+
* 只用于内置目录尚未收录的新模型/新供应商。数据是公开快照
|
|
5
|
+
*(默认 https://models.dev/api.json),**默认不自动拉取**(catalogRefreshHours=0);
|
|
6
|
+
* 拉取方式二选一:
|
|
7
|
+
* - 配置 catalogRefreshHours > 0:启动/过期后后台自动刷新;
|
|
8
|
+
* - 配置卡片「手动拉取」或 POST /catalog/refresh:立即拉取。
|
|
9
|
+
* 拉取可经 catalogProxy 代理(HTTP 代理,如 http://127.0.0.1:7890)。
|
|
10
|
+
* 成功落盘缓存(storages/dsh-plus-llm-pi/models-dev.json);任何失败都退化为
|
|
11
|
+
* "仅内置目录",绝不阻塞 route 注册。
|
|
12
|
+
*
|
|
13
|
+
* 保守原则:models.dev 数据未经 pi 官方校正(无 compat/thinkingLevelMap),
|
|
14
|
+
* 且模态声明不被采信——继承自本源的模型 input 一律走 text-only 兜底,
|
|
15
|
+
* 视觉等模态须用户在条目上显式声明(防 over-claiming 导致会话重复失败请求)。
|
|
16
|
+
* @module llm-pi/catalog/models-dev
|
|
17
|
+
*/
|
|
18
|
+
import { request as httpRequest } from 'node:http'
|
|
19
|
+
import { request as httpsRequest } from 'node:https'
|
|
20
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
|
|
21
|
+
import { dirname } from 'node:path'
|
|
22
|
+
|
|
23
|
+
import HttpsProxyAgentModule from 'https-proxy-agent'
|
|
24
|
+
|
|
25
|
+
import type { ModelBase } from './builtin.ts'
|
|
26
|
+
|
|
27
|
+
/** 仅 https 目标走代理(http 目标直连;代理通常只提供 CONNECT 隧道)。 */
|
|
28
|
+
const { HttpsProxyAgent } = HttpsProxyAgentModule as unknown as {
|
|
29
|
+
HttpsProxyAgent: new (proxy: string) => unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelsDevStatus {
|
|
33
|
+
fetchedAt: string | null
|
|
34
|
+
providers: number
|
|
35
|
+
models: number
|
|
36
|
+
error: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ModelsDevModelEntry {
|
|
40
|
+
id?: string
|
|
41
|
+
name?: string
|
|
42
|
+
reasoning?: boolean
|
|
43
|
+
limit?: { context?: number; output?: number }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ModelsDevProviderEntry {
|
|
47
|
+
id?: string
|
|
48
|
+
name?: string
|
|
49
|
+
models?: Record<string, ModelsDevModelEntry>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ModelsDevDocument = Record<string, ModelsDevProviderEntry>
|
|
53
|
+
|
|
54
|
+
interface CacheFile {
|
|
55
|
+
fetchedAt: string
|
|
56
|
+
data: ModelsDevDocument
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FETCH_TIMEOUT_MS = 20000
|
|
60
|
+
/** 目录文档体上限(api.json 全量约 1-2MB,放宽到 20MB 防未来膨胀)。 */
|
|
61
|
+
const MAX_RESPONSE_BYTES = 20 * 1024 * 1024
|
|
62
|
+
|
|
63
|
+
interface JsonResponse {
|
|
64
|
+
status: number
|
|
65
|
+
body: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 极简 JSON GET(node:http(s) 实现):支持 HTTP 代理(仅 https 目标)与超时。
|
|
70
|
+
* 不跟随重定向(models.dev 直链无重定向;自定义端点需自行保证可直达)。
|
|
71
|
+
*/
|
|
72
|
+
function fetchJson(url: string, proxy: string, timeoutMs: number): Promise<JsonResponse> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const target = new URL(url)
|
|
75
|
+
const request = target.protocol === 'https:' ? httpsRequest : httpRequest
|
|
76
|
+
const agent = target.protocol === 'https:' && proxy.length > 0 ? new HttpsProxyAgent(proxy) : undefined
|
|
77
|
+
const req = request(
|
|
78
|
+
url,
|
|
79
|
+
{ agent, timeout: timeoutMs, headers: { accept: 'application/json' } },
|
|
80
|
+
(response) => {
|
|
81
|
+
const chunks: Buffer[] = []
|
|
82
|
+
let size = 0
|
|
83
|
+
response.on('data', (chunk: Buffer) => {
|
|
84
|
+
size += chunk.length
|
|
85
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
86
|
+
req.destroy(new Error('响应超过 20MB 上限'))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
chunks.push(chunk)
|
|
90
|
+
})
|
|
91
|
+
response.on('end', () => {
|
|
92
|
+
resolve({ status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf8') })
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
req.on('timeout', () => req.destroy(new Error(`请求超时(${timeoutMs}ms)`)))
|
|
97
|
+
req.on('error', reject)
|
|
98
|
+
req.end()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isDocument(value: unknown): value is ModelsDevDocument {
|
|
103
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 单个 models.dev 模型条目 → 继承 base(仅采信名称/容量/推理能力)。 */
|
|
107
|
+
function toModelBase(entry: ModelsDevModelEntry): ModelBase {
|
|
108
|
+
const base: ModelBase = {}
|
|
109
|
+
if (typeof entry.name === 'string' && entry.name.length > 0) base.name = entry.name
|
|
110
|
+
const context = entry.limit?.context
|
|
111
|
+
if (typeof context === 'number' && Number.isInteger(context) && context > 0) base.contextWindow = context
|
|
112
|
+
const output = entry.limit?.output
|
|
113
|
+
if (typeof output === 'number' && Number.isInteger(output) && output > 0) base.maxTokens = output
|
|
114
|
+
base.reasoning = entry.reasoning === true
|
|
115
|
+
return base
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class ModelsDevSource {
|
|
119
|
+
private readonly cacheFile: string
|
|
120
|
+
private url: string
|
|
121
|
+
private ttlHours: number
|
|
122
|
+
private proxy: string
|
|
123
|
+
private readonly log: (message: string) => void
|
|
124
|
+
|
|
125
|
+
private document: ModelsDevDocument | undefined
|
|
126
|
+
private fetchedAt: string | null = null
|
|
127
|
+
private lastError: string | null = null
|
|
128
|
+
private refreshing: Promise<void> | undefined
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
cacheFile: string,
|
|
132
|
+
url: string,
|
|
133
|
+
ttlHours: number,
|
|
134
|
+
log: (message: string) => void,
|
|
135
|
+
proxy = '',
|
|
136
|
+
) {
|
|
137
|
+
this.cacheFile = cacheFile
|
|
138
|
+
this.url = url
|
|
139
|
+
this.ttlHours = ttlHours
|
|
140
|
+
this.log = log
|
|
141
|
+
this.proxy = proxy
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 配置变更时更新端点/TTL/代理并触发刷新(去抖由 refresh 的进行中复用承担)。 */
|
|
145
|
+
reconfigure(url: string, ttlHours: number, proxy: string): void {
|
|
146
|
+
if (url === this.url && ttlHours === this.ttlHours && proxy === this.proxy) return
|
|
147
|
+
this.url = url
|
|
148
|
+
this.ttlHours = ttlHours
|
|
149
|
+
this.proxy = proxy
|
|
150
|
+
if (ttlHours > 0 && this.isStale()) void this.refresh()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 是否已有可用数据(缓存或拉取成功);与自动拉取开关无关。 */
|
|
154
|
+
get enabled(): boolean {
|
|
155
|
+
return this.document !== undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 加载缓存,并在启用自动拉取且缓存过期时后台刷新;构造后调用一次,永不抛错。 */
|
|
159
|
+
async ensureLoaded(): Promise<void> {
|
|
160
|
+
this.loadCache()
|
|
161
|
+
if (this.ttlHours > 0 && this.isStale()) await this.refresh()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 强制刷新(手动拉取/配置变更触发);ttlHours=0 时同样生效(手动拉取不受自动开关限制)。 */
|
|
165
|
+
async refresh(): Promise<void> {
|
|
166
|
+
this.refreshing ??= this.doFetch().finally(() => {
|
|
167
|
+
this.refreshing = undefined
|
|
168
|
+
})
|
|
169
|
+
await this.refreshing
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 查继承 base;未命中/未启用返回 undefined。 */
|
|
173
|
+
lookup(provider: string, modelId: string): ModelBase | undefined {
|
|
174
|
+
const entry = this.document?.[provider]?.models?.[modelId]
|
|
175
|
+
if (entry === undefined) return undefined
|
|
176
|
+
return toModelBase(entry)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 全部 provider id(UI extends 选择器用)。 */
|
|
180
|
+
providerIds(): string[] {
|
|
181
|
+
return Object.keys(this.document ?? {})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 某 provider 的模型 id 列表(UI extends 选择器用)。 */
|
|
185
|
+
modelIds(provider: string): string[] {
|
|
186
|
+
return Object.keys(this.document?.[provider]?.models ?? {})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
status(): ModelsDevStatus {
|
|
190
|
+
const providers = Object.keys(this.document ?? {})
|
|
191
|
+
const models = providers.reduce(
|
|
192
|
+
(total, p) => total + Object.keys(this.document?.[p]?.models ?? {}).length,
|
|
193
|
+
0,
|
|
194
|
+
)
|
|
195
|
+
return {
|
|
196
|
+
fetchedAt: this.fetchedAt,
|
|
197
|
+
providers: providers.length,
|
|
198
|
+
models,
|
|
199
|
+
error: this.lastError,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isStale(): boolean {
|
|
204
|
+
if (this.fetchedAt === null) return true
|
|
205
|
+
const ageMs = Date.now() - Date.parse(this.fetchedAt)
|
|
206
|
+
return !Number.isFinite(ageMs) || ageMs > this.ttlHours * 3600_000
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private loadCache(): void {
|
|
210
|
+
try {
|
|
211
|
+
const raw = JSON.parse(readFileSync(this.cacheFile, 'utf8')) as CacheFile
|
|
212
|
+
if (!isDocument(raw.data) || typeof raw.fetchedAt !== 'string') throw new Error('缓存形状非法')
|
|
213
|
+
this.document = raw.data
|
|
214
|
+
this.fetchedAt = raw.fetchedAt
|
|
215
|
+
} catch {
|
|
216
|
+
// 无缓存或缓存损坏:静默留给刷新补齐
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async doFetch(): Promise<void> {
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetchJson(this.url, this.proxy, FETCH_TIMEOUT_MS)
|
|
223
|
+
if (response.status < 200 || response.status >= 300) {
|
|
224
|
+
throw new Error(`HTTP ${response.status}`)
|
|
225
|
+
}
|
|
226
|
+
const data: unknown = JSON.parse(response.body)
|
|
227
|
+
if (!isDocument(data)) throw new Error('响应不是 models.dev 目录文档')
|
|
228
|
+
this.document = data
|
|
229
|
+
this.fetchedAt = new Date().toISOString()
|
|
230
|
+
this.lastError = null
|
|
231
|
+
this.persistCache(data)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.lastError = error instanceof Error ? error.message : String(error)
|
|
234
|
+
this.log(`models.dev 目录拉取失败(沿用缓存/仅内置目录):${this.lastError}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private persistCache(data: ModelsDevDocument): void {
|
|
239
|
+
try {
|
|
240
|
+
mkdirSync(dirname(this.cacheFile), { recursive: true })
|
|
241
|
+
const tmp = `${this.cacheFile}.tmp`
|
|
242
|
+
writeFileSync(tmp, JSON.stringify({ fetchedAt: this.fetchedAt, data }), 'utf8')
|
|
243
|
+
renameSync(tmp, this.cacheFile)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.log(`models.dev 缓存写入失败:${error instanceof Error ? error.message : String(error)}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片数据通道:同源 fetch 调自建 webServer 路由(notify-email 同款模式;
|
|
3
|
+
* 官方 settings.* RPC 白名单硬编码不含第三方 namespace)。
|
|
4
|
+
* @module llm-pi/client/api
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface WireModelsDevStatus {
|
|
8
|
+
fetchedAt: string | null
|
|
9
|
+
providers: number
|
|
10
|
+
error: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WireProvider {
|
|
14
|
+
extends?: string
|
|
15
|
+
displayName?: string
|
|
16
|
+
api?: string
|
|
17
|
+
baseURL?: string
|
|
18
|
+
apiKeyEnv?: string
|
|
19
|
+
headers?: Record<string, string>
|
|
20
|
+
compat?: Record<string, unknown>
|
|
21
|
+
defaultContextWindow?: number
|
|
22
|
+
defaultMaxTokens?: number
|
|
23
|
+
defaultInput?: string[]
|
|
24
|
+
reasoning?: string
|
|
25
|
+
thinkingBudgets?: { minimal: number; low: number; medium: number; high: number }
|
|
26
|
+
cacheRetention?: string
|
|
27
|
+
transport?: string
|
|
28
|
+
timeoutMs?: number
|
|
29
|
+
websocketConnectTimeoutMs?: number
|
|
30
|
+
streamIdleTimeoutMs?: number
|
|
31
|
+
retryPolicy?: unknown
|
|
32
|
+
models?: WireModel[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WireModel {
|
|
36
|
+
id: string
|
|
37
|
+
extends?: string
|
|
38
|
+
name?: string
|
|
39
|
+
contextWindow?: number
|
|
40
|
+
maxTokens?: number
|
|
41
|
+
input?: string[]
|
|
42
|
+
reasoningEfforts?: false | Record<string, string | null>
|
|
43
|
+
compat?: Record<string, unknown>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** GET /config 返回(config.ts WireConfig)。 */
|
|
47
|
+
export interface WireConfig {
|
|
48
|
+
enabled: boolean
|
|
49
|
+
catalogUrl: string
|
|
50
|
+
catalogRefreshHours: number
|
|
51
|
+
catalogProxy: string
|
|
52
|
+
providers: Record<string, WireProvider>
|
|
53
|
+
writable: boolean
|
|
54
|
+
kitSource: string
|
|
55
|
+
modelsDevStatus: WireModelsDevStatus | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** PUT /config 提交形状(config.ts WirePatchInput;providers 全量替换)。 */
|
|
59
|
+
export interface WirePatchInput {
|
|
60
|
+
enabled?: boolean
|
|
61
|
+
catalogUrl?: string
|
|
62
|
+
catalogRefreshHours?: number
|
|
63
|
+
catalogProxy?: string
|
|
64
|
+
providers?: Record<string, WireProvider>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** GET /catalog?provider=&source= 返回。 */
|
|
68
|
+
export interface CatalogResult {
|
|
69
|
+
providers: string[]
|
|
70
|
+
models: string[]
|
|
71
|
+
status?: WireModelsDevStatus
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ROUTE_CONFIG = '/dsh-plus/llm-pi/config'
|
|
75
|
+
const ROUTE_CATALOG = '/dsh-plus/llm-pi/catalog'
|
|
76
|
+
|
|
77
|
+
async function parse<T>(res: Response): Promise<T> {
|
|
78
|
+
const body = (await res.json()) as T & { error?: string }
|
|
79
|
+
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`)
|
|
80
|
+
return body
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function fetchConfig(): Promise<WireConfig> {
|
|
84
|
+
return parse<WireConfig>(await fetch(ROUTE_CONFIG, { credentials: 'same-origin' }))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function saveConfig(patch: WirePatchInput): Promise<WireConfig> {
|
|
88
|
+
return parse<WireConfig>(
|
|
89
|
+
await fetch(ROUTE_CONFIG, {
|
|
90
|
+
method: 'PUT',
|
|
91
|
+
credentials: 'same-origin',
|
|
92
|
+
headers: { 'content-type': 'application/json' },
|
|
93
|
+
body: JSON.stringify(patch),
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 目录查询:provider 为空时只返回该源的 provider 列表。 */
|
|
99
|
+
export async function fetchCatalog(provider: string, source: 'builtin' | 'models-dev'): Promise<CatalogResult> {
|
|
100
|
+
const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`
|
|
101
|
+
return parse<CatalogResult>(await fetch(url, { credentials: 'same-origin' }))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 手动拉取 models.dev 目录:POST /catalog/refresh → 最新快照状态。 */
|
|
105
|
+
export async function refreshCatalog(): Promise<{ status: WireModelsDevStatus }> {
|
|
106
|
+
return parse<{ status: WireModelsDevStatus }>(
|
|
107
|
+
await fetch(`${ROUTE_CATALOG}/refresh`, { method: 'POST', credentials: 'same-origin' }),
|
|
108
|
+
)
|
|
109
|
+
}
|