kiku-agent 1.2.0 → 1.3.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/README.md +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { findRegistryEntry } from '../../src/lib/vram/modelRegistry.js';
|
|
5
|
+
|
|
6
|
+
const GGUF_MAGIC = 0x46554747;
|
|
7
|
+
const MAX_HEADER_BYTES = 256 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
const GGUF_VALUE_TYPES = Object.freeze({
|
|
10
|
+
UINT8: 0, INT8: 1, UINT16: 2, INT16: 3, UINT32: 4, INT32: 5,
|
|
11
|
+
FLOAT32: 6, BOOL: 7, STRING: 8, ARRAY: 9, UINT64: 10, INT64: 11, FLOAT64: 12,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const GGUF_FILE_TYPES = Object.freeze({
|
|
15
|
+
0: 'fp32', 1: 'fp16', 2: 'q4_0', 3: 'q4_1', 7: 'q8_0', 8: 'q5_0', 9: 'q5_1',
|
|
16
|
+
10: 'q2_k', 11: 'q3_k_s', 12: 'q3_k_m', 13: 'q3_k_l', 14: 'q4_k_s', 15: 'q4_k_m',
|
|
17
|
+
16: 'q5_k_s', 17: 'q5_k_m', 18: 'q6_k', 19: 'iq2_xxs', 22: 'iq3_xs', 24: 'iq1_s',
|
|
18
|
+
25: 'iq4_nl', 26: 'iq3_s', 28: 'iq2_s', 30: 'iq4_xs', 32: 'bf16', 38: 'mxfp4',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const GGUF_SCALAR_READERS = Object.freeze({
|
|
22
|
+
[GGUF_VALUE_TYPES.UINT8]: 'readUInt8',
|
|
23
|
+
[GGUF_VALUE_TYPES.INT8]: 'readInt8',
|
|
24
|
+
[GGUF_VALUE_TYPES.UINT16]: 'readUInt16',
|
|
25
|
+
[GGUF_VALUE_TYPES.INT16]: 'readInt16',
|
|
26
|
+
[GGUF_VALUE_TYPES.UINT32]: 'readUInt32',
|
|
27
|
+
[GGUF_VALUE_TYPES.INT32]: 'readInt32',
|
|
28
|
+
[GGUF_VALUE_TYPES.FLOAT32]: 'readFloat32',
|
|
29
|
+
[GGUF_VALUE_TYPES.BOOL]: 'readBool',
|
|
30
|
+
[GGUF_VALUE_TYPES.UINT64]: 'readUInt64',
|
|
31
|
+
[GGUF_VALUE_TYPES.INT64]: 'readInt64',
|
|
32
|
+
[GGUF_VALUE_TYPES.FLOAT64]: 'readFloat64',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
class GgufReader {
|
|
36
|
+
constructor(fd) {
|
|
37
|
+
this.fd = fd;
|
|
38
|
+
this.buffer = Buffer.alloc(0);
|
|
39
|
+
this.cursor = 0;
|
|
40
|
+
this.totalRead = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ensure(byteCount) {
|
|
44
|
+
if (this.buffer.length - this.cursor >= byteCount) return;
|
|
45
|
+
while (this.buffer.length - this.cursor < byteCount) {
|
|
46
|
+
if (this.totalRead > MAX_HEADER_BYTES) {
|
|
47
|
+
throw new Error(`GGUF header exceeded ${MAX_HEADER_BYTES / (1024 * 1024)} MB read budget`);
|
|
48
|
+
}
|
|
49
|
+
const chunk = Buffer.alloc(1024 * 1024);
|
|
50
|
+
const bytesRead = fs.readSync(this.fd, chunk, 0, chunk.length, this.totalRead);
|
|
51
|
+
if (bytesRead <= 0) throw new Error('Unexpected end of GGUF file while parsing header');
|
|
52
|
+
this.totalRead += bytesRead;
|
|
53
|
+
this.buffer = this.cursor > 0
|
|
54
|
+
? Buffer.concat([this.buffer.subarray(this.cursor), chunk.subarray(0, bytesRead)])
|
|
55
|
+
: Buffer.concat([this.buffer, chunk.subarray(0, bytesRead)]);
|
|
56
|
+
this.cursor = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
u8() { this.ensure(1); return this.buffer.readUInt8(this.cursor++); }
|
|
61
|
+
u32() { this.ensure(4); const v = this.buffer.readUInt32LE(this.cursor); this.cursor += 4; return v; }
|
|
62
|
+
u64() {
|
|
63
|
+
this.ensure(8);
|
|
64
|
+
const v = this.buffer.readBigUInt64LE(this.cursor);
|
|
65
|
+
this.cursor += 8;
|
|
66
|
+
return Number(v);
|
|
67
|
+
}
|
|
68
|
+
string() {
|
|
69
|
+
const len = this.u64();
|
|
70
|
+
this.ensure(len);
|
|
71
|
+
const v = this.buffer.toString('utf8', this.cursor, this.cursor + len);
|
|
72
|
+
this.cursor += len;
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
value(type) {
|
|
76
|
+
if (type === GGUF_VALUE_TYPES.STRING) return this.string();
|
|
77
|
+
if (type === GGUF_VALUE_TYPES.ARRAY) {
|
|
78
|
+
const elemType = this.u32();
|
|
79
|
+
const count = this.u64();
|
|
80
|
+
if (count > 5_000_000) throw new Error('GGUF metadata array unreasonably large');
|
|
81
|
+
const out = new Array(count);
|
|
82
|
+
const scalarReader = GGUF_SCALAR_READERS[elemType];
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
out[i] = scalarReader ? this[scalarReader]() : this.value(elemType);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
const scalarReader = GGUF_SCALAR_READERS[type];
|
|
89
|
+
if (!scalarReader) throw new Error(`Unknown GGUF value type ${type}`);
|
|
90
|
+
return this[scalarReader]();
|
|
91
|
+
}
|
|
92
|
+
readBool() { return this.u8() !== 0; }
|
|
93
|
+
readInt8() { this.ensure(1); const v = this.buffer.readInt8(this.cursor); this.cursor += 1; return v; }
|
|
94
|
+
readUInt8() { return this.u8(); }
|
|
95
|
+
readUInt16() { this.ensure(2); const v = this.buffer.readUInt16LE(this.cursor); this.cursor += 2; return v; }
|
|
96
|
+
readInt16() { this.ensure(2); const v = this.buffer.readInt16LE(this.cursor); this.cursor += 2; return v; }
|
|
97
|
+
readInt32() { this.ensure(4); const v = this.buffer.readInt32LE(this.cursor); this.cursor += 4; return v; }
|
|
98
|
+
readUInt32() { return this.u32(); }
|
|
99
|
+
readFloat32() { this.ensure(4); const v = this.buffer.readFloatLE(this.cursor); this.cursor += 4; return v; }
|
|
100
|
+
readFloat64() { this.ensure(8); const v = this.buffer.readDoubleLE(this.cursor); this.cursor += 8; return v; }
|
|
101
|
+
readInt64() { this.ensure(8); const v = this.buffer.readBigInt64LE(this.cursor); this.cursor += 8; return Number(v); }
|
|
102
|
+
readUInt64() { return this.u64(); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
const PARSED_GGUF_CACHE_LIMIT = 100;
|
|
107
|
+
const parsedGgufCache = new Map();
|
|
108
|
+
|
|
109
|
+
export function parseGgufMetadata(filePath) {
|
|
110
|
+
let cacheKey = null;
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.statSync(filePath);
|
|
113
|
+
cacheKey = `${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
114
|
+
if (parsedGgufCache.has(cacheKey)) {
|
|
115
|
+
const cached = parsedGgufCache.get(cacheKey);
|
|
116
|
+
return cached ? { ...cached } : cached;
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
const parsed = parseGgufMetadataUncached(filePath);
|
|
120
|
+
if (cacheKey && parsed) {
|
|
121
|
+
if (parsedGgufCache.size >= PARSED_GGUF_CACHE_LIMIT) {
|
|
122
|
+
parsedGgufCache.delete(parsedGgufCache.keys().next().value);
|
|
123
|
+
}
|
|
124
|
+
parsedGgufCache.set(cacheKey, parsed);
|
|
125
|
+
return { ...parsed };
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseGgufMetadataUncached(filePath) {
|
|
131
|
+
let fd = null;
|
|
132
|
+
try {
|
|
133
|
+
fd = fs.openSync(filePath, 'r');
|
|
134
|
+
const reader = new GgufReader(fd);
|
|
135
|
+
reader.ensure(4);
|
|
136
|
+
const magic = reader.buffer.readUInt32LE(reader.cursor);
|
|
137
|
+
reader.cursor += 4;
|
|
138
|
+
if (magic !== GGUF_MAGIC) return null;
|
|
139
|
+
reader.u32();
|
|
140
|
+
|
|
141
|
+
const tensorCount = reader.u64();
|
|
142
|
+
const kvCount = reader.u64();
|
|
143
|
+
|
|
144
|
+
const kv = {};
|
|
145
|
+
for (let i = 0; i < kvCount; i++) {
|
|
146
|
+
const key = reader.string();
|
|
147
|
+
const type = reader.u32();
|
|
148
|
+
kv[key] = reader.value(type);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
let paramCount = 0;
|
|
153
|
+
for (let i = 0; i < tensorCount; i++) {
|
|
154
|
+
reader.string();
|
|
155
|
+
const nDims = reader.u32();
|
|
156
|
+
let elements = 1;
|
|
157
|
+
for (let d = 0; d < nDims; d++) elements *= reader.u64();
|
|
158
|
+
reader.u32();
|
|
159
|
+
reader.u64();
|
|
160
|
+
paramCount += elements;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const arch = typeof kv['general.architecture'] === 'string' ? kv['general.architecture'] : null;
|
|
164
|
+
const prefix = arch ? `${arch}.` : '';
|
|
165
|
+
const pick = (key) => {
|
|
166
|
+
const v = prefix ? kv[`${prefix}${key}`] : undefined;
|
|
167
|
+
return v === undefined ? null : v;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const name = typeof kv['general.name'] === 'string' ? kv['general.name'] : path.basename(filePath);
|
|
171
|
+
const queryHeads = pick('attention.head_count');
|
|
172
|
+
const hiddenSize = pick('embedding_length');
|
|
173
|
+
const headDim = pick('attention.key_length') ?? pick('attention.value_length')
|
|
174
|
+
?? (hiddenSize && queryHeads ? Math.round(hiddenSize / queryHeads) : null);
|
|
175
|
+
const swaWindow = pick('attention.swa_size') ?? pick('attention.sliding_window')
|
|
176
|
+
?? pick('attention.window_size');
|
|
177
|
+
let fileSizeBytes = null;
|
|
178
|
+
try {
|
|
179
|
+
fileSizeBytes = fs.statSync(filePath).size;
|
|
180
|
+
} catch {
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
const fileType = kv['general.file_type'];
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
filePath,
|
|
187
|
+
fileName: path.basename(filePath),
|
|
188
|
+
fileSizeBytes,
|
|
189
|
+
arch,
|
|
190
|
+
name,
|
|
191
|
+
quant: GGUF_FILE_TYPES[fileType] ?? extractQuantFromName(path.basename(filePath)),
|
|
192
|
+
layers: pick('block_count'),
|
|
193
|
+
queryHeads,
|
|
194
|
+
kvHeads: pick('attention.head_count_kv') ?? queryHeads,
|
|
195
|
+
headDim,
|
|
196
|
+
hiddenSize,
|
|
197
|
+
contextLength: pick('context_length'),
|
|
198
|
+
expertCount: pick('expert_count'),
|
|
199
|
+
swa: Number.isFinite(Number(swaWindow)) && Number(swaWindow) > 0
|
|
200
|
+
? { windowTokens: Number(swaWindow) }
|
|
201
|
+
: null,
|
|
202
|
+
paramCount: paramCount > 0 ? paramCount : null,
|
|
203
|
+
};
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return { error: err.message, filePath };
|
|
206
|
+
} finally {
|
|
207
|
+
if (fd !== null) {
|
|
208
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
export function extractQuantFromName(text) {
|
|
215
|
+
if (!text) return null;
|
|
216
|
+
const matches = String(text).match(/\b(iq[1-4]_[a-z0-9]+|q[2-8]_[a-z0-9_]+|q[2-8]_0|bf16|fp16|f16|fp32|f32|q8)\b/gi);
|
|
217
|
+
return matches ? matches[matches.length - 1].toLowerCase() : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
export function describeVisionConfig(visionConfig) {
|
|
222
|
+
if (!visionConfig || typeof visionConfig !== 'object') return null;
|
|
223
|
+
const hidden = Number(visionConfig.hidden_size) || 1024;
|
|
224
|
+
const layers = Number(visionConfig.num_hidden_layers) || 24;
|
|
225
|
+
const patchSize = Number(visionConfig.patch_size) || 14;
|
|
226
|
+
const imageSize = Number(visionConfig.image_size) || 336;
|
|
227
|
+
|
|
228
|
+
const params = layers * 12 * hidden * hidden + hidden * patchSize * patchSize * 3;
|
|
229
|
+
let tokensPerImage = Math.floor(imageSize / patchSize) ** 2 + 1;
|
|
230
|
+
tokensPerImage = Math.min(2048, Math.max(576, tokensPerImage));
|
|
231
|
+
return { encoderParamsB: Number((params / 1e9).toFixed(3)), tokensPerImage };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
export function parseHfConfigJson(configJson, modelId = null) {
|
|
236
|
+
if (!configJson || typeof configJson !== 'object') return null;
|
|
237
|
+
const cfg = configJson.text_config && typeof configJson.text_config === 'object' ? configJson.text_config : configJson;
|
|
238
|
+
const queryHeads = cfg.num_attention_heads ?? null;
|
|
239
|
+
const hiddenSize = cfg.hidden_size ?? null;
|
|
240
|
+
const headDim = cfg.head_dim ?? (hiddenSize && queryHeads ? Math.round(hiddenSize / queryHeads) : null);
|
|
241
|
+
const isVlm = Boolean(configJson.vision_config || cfg.vision_config) ||
|
|
242
|
+
/llava|vl|vision|paligemma|multimodal/i.test(String(configJson.model_type || cfg.model_type || '') + String((configJson.architectures || cfg.architectures || []).join(' ')));
|
|
243
|
+
return {
|
|
244
|
+
modelId,
|
|
245
|
+
arch: cfg.model_type || configJson.model_type || (configJson.architectures ? configJson.architectures[0] : null),
|
|
246
|
+
name: modelId || configJson._name_or_path || cfg._name_or_path || null,
|
|
247
|
+
quant: null,
|
|
248
|
+
layers: cfg.num_hidden_layers ?? null,
|
|
249
|
+
queryHeads,
|
|
250
|
+
kvHeads: cfg.num_key_value_heads ?? queryHeads,
|
|
251
|
+
headDim,
|
|
252
|
+
hiddenSize,
|
|
253
|
+
contextLength: cfg.max_position_embeddings ?? null,
|
|
254
|
+
expertCount: cfg.num_local_experts ?? cfg.num_experts ?? null,
|
|
255
|
+
swa: Number.isFinite(Number(cfg.sliding_window)) && Number(cfg.sliding_window) > 0
|
|
256
|
+
? { windowTokens: Number(cfg.sliding_window) }
|
|
257
|
+
: null,
|
|
258
|
+
paramCount: null,
|
|
259
|
+
vision: isVlm ? (describeVisionConfig(configJson.vision_config || cfg.vision_config) || { encoderParamsB: null, tokensPerImage: 576 }) : null,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function candidateGgufDirectories() {
|
|
264
|
+
const home = os.homedir();
|
|
265
|
+
const dirs = [];
|
|
266
|
+
if (process.env.LMSTUDIO_MODELS_DIR) dirs.push(process.env.LMSTUDIO_MODELS_DIR);
|
|
267
|
+
dirs.push(path.join(home, '.lmstudio', 'models'));
|
|
268
|
+
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
|
|
269
|
+
dirs.push(path.join(process.env.LOCALAPPDATA, 'LM Studio', 'models'));
|
|
270
|
+
}
|
|
271
|
+
if (process.platform === 'linux') {
|
|
272
|
+
dirs.push(path.join(home, '.cache', 'lm-studio', 'models'));
|
|
273
|
+
}
|
|
274
|
+
return [...new Set(dirs)];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function listGgufFiles(dir, depth = 0, limit = 200, acc = []) {
|
|
278
|
+
if (depth > 4 || acc.length >= limit) return acc;
|
|
279
|
+
let entries = [];
|
|
280
|
+
try {
|
|
281
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
282
|
+
} catch {
|
|
283
|
+
return acc;
|
|
284
|
+
}
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
if (acc.length >= limit) break;
|
|
287
|
+
const full = path.join(dir, entry.name);
|
|
288
|
+
if (entry.isDirectory()) {
|
|
289
|
+
listGgufFiles(full, depth + 1, limit, acc);
|
|
290
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.gguf')) {
|
|
291
|
+
acc.push(full);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return acc;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let cachedGgufs = null;
|
|
298
|
+
let lastGgufScanTime = 0;
|
|
299
|
+
const GGUF_SCAN_TTL_MS = 30_000;
|
|
300
|
+
|
|
301
|
+
export function listInstalledGgufs(force = false) {
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
if (!force && cachedGgufs && (now - lastGgufScanTime < GGUF_SCAN_TTL_MS)) {
|
|
304
|
+
return [...cachedGgufs];
|
|
305
|
+
}
|
|
306
|
+
const dirs = candidateGgufDirectories();
|
|
307
|
+
const models = [];
|
|
308
|
+
const seenPaths = new Set();
|
|
309
|
+
for (const dir of dirs) {
|
|
310
|
+
const files = listGgufFiles(dir);
|
|
311
|
+
for (const f of files) {
|
|
312
|
+
if (seenPaths.has(f)) continue;
|
|
313
|
+
seenPaths.add(f);
|
|
314
|
+
try {
|
|
315
|
+
const stat = fs.statSync(f);
|
|
316
|
+
const fileName = path.basename(f);
|
|
317
|
+
const isMmproj = fileName.toLowerCase().startsWith('mmproj-');
|
|
318
|
+
const quant = extractQuantFromName(fileName);
|
|
319
|
+
const relDir = path.basename(path.dirname(f));
|
|
320
|
+
const label = relDir && relDir !== 'models' ? `${relDir} (${fileName})` : fileName;
|
|
321
|
+
models.push({
|
|
322
|
+
path: f,
|
|
323
|
+
fileName,
|
|
324
|
+
label,
|
|
325
|
+
fileSizeBytes: stat.size,
|
|
326
|
+
sizeGb: Number((stat.size / (1024 ** 3)).toFixed(2)),
|
|
327
|
+
quant,
|
|
328
|
+
isMmproj,
|
|
329
|
+
});
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
cachedGgufs = models;
|
|
334
|
+
lastGgufScanTime = now;
|
|
335
|
+
return [...models];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function stripProviderPrefix(modelName) {
|
|
339
|
+
return String(modelName || '').toLowerCase().split('/').pop().replace(/[^a-z0-9.-]/g, '');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function fetchJson(url, timeoutMs = 4000) {
|
|
343
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), headers: { 'User-Agent': 'kiku-agent-vram' } });
|
|
344
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
345
|
+
return res.json();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Resolve the architecture configuration for a target model.
|
|
350
|
+
*
|
|
351
|
+
* @param {object} options
|
|
352
|
+
* @param {string} [options.modelPath] Path to a .gguf checkpoint (parsed directly).
|
|
353
|
+
* @param {string} [options.modelName] Model id (registry / LM Studio dir / HF lookup).
|
|
354
|
+
* @param {object} [options.overrides] Explicit geometry, e.g. { layers, kvHeads, headDim,
|
|
355
|
+
* hiddenSize, queryHeads, paramsB, contextLength, quant }.
|
|
356
|
+
* @param {boolean} [options.allowNetwork=true] Permit HuggingFace config.json lookups.
|
|
357
|
+
* @returns {Promise<{source: string, config: object|null, approx: boolean, error?: string}>}
|
|
358
|
+
*/
|
|
359
|
+
export async function resolveModelConfig({ modelPath = null, modelName = null, overrides = {}, allowNetwork = true } = {}) {
|
|
360
|
+
const overrideConfig = buildOverrideConfig(overrides);
|
|
361
|
+
const completeOverride = overrideConfig?.layers && overrideConfig.kvHeads &&
|
|
362
|
+
(overrideConfig.headDim || (overrideConfig.hiddenSize && overrideConfig.queryHeads)) &&
|
|
363
|
+
(overrideConfig.paramCount || overrideConfig.paramsB);
|
|
364
|
+
if (completeOverride) {
|
|
365
|
+
return { source: 'overrides', config: overrideConfig, approx: false };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (modelPath) {
|
|
369
|
+
try {
|
|
370
|
+
if (fs.existsSync(modelPath) && fs.statSync(modelPath).isFile() && modelPath.toLowerCase().endsWith('.gguf')) {
|
|
371
|
+
const parsed = parseGgufMetadata(modelPath);
|
|
372
|
+
if (parsed && !parsed.error) {
|
|
373
|
+
return { source: 'gguf', config: parsed, approx: false };
|
|
374
|
+
}
|
|
375
|
+
return { source: 'gguf', config: null, approx: false, error: parsed?.error || 'Unreadable GGUF header' };
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
return { source: 'gguf', config: null, approx: false, error: err.message };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const quantFromName = extractQuantFromName(modelName || modelPath || '');
|
|
383
|
+
|
|
384
|
+
if (modelName) {
|
|
385
|
+
const needle = stripProviderPrefix(modelName);
|
|
386
|
+
const requestedQuant = overrides.quant ? (extractQuantFromName(overrides.quant) || String(overrides.quant).toLowerCase()) : null;
|
|
387
|
+
const quantNeedle = extractQuantFromName(needle) || requestedQuant;
|
|
388
|
+
for (const dir of candidateGgufDirectories()) {
|
|
389
|
+
const files = listGgufFiles(dir);
|
|
390
|
+
const matches = files.filter(f => stripProviderPrefix(f).includes(needle));
|
|
391
|
+
if (matches.length === 0) continue;
|
|
392
|
+
const preferred = (quantNeedle && matches.find(f => {
|
|
393
|
+
const fileQuant = extractQuantFromName(f);
|
|
394
|
+
return fileQuant === quantNeedle || f.toLowerCase().includes(String(quantNeedle).toLowerCase());
|
|
395
|
+
})) || matches[0];
|
|
396
|
+
const parsed = parseGgufMetadata(preferred);
|
|
397
|
+
if (parsed && !parsed.error) {
|
|
398
|
+
return { source: 'gguf', config: parsed, approx: false };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (modelName && allowNetwork && /^[^/\s]+\/[^/\s]+$/.test(modelName)) {
|
|
404
|
+
try {
|
|
405
|
+
const [configJson, apiInfo] = await Promise.all([
|
|
406
|
+
fetchJson(`https://huggingface.co/${modelName}/resolve/main/config.json`).catch(() => null),
|
|
407
|
+
fetchJson(`https://huggingface.co/api/models/${modelName}`).catch(() => null),
|
|
408
|
+
]);
|
|
409
|
+
if (configJson) {
|
|
410
|
+
const config = parseHfConfigJson(configJson, modelName);
|
|
411
|
+
if (config) {
|
|
412
|
+
const total = apiInfo?.safetensors?.total;
|
|
413
|
+
if (Number.isFinite(Number(total)) && Number(total) > 0) config.paramCount = Number(total);
|
|
414
|
+
if (apiInfo?.safetensors?.parameters && !total) {
|
|
415
|
+
const sum = Object.values(apiInfo.safetensors.parameters).reduce((a, b) => a + Number(b || 0), 0);
|
|
416
|
+
if (sum > 0) config.paramCount = sum;
|
|
417
|
+
}
|
|
418
|
+
if (quantFromName) config.quant = quantFromName;
|
|
419
|
+
return { source: 'hf', config, approx: config.paramCount === null };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const registryEntry = modelName ? findRegistryEntry(modelName) : null;
|
|
428
|
+
if (registryEntry) {
|
|
429
|
+
return {
|
|
430
|
+
source: 'registry',
|
|
431
|
+
approx: true,
|
|
432
|
+
config: {
|
|
433
|
+
modelId: modelName,
|
|
434
|
+
arch: null,
|
|
435
|
+
name: registryEntry.label,
|
|
436
|
+
quant: quantFromName ?? registryEntry.defaultQuant ?? null,
|
|
437
|
+
layers: registryEntry.layers,
|
|
438
|
+
queryHeads: registryEntry.queryHeads,
|
|
439
|
+
kvHeads: registryEntry.kvHeads,
|
|
440
|
+
headDim: registryEntry.headDim,
|
|
441
|
+
hiddenSize: registryEntry.hiddenSize,
|
|
442
|
+
contextLength: registryEntry.contextLength,
|
|
443
|
+
expertCount: null,
|
|
444
|
+
swa: registryEntry.swa || null,
|
|
445
|
+
paramCount: Math.round(registryEntry.paramsB * 1e9),
|
|
446
|
+
paramsB: registryEntry.paramsB,
|
|
447
|
+
vision: registryEntry.vision || null,
|
|
448
|
+
registryNotes: registryEntry.notes || null,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (overrideConfig) {
|
|
454
|
+
return { source: 'overrides', config: overrideConfig, approx: true };
|
|
455
|
+
}
|
|
456
|
+
return { source: 'none', config: null, approx: true };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function buildOverrideConfig(overrides = {}) {
|
|
460
|
+
const keys = ['layers', 'queryHeads', 'kvHeads', 'headDim', 'hiddenSize', 'paramCount', 'paramsB', 'contextLength', 'quant', 'expertCount'];
|
|
461
|
+
const picked = {};
|
|
462
|
+
let found = false;
|
|
463
|
+
for (const key of keys) {
|
|
464
|
+
if (overrides[key] !== undefined && overrides[key] !== null && overrides[key] !== '') {
|
|
465
|
+
picked[key] = overrides[key];
|
|
466
|
+
found = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!found) return null;
|
|
470
|
+
return {
|
|
471
|
+
modelId: overrides.modelId || null,
|
|
472
|
+
name: overrides.modelName || null,
|
|
473
|
+
arch: null,
|
|
474
|
+
quant: picked.quant ?? null,
|
|
475
|
+
layers: picked.layers ?? null,
|
|
476
|
+
queryHeads: picked.queryHeads ?? null,
|
|
477
|
+
kvHeads: picked.kvHeads ?? picked.queryHeads ?? null,
|
|
478
|
+
headDim: picked.headDim ?? (picked.hiddenSize && picked.queryHeads ? Math.round(picked.hiddenSize / picked.queryHeads) : null),
|
|
479
|
+
hiddenSize: picked.hiddenSize ?? null,
|
|
480
|
+
contextLength: picked.contextLength ?? null,
|
|
481
|
+
expertCount: picked.expertCount ?? null,
|
|
482
|
+
swa: overrides.swa || null,
|
|
483
|
+
paramCount: picked.paramCount ?? (picked.paramsB ? Math.round(Number(picked.paramsB) * 1e9) : null),
|
|
484
|
+
paramsB: picked.paramsB ?? null,
|
|
485
|
+
vision: overrides.vision || null,
|
|
486
|
+
};
|
|
487
|
+
}
|