dsh-provider-hub 0.1.0 → 0.1.1
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/README.md +5 -3
- package/core/providers.js +33 -0
- package/package.json +1 -1
- package/src/client.js +7 -5
- package/src/index.js +38 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
为 DSH 扩大可用的服务商:**选一个服务商,粘贴 API Key,点启用**——插件把官方 `llm-pi-ai` 路由与凭据一次性写好,立即生效、无需重启、无需手改 `settings.yaml`。
|
|
6
6
|
|
|
7
|
-
内置
|
|
7
|
+
内置 23 家预设(OpenAI 兼容协议 + Anthropic Messages 协议),另有「自定义服务商」入口可接入任意 OpenAI 兼容端点;DSH 已内置的服务商默认折叠、不重复展示。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -42,7 +42,9 @@ dsh plugin --profile web add link:/path/to/dsh-provider-hub
|
|
|
42
42
|
|
|
43
43
|
## 预设清单
|
|
44
44
|
|
|
45
|
-
OpenAI、OpenRouter、Anthropic、Google Gemini(OpenAI 兼容)、xAI Grok、Mistral、Groq、Together、Fireworks、DeepInfra、Novita、硅基流动、月之暗面 Kimi(国内/国际)、智谱 GLM(国内)、Z.ai GLM(国际)、MiniMax、阿里云百炼 Qwen
|
|
45
|
+
OpenAI、OpenRouter、Anthropic、Google Gemini(OpenAI 兼容)、xAI Grok、Mistral、Groq、Together、Fireworks、DeepInfra、Novita、硅基流动、月之暗面 Kimi(国内/国际)、智谱 GLM(国内)、Z.ai GLM(国际)、MiniMax、阿里云百炼 Qwen、百度千帆、腾讯混元、火山方舟(豆包)、**StepFun Step Plan(国内/国际)**。
|
|
46
|
+
|
|
47
|
+
关于内置:DSH 自带 DeepSeek 官方适配器(`llm-deepseek`),其目录里的 `stepfun` 指向普通付费端点 `api.stepfun.com/v1`——**Step Plan 订阅端点(`step_plan/v1`)没有内置条目,因此本插件提供该预设**;其余与本插件重合的服务商(openai、openrouter 等 13 个)由 DSH 原生目录覆盖,默认折叠在「显示 DSH 已内置」开关后。已配置过的 Key(包括来自环境变量的)会被识别,留空输入框即可直接启用。
|
|
46
48
|
|
|
47
49
|
## 注意
|
|
48
50
|
|
|
@@ -56,7 +58,7 @@ OpenAI、OpenRouter、Anthropic、Google Gemini(OpenAI 兼容)、xAI Grok、
|
|
|
56
58
|
|
|
57
59
|
Expand the providers DSH can use: **pick a provider, paste the API key, press Enable**. The plugin writes the official `llm-pi-ai` route and its credential in one step - effective immediately, no restart, no hand-editing `settings.yaml`.
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
23 built-in presets (OpenAI-compatible and Anthropic Messages protocols) plus a custom-endpoint form for any OpenAI-compatible gateway. Presets your target app already ships natively are collapsed by default, never deleted.
|
|
60
62
|
|
|
61
63
|
### Install
|
|
62
64
|
|
package/core/providers.js
CHANGED
|
@@ -325,6 +325,39 @@ export const PRESETS = [
|
|
|
325
325
|
{ id: 'doubao-seed-1-6-thinking-250715', name: '豆包 Seed 1.6 Thinking', contextWindow: 262144, maxTokens: 32768, input: textImage },
|
|
326
326
|
],
|
|
327
327
|
},
|
|
328
|
+
{
|
|
329
|
+
// DSH's built-in catalog ships `stepfun` pointed at the pay-as-you-go
|
|
330
|
+
// endpoint (api.stepfun.com/v1); Step Plan subscribers need the step_plan
|
|
331
|
+
// route instead, which no native entry provides.
|
|
332
|
+
id: 'stepfun-step-plan',
|
|
333
|
+
name: 'StepFun Step Plan(国内)',
|
|
334
|
+
docs: 'https://platform.stepfun.com/docs/zh/step-plan/quick-start',
|
|
335
|
+
keyUrl: 'https://platform.stepfun.com/account-overview',
|
|
336
|
+
env: 'STEPFUN_API_KEY',
|
|
337
|
+
baseURL: 'https://api.stepfun.com/step_plan/v1',
|
|
338
|
+
api: 'openai-completions',
|
|
339
|
+
models: [
|
|
340
|
+
{ id: 'step-5-preview', name: 'Step 5 Preview', contextWindow: 1024000, maxTokens: 65536, input: textImage, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
341
|
+
{ id: 'step-3.7-flash', name: 'Step 3.7 Flash', contextWindow: 256000, maxTokens: 256000, input: textImage, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
342
|
+
{ id: 'step-3.5-flash', name: 'Step 3.5 Flash', contextWindow: 256000, maxTokens: 256000, input: text, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
343
|
+
{ id: 'step-3.5-flash-2603', name: 'Step 3.5 Flash 2603', contextWindow: 256000, maxTokens: 256000, input: text, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'stepfun-step-plan-global',
|
|
348
|
+
name: 'StepFun Step Plan(国际)',
|
|
349
|
+
docs: 'https://platform.stepfun.ai/docs/en/step-plan/quick-start',
|
|
350
|
+
keyUrl: 'https://platform.stepfun.ai/account-overview',
|
|
351
|
+
env: 'STEPFUN_GLOBAL_API_KEY',
|
|
352
|
+
baseURL: 'https://api.stepfun.ai/step_plan/v1',
|
|
353
|
+
api: 'openai-completions',
|
|
354
|
+
models: [
|
|
355
|
+
{ id: 'step-5-preview', name: 'Step 5 Preview', contextWindow: 1024000, maxTokens: 65536, input: textImage, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
356
|
+
{ id: 'step-3.7-flash', name: 'Step 3.7 Flash', contextWindow: 256000, maxTokens: 256000, input: textImage, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
357
|
+
{ id: 'step-3.5-flash', name: 'Step 3.5 Flash', contextWindow: 256000, maxTokens: 256000, input: text, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
358
|
+
{ id: 'step-3.5-flash-2603', name: 'Step 3.5 Flash 2603', contextWindow: 256000, maxTokens: 256000, input: text, reasoningEfforts: LOW_MEDIUM_HIGH },
|
|
359
|
+
],
|
|
360
|
+
},
|
|
328
361
|
]
|
|
329
362
|
|
|
330
363
|
/** Look one preset up by id. */
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -51,7 +51,8 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
'status.stored': '已写入',
|
|
52
52
|
'status.noKey': '缺 Key',
|
|
53
53
|
'key.placeholder': '粘贴 {env}',
|
|
54
|
-
'key.savedHint': '
|
|
54
|
+
'key.savedHint': '凭据里已有 Key',
|
|
55
|
+
'key.placeholderKeep': '留空则用已有 Key',
|
|
55
56
|
'action.enable': '启用',
|
|
56
57
|
'action.remove': '移除',
|
|
57
58
|
'remove.confirm': '移除 {route}?已保存的 Key 会保留在凭据服务里。',
|
|
@@ -84,7 +85,8 @@ window.__ModuleLoader__.load({
|
|
|
84
85
|
'status.stored': 'written',
|
|
85
86
|
'status.noKey': 'no key',
|
|
86
87
|
'key.placeholder': 'Paste {env}',
|
|
87
|
-
'key.savedHint': '
|
|
88
|
+
'key.savedHint': 'key already stored',
|
|
89
|
+
'key.placeholderKeep': 'Leave empty for the stored key',
|
|
88
90
|
'action.enable': 'Enable',
|
|
89
91
|
'action.remove': 'Remove',
|
|
90
92
|
'remove.confirm': 'Remove {route}? The stored key stays in the credentials service.',
|
|
@@ -185,7 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
185
187
|
|
|
186
188
|
const enablePreset = async (preset) => {
|
|
187
189
|
const key = String(keys[preset.id] || '').trim()
|
|
188
|
-
if (key === '') {
|
|
190
|
+
if (key === '' && preset.envConfigured !== true) {
|
|
189
191
|
setError(tr('error.keyRequired'))
|
|
190
192
|
return
|
|
191
193
|
}
|
|
@@ -284,7 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
284
286
|
' ',
|
|
285
287
|
h('a', { style: S.link, href: preset.keyUrl, target: '_blank', rel: 'noreferrer' }, `[${tr('getKey')}]`),
|
|
286
288
|
),
|
|
287
|
-
h('span', { style: S.sub }, `${preset.env} · ${tr('models.count', { n: preset.models.length })} · ${preset.models.join(', ')}`),
|
|
289
|
+
h('span', { style: S.sub }, `${preset.env} · ${tr('models.count', { n: preset.models.length })} · ${preset.models.join(', ')}${preset.envConfigured === true ? ' · ' + tr('key.savedHint') : ''}`),
|
|
288
290
|
),
|
|
289
291
|
preset.configured ? statusBadge(preset.id) : null,
|
|
290
292
|
preset.configured
|
|
@@ -293,7 +295,7 @@ window.__ModuleLoader__.load({
|
|
|
293
295
|
style: S.input,
|
|
294
296
|
type: 'password',
|
|
295
297
|
autoComplete: 'off',
|
|
296
|
-
placeholder: tr('key.placeholder', { env: preset.env }),
|
|
298
|
+
placeholder: preset.envConfigured === true ? tr('key.placeholderKeep') : tr('key.placeholder', { env: preset.env }),
|
|
297
299
|
value: keys[preset.id] || '',
|
|
298
300
|
onChange: (event) => setKeys((current) => ({ ...current, [preset.id]: event.target.value })),
|
|
299
301
|
}),
|
package/src/index.js
CHANGED
|
@@ -148,6 +148,24 @@ async function stateOf(ctx) {
|
|
|
148
148
|
const coverage = coverageOf(PRESETS, nativeIds)
|
|
149
149
|
const coveredBy = new Map(coverage.covered.map((entry) => [entry.id, entry.as]))
|
|
150
150
|
|
|
151
|
+
// One credential probe per referenced env; presets whose ref is already
|
|
152
|
+
// configured elsewhere (process env, credentials service, another route)
|
|
153
|
+
// can be enabled without pasting the key again.
|
|
154
|
+
const credentialConfigured = new Map()
|
|
155
|
+
if (credentials) {
|
|
156
|
+
const refs = new Set(PRESETS.map((preset) => preset.env))
|
|
157
|
+
for (const profile of Object.values(providers)) {
|
|
158
|
+
if (typeof profile?.apiKeyEnv === 'string') refs.add(profile.apiKeyEnv)
|
|
159
|
+
}
|
|
160
|
+
for (const ref of refs) {
|
|
161
|
+
try {
|
|
162
|
+
credentialConfigured.set(ref, (await credentials.describe(ref))?.configured === true)
|
|
163
|
+
} catch {
|
|
164
|
+
credentialConfigured.set(ref, false)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
const routes = {}
|
|
152
170
|
for (const [route, profile] of Object.entries(providers)) {
|
|
153
171
|
const env = profile && typeof profile.apiKeyEnv === 'string' ? profile.apiKeyEnv : undefined
|
|
@@ -187,6 +205,7 @@ async function stateOf(ctx) {
|
|
|
187
205
|
models: preset.models.map((model) => model.id),
|
|
188
206
|
configured: Object.hasOwn(providers, preset.id),
|
|
189
207
|
keyConfigured: routes[preset.id]?.keyConfigured === true,
|
|
208
|
+
envConfigured: credentialConfigured.get(preset.env) === true,
|
|
190
209
|
live: live.has(preset.id),
|
|
191
210
|
covered: coveredBy.has(preset.id),
|
|
192
211
|
coveredBy: coveredBy.get(preset.id),
|
|
@@ -200,7 +219,6 @@ async function enable(ctx, body) {
|
|
|
200
219
|
const { settings, credentials } = services(ctx)
|
|
201
220
|
if (!settings || !credentials) throw new HttpError(503, 'service-missing', 'DSH 设置或凭据服务不可用')
|
|
202
221
|
const key = typeof body.key === 'string' ? body.key.trim() : ''
|
|
203
|
-
if (key === '') throw new HttpError(400, 'key-required', '请填写 API Key')
|
|
204
222
|
let route
|
|
205
223
|
let profile
|
|
206
224
|
try {
|
|
@@ -212,7 +230,25 @@ async function enable(ctx, body) {
|
|
|
212
230
|
const descriptor = piAiDescriptor(settings)
|
|
213
231
|
if (!descriptor) throw new HttpError(503, 'namespace-missing', 'llm-pi-ai 设置命名空间未注册')
|
|
214
232
|
|
|
215
|
-
|
|
233
|
+
if (key === '') {
|
|
234
|
+
// An empty key is allowed when the referenced credential already resolves
|
|
235
|
+
// (process env, credentials service, another route): the profile then just
|
|
236
|
+
// references the existing ref.
|
|
237
|
+
const existing = await credentials.describe(profile.apiKeyEnv).catch(() => undefined)
|
|
238
|
+
if (existing?.configured !== true) {
|
|
239
|
+
throw new HttpError(400, 'key-required', `请填写 API Key(${profile.apiKeyEnv} 尚未配置)`)
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
try {
|
|
243
|
+
await credentials.set(profile.apiKeyEnv, key)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new HttpError(
|
|
246
|
+
409,
|
|
247
|
+
'credential-not-writable',
|
|
248
|
+
`无法写入凭据 ${profile.apiKeyEnv}:${String((error && error.message) || error)}。若该 Key 来自环境变量,请留空输入框、直接用已有 Key 启用。`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
216
252
|
try {
|
|
217
253
|
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', route], value: profile }], descriptor.revision)
|
|
218
254
|
} catch (error) {
|