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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  为 DSH 扩大可用的服务商:**选一个服务商,粘贴 API Key,点启用**——插件把官方 `llm-pi-ai` 路由与凭据一次性写好,立即生效、无需重启、无需手改 `settings.yaml`。
6
6
 
7
- 内置 20 家预设(OpenAI 兼容协议 + Anthropic Messages 协议),另有「自定义服务商」入口可接入任意 OpenAI 兼容端点。
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、百度千帆、腾讯混元、火山方舟(豆包)。内置 DeepSeek 官方与 StepFun 已有官方适配器,本插件不重复提供。
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
- 20 built-in presets (OpenAI-compatible and Anthropic Messages protocols) plus a custom-endpoint form for any OpenAI-compatible gateway.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-provider-hub",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "只填 Key 的服务商预设:为 DSH 一键写入官方 llm-pi-ai 路由与凭据,内置 20 家 OpenAI 兼容 / Anthropic Messages 预设与自定义端点。核心为纯 JS,可被桌面端等其它外壳复用。",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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': '凭据服务里已有 Key;留空则保留现有 Key。',
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': 'A key already exists; leave empty to keep it.',
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
- await credentials.set(profile.apiKeyEnv, key)
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) {