dsh-model-params 0.1.2 → 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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## English
4
4
 
5
- **Current release: 0.1.0** — models.dev parameter assistant inside the official DeepSeek Harness Models settings page.
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
- - Nothing leaves the machine except the models.dev catalog request (Host side, optional `HTTPS_PROXY`), and no credentials are ever sent.
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
- - Host 只负责拉取并缓存 models.dev 目录(6 小时,支持 `HTTPS_PROXY`),不接触任何 LLM 配置与凭据。
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 response = await fetch(API + '?ids=' + encodeURIComponent(ids.join(',')), { cache: 'no-store', signal: controller.signal })
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
- let cache = null // { at: number, index: Map, fetchedAt: string }
24
+ const caches = new Map() // proxy key -> { at: number, index: Map, fetchedAt: string }
25
25
 
26
- function proxyUrl() {
26
+ function environmentProxyUrl() {
27
27
  return process.env.HTTPS_PROXY || process.env.https_proxy
28
28
  }
29
29
 
30
- async function fetchCatalog() {
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 response = await fetch(MODELS_DEV_URL, dispatcher ? { ...options, dispatcher } : options)
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
- try {
49
- const document = await attempt()
50
- const index = parseModelsDevCatalog(document)
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
- try {
56
- const dispatcher = new ProxyAgent(proxy)
57
- try {
58
- const document = await attempt(dispatcher)
59
- cache = { at: now, index: parseModelsDevCatalog(document), fetchedAt: new Date().toISOString(), pending: undefined }
60
- return cache
61
- } finally {
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) cache = { at: now, index: undefined, fetchedAt: undefined, pending }
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 ids = parseIds(req.url ? new URL(req.url, 'http://local').searchParams.get('ids') : '')
121
- const current = await fetchCatalog()
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-params",
3
- "version": "0.1.2",
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",