@otto-code/brain 0.9.0 → 0.9.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.
@@ -9,12 +9,95 @@ import os from "node:os";
9
9
  import path from "node:path";
10
10
  import * as gguf from "../gguf.js";
11
11
  export const LMSTUDIO_MODELS_DIR = path.join(os.homedir(), ".lmstudio", "models");
12
- // The quant is the terminal GGUF filename suffix, optionally preceded by a
13
- // source-defined qualifier such as Muse's `UD-`. Matching a known substring
14
- // truncates newer labels (for example Q2_K_XL -> Q2_K) and misses them when
15
- // their exact spelling is not in a hard-coded list.
16
- const QUANT_SUFFIX = /(?:^|[-_])((?:UD-)?(?:IQ[1-4]|Q[2-8])(?:_[A-Z0-9]+)*|NVFP\d+|MXFP\d+|BF16|F16|F32)(?:-(?:MTP|IMATRIX|DISTILL))?(?:-\d{5}-OF-\d{5})?\.GGUF$/i;
17
12
  const MULTIPART = /-(\d{5})-of-(\d{5})\.gguf$/i;
13
+ const QUANT_TRAILING_MARKERS = ["-MTP", "-IMATRIX", "-DISTILL"];
14
+ function isDigit(char) {
15
+ return char >= "0" && char <= "9";
16
+ }
17
+ function isUppercaseLetterOrDigit(char) {
18
+ return (char >= "A" && char <= "Z") || isDigit(char);
19
+ }
20
+ function isQuant(value) {
21
+ if (["BF16", "F16", "F32"].includes(value))
22
+ return true;
23
+ if (value.startsWith("IQ")) {
24
+ return value.length === 3 && value[2] >= "1" && value[2] <= "4";
25
+ }
26
+ if (value.startsWith("NVFP") || value.startsWith("MXFP")) {
27
+ const digits = value.slice(4);
28
+ return digits.length > 0 && [...digits].every(isDigit);
29
+ }
30
+ if (!value.startsWith("Q") || value.length < 2 || value[1] < "2" || value[1] > "8") {
31
+ return false;
32
+ }
33
+ let separatorExpected = false;
34
+ for (const char of value.slice(2)) {
35
+ if (char === "_") {
36
+ if (separatorExpected)
37
+ return false;
38
+ separatorExpected = true;
39
+ }
40
+ else if (isUppercaseLetterOrDigit(char)) {
41
+ separatorExpected = false;
42
+ }
43
+ else {
44
+ return false;
45
+ }
46
+ }
47
+ return !separatorExpected;
48
+ }
49
+ function startsQuant(value, index) {
50
+ const first = value[index]?.toUpperCase();
51
+ const second = value[index + 1]?.toUpperCase();
52
+ if (first === "Q")
53
+ return second >= "2" && second <= "8";
54
+ if (first === "I")
55
+ return second === "Q" && value[index + 2] >= "1" && value[index + 2] <= "4";
56
+ if (first === "N")
57
+ return value.slice(index, index + 4).toUpperCase() === "NVFP";
58
+ if (first === "M")
59
+ return value.slice(index, index + 4).toUpperCase() === "MXFP";
60
+ if (first === "B")
61
+ return value.slice(index, index + 4).toUpperCase() === "BF16";
62
+ if (first === "F") {
63
+ const prefix = value.slice(index, index + 3).toUpperCase();
64
+ return prefix === "F16" || prefix === "F32";
65
+ }
66
+ return false;
67
+ }
68
+ function stripQuantSuffix(filename) {
69
+ let stem = path.basename(filename);
70
+ if (!stem.toLowerCase().endsWith(".gguf"))
71
+ return null;
72
+ stem = stem.slice(0, -".gguf".length);
73
+ const multipart = stem.slice(-15);
74
+ if (multipart.length === 15 &&
75
+ multipart[0] === "-" &&
76
+ multipart[6] === "-" &&
77
+ multipart.slice(7, 9).toLowerCase() === "of" &&
78
+ multipart[9] === "-" &&
79
+ [...multipart.slice(1, 6), ...multipart.slice(10)].every(isDigit)) {
80
+ stem = stem.slice(0, -multipart.length);
81
+ }
82
+ const upperStem = stem.toUpperCase();
83
+ for (const marker of QUANT_TRAILING_MARKERS) {
84
+ if (upperStem.endsWith(marker)) {
85
+ stem = stem.slice(0, -marker.length);
86
+ break;
87
+ }
88
+ }
89
+ let candidateStart = -1;
90
+ for (let index = 0; index < stem.length; index += 1) {
91
+ if (index > 0 && stem[index - 1] !== "-" && stem[index - 1] !== "_")
92
+ continue;
93
+ if (startsQuant(stem, index))
94
+ candidateStart = index;
95
+ }
96
+ if (candidateStart === -1)
97
+ return null;
98
+ const candidate = stem.slice(candidateStart).toUpperCase();
99
+ return isQuant(candidate) ? candidate : null;
100
+ }
18
101
  function walk(dir, out = []) {
19
102
  let entries;
20
103
  try {
@@ -33,9 +116,10 @@ function walk(dir, out = []) {
33
116
  return out;
34
117
  }
35
118
  export function detectQuant(filename) {
36
- const match = path.basename(filename).match(QUANT_SUFFIX);
37
- // `UD-` is a source filename qualifier, not part of the user-facing quant.
38
- return match?.[1]?.replace(/^UD-/i, "").toUpperCase() ?? null;
119
+ // A source qualifier such as `UD-` is not part of the user-facing quant.
120
+ // Scan terminal candidates rather than matching a nested filename regex:
121
+ // model filenames are host input and this remains linear for malformed names.
122
+ return stripQuantSuffix(filename);
39
123
  }
40
124
  export function isProjectorFile(filename) {
41
125
  return /^mmproj/i.test(path.basename(filename));
@@ -249,7 +249,11 @@ function slug(spec) {
249
249
  .replace(/(^-|-$)/g, "");
250
250
  }
251
251
  function displayNameForManagedRuntime(label, version) {
252
- return `${label.replace(/\s*\(managed\)$/iu, "")} · ${version} (Otto managed)`;
252
+ const suffix = "(managed)";
253
+ const withoutSuffix = label.toLowerCase().endsWith(suffix)
254
+ ? label.slice(0, -suffix.length).trimEnd()
255
+ : label;
256
+ return `${withoutSuffix} · ${version} (Otto managed)`;
253
257
  }
254
258
  function legacyManagedDisplayName(dirName, version) {
255
259
  const cuda = /^cuda-(\d+)-(\d+)-managed(?:-|$)/iu.exec(dirName);
@@ -355,6 +355,11 @@ export function createHostApi(deps) {
355
355
  sendError(res, 403, "remote configuration is disabled on this brain; enable it with `otto brain share --allow-config`");
356
356
  return false;
357
357
  };
358
+ /** Keep implementation details in the local service log, never the HTTP response. */
359
+ const sendOperationError = (res, status, operation, error) => {
360
+ deps.log?.("server", `${operation}: ${errorMessage(error)}`);
361
+ sendError(res, status, operation);
362
+ };
358
363
  const inventory = async () => {
359
364
  const [gpu, store] = [await deps.queryGpuInfo(), deps.getProfilesStore()];
360
365
  const defaults = deps.getProfileDefaults();
@@ -445,7 +450,7 @@ export function createHostApi(deps) {
445
450
  });
446
451
  }
447
452
  catch (error) {
448
- sendError(res, 400, errorMessage(error));
453
+ sendOperationError(res, 400, "could not update the model profile", error);
449
454
  }
450
455
  })();
451
456
  });
@@ -537,7 +542,7 @@ export function createHostApi(deps) {
537
542
  });
538
543
  }
539
544
  catch (error) {
540
- sendError(res, 400, errorMessage(error));
545
+ sendOperationError(res, 400, "could not calculate the model budget", error);
541
546
  }
542
547
  })();
543
548
  };
@@ -630,7 +635,7 @@ export function createHostApi(deps) {
630
635
  });
631
636
  }
632
637
  catch (error) {
633
- sendError(res, 409, errorMessage(error));
638
+ sendOperationError(res, 409, "could not remove the model component", error);
634
639
  }
635
640
  };
636
641
  const handleLogs = (res, params) => {
@@ -778,7 +783,7 @@ export function createHostApi(deps) {
778
783
  });
779
784
  }
780
785
  catch (error) {
781
- sendError(res, 409, errorMessage(error));
786
+ sendOperationError(res, 409, "could not start the benchmark", error);
782
787
  }
783
788
  });
784
789
  return true;
@@ -916,7 +921,7 @@ export function createHostApi(deps) {
916
921
  });
917
922
  }
918
923
  catch (error) {
919
- sendError(res, 400, errorMessage(error));
924
+ sendOperationError(res, 400, "could not start the job", error);
920
925
  }
921
926
  });
922
927
  return true;
@@ -1091,7 +1096,7 @@ export function createHostApi(deps) {
1091
1096
  }));
1092
1097
  }
1093
1098
  catch (error) {
1094
- sendError(res, 500, errorMessage(error));
1099
+ sendOperationError(res, 500, "could not build the model details", error);
1095
1100
  }
1096
1101
  })();
1097
1102
  return true;
@@ -1,12 +1,20 @@
1
- const TAGGED_LINE = /^\[(?:brain|llama-server)\]/u;
2
- const SOURCE_AND_AREA = /^(\[(?:brain|llama-server)\])(?:\s+(\[(?:library|model|api|server)\]))?\s*(.*)$/u;
3
1
  const LLAMA_SERVER_PREFIX = /^\d+(?:\.\d+){3}\s+[A-Z]\s+\S+\s+(?:\S+:\s+)?(.+)$/u;
2
+ const SOURCES = ["brain", "llama-server"];
3
+ const AREAS = ["library", "model", "api", "server"];
4
+ function taggedValue(line, values) {
5
+ for (const value of values) {
6
+ const tag = `[${value}]`;
7
+ if (line.startsWith(tag))
8
+ return tag;
9
+ }
10
+ return null;
11
+ }
4
12
  /**
5
13
  * Every service-owned event carries both its process source and operation area.
6
14
  * llama-server output is separately marked by `formatLlamaServerLog`.
7
15
  */
8
16
  export function formatBrainLog(area, message) {
9
- return TAGGED_LINE.test(message) ? message : `[brain] [${area}] ${message}`;
17
+ return taggedValue(message, SOURCES) ? message : `[brain] [${area}] ${message}`;
10
18
  }
11
19
  /**
12
20
  * Remove llama.cpp's elapsed-time, level and component columns. Otto owns the
@@ -18,15 +26,21 @@ export function stripLlamaServerPrefix(message) {
18
26
  }
19
27
  /** Preserve the useful llama.cpp message while making its process boundary explicit. */
20
28
  export function formatLlamaServerLog(message) {
21
- return TAGGED_LINE.test(message) ? message : `[llama-server] ${stripLlamaServerPrefix(message)}`;
29
+ return taggedValue(message, SOURCES)
30
+ ? message
31
+ : `[llama-server] ${stripLlamaServerPrefix(message)}`;
22
32
  }
23
33
  /** Place source tags ahead of the timestamp so they are scannable in a dense log. */
24
34
  export function timestampBrainLogLine(timestamp, line) {
25
35
  const tagged = formatBrainLog("server", line);
26
- const match = SOURCE_AND_AREA.exec(tagged);
27
- if (!match)
36
+ const source = taggedValue(tagged, SOURCES);
37
+ if (!source)
28
38
  return `${timestamp} ${tagged}`;
29
- const [, source, area, message] = match;
39
+ let remainder = tagged.slice(source.length).trimStart();
40
+ const area = taggedValue(remainder, AREAS);
41
+ if (area)
42
+ remainder = remainder.slice(area.length).trimStart();
43
+ const message = remainder;
30
44
  return `${source}${area ? ` ${area}` : ""} ${timestamp}${message ? ` ${message}` : ""}`;
31
45
  }
32
46
  //# sourceMappingURL=log-format.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/brain",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Otto Brain - self-contained host for local GGUF models, with measured VRAM budgeting and reasoning-budget control",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "bin": {