llm-tuner 0.4.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-tuner",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Detects your hardware and finds the fastest configuration to run local LLMs with LM Studio or Ollama.",
5
5
  "keywords": [
6
6
  "llm",
@@ -170,9 +170,10 @@ export const lmstudio = {
170
170
 
171
171
  async benchmarkViaLms(ctx, model, candidate, opts) {
172
172
  const bin = ctx.detection.bin;
173
- const restore = await this.writeModelConfig(ctx, model.key, candidate);
173
+ const modelKey = model.key || model;
174
+ const restore = await this.writeModelConfig(ctx, modelKey, candidate, model.meta);
174
175
  try {
175
- const args = ['load', model.key, '-y', '--identifier', 'llm-tuner-bench', '-c', String(candidate.ctx), '--gpu', candidate.fullOffload ? 'max' : String(candidate.gpuLayers / candidate.totalLayers)];
176
+ const args = ['load', modelKey, '-y', '--identifier', 'llm-tuner-bench', '-c', String(candidate.ctx), '--gpu', candidate.fullOffload ? 'max' : String(candidate.gpuLayers / candidate.totalLayers)];
176
177
  const r = await run(bin, args, { timeout: 600_000 });
177
178
  if (r.code !== 0) return { ok: false, error: (r.stderr || r.stdout).slice(-400) };
178
179
  return await runApiBenchmark({ kind: 'lmstudio', baseUrl: 'http://127.0.0.1:1234', model: 'llm-tuner-bench', ...opts });
@@ -182,17 +183,52 @@ export const lmstudio = {
182
183
  }
183
184
  },
184
185
 
185
- configPaths(ctx, key) {
186
+ /** Resolve the concrete indexedModelIdentifier for a given key or file. */
187
+ async resolveConcreteIdentifier(ctx, key, meta) {
188
+ if (!key) return key;
189
+ if (key.includes('/') && key.endsWith('.gguf')) return key;
190
+ const home = ctx.detection?.home || (await lmsHome());
191
+ const idx = await readJson(path.join(home, '.internal', 'model-index-cache.json'), { models: [] });
192
+ const entries = idx.models || [];
193
+ const byId = (id) => entries.find((e) => e.indexedModelIdentifier === id && !e.virtual);
194
+ let entry = null;
195
+ if (meta?.file) {
196
+ entry = entries.find((e) => e.entryPoint?.absPath === meta.file || (e.concreteModelDirAbsolutePath && meta.file.startsWith(e.concreteModelDirAbsolutePath)));
197
+ }
198
+ if (!entry) {
199
+ entry = entries.find((e) => e.indexedModelIdentifier === key || e.defaultIdentifier === key || e.virtual?.baseChain?.[0] === key);
200
+ }
201
+ const concrete = entry?.virtual?.concreteModelIndexedModelIdentifier || entry?.virtual?.baseChain?.at?.(-1);
202
+ if (concrete) entry = byId(concrete) || entry;
203
+ if (entry?.indexedModelIdentifier) return entry.indexedModelIdentifier;
204
+ if (entry?.entryPoint?.relPath) return entry.entryPoint.relPath;
205
+ if (ctx.detection?.bin) {
206
+ const r = await run(ctx.detection.bin, ['ls', '--json']);
207
+ if (r.code === 0) {
208
+ try {
209
+ const list = JSON.parse(r.stdout);
210
+ const found = (Array.isArray(list) ? list : []).find((m) => m && (m.modelKey === key || m.path === key || m.indexedModelIdentifier === key));
211
+ if (found?.indexedModelIdentifier || found?.path) return found.indexedModelIdentifier || found.path;
212
+ } catch { /* ignore */ }
213
+ }
214
+ }
215
+ return key;
216
+ },
217
+
218
+ async configPaths(ctx, key, meta) {
186
219
  const internal = path.join(ctx.detection.home, '.internal');
220
+ const concrete = key ? await this.resolveConcreteIdentifier(ctx, key, meta) : '';
187
221
  return {
188
- modelConfig: path.join(internal, 'user-concrete-model-default-config', `${key}.json`),
222
+ modelConfig: path.join(internal, 'user-concrete-model-default-config', `${concrete || key}.json`),
223
+ aliasConfig: concrete && concrete !== key ? path.join(internal, 'user-concrete-model-default-config', `${key}.json`) : null,
189
224
  hardwareConfig: path.join(internal, 'hardware-config.json'),
190
225
  };
191
226
  },
192
227
 
193
- async writeModelConfig(ctx, key, c) {
194
- const file = this.configPaths(ctx, key).modelConfig;
195
- const cur = (await readJson(file)) || { preset: '', operation: { fields: [] }, load: { fields: [] } };
228
+ async writeModelConfig(ctx, key, c, meta) {
229
+ const paths = await this.configPaths(ctx, key, meta);
230
+ const file = paths.modelConfig;
231
+ const cur = (await readJson(file)) || (paths.aliasConfig ? (await readJson(paths.aliasConfig)) : null) || { preset: '', operation: { fields: [] }, load: { fields: [] } };
196
232
  const fields = new Map((cur.load?.fields || []).map((f) => [f.key, f.value]));
197
233
  const set = (k, v) => fields.set(k, v);
198
234
  set('llm.load.contextLength', c.ctx);
@@ -207,15 +243,24 @@ export const lmstudio = {
207
243
  set('llm.load.llama.kCacheQuantizationType', { checked: c.kvType !== 'f16', value: c.kvType });
208
244
  set('llm.load.llama.vCacheQuantizationType', { checked: c.kvType !== 'f16', value: c.kvType });
209
245
  if (c.cpuMoeLayers) set('llm.load.numCpuExpertLayersRatio', +(c.cpuMoeLayers / (c.totalLayers - 1)).toFixed(3));
210
- const next = { ...cur, load: { ...(cur.load || {}), fields: [...fields].map(([key, value]) => ({ key, value })) } };
211
- if (JSON.stringify(next) === JSON.stringify(cur)) return { file, backup: null, unchanged: true };
246
+ const next = { ...cur, load: { ...(cur.load || {}), fields: [...fields].map(([k, value]) => ({ key: k, value })) } };
247
+ const fileExists = await exists(file);
248
+ if (fileExists && JSON.stringify(next) === JSON.stringify(cur)) {
249
+ if (paths.aliasConfig && !(await exists(paths.aliasConfig))) {
250
+ await writeJsonWithBackup(paths.aliasConfig, next).catch(() => null);
251
+ }
252
+ return { file, backup: null, unchanged: true };
253
+ }
212
254
  const backup = await writeJsonWithBackup(file, next);
255
+ if (paths.aliasConfig) {
256
+ await writeJsonWithBackup(paths.aliasConfig, next).catch(() => null);
257
+ }
213
258
  return { file, backup };
214
259
  },
215
260
 
216
261
  /** hardware-config.json is superjson: {"json":[[backendName,{fields:[...]}]],"meta":{...}} */
217
262
  async writeHardwareConfig(ctx, c) {
218
- const file = this.configPaths(ctx, '').hardwareConfig;
263
+ const file = (await this.configPaths(ctx, '')).hardwareConfig;
219
264
  const be = await this.backend(ctx);
220
265
  const backendName = be?.name || 'llama.cpp';
221
266
  const cur = (await readJson(file)) || { json: [], meta: { values: ['map'] } };
@@ -269,7 +314,7 @@ export const lmstudio = {
269
314
 
270
315
  /** Does hardware-config.json already match what this candidate needs? */
271
316
  async isHardwareApplied(ctx, c) {
272
- const file = this.configPaths(ctx, '').hardwareConfig;
317
+ const file = (await this.configPaths(ctx, '')).hardwareConfig;
273
318
  const be = await this.backend(ctx);
274
319
  const cur = await readJson(file);
275
320
  const entry = (cur?.json || []).find(([name]) => name === (be?.name || 'llama.cpp'));
@@ -288,7 +333,8 @@ export const lmstudio = {
288
333
  * overwritten by the app on exit, so the app is closed while it changes.
289
334
  */
290
335
  async apply(ctx, model, c, { closeApp = true, relaunch = true, dryRun = false } = {}) {
291
- const paths = this.configPaths(ctx, model.key);
336
+ const modelKey = model.key || model;
337
+ const paths = await this.configPaths(ctx, modelKey, model.meta);
292
338
  const hardwareOk = await this.isHardwareApplied(ctx, c);
293
339
  const det = await this.detect();
294
340
  const plan = { files: hardwareOk ? [paths.modelConfig] : [paths.modelConfig, paths.hardwareConfig], restartsApp: !hardwareOk && det.appRunning, candidate: c };
@@ -304,7 +350,7 @@ export const lmstudio = {
304
350
  if (b.backup) backups.push(b.backup);
305
351
  changes.push({ code: 'changes.lmsHardware' });
306
352
  }
307
- const a = await this.writeModelConfig(ctx, model.key, c);
353
+ const a = await this.writeModelConfig(ctx, modelKey, c, model.meta);
308
354
  if (a.backup) backups.push(a.backup);
309
355
  if (!a.unchanged) changes.push(c.fullOffload
310
356
  ? { code: 'changes.lmsModelFull', params: { ctx: c.ctx, kvType: c.kvType, threads: c.threads } }
@@ -317,16 +363,23 @@ export const lmstudio = {
317
363
  },
318
364
 
319
365
  /** Load the model the normal way (using the saved config) and optionally measure it. */
320
- async load(ctx, model, { depthTokens = 0, genTokens = 200, onProgress } = {}) {
366
+ async load(ctx, model, { candidate, depthTokens = 0, genTokens = 200, onProgress } = {}) {
321
367
  const bin = ctx.detection.bin;
322
368
  if (!bin) return benchError('bench.lmsUnavailable');
323
369
  await run(bin, ['server', 'start']);
324
370
  await run(bin, ['unload', '--all']);
325
371
  onProgress?.({ phase: 'load' });
326
- const r = await run(bin, ['load', model.key, '-y'], { timeout: 600_000 });
372
+ const modelKey = model.key || model;
373
+ const args = ['load', modelKey, '-y'];
374
+ if (candidate?.ctx) args.push('-c', String(candidate.ctx));
375
+ if (candidate?.gpuLayers != null && candidate?.totalLayers) {
376
+ args.push('--gpu', candidate.fullOffload ? 'max' : String(+(candidate.gpuLayers / candidate.totalLayers).toFixed(3)));
377
+ }
378
+ if (candidate?.parallel) args.push('--parallel', String(candidate.parallel));
379
+ const r = await run(bin, args, { timeout: 600_000 });
327
380
  if (r.code !== 0) return { ok: false, error: (r.stderr || r.stdout).slice(-400) };
328
381
  const url = ctx.detection?.apiUrl || apiUrl();
329
- return runApiBenchmark({ kind: 'lmstudio', baseUrl: url, model: model.key, depthTokens, genTokens, onProgress });
382
+ return runApiBenchmark({ kind: 'lmstudio', baseUrl: url, model: modelKey, depthTokens, genTokens, onProgress });
330
383
  },
331
384
 
332
385
  async chat(ctx, model, messages) {