@doitian/dsh-provider-aliyun 0.1.0 → 0.2.0

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/lib/models.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Turn shipped catalog entries into the pi-ai model descriptors the adapter
3
- * dispatches with.
2
+ * Turn catalog entries into the pi-ai model descriptors the adapter dispatches
3
+ * with, and decide which entries a route advertises right now.
4
4
  *
5
5
  * A catalog entry states only model facts; everything deployment-specific —
6
6
  * the route it belongs to, the endpoint, the protocol, the compatibility
@@ -12,6 +12,172 @@
12
12
  /** Aliyun bills per token; the harness's cost surfaces are not fed from here. */
13
13
  const NO_COST = Object.freeze({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })
14
14
 
15
+ /**
16
+ * Compile configured pattern sources, reporting the ones that are not regular
17
+ * expressions instead of failing the mount.
18
+ *
19
+ * A typo in a profile should cost that one pattern, not the route: the rest of
20
+ * the filter still does its job, and the diagnostic names the pattern to fix.
21
+ *
22
+ * @param {readonly string[]} sources - pattern sources from configuration.
23
+ * @param {(source: string, error: Error) => void} [onInvalid] - called once per source that will not compile.
24
+ * @returns {RegExp[]} the compiled patterns, case-insensitive.
25
+ */
26
+ export function compilePatterns(sources, onInvalid) {
27
+ const patterns = []
28
+ // A missing list means "no patterns of this kind", which is what an explicit
29
+ // empty list means too; only a source that will not compile is reported.
30
+ for (const source of sources ?? []) {
31
+ try {
32
+ patterns.push(new RegExp(source, 'i'))
33
+ } catch (error) {
34
+ onInvalid?.(source, error)
35
+ }
36
+ }
37
+ return patterns
38
+ }
39
+
40
+ /**
41
+ * Choose which of the endpoint's ids this route advertises.
42
+ *
43
+ * A listing is a workspace's whole catalogue, so membership needs one more
44
+ * decision than "what did the endpoint say": which of those ids a *chat route*
45
+ * can serve. Two independent signals answer it, and either is enough: a pattern
46
+ * the profile configures, and the metadata snapshot, which states outright
47
+ * whether a model answers with text.
48
+ *
49
+ * Two rules keep the filter from hiding something the user asked for:
50
+ *
51
+ * - An id named in `models` (`pinned`) is always advertised, whatever the
52
+ * patterns and the snapshot say. Naming a model is a stronger statement than
53
+ * either.
54
+ * - `mode: 'all'` skips the include half entirely and keeps only exclusions,
55
+ * which is how a profile opts back into the raw listing.
56
+ *
57
+ * Endpoint order is preserved: the picker shows the models in the order the
58
+ * endpoint listed them.
59
+ *
60
+ * @param {readonly string[]} ids - the endpoint's ids, in endpoint order.
61
+ * @param {object} [options] - the filter.
62
+ * @param {'chat' | 'patterns' | 'all'} [options.mode] - which signals admit an id.
63
+ * @param {readonly RegExp[]} [options.include] - families to keep in either gated mode.
64
+ * @param {readonly RegExp[]} [options.exclude] - ids to drop in every mode.
65
+ * @param {readonly string[]} [options.pinned] - ids that always survive.
66
+ * @param {(id: string) => boolean} [options.known] - whether the metadata snapshot knows this id as a text-answering model.
67
+ * @returns {string[]} the ids to advertise.
68
+ */
69
+ export function selectIds(ids, { mode = 'chat', include = [], exclude = [], pinned = [], known } = {}) {
70
+ const always = new Set(pinned)
71
+ return ids.filter((id) => {
72
+ if (always.has(id)) return true
73
+ if (exclude.some((pattern) => pattern.test(id))) return false
74
+ if (mode === 'all') return true
75
+ if (include.some((pattern) => pattern.test(id))) return true
76
+ // `patterns` is the tight mode: only what the profile lists, so a picker can
77
+ // be narrowed without the registry quietly widening it again.
78
+ return mode === 'chat' && known?.(id) === true
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Spell an id the one way both sides can agree on.
84
+ *
85
+ * Registries and endpoints disagree about dots: models.dev publishes
86
+ * `qwen2-5-32b-instruct` where an endpoint answers `qwen2.5-32b-instruct`, and
87
+ * the same model appears under both spellings. Case is ignored for the same
88
+ * reason (`MiniMax-M2.5` beside `MiniMax/MiniMax-M2.5`).
89
+ *
90
+ * @param {string} id - a model id from either side.
91
+ * @returns {string} the comparable spelling.
92
+ */
93
+ function normalizeId(id) {
94
+ return id.toLowerCase().replace(/\./g, '-')
95
+ }
96
+
97
+ /**
98
+ * Index a metadata snapshot for lookup by the ids an endpoint answers with.
99
+ *
100
+ * A vendor-prefixed id (`vanchin/deepseek-v4.1-flash`) falls back to its last
101
+ * path segment, because the registry publishes the canonical id and the
102
+ * endpoint often lists both spellings of the same model.
103
+ *
104
+ * @param {object} snapshot - the generated snapshot, as `lib/metadata.json` holds it.
105
+ * @returns {{ facts: (id: string) => object | undefined, knows: (id: string) => boolean, size: number }} the lookup.
106
+ */
107
+ export function createMetadataIndex(snapshot) {
108
+ const models = snapshot?.models ?? {}
109
+ const byId = new Map()
110
+ for (const [id, facts] of Object.entries(models)) byId.set(normalizeId(id), facts)
111
+ const facts = (id) => {
112
+ const key = normalizeId(id)
113
+ return byId.get(key) ?? byId.get(key.slice(key.lastIndexOf('/') + 1))
114
+ }
115
+ // Membership asks a stricter question than facts do: an alias resolving to the
116
+ // same model is worth facts, but admitting it would advertise one model twice,
117
+ // once under a vendor prefix that may route somewhere else entirely.
118
+ const knows = (id) => byId.has(normalizeId(id))
119
+ return { facts, knows, size: byId.size }
120
+ }
121
+
122
+ /**
123
+ * Resolve the entries one route advertises right now.
124
+ *
125
+ * The endpoint answers *membership* — it is the only authority on what it
126
+ * serves — while this package answers *facts*. Both layers matter: an id the
127
+ * fallback catalog names keeps its verified capacities, and an id only the
128
+ * endpoint knows is advertised with whatever facts the metadata snapshot holds,
129
+ * or with the configured conservative defaults when even that is silent —
130
+ * rather than being dropped, because a model the user can select and correct is
131
+ * worth more than one this package silently hides.
132
+ *
133
+ * Membership follows the listing exactly, including removals: a model the
134
+ * endpoint stops serving stops being selectable. `ids === undefined` means no
135
+ * listing has ever arrived, which is the fallback catalog's whole job.
136
+ *
137
+ * @param {object} input - the layers, most authoritative first.
138
+ * @param {readonly string[] | undefined} input.ids - ids already selected by {@link selectIds}, or `undefined` before any listing.
139
+ * @param {readonly import('./catalog.js').AliyunModel[]} input.fallback - shipped entries, which also carry metadata.
140
+ * @param {{ contextWindow: number, maxTokens: number, input: readonly string[] }} input.defaults - capacities for an id neither layer names.
141
+ * @param {{ facts: (id: string) => object | undefined }} [input.metadata] - the generated snapshot's lookup.
142
+ * @returns {import('./catalog.js').AliyunModel[]} the entries to advertise, in endpoint order.
143
+ */
144
+ export function mergeCatalog({ ids, fallback, defaults, metadata }) {
145
+ if (ids === undefined) return [...fallback]
146
+ const pinned = new Map(fallback.map((entry) => [entry.id, entry]))
147
+ return ids.map((id) => {
148
+ const written = pinned.get(id)
149
+ if (written !== undefined) return written
150
+ const facts = metadata?.facts(id)
151
+ if (facts === undefined) {
152
+ return {
153
+ id,
154
+ name: id,
155
+ contextWindow: defaults.contextWindow,
156
+ maxTokens: defaults.maxTokens,
157
+ input: [...defaults.input],
158
+ // Nothing is known about this model's thinking, and claiming it can
159
+ // think would offer levels the endpoint may refuse. Configuration can
160
+ // say so.
161
+ reasoning: false,
162
+ }
163
+ }
164
+ const positive = (value, fallbackValue) => (Number.isInteger(value) && value > 0 ? value : fallbackValue)
165
+ const reasoning = facts.reasoning === true
166
+ return {
167
+ id,
168
+ name: typeof facts.name === 'string' && facts.name.length > 0 ? facts.name : id,
169
+ contextWindow: positive(facts.contextWindow, defaults.contextWindow),
170
+ maxTokens: positive(facts.maxTokens, defaults.maxTokens),
171
+ input: Array.isArray(facts.input) && facts.input.length > 0 ? [...facts.input] : [...defaults.input],
172
+ reasoning,
173
+ // The snapshot states that a model thinks, not how its levels are spelled.
174
+ // This endpoint takes a boolean, so what the map decides is which levels a
175
+ // selector offers — and these are the levels the shipped entries use.
176
+ ...reasoning ? { thinkingLevelMap: { low: 'low', medium: 'medium', high: 'high' } } : {},
177
+ }
178
+ })
179
+ }
180
+
15
181
  /**
16
182
  * Materialize one route's models.
17
183
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@doitian/dsh-provider-aliyun",
3
- "version": "0.1.0",
4
- "description": "Aliyun DashScope (Bailian) model provider for DeepSeek Harness: registers an `aliyun` route carrying the Qwen model catalog, so the model list ships in the package instead of a profile.",
3
+ "version": "0.2.0",
4
+ "description": "Aliyun DashScope (Bailian) model provider for DeepSeek Harness: registers an `aliyun` route whose model list comes from the configured endpoint itself, with a shipped fallback catalog for capacities and offline use.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -12,6 +12,7 @@
12
12
  "dashscope",
13
13
  "bailian",
14
14
  "qwen",
15
+ "model-discovery",
15
16
  "llm",
16
17
  "provider"
17
18
  ],
@@ -37,6 +38,8 @@
37
38
  "lib/index.js",
38
39
  "lib/adapter.js",
39
40
  "lib/catalog.js",
41
+ "lib/discovery.js",
42
+ "lib/metadata.json",
40
43
  "lib/models.js",
41
44
  "lib/provider.js",
42
45
  "cordis.patch.yml"
@@ -49,6 +52,9 @@
49
52
  },
50
53
  "scripts": {
51
54
  "validate": "node scripts/validate.mjs",
55
+ "probe": "node scripts/probe.mjs",
56
+ "generate:metadata": "node scripts/generate-metadata.mjs",
57
+ "link:desktop": "node scripts/link-desktop.mjs",
52
58
  "test": "node --test",
53
59
  "prepack": "npm run validate && npm run test"
54
60
  },