dsh-model-params 0.1.1 → 0.1.4
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/lib/client.js +24 -2
- package/lib/index.js +60 -27
- package/lib/modelsdev.js +21 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## English
|
|
4
4
|
|
|
5
|
-
**Current release: 0.1.
|
|
5
|
+
**Current release: 0.1.4** — models.dev parameter assistant inside the official DeepSeek Harness Models settings page.
|
|
6
6
|
|
|
7
7
|
For every configured `llm-pi-ai` provider card (custom OpenAI-compatible gateways such as the ones you add under Settings → Models), a `models.dev 参数` control fetches the official models.dev records for the provider's configured model ids and proposes the metadata pi-ai needs: context window, max output tokens, and reasoning-effort levels. One click writes the merged `models` array back to that provider profile through the official revision-aware Settings API.
|
|
8
8
|
|
|
@@ -10,7 +10,8 @@ Safety boundaries:
|
|
|
10
10
|
|
|
11
11
|
- The control never auto-writes. You open the panel, review the per-model diff, then click **应用并写入该 provider** — it writes only that provider's `models` array in the `llm-pi-ai` namespace.
|
|
12
12
|
- Default policy fills **missing** parameters only; tick **覆盖已有值** to also replace differing ones.
|
|
13
|
-
-
|
|
13
|
+
- At the top of the panel, optionally enable one shared models.dev request proxy and edit its URL. The setting is shared by every provider card, applies to the next lookup, and does not change any LLM provider `baseURL`.
|
|
14
|
+
- Nothing leaves the machine except the models.dev catalog request (Host side, optional shared proxy or `HTTPS_PROXY` fallback), and no credentials are ever sent.
|
|
14
15
|
- Models whose id models.dev does not know are reported as 未收录 and left untouched.
|
|
15
16
|
|
|
16
17
|
models.dev model ids may collide across catalog providers; the matcher prefers the record whose provider id relates to the model id's vendor prefix and otherwise takes the first metadata-bearing record.
|
|
@@ -20,7 +21,8 @@ models.dev model ids may collide across catalog providers; the matcher prefers t
|
|
|
20
21
|
面向 DeepSeek Harness Web 官方 **设置 → 模型** 页的 models.dev 参数助手。每个已配置的 `llm-pi-ai` provider 卡片内提供 `models.dev 参数` 入口:按该 provider 的模型 id 在 models.dev 官方目录中匹配,预览上下文窗口 / max 输出 / 推理档位,确认后一次性写回该 provider 的 `models` 配置(官方 revision-aware Settings API,仅写 `llm-pi-ai` 命名空间下当前 provider 的数组)。
|
|
21
22
|
|
|
22
23
|
- 默认仅补缺失参数;勾选「覆盖已有值」才替换差异值。
|
|
23
|
-
-
|
|
24
|
+
- 窗口顶部可启用一个所有 provider 共用的 models.dev 请求代理并编辑地址;它只影响下一次参数查询,不改变任何 LLM provider 的 `baseURL`。
|
|
25
|
+
- Host 只负责拉取并缓存 models.dev 目录(6 小时,支持公共代理或 `HTTPS_PROXY` 回退),不接触任何 LLM 配置与凭据。
|
|
24
26
|
- models.dev 未收录的模型 id 会明确标记,不写入。
|
|
25
27
|
|
|
26
28
|
## Development
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,9 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
const FETCH_TIMEOUT_MS = 20000
|
|
26
26
|
const CSS_ID = 'dsh-model-params/style'
|
|
27
27
|
const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
28
|
+
const PROXY_DEFAULT = 'http://127.0.0.1:7890'
|
|
29
|
+
const proxyState = { enabled: false, url: PROXY_DEFAULT, listeners: new Set() }
|
|
30
|
+
const notifyProxy = () => proxyState.listeners.forEach((listener) => listener())
|
|
28
31
|
|
|
29
32
|
const CSS = [
|
|
30
33
|
'.dpmRow{display:flex;align-items:center;gap:8px;margin:2px 0}',
|
|
@@ -34,6 +37,9 @@ window.__ModuleLoader__.load({
|
|
|
34
37
|
'.dpmWrap{position:relative;display:inline-flex}',
|
|
35
38
|
'.dpmPanel{position:absolute;z-index:30;left:0;top:calc(100% + 6px);width:min(480px,calc(100vw - 48px));max-height:min(420px,calc(100vh - 120px));overflow:auto;display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-size:12px;line-height:1.5}',
|
|
36
39
|
'.dpmPanel h4{margin:0;font-size:13px}',
|
|
40
|
+
'.dpmProxy{display:flex;flex-direction:column;gap:6px;padding:7px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}',
|
|
41
|
+
'.dpmProxy label{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);cursor:pointer}',
|
|
42
|
+
'.dpmProxy input[type=text]{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:11px}',
|
|
37
43
|
'.dpmHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}',
|
|
38
44
|
'.dpmErr{color:var(--dsw-alias-state-error-primary);margin:0;font-size:11px;word-break:break-all}',
|
|
39
45
|
'.dpmList{display:flex;flex-direction:column;gap:6px}',
|
|
@@ -82,6 +88,12 @@ window.__ModuleLoader__.load({
|
|
|
82
88
|
const [failed, setFailed] = React.useState('')
|
|
83
89
|
const [saved, setSaved] = React.useState('')
|
|
84
90
|
const [overwrite, setOverwrite] = React.useState(false)
|
|
91
|
+
const [, refreshProxy] = React.useState(0)
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
const rerender = () => refreshProxy((value) => value + 1)
|
|
94
|
+
proxyState.listeners.add(rerender)
|
|
95
|
+
return () => proxyState.listeners.delete(rerender)
|
|
96
|
+
}, [])
|
|
85
97
|
const [result, setResult] = React.useState(null) // { matched: [], unmatched: [] }
|
|
86
98
|
const ref = React.useRef(null)
|
|
87
99
|
|
|
@@ -115,7 +127,8 @@ window.__ModuleLoader__.load({
|
|
|
115
127
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
116
128
|
try {
|
|
117
129
|
const ids = models.map((model) => model.id).filter(Boolean)
|
|
118
|
-
const
|
|
130
|
+
const query = '?ids=' + encodeURIComponent(ids.join(',')) + (proxyState.enabled ? '&proxy=' + encodeURIComponent((proxyState.url.trim() || PROXY_DEFAULT)) : '')
|
|
131
|
+
const response = await fetch(API + query, { cache: 'no-store', signal: controller.signal })
|
|
119
132
|
const data = await response.json()
|
|
120
133
|
if (!data || data.ok !== true) throw new Error((data && data.error) || 'lookup failed')
|
|
121
134
|
setResult({ matched: data.matched || [], unmatched: data.unmatched || [] })
|
|
@@ -171,7 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
171
184
|
return found ? (mergeModel(model, found) || model) : model
|
|
172
185
|
})
|
|
173
186
|
const touched = nextModels.some((model, index) => model !== models[index])
|
|
174
|
-
if (!touched) { setSaved('没有可写入的变更(仅补缺失且无缺失)。'); return }
|
|
187
|
+
if (!touched) { setSaved('没有可写入的变更(仅补缺失且无缺失)。'); setOpen(false); return }
|
|
175
188
|
setBusy(true)
|
|
176
189
|
setFailed('')
|
|
177
190
|
setSaved('')
|
|
@@ -183,6 +196,7 @@ window.__ModuleLoader__.load({
|
|
|
183
196
|
throw new Error(detail || '设置被拒绝')
|
|
184
197
|
}
|
|
185
198
|
setSaved('已写入 provider "' + route + '" 的模型参数。')
|
|
199
|
+
setOpen(false)
|
|
186
200
|
setResult(null)
|
|
187
201
|
} catch (error) {
|
|
188
202
|
setFailed(String((error && error.message) || error))
|
|
@@ -200,6 +214,14 @@ window.__ModuleLoader__.load({
|
|
|
200
214
|
busy ? '查询中…' : ('models.dev 参数' + (summary ? ' · ' + summary : ''))),
|
|
201
215
|
open && e('div', { className: 'dpmPanel', role: 'dialog', 'aria-label': displayName + ' models.dev 参数' },
|
|
202
216
|
e('h4', null, displayName + ' · models.dev 官方参数'),
|
|
217
|
+
e('div', { className: 'dpmProxy' },
|
|
218
|
+
e('label', null,
|
|
219
|
+
e('input', { type: 'checkbox', checked: proxyState.enabled, onChange: (ev) => { proxyState.enabled = ev.target.checked; notifyProxy() } }),
|
|
220
|
+
'使用公共 models.dev 请求代理',
|
|
221
|
+
),
|
|
222
|
+
e('input', { type: 'text', value: proxyState.url, disabled: !proxyState.enabled, placeholder: PROXY_DEFAULT, 'aria-label': '公共 models.dev 请求代理地址', onChange: (ev) => { proxyState.url = ev.target.value; notifyProxy() } }),
|
|
223
|
+
e('p', { className: 'dpmHint' }, '该地址对所有 provider 共用,仅代理 models.dev 参数查询,不改变 LLM provider 地址。'),
|
|
224
|
+
),
|
|
203
225
|
failed && e('p', { className: 'dpmErr', role: 'alert' }, failed),
|
|
204
226
|
saved && e('p', { className: 'dpmSaved', role: 'status' }, saved),
|
|
205
227
|
!hasModels && e('p', { className: 'dpmHint' }, '该 provider 的 models 列表为空;先在官方页添加模型条目后再获取参数。'),
|
package/lib/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// and the Host never touches user LLM settings — writes happen on the Client
|
|
10
10
|
// through the official Settings scope of the provider namespace.
|
|
11
11
|
|
|
12
|
-
import { ProxyAgent } from 'undici'
|
|
12
|
+
import { ProxyAgent, fetch as undiciFetch } from 'undici'
|
|
13
13
|
import { lookupModels, parseModelsDevCatalog } from './modelsdev.js'
|
|
14
14
|
|
|
15
15
|
export const name = 'dsh-model-params'
|
|
@@ -21,14 +21,30 @@ const CACHE_TTL_MS = 6 * 60 * 60 * 1000
|
|
|
21
21
|
const MAX_IDS = 100
|
|
22
22
|
const MAX_ID_LENGTH = 240
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const caches = new Map() // proxy key -> { at: number, index: Map, fetchedAt: string }
|
|
25
25
|
|
|
26
|
-
function
|
|
26
|
+
function environmentProxyUrl() {
|
|
27
27
|
return process.env.HTTPS_PROXY || process.env.https_proxy
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
function cleanProxyUrl(value) {
|
|
31
|
+
if (typeof value !== 'string' || !value.trim()) return undefined
|
|
32
|
+
const url = value.trim()
|
|
33
|
+
try {
|
|
34
|
+
const parsed = new URL(url)
|
|
35
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined
|
|
36
|
+
return url
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchCatalog(requestedProxy) {
|
|
43
|
+
const proxy = requestedProxy === undefined ? environmentProxyUrl() : cleanProxyUrl(requestedProxy)
|
|
44
|
+
if (requestedProxy !== undefined && !proxy) throw Object.assign(new Error('代理地址无效:请输入 http:// 或 https:// URL'), { status: 400 })
|
|
45
|
+
const cacheKey = proxy || ''
|
|
31
46
|
const now = Date.now()
|
|
47
|
+
let cache = caches.get(cacheKey)
|
|
32
48
|
if (cache && now - cache.at < CACHE_TTL_MS) return cache
|
|
33
49
|
if (cache && cache.pending) {
|
|
34
50
|
await cache.pending
|
|
@@ -39,36 +55,45 @@ async function fetchCatalog() {
|
|
|
39
55
|
headers: { accept: 'application/json' },
|
|
40
56
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
41
57
|
}
|
|
42
|
-
const proxy = proxyUrl()
|
|
43
58
|
const attempt = async (dispatcher) => {
|
|
44
|
-
const
|
|
59
|
+
const request = dispatcher ? undiciFetch : fetch
|
|
60
|
+
const response = await request(MODELS_DEV_URL, dispatcher ? { ...options, dispatcher } : options)
|
|
45
61
|
if (!response.ok) throw new Error(`models.dev HTTP ${response.status}`)
|
|
46
62
|
return response.json()
|
|
47
63
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
cache = { at: now, index, fetchedAt: new Date().toISOString(), pending: undefined }
|
|
64
|
+
const save = (document) => {
|
|
65
|
+
cache = { at: Date.now(), index: parseModelsDevCatalog(document), fetchedAt: new Date().toISOString(), pending: undefined }
|
|
66
|
+
caches.set(cacheKey, cache)
|
|
52
67
|
return cache
|
|
68
|
+
}
|
|
69
|
+
if (requestedProxy !== undefined) {
|
|
70
|
+
const dispatcher = new ProxyAgent(proxy)
|
|
71
|
+
try {
|
|
72
|
+
return save(await attempt(dispatcher))
|
|
73
|
+
} finally {
|
|
74
|
+
void dispatcher.close().catch(() => {})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return save(await attempt())
|
|
53
79
|
} catch (error) {
|
|
54
|
-
if (proxy)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
void dispatcher.close().catch(() => {})
|
|
63
|
-
}
|
|
64
|
-
} catch (proxyError) {
|
|
65
|
-
throw new Error(`models.dev fetch failed (direct and via proxy): ${String(error?.message || error)} / ${String(proxyError?.message || proxyError)}`)
|
|
66
|
-
}
|
|
80
|
+
if (!proxy) throw error
|
|
81
|
+
const dispatcher = new ProxyAgent(proxy)
|
|
82
|
+
try {
|
|
83
|
+
return save(await attempt(dispatcher))
|
|
84
|
+
} catch (proxyError) {
|
|
85
|
+
throw new Error(`models.dev fetch failed (direct and via proxy): ${String(error?.message || error)} / ${String(proxyError?.message || proxyError)}`)
|
|
86
|
+
} finally {
|
|
87
|
+
void dispatcher.close().catch(() => {})
|
|
67
88
|
}
|
|
68
|
-
throw error
|
|
69
89
|
}
|
|
70
90
|
})()
|
|
71
|
-
if (!cache)
|
|
91
|
+
if (!cache) {
|
|
92
|
+
cache = { at: now, index: undefined, fetchedAt: undefined, pending }
|
|
93
|
+
caches.set(cacheKey, cache)
|
|
94
|
+
} else {
|
|
95
|
+
cache.pending = pending
|
|
96
|
+
}
|
|
72
97
|
try {
|
|
73
98
|
await pending
|
|
74
99
|
} finally {
|
|
@@ -86,6 +111,12 @@ function parseIds(query) {
|
|
|
86
111
|
return ids
|
|
87
112
|
}
|
|
88
113
|
|
|
114
|
+
function parseProxy(query) {
|
|
115
|
+
const raw = typeof query === 'string' ? query.trim() : ''
|
|
116
|
+
if (raw.length > 512) throw Object.assign(new Error('proxy URL too long'), { status: 400 })
|
|
117
|
+
return raw || undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
function originAllowed(req) {
|
|
90
121
|
const origin = req.headers.origin
|
|
91
122
|
if (!origin) return true
|
|
@@ -117,8 +148,10 @@ export function apply(ctx) {
|
|
|
117
148
|
return
|
|
118
149
|
}
|
|
119
150
|
try {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
151
|
+
const url = req.url ? new URL(req.url, 'http://local') : new URL('http://local')
|
|
152
|
+
const ids = parseIds(url.searchParams.get('ids'))
|
|
153
|
+
const proxy = url.searchParams.get('proxy') === null ? undefined : parseProxy(url.searchParams.get('proxy'))
|
|
154
|
+
const current = await fetchCatalog(proxy)
|
|
122
155
|
if (!current.index) throw new Error('models.dev catalog unavailable')
|
|
123
156
|
const result = lookupModels(current.index, ids)
|
|
124
157
|
json(res, 200, {
|
package/lib/modelsdev.js
CHANGED
|
@@ -110,17 +110,30 @@ export function lookupModels(index, ids, hints = {}) {
|
|
|
110
110
|
const modelId = clean(raw)
|
|
111
111
|
if (!modelId) continue
|
|
112
112
|
const bare = modelId.slice(modelId.lastIndexOf('/') + 1)
|
|
113
|
-
|
|
113
|
+
// Prefer un-prefixed (official vendor) records over gateway mirrors whose
|
|
114
|
+
// catalog keys embed the same prefix ("deepseek/deepseek-v4-flash").
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
const candidates = []
|
|
117
|
+
for (const key of [bare, modelId]) {
|
|
118
|
+
const lower = key.toLowerCase()
|
|
119
|
+
const exact = index.get(lower)
|
|
120
|
+
if (exact) {
|
|
121
|
+
for (const entry of exact) {
|
|
122
|
+
if (!seen.has(entry.providerId)) {
|
|
123
|
+
seen.add(entry.providerId)
|
|
124
|
+
candidates.push(entry)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
114
129
|
const hinted = hintedProviderOf(modelId, bare, hints)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
selected = hintedEntry || exact[0]
|
|
119
|
-
} else if (hinted) {
|
|
130
|
+
const hintedEntry = candidates.find((entry) => entry.providerId === hinted)
|
|
131
|
+
let selected = hintedEntry || candidates[0]
|
|
132
|
+
if (!selected && hinted) {
|
|
120
133
|
// Near match inside the hinted vendor only: versioned ids such as
|
|
121
134
|
// `deepseek-v4-flash-0731` resolve to the family's facts.
|
|
122
|
-
const
|
|
123
|
-
|
|
135
|
+
const near = [...index.values()].flat()
|
|
136
|
+
.filter((entry) => entry.providerId === hinted)
|
|
124
137
|
.filter((entry) => entry.modelId.includes(bare) || bare.includes(entry.modelId))
|
|
125
138
|
.sort((a, b) => a.modelId.length - b.modelId.length)[0]
|
|
126
139
|
selected = near
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-params",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "EN: models.dev parameter assistant for the official DeepSeek Harness Models page: per-provider one-click context / max-output / reasoning-effort fill. ZH: 官方模型设置页的 models.dev 参数助手:按 provider 一键补全上下文、max 输出与推理档位。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|