dsh-model-params 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/lib/modelsdev.js +90 -43
  2. package/package.json +1 -1
package/lib/modelsdev.js CHANGED
@@ -1,39 +1,62 @@
1
1
  // dsh-model-params — pure models.dev catalog parsing and matching helpers.
2
2
  //
3
- // models.dev/api.json shape (defensive reads only):
4
- // { providers: { <id>: { id, name, models: [ { id, name, limit?: { context?, output? },
5
- // reasoning_options?: [ { type: 'effort', values?: string[] } ] } ] } } }
3
+ // REAL models.dev/api.json shape (verified live):
4
+ // root = flat provider map { <id>: { id, name, ..., models: {
5
+ // <modelId>: { id, name, family, reasoning?, reasoning_options?:
6
+ // [ {type:'toggle'}, {type:'effort', values:[...]} ], limit?: { context?, output? } } } } }
6
7
  //
7
- // A requested model id may carry a vendor prefix ("z-ai/glm-5.3-flash",
8
- // "deepseek/deepseek-v4-flash"). Matching therefore tries the exact id first
9
- // and then the segment after the last "/". Reasoning-effort values offered by
10
- // models.dev are intersected with the pi-ai THINKING_LEVELS key space
11
- // (off/minimal/low/medium/high/xhigh/max) so a write can never fail schema
12
- // validation.
8
+ // Matching mirrors the upstream dsh-llm-newapi approach: keys tried are the
9
+ // full gateway id and its last path segment; family-prefix hints decide which
10
+ // catalog provider leads; inside the hinted provider a NEAR key (one side
11
+ // contains the other) still yields facts for versioned ids like
12
+ // `deepseek-v4-flash-0731`. Reasoning-effort values are intersected with the
13
+ // pi-ai THINKING_LEVELS space so writes can never fail schema validation.
13
14
 
14
15
  export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
15
16
 
17
+ /** Family-prefix → catalog provider, mirroring upstream defaults. */
18
+ export const DEFAULT_PROVIDER_HINTS = Object.freeze({
19
+ glm: 'zai',
20
+ gpt: 'openai',
21
+ o: 'openai',
22
+ claude: 'anthropic',
23
+ deepseek: 'deepseek',
24
+ gemini: 'google',
25
+ grok: 'xai',
26
+ hunyuan: 'tencent',
27
+ qwen: 'alibaba',
28
+ kimi: 'moonshotai',
29
+ mimo: 'xiaomi',
30
+ minimax: 'minimax',
31
+ })
32
+
16
33
  const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
17
34
  const clean = (value) => nonEmpty(value) ? value.trim() : undefined
18
35
  const positiveInt = (value) => Number.isSafeInteger(value) && value > 0 ? value : undefined
19
36
 
20
- /** Parse one models.dev catalog document into a detached match index. */
37
+ /**
38
+ * Parse a models.dev document into a detached map keyed by lower-cased model
39
+ * id. Tolerant of both the real flat-provider root and a `{providers}` wrap.
40
+ * @returns {Map<string, object[]>} entries with providerId/providerName/modelId/name/context/maxOutput/efforts.
41
+ */
21
42
  export function parseModelsDevCatalog(document) {
22
- const root = document && typeof document === 'object' ? document : {}
23
- const providers = root.providers && typeof root.providers === 'object' && !Array.isArray(root.providers) ? root.providers : {}
43
+ const root0 = document && typeof document === 'object' ? document : {}
44
+ const providersRoot = root0.providers && typeof root0.providers === 'object' && !Array.isArray(root0.providers)
45
+ ? root0.providers
46
+ : root0
24
47
  const byId = new Map()
25
- for (const [providerId, provider] of Object.entries(providers)) {
26
- if (!provider || typeof provider !== 'object') continue
48
+ for (const [providerId, provider] of Object.entries(providersRoot)) {
49
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider.models)) continue
27
50
  const providerName = clean(provider.name) || clean(provider.id) || providerId
28
- const models = Array.isArray(provider.models) ? provider.models : []
29
- for (const model of models) {
51
+ const models = provider.models && typeof provider.models === 'object' ? provider.models : {}
52
+ for (const [key, model] of Object.entries(models)) {
30
53
  if (!model || typeof model !== 'object') continue
31
- const modelId = clean(model.id)
54
+ const modelId = clean(model.id) || clean(key)
32
55
  if (!modelId) continue
33
56
  const limit = model.limit && typeof model.limit === 'object' ? model.limit : {}
34
57
  const context = positiveInt(limit.context)
35
58
  const output = positiveInt(limit.output)
36
- let efforts = []
59
+ const efforts = []
37
60
  const options = Array.isArray(model.reasoning_options) ? model.reasoning_options : []
38
61
  for (const option of options) {
39
62
  if (!option || option.type !== 'effort' || !Array.isArray(option.values)) continue
@@ -43,7 +66,6 @@ export function parseModelsDevCatalog(document) {
43
66
  }
44
67
  }
45
68
  if (context === undefined && output === undefined && efforts.length === 0) continue
46
- const key = modelId.toLowerCase()
47
69
  const entry = {
48
70
  providerId,
49
71
  providerName,
@@ -53,49 +75,74 @@ export function parseModelsDevCatalog(document) {
53
75
  ...(output !== undefined ? { maxOutput: output } : {}),
54
76
  ...(efforts.length ? { efforts } : {}),
55
77
  }
56
- const list = byId.get(key)
78
+ const list = byId.get(modelId.toLowerCase())
57
79
  if (list) list.push(entry)
58
- else byId.set(key, [entry])
80
+ else byId.set(modelId.toLowerCase(), [entry])
59
81
  }
60
82
  }
61
83
  return byId
62
84
  }
63
85
 
64
- /** Prefer the entry whose provider matches a hint; otherwise the first. */
65
- function pickEntry(entries, hint) {
66
- if (entries.length <= 1) return entries[0]
67
- const lower = hint ? String(hint).toLowerCase() : ''
68
- if (lower) {
69
- const preferred = entries.find((entry) =>
70
- entry.providerId.toLowerCase().includes(lower) || lower.includes(entry.providerId.toLowerCase()))
71
- if (preferred) return preferred
72
- }
73
- return entries[0]
86
+ /** Provider hinted for one gateway id by exact model rule or longest prefix. */
87
+ export function hintedProviderOf(id, bare, hints = {}) {
88
+ const exact = hints.models ? (hints.models[id] || hints.models[bare]) : undefined
89
+ if (exact) return exact
90
+ const lower = String(bare).toLowerCase()
91
+ const entries = Object.entries({ ...DEFAULT_PROVIDER_HINTS, ...(hints.defaults || {}) })
92
+ const hit = entries
93
+ .filter(([prefix]) => lower.startsWith(prefix.toLowerCase()))
94
+ .sort((a, b) => b[0].length - a[0].length)[0]
95
+ return hit ? hit[1] : undefined
74
96
  }
75
97
 
76
98
  /**
77
- * Look up requested model ids. Returns entries only for ids with at least one
78
- * metadata-bearing models.dev record; every other id is reported as unmatched.
79
- * @param {Map<string, object[]>} index - output of parseModelsDevCatalog.
80
- * @param {string[]} ids - configured model ids (vendor prefixes allowed).
81
- * @param {(id: string) => string|undefined} hintFor - optional provider hint per id.
99
+ * Match requested ids against the catalog. Keys tried are the full id and its
100
+ * last path segment; within the hinted provider a NEAR key also matches.
101
+ * @param {Map<string, object[]>} index - parseModelsDevCatalog output.
102
+ * @param {string[]} ids - gateway model ids.
103
+ * @param {{ models?: object, defaults?: object }} hints - optional provider hints.
82
104
  * @returns {{ matched: object[], unmatched: string[] }}
83
105
  */
84
- export function lookupModels(index, ids, hintFor = () => undefined) {
106
+ export function lookupModels(index, ids, hints = {}) {
85
107
  const matched = []
86
108
  const unmatched = []
87
109
  for (const raw of ids || []) {
88
110
  const modelId = clean(raw)
89
111
  if (!modelId) continue
90
- const exact = index.get(modelId.toLowerCase())
91
- const base = modelId.slice(modelId.lastIndexOf('/') + 1)
92
- const byBase = base !== modelId ? index.get(base.toLowerCase()) : undefined
93
- const entries = byBase && byBase.length && (!exact || byBase.length < exact.length) ? byBase : exact
94
- if (!entries || !entries.length) {
112
+ const bare = modelId.slice(modelId.lastIndexOf('/') + 1)
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
+ }
129
+ const hinted = hintedProviderOf(modelId, bare, hints)
130
+ const hintedEntry = candidates.find((entry) => entry.providerId === hinted)
131
+ let selected = hintedEntry || candidates[0]
132
+ if (!selected && hinted) {
133
+ // Near match inside the hinted vendor only: versioned ids such as
134
+ // `deepseek-v4-flash-0731` resolve to the family's facts.
135
+ const near = [...index.values()].flat()
136
+ .filter((entry) => entry.providerId === hinted)
137
+ .filter((entry) => entry.modelId.includes(bare) || bare.includes(entry.modelId))
138
+ .sort((a, b) => a.modelId.length - b.modelId.length)[0]
139
+ selected = near
140
+ }
141
+ if (!selected) {
95
142
  unmatched.push(modelId)
96
143
  continue
97
144
  }
98
- matched.push({ requested: modelId, ...pickEntry(entries, hintFor(modelId)) })
145
+ matched.push({ requested: modelId, ...selected, ...(selected.providerId === hinted ? { hinted: true } : {}) })
99
146
  }
100
147
  return { matched, unmatched }
101
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-params",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",