dsh-model-groups 0.0.0-stage → 0.3.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.
@@ -0,0 +1,113 @@
1
+ import { isGroupExpanded } from './group-state.js';
2
+
3
+ // This namespace is a selection identity, never an upstream model id or secret.
4
+ export const KEY_MODEL_PREFIX = '@dsh-key/';
5
+ const KEY_ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/;
6
+ const forbiddenEntry = id => ['__proto__', 'prototype', 'constructor'].includes(id);
7
+
8
+ export function decodeKeyModelId(model) {
9
+ if (typeof model !== 'string' || !model.startsWith(KEY_MODEL_PREFIX)) return undefined;
10
+ const parts = model.slice(KEY_MODEL_PREFIX.length).split('/');
11
+ if (parts.length !== 2) return undefined;
12
+ try {
13
+ const entryId = decodeURIComponent(parts[0]);
14
+ const modelId = decodeURIComponent(parts[1]);
15
+ if (!KEY_ENTRY_ID.test(entryId) || forbiddenEntry(entryId) || !modelId || modelId.startsWith(KEY_MODEL_PREFIX)) return undefined;
16
+ if (`${KEY_MODEL_PREFIX}${encodeURIComponent(entryId)}/${encodeURIComponent(modelId)}` !== model) return undefined;
17
+ return { entryId, modelId };
18
+ } catch { return undefined; }
19
+ }
20
+
21
+ export function keyEntryGroupKey(provider, entryId) {
22
+ return `@key/${encodeURIComponent(provider)}/${encodeURIComponent(entryId)}`;
23
+ }
24
+
25
+ export function keyEntryName(providers, provider, entryId) {
26
+ const entries = Object.hasOwn(providers ?? {}, provider) ? providers[provider]?.keyEntries : undefined;
27
+ const entry = entries && Object.hasOwn(entries, entryId) ? entries[entryId] : undefined;
28
+ return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : entryId;
29
+ }
30
+
31
+ /** Preserve every original provider fold; only real multi-key routes gain children.
32
+ * `providers` may be undefined while the native settings form is loading. In
33
+ * that case aliases still remain identifiable, without inventing credentials.
34
+ */
35
+ export function buildModelHierarchy(groups, providers, preferences, current = null) {
36
+ return groups.map(group => {
37
+ const profile = Object.hasOwn(providers ?? {}, group.id) ? providers[group.id] : undefined;
38
+ const aliases = group.models.filter(model => decodeKeyModelId(model.id));
39
+ const multi = profile?.multiKey === true || (providers === undefined && aliases.length > 0);
40
+ const root = {
41
+ kind: multi ? 'multi' : 'provider', key: group.id, id: group.id,
42
+ name: group.provider?.name ?? group.name ?? group.id, group,
43
+ current: current?.provider === group.id,
44
+ expanded: isGroupExpanded(preferences, group.id, current?.provider),
45
+ };
46
+ if (!multi) {
47
+ // A catalog refresh can race with disabling multi-key. Do not offer stale
48
+ // aliases as if they belonged to the single-key credential.
49
+ return { ...root, group: { ...group, models: group.models.filter(model => !model.id.startsWith(KEY_MODEL_PREFIX)) }, modelCount: group.models.filter(model => !model.id.startsWith(KEY_MODEL_PREFIX)).length };
50
+ }
51
+ const byEntry = new Map();
52
+ const configured = profile?.keyEntries && typeof profile.keyEntries === 'object' ? profile.keyEntries : {};
53
+ for (const entryId of Object.keys(configured)) {
54
+ if (KEY_ENTRY_ID.test(entryId) && !forbiddenEntry(entryId)) byEntry.set(entryId, []);
55
+ }
56
+ for (const model of aliases) {
57
+ const { entryId } = decodeKeyModelId(model.id);
58
+ // Do not revive a removed entry from an old directory snapshot.
59
+ if (providers !== undefined && !Object.hasOwn(configured, entryId)) continue;
60
+ if (!byEntry.has(entryId)) byEntry.set(entryId, []);
61
+ byEntry.get(entryId).push(model);
62
+ }
63
+ const selectedEntry = current?.provider === group.id ? decodeKeyModelId(current.model)?.entryId : undefined;
64
+ const children = [...byEntry].filter(([, models]) => models.length > 0).map(([entryId, models]) => {
65
+ const key = keyEntryGroupKey(group.id, entryId);
66
+ const active = root.current && selectedEntry === entryId;
67
+ return { ...group, key, entryId, name: keyEntryName(providers, group.id, entryId), models, current: active,
68
+ expanded: Object.hasOwn(preferences, key) ? preferences[key] : active };
69
+ });
70
+ // Legacy raw selections keep their exact original credential. Offer only the
71
+ // current raw model here, not an extra editable/key-group configuration.
72
+ if (root.current && !selectedEntry) {
73
+ const legacy = group.models.filter(model => model.id === current.model && !model.id.startsWith(KEY_MODEL_PREFIX));
74
+ if (legacy.length) {
75
+ const key = keyEntryGroupKey(group.id, '@legacy');
76
+ children.push({ ...group, key, entryId: undefined, legacy: true, name: 'Legacy single key', models: legacy, current: true,
77
+ expanded: Object.hasOwn(preferences, key) ? preferences[key] : true });
78
+ }
79
+ }
80
+ return { ...root, children, modelCount: children.reduce((sum, child) => sum + child.models.length, 0) };
81
+ });
82
+ }
83
+
84
+ export function visibleHierarchyModels(entries) {
85
+ return entries.flatMap(entry => {
86
+ if (!entry.expanded) return [];
87
+ const groups = entry.kind === 'multi' ? entry.children.filter(child => child.expanded) : [entry.group];
88
+ return groups.flatMap(group => group.models.map(model => ({ provider: group.id, model: model.id })));
89
+ });
90
+ }
91
+
92
+ export function hierarchyExpansionKeys(entries) {
93
+ return entries.flatMap(entry => [entry.key, ...(entry.kind === 'multi' ? entry.children.map(child => child.key) : [])]);
94
+ }
95
+
96
+ /** Search temporarily expands matching provider/key paths, never saved folds. */
97
+ export function filterModelHierarchy(entries, query, rankModels) {
98
+ const needle = query.trim();
99
+ if (!needle) return entries;
100
+ const matchesName = names => rankModels(names.filter(Boolean).map(name => ({ name })), needle).length > 0;
101
+ return entries.flatMap(entry => {
102
+ const all = matchesName([entry.name, entry.id]);
103
+ if (entry.kind !== 'multi') {
104
+ const models = all ? entry.group.models : rankModels(entry.group.models, needle);
105
+ return models.length ? [{ ...entry, expanded: true, modelCount: models.length, group: { ...entry.group, models } }] : [];
106
+ }
107
+ const children = entry.children.flatMap(child => {
108
+ const models = all || matchesName([child.name, child.entryId]) ? child.models : rankModels(child.models, needle);
109
+ return models.length ? [{ ...child, expanded: true, models }] : [];
110
+ });
111
+ return children.length ? [{ ...entry, expanded: true, children, modelCount: children.reduce((sum, child) => sum + child.models.length, 0) }] : [];
112
+ });
113
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Client-only picker. Native multi-key support is installed explicitly via
2
+ // scripts/native-patches.mjs, never by mutating runtime code during apply.
3
+ // This plugin owns no provider credentials or independent grouping settings.
4
+ export function apply() {}