llm-checker 3.4.0 → 3.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/README.md CHANGED
@@ -133,9 +133,22 @@ llm-checker ai-run --calibrated --category coding --prompt "Refactor this functi
133
133
 
134
134
  LLM Checker is published in all primary channels:
135
135
 
136
- - npm (latest): [`llm-checker@latest`](https://www.npmjs.com/package/llm-checker)
136
+ - npm (latest, recommended): [`llm-checker@latest`](https://www.npmjs.com/package/llm-checker)
137
137
  - GitHub Releases: [Release history](https://github.com/Pavelevich/llm-checker/releases)
138
- - GitHub Packages: [`@pavelevich/llm-checker`](https://github.com/users/Pavelevich/packages/npm/package/llm-checker)
138
+ - GitHub Packages (legacy mirror, may lag): [`@pavelevich/llm-checker`](https://github.com/users/Pavelevich/packages/npm/package/llm-checker)
139
+
140
+ ### Important: Use npm for Latest Builds
141
+
142
+ If you need the newest release, install from npm (`llm-checker`), not the scoped GitHub Packages mirror.
143
+
144
+ If you installed `@pavelevich/llm-checker` and version looks old:
145
+
146
+ ```bash
147
+ npm uninstall -g @pavelevich/llm-checker
148
+ npm install -g llm-checker@latest
149
+ hash -r
150
+ llm-checker --version
151
+ ```
139
152
 
140
153
  ### v3.3.0 Highlights
141
154
 
@@ -148,7 +161,9 @@ LLM Checker is published in all primary channels:
148
161
  - Hardened Jetson CUDA detection to avoid false CPU-only fallback.
149
162
  - Documentation reorganized under `docs/` with clearer onboarding paths.
150
163
 
151
- ### Optional: Install from GitHub Packages
164
+ ### Optional (Legacy): Install from GitHub Packages
165
+
166
+ Use this only if you explicitly need GitHub Packages. It may not match npm latest.
152
167
 
153
168
  ```bash
154
169
  # 1) Configure registry + token (PAT with read:packages)
@@ -261,6 +276,11 @@ Once connected, Claude can use these tools:
261
276
  | `installed` | Rank your already-downloaded Ollama models |
262
277
  | `search` | Search the Ollama model catalog with filters |
263
278
  | `smart_recommend` | Advanced recommendations using the full scoring engine |
279
+ | `ollama_plan` | Build a capacity plan for local models with recommended context/parallel/memory settings |
280
+ | `ollama_plan_env` | Return ready-to-paste `export ...` env vars from the recommended or fallback plan profile |
281
+ | `policy_validate` | Validate a policy file against the v1 schema and return structured validation output |
282
+ | `audit_export` | Run policy compliance export (`json`/`csv`/`sarif`/`all`) for `check` or `recommend` flows |
283
+ | `calibrate` | Generate calibration artifacts from a prompt suite with typed MCP inputs |
264
284
 
265
285
  **Ollama Management:**
266
286
 
@@ -281,6 +301,8 @@ Once connected, Claude can use these tools:
281
301
  | `cleanup_models` | Analyze installed models — find redundancies, cloud-only models, oversized models, and upgrade candidates |
282
302
  | `project_recommend` | Scan a project directory (languages, frameworks, size) and recommend the best model for that codebase |
283
303
  | `ollama_monitor` | Real-time system status: RAM usage, loaded models, memory headroom analysis |
304
+ | `cli_help` | List all allowlisted CLI commands exposed through MCP |
305
+ | `cli_exec` | Execute any allowlisted `llm-checker` CLI command with custom args (policy/audit/calibrate/sync/ai-run/etc.) |
284
306
 
285
307
  ### Example Prompts
286
308
 
@@ -4155,8 +4155,8 @@ program
4155
4155
  }
4156
4156
 
4157
4157
  if (backend === 'cuda' && info.info) {
4158
- console.log(` Driver: ${info.info.driver}`);
4159
- console.log(` CUDA: ${info.info.cuda}`);
4158
+ console.log(` Driver: ${info.info.driver || 'unknown'}`);
4159
+ console.log(` CUDA: ${info.info.cuda || 'unknown'}`);
4160
4160
  console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
4161
4161
  for (const gpu of info.info.gpus) {
4162
4162
  console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
@@ -101,13 +101,89 @@ function nsToSec(ns) {
101
101
  return (ns / 1e9).toFixed(2);
102
102
  }
103
103
 
104
+ function tryParseJSON(text) {
105
+ try {
106
+ return JSON.parse(text);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function formatExportBlock(envObject) {
113
+ if (!envObject || typeof envObject !== "object") return "";
114
+ const entries = Object.entries(envObject).filter(([, value]) => value !== undefined && value !== null);
115
+ if (entries.length === 0) return "";
116
+ return entries
117
+ .map(([key, value]) => `export ${key}="${String(value)}"`)
118
+ .join("\n");
119
+ }
120
+
121
+ function summarizeOllamaPlan(payload) {
122
+ if (!payload || typeof payload !== "object") return null;
123
+ const plan = payload.plan;
124
+ if (!plan || typeof plan !== "object") return null;
125
+
126
+ const selectedModels = Array.isArray(plan.models)
127
+ ? plan.models.map((model) => model?.name).filter(Boolean)
128
+ : [];
129
+ const hardware = plan.hardware || {};
130
+ const memory = plan.memory || {};
131
+ const recommendation = plan.recommendation || {};
132
+ const risk = plan.risk || {};
133
+
134
+ const lines = [
135
+ "OLLAMA CAPACITY PLAN",
136
+ `Hardware: ${hardware.backendName || hardware.backend || "unknown"}`,
137
+ `Models: ${selectedModels.length > 0 ? selectedModels.join(", ") : "none selected"}`,
138
+ "",
139
+ "Recommended envelope:",
140
+ ` Context: ${plan.envelope?.context?.recommended ?? "?"}`,
141
+ ` Parallel: ${plan.envelope?.parallel?.recommended ?? "?"}`,
142
+ ` Loaded models: ${plan.envelope?.loaded_models?.recommended ?? "?"}`,
143
+ ` Estimated memory: ${memory.recommendedEstimatedGB ?? "?"}GB / ${memory.budgetGB ?? "?"}GB (${memory.utilizationPercent ?? "?"}%)`,
144
+ ` Risk: ${(risk.level || "unknown").toUpperCase()} (${risk.score ?? "?"}/100)`,
145
+ ];
146
+
147
+ if (recommendation && Object.keys(recommendation).length > 0) {
148
+ lines.push("");
149
+ lines.push("Recommended env vars:");
150
+ if (recommendation.num_ctx !== undefined) lines.push(` export OLLAMA_NUM_CTX="${recommendation.num_ctx}"`);
151
+ if (recommendation.num_parallel !== undefined) lines.push(` export OLLAMA_NUM_PARALLEL="${recommendation.num_parallel}"`);
152
+ if (recommendation.max_loaded_models !== undefined) lines.push(` export OLLAMA_MAX_LOADED_MODELS="${recommendation.max_loaded_models}"`);
153
+ if (recommendation.max_queue !== undefined) lines.push(` export OLLAMA_MAX_QUEUE="${recommendation.max_queue}"`);
154
+ if (recommendation.keep_alive !== undefined) lines.push(` export OLLAMA_KEEP_ALIVE="${recommendation.keep_alive}"`);
155
+ if (recommendation.flash_attention !== undefined) lines.push(` export OLLAMA_FLASH_ATTENTION="${recommendation.flash_attention}"`);
156
+ }
157
+
158
+ return lines.join("\n");
159
+ }
160
+
161
+ const ALLOWED_CLI_COMMANDS = new Set([
162
+ "policy",
163
+ "audit",
164
+ "calibrate",
165
+ "check",
166
+ "ollama",
167
+ "installed",
168
+ "ollama-plan",
169
+ "recommend",
170
+ "list-models",
171
+ "ai-check",
172
+ "ai-run",
173
+ "demo",
174
+ "sync",
175
+ "search",
176
+ "smart-recommend",
177
+ "hw-detect",
178
+ ]);
179
+
104
180
  // ============================================================================
105
181
  // MCP SERVER
106
182
  // ============================================================================
107
183
 
108
184
  const server = new McpServer({
109
185
  name: "llm-checker",
110
- version: "3.2.0",
186
+ version: "3.4.0",
111
187
  });
112
188
 
113
189
  // ============================================================================
@@ -198,6 +274,352 @@ server.tool(
198
274
  }
199
275
  );
200
276
 
277
+ server.tool(
278
+ "ollama_plan",
279
+ "Build an Ollama capacity plan for selected local models and return recommended context/parallel/memory settings",
280
+ {
281
+ models: z
282
+ .array(z.string())
283
+ .optional()
284
+ .describe("Optional list of model tags/families to include (default: all local models)"),
285
+ ctx: z.number().int().positive().optional().describe("Target context window in tokens"),
286
+ concurrency: z.number().int().positive().optional().describe("Target parallel request count"),
287
+ objective: z
288
+ .enum(["latency", "balanced", "throughput"])
289
+ .optional()
290
+ .describe("Optimization objective"),
291
+ reserve_gb: z.number().min(0).optional().describe("Memory reserve in GB for OS/background workloads"),
292
+ },
293
+ async ({ models, ctx, concurrency, objective, reserve_gb }) => {
294
+ const args = ["ollama-plan", "--json"];
295
+ if (Array.isArray(models) && models.length > 0) args.push("--models", ...models);
296
+ if (ctx !== undefined) args.push("--ctx", String(ctx));
297
+ if (concurrency !== undefined) args.push("--concurrency", String(concurrency));
298
+ if (objective) args.push("--objective", objective);
299
+ if (reserve_gb !== undefined) args.push("--reserve-gb", String(reserve_gb));
300
+
301
+ const result = await run(args, 180000);
302
+ const payload = tryParseJSON(result);
303
+
304
+ if (!payload) {
305
+ return {
306
+ content: [{ type: "text", text: result }],
307
+ };
308
+ }
309
+
310
+ const summary = summarizeOllamaPlan(payload);
311
+ const output = summary
312
+ ? `${summary}\n\nRAW JSON:\n${JSON.stringify(payload, null, 2)}`
313
+ : JSON.stringify(payload, null, 2);
314
+
315
+ return {
316
+ content: [{ type: "text", text: output }],
317
+ };
318
+ }
319
+ );
320
+
321
+ server.tool(
322
+ "ollama_plan_env",
323
+ "Return shell export commands from an Ollama capacity plan (recommended or fallback profile)",
324
+ {
325
+ profile: z
326
+ .enum(["recommended", "fallback"])
327
+ .optional()
328
+ .describe("Which profile to return (default: recommended)"),
329
+ models: z
330
+ .array(z.string())
331
+ .optional()
332
+ .describe("Optional list of model tags/families to include (default: all local models)"),
333
+ ctx: z.number().int().positive().optional().describe("Target context window in tokens"),
334
+ concurrency: z.number().int().positive().optional().describe("Target parallel request count"),
335
+ objective: z
336
+ .enum(["latency", "balanced", "throughput"])
337
+ .optional()
338
+ .describe("Optimization objective"),
339
+ reserve_gb: z.number().min(0).optional().describe("Memory reserve in GB for OS/background workloads"),
340
+ },
341
+ async ({ profile, models, ctx, concurrency, objective, reserve_gb }) => {
342
+ const args = ["ollama-plan", "--json"];
343
+ if (Array.isArray(models) && models.length > 0) args.push("--models", ...models);
344
+ if (ctx !== undefined) args.push("--ctx", String(ctx));
345
+ if (concurrency !== undefined) args.push("--concurrency", String(concurrency));
346
+ if (objective) args.push("--objective", objective);
347
+ if (reserve_gb !== undefined) args.push("--reserve-gb", String(reserve_gb));
348
+
349
+ const result = await run(args, 180000);
350
+ const payload = tryParseJSON(result);
351
+ if (!payload?.plan) {
352
+ return {
353
+ content: [{ type: "text", text: `Failed to parse ollama-plan output:\n${result}` }],
354
+ isError: true,
355
+ };
356
+ }
357
+
358
+ const selectedProfile = profile || "recommended";
359
+ const plan = payload.plan;
360
+ let envValues = null;
361
+
362
+ if (selectedProfile === "fallback") {
363
+ const fallback = plan.fallback || {};
364
+ envValues = {
365
+ OLLAMA_NUM_CTX: fallback.num_ctx,
366
+ OLLAMA_NUM_PARALLEL: fallback.num_parallel,
367
+ OLLAMA_MAX_LOADED_MODELS: fallback.max_loaded_models,
368
+ };
369
+ } else {
370
+ envValues = plan.shell?.env || null;
371
+ if (!envValues) {
372
+ const recommendation = plan.recommendation || {};
373
+ envValues = {
374
+ OLLAMA_NUM_CTX: recommendation.num_ctx,
375
+ OLLAMA_NUM_PARALLEL: recommendation.num_parallel,
376
+ OLLAMA_MAX_LOADED_MODELS: recommendation.max_loaded_models,
377
+ OLLAMA_MAX_QUEUE: recommendation.max_queue,
378
+ OLLAMA_KEEP_ALIVE: recommendation.keep_alive,
379
+ OLLAMA_FLASH_ATTENTION: recommendation.flash_attention,
380
+ };
381
+ }
382
+ }
383
+
384
+ const exports = formatExportBlock(envValues);
385
+ if (!exports) {
386
+ return {
387
+ content: [{ type: "text", text: "No environment values available for this plan/profile." }],
388
+ isError: true,
389
+ };
390
+ }
391
+
392
+ return {
393
+ content: [
394
+ {
395
+ type: "text",
396
+ text: [`PROFILE: ${selectedProfile.toUpperCase()}`, "", exports].join("\n"),
397
+ },
398
+ ],
399
+ };
400
+ }
401
+ );
402
+
403
+ server.tool(
404
+ "cli_help",
405
+ "List all llm-checker CLI commands exposed via cli_exec",
406
+ {},
407
+ async () => {
408
+ const commands = [...ALLOWED_CLI_COMMANDS].sort();
409
+ const lines = [
410
+ "Available commands for cli_exec:",
411
+ ...commands.map((command) => ` - ${command}`),
412
+ "",
413
+ "Examples:",
414
+ ' cli_exec command="ollama-plan" args=["--json"]',
415
+ ' cli_exec command="policy" args=["validate","--file","policy.yaml","--json"]',
416
+ ' cli_exec command="search" args=["qwen","--use-case","coding","--limit","5"]',
417
+ ];
418
+ return { content: [{ type: "text", text: lines.join("\n") }] };
419
+ }
420
+ );
421
+
422
+ server.tool(
423
+ "cli_exec",
424
+ "Execute any supported llm-checker CLI command (allowlisted) with custom arguments",
425
+ {
426
+ command: z.string().describe("Top-level command (use cli_help to list allowed commands)"),
427
+ args: z
428
+ .array(z.string())
429
+ .optional()
430
+ .describe("Additional CLI args, exactly as used in terminal (without shell quoting)"),
431
+ timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Execution timeout in milliseconds"),
432
+ },
433
+ async ({ command, args, timeout_ms }) => {
434
+ const trimmedCommand = String(command || "").trim();
435
+ if (!ALLOWED_CLI_COMMANDS.has(trimmedCommand)) {
436
+ return {
437
+ content: [
438
+ {
439
+ type: "text",
440
+ text: `Unsupported command "${trimmedCommand}". Use cli_help to list allowed commands.`,
441
+ },
442
+ ],
443
+ isError: true,
444
+ };
445
+ }
446
+
447
+ const safeArgs = Array.isArray(args) ? args : [];
448
+ if (safeArgs.length > 100) {
449
+ return {
450
+ content: [{ type: "text", text: "Too many arguments. Limit is 100." }],
451
+ isError: true,
452
+ };
453
+ }
454
+
455
+ const result = await run([trimmedCommand, ...safeArgs], timeout_ms || 180000);
456
+ return { content: [{ type: "text", text: result }] };
457
+ }
458
+ );
459
+
460
+ server.tool(
461
+ "policy_validate",
462
+ "Validate a policy file against the v1 schema and return structured validation output",
463
+ {
464
+ file: z.string().optional().describe("Policy file path (default: policy.yaml)"),
465
+ },
466
+ async ({ file }) => {
467
+ const args = ["policy", "validate", "--json"];
468
+ if (file) args.push("--file", file);
469
+
470
+ const result = await run(args, 120000);
471
+ const payload = tryParseJSON(result);
472
+ if (!payload) {
473
+ return {
474
+ content: [{ type: "text", text: result }],
475
+ };
476
+ }
477
+
478
+ const status = payload.valid ? "VALID" : "INVALID";
479
+ const header = [
480
+ `POLICY VALIDATION: ${status}`,
481
+ `File: ${payload.file || file || "policy.yaml"}`,
482
+ `Errors: ${payload.errorCount ?? (Array.isArray(payload.errors) ? payload.errors.length : 0)}`,
483
+ ].join("\n");
484
+
485
+ return {
486
+ content: [{ type: "text", text: `${header}\n\n${JSON.stringify(payload, null, 2)}` }],
487
+ isError: !payload.valid,
488
+ };
489
+ }
490
+ );
491
+
492
+ server.tool(
493
+ "audit_export",
494
+ "Run policy compliance audit export (json/csv/sarif/all) for check/recommend flows",
495
+ {
496
+ policy: z.string().describe("Policy file path"),
497
+ command: z
498
+ .enum(["check", "recommend"])
499
+ .optional()
500
+ .describe("Evaluation source (default: check)"),
501
+ format: z
502
+ .enum(["json", "csv", "sarif", "all"])
503
+ .optional()
504
+ .describe("Export format (default: json)"),
505
+ out: z.string().optional().describe("Output file path (single format only)"),
506
+ out_dir: z.string().optional().describe("Output directory when --out is omitted"),
507
+ use_case: z.string().optional().describe("Use case when command=check"),
508
+ category: z.string().optional().describe("Category hint when command=recommend"),
509
+ optimize: z
510
+ .enum(["balanced", "speed", "quality", "context", "coding"])
511
+ .optional()
512
+ .describe("Optimization profile when command=recommend"),
513
+ runtime: z
514
+ .enum(["ollama", "vllm", "mlx"])
515
+ .optional()
516
+ .describe("Runtime backend for check mode"),
517
+ include_cloud: z.boolean().optional().describe("Include cloud models in check-mode analysis"),
518
+ max_size: z.string().optional().describe('Maximum model size for check mode (example: "24B" or "12GB")'),
519
+ min_size: z.string().optional().describe('Minimum model size for check mode (example: "3B" or "2GB")'),
520
+ limit: z.number().int().positive().optional().describe("Model analysis limit for check mode"),
521
+ verbose: z.boolean().optional().describe("Enable verbose progress (default: true)"),
522
+ },
523
+ async ({
524
+ policy,
525
+ command,
526
+ format,
527
+ out,
528
+ out_dir,
529
+ use_case,
530
+ category,
531
+ optimize,
532
+ runtime,
533
+ include_cloud,
534
+ max_size,
535
+ min_size,
536
+ limit,
537
+ verbose,
538
+ }) => {
539
+ const args = ["audit", "export", "--policy", policy];
540
+ if (command) args.push("--command", command);
541
+ if (format) args.push("--format", format);
542
+ if (out) args.push("--out", out);
543
+ if (out_dir) args.push("--out-dir", out_dir);
544
+ if (use_case) args.push("--use-case", use_case);
545
+ if (category) args.push("--category", category);
546
+ if (optimize) args.push("--optimize", optimize);
547
+ if (runtime) args.push("--runtime", runtime);
548
+ if (include_cloud) args.push("--include-cloud");
549
+ if (max_size) args.push("--max-size", max_size);
550
+ if (min_size) args.push("--min-size", min_size);
551
+ if (limit !== undefined) args.push("--limit", String(limit));
552
+ if (verbose === false) args.push("--no-verbose");
553
+
554
+ const result = await run(args, 300000);
555
+ const hadFailure =
556
+ /audit export failed:/i.test(result) ||
557
+ /blocking violations detected/i.test(result) ||
558
+ /enforcement result:\s*blocking/i.test(result);
559
+ return {
560
+ content: [{ type: "text", text: result }],
561
+ isError: hadFailure,
562
+ };
563
+ }
564
+ );
565
+
566
+ server.tool(
567
+ "calibrate",
568
+ "Generate calibration artifacts from a JSONL prompt suite (dry-run, contract-only, or full benchmark mode)",
569
+ {
570
+ suite: z.string().describe("Prompt suite path in JSONL format"),
571
+ models: z.array(z.string()).describe("Model identifiers to include"),
572
+ output: z.string().describe("Calibration result output path (.json/.yaml/.yml)"),
573
+ runtime: z
574
+ .enum(["ollama", "vllm", "mlx"])
575
+ .optional()
576
+ .describe("Inference runtime backend"),
577
+ mode: z
578
+ .enum(["dry-run", "contract-only", "full"])
579
+ .optional()
580
+ .describe("Execution mode"),
581
+ objective: z
582
+ .enum(["speed", "quality", "balanced"])
583
+ .optional()
584
+ .describe("Calibration objective"),
585
+ policy_out: z.string().optional().describe("Optional calibration policy output path"),
586
+ warmup: z.number().int().positive().optional().describe("Warmup runs per prompt in full mode"),
587
+ iterations: z.number().int().positive().optional().describe("Measured iterations per prompt in full mode"),
588
+ timeout_ms: z.number().int().positive().optional().describe("Per-prompt timeout in full mode (ms)"),
589
+ dry_run: z.boolean().optional().describe("Shortcut flag for dry-run mode"),
590
+ },
591
+ async ({
592
+ suite,
593
+ models,
594
+ output,
595
+ runtime,
596
+ mode,
597
+ objective,
598
+ policy_out,
599
+ warmup,
600
+ iterations,
601
+ timeout_ms,
602
+ dry_run,
603
+ }) => {
604
+ const args = ["calibrate", "--suite", suite, "--models", ...models, "--output", output];
605
+ if (runtime) args.push("--runtime", runtime);
606
+ if (mode) args.push("--mode", mode);
607
+ if (objective) args.push("--objective", objective);
608
+ if (policy_out) args.push("--policy-out", policy_out);
609
+ if (warmup !== undefined) args.push("--warmup", String(warmup));
610
+ if (iterations !== undefined) args.push("--iterations", String(iterations));
611
+ if (timeout_ms !== undefined) args.push("--timeout-ms", String(timeout_ms));
612
+ if (dry_run) args.push("--dry-run");
613
+
614
+ const result = await run(args, 600000);
615
+ const hadFailure = /calibration failed:/i.test(result);
616
+ return {
617
+ content: [{ type: "text", text: result }],
618
+ isError: hadFailure,
619
+ };
620
+ }
621
+ );
622
+
201
623
  // ============================================================================
202
624
  // OLLAMA MANAGEMENT TOOLS
203
625
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-checker",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Intelligent CLI tool with AI-powered model selection that analyzes your hardware and recommends optimal LLM models for your system",
5
5
  "bin": {
6
6
  "llm-checker": "bin/cli.js",
@@ -322,7 +322,7 @@ class CUDADetector {
322
322
  const modelRaw = this.readJetsonModel();
323
323
  const model = this.normalizeJetsonModel(modelRaw);
324
324
  const cudaVersion = this.detectJetsonCudaVersion();
325
- const driverVersion = this.detectJetsonDriverVersion();
325
+ const driverVersion = this.detectJetsonDriverVersion() || 'unknown';
326
326
  const totalSystemGB = Math.max(1, Math.round(os.totalmem() / (1024 ** 3)));
327
327
  const sharedGpuMemoryGB = Math.max(1, Math.round(totalSystemGB * 0.85));
328
328
  const capabilities = this.getJetsonCapabilities(modelRaw || model);
@@ -423,11 +423,26 @@ class CUDADetector {
423
423
  }
424
424
 
425
425
  detectJetsonDriverVersion() {
426
- const versionInfo = this.readFileIfExists('/proc/driver/nvidia/version');
427
- if (!versionInfo) return null;
426
+ const driverSources = [
427
+ '/proc/driver/nvidia/version',
428
+ '/sys/module/nvidia/version'
429
+ ];
430
+
431
+ for (const source of driverSources) {
432
+ const versionInfo = this.readFileIfExists(source);
433
+ if (!versionInfo) continue;
434
+
435
+ const kernelMatch = versionInfo.match(/Kernel Module(?:\s+for\s+\w+)?\s+([0-9]+(?:\.[0-9]+){1,3})/i);
436
+ if (kernelMatch) return kernelMatch[1];
437
+
438
+ const nvrmMatch = versionInfo.match(/NVRM version:\s*.*?([0-9]+(?:\.[0-9]+){1,3})/i);
439
+ if (nvrmMatch) return nvrmMatch[1];
428
440
 
429
- const match = versionInfo.match(/Kernel Module\s+([0-9.]+)/i);
430
- return match ? match[1] : null;
441
+ const genericMatch = versionInfo.match(/\b([0-9]+(?:\.[0-9]+){1,3})\b/);
442
+ if (genericMatch) return genericMatch[1];
443
+ }
444
+
445
+ return null;
431
446
  }
432
447
 
433
448
  getJetsonCapabilities(model) {
@@ -734,10 +749,13 @@ class CUDADetector {
734
749
  const primary = this.getPrimaryGPU();
735
750
  const gpuName = primary.name.toLowerCase()
736
751
  .replace(/nvidia|geforce|quadro|tesla/gi, '')
737
- .replace(/\s+/g, '-')
738
- .trim();
752
+ .replace(/[^a-z0-9]+/gi, '-')
753
+ .replace(/-+/g, '-')
754
+ .replace(/^-|-$/g, '');
755
+ const normalizedGpuName = gpuName || 'gpu';
756
+ const normalizedVRAM = Number.isFinite(info.totalVRAM) ? Math.max(0, Math.round(info.totalVRAM)) : 0;
739
757
 
740
- return `cuda-${gpuName}-${info.totalVRAM}gb${info.isMultiGPU ? '-x' + info.gpus.length : ''}`;
758
+ return `cuda-${normalizedGpuName}-${normalizedVRAM}gb${info.isMultiGPU ? '-x' + info.gpus.length : ''}`;
741
759
  }
742
760
 
743
761
  /**