@infersec/conduit 1.112.0 → 1.113.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/dist/cli.js CHANGED
@@ -19972,6 +19972,32 @@ object$5({
19972
19972
  sizeBytes: number$1().int().nonnegative().nullable()
19973
19973
  });
19974
19974
 
19975
+ const EngineExecutionErrorTypeSchema = _enum$1([
19976
+ "config",
19977
+ "crash",
19978
+ "oom",
19979
+ "prompt",
19980
+ "unknown"
19981
+ ]);
19982
+ const EngineExecutionReportPayloadSchema = object$5({
19983
+ avgTps: number$1().nonnegative().finite().default(0),
19984
+ completionTokens: number$1().int().nonnegative().default(0),
19985
+ durationMs: number$1().int().nonnegative().default(0),
19986
+ engineType: LLMEngineSchema.nullable(),
19987
+ engineVersion: string$2().max(64).nullable().default(null),
19988
+ errorDetail: string$2().max(2048).nullable().default(null),
19989
+ errorType: EngineExecutionErrorTypeSchema.nullable(),
19990
+ extraArgs: array$1(tuple([string$2().min(1).max(128), string$2().max(512)]))
19991
+ .max(256)
19992
+ .default([]),
19993
+ finishedAtISO: string$2().datetime({ offset: true }),
19994
+ peakTps: number$1().nonnegative().finite().nullable().default(null),
19995
+ promptTokens: number$1().int().nonnegative().default(0),
19996
+ runAtISO: string$2().datetime({ offset: true }),
19997
+ success: boolean$1(),
19998
+ ttftMs: number$1().int().nonnegative().default(0),
19999
+ totalTokens: number$1().int().nonnegative().default(0)
20000
+ });
19975
20001
  const InferenceAgentLLMMetricsPayloadSchema = object$5({
19976
20002
  bytes: number$1().int().nonnegative(),
19977
20003
  completionTokens: number$1().int().nonnegative(),
@@ -20310,6 +20336,23 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
20310
20336
  }
20311
20337
  }
20312
20338
  },
20339
+ "/conduit/api/v1/source/:sourceID/engine/execution": {
20340
+ POST: {
20341
+ auth: {
20342
+ type: "api-key"
20343
+ },
20344
+ body: EngineExecutionReportPayloadSchema,
20345
+ parameters: {
20346
+ sourceID: ULIDSchema
20347
+ },
20348
+ response: {
20349
+ schema: object$5({
20350
+ acknowledged: literal(true)
20351
+ }),
20352
+ type: "rest"
20353
+ }
20354
+ }
20355
+ },
20313
20356
  "/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
20314
20357
  POST: {
20315
20358
  auth: {
@@ -113767,6 +113810,19 @@ function createAPIClient({ apiKey, apiURL, inferenceSourceID, logger }) {
113767
113810
  route: "/conduit/api/v1/source/:sourceID/state"
113768
113811
  });
113769
113812
  },
113813
+ reportEngineExecution: async (payload) => {
113814
+ await fetchByReference({
113815
+ baseURL: apiURL,
113816
+ body: payload,
113817
+ fetch: fetchWithAPIKey,
113818
+ method: "POST",
113819
+ parameters: {
113820
+ sourceID: inferenceSourceID
113821
+ },
113822
+ reference: API_SERVICE_CONDUIT_API_REFERENCE,
113823
+ route: "/conduit/api/v1/source/:sourceID/engine/execution"
113824
+ });
113825
+ },
113770
113826
  reportPromptMetrics: async (payload) => {
113771
113827
  await fetchByReference({
113772
113828
  baseURL: apiURL,
@@ -125627,6 +125683,9 @@ class ModelManager extends EventEmitter {
125627
125683
  lifecycleState = "stopped";
125628
125684
  downloadLockHandle = null;
125629
125685
  stopRequested = false;
125686
+ lastEngineExitCode = null;
125687
+ lastEngineExitSignal = null;
125688
+ reachedRunningState = false;
125630
125689
  modelsDirectory;
125631
125690
  constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
125632
125691
  super();
@@ -125749,6 +125808,9 @@ class ModelManager extends EventEmitter {
125749
125808
  this.lifecycleState = "starting";
125750
125809
  this.lastEngineError = null;
125751
125810
  this.stopRequested = false;
125811
+ this.lastEngineExitCode = null;
125812
+ this.lastEngineExitSignal = null;
125813
+ this.reachedRunningState = false;
125752
125814
  this.logger.info("Starting LLM engine", {
125753
125815
  agentEngineType: this.engine
125754
125816
  });
@@ -125779,6 +125841,7 @@ class ModelManager extends EventEmitter {
125779
125841
  throw err;
125780
125842
  }
125781
125843
  this.lifecycleState = "running";
125844
+ this.reachedRunningState = true;
125782
125845
  this.emit("engineReady");
125783
125846
  }
125784
125847
  async stop() {
@@ -125803,6 +125866,7 @@ class ModelManager extends EventEmitter {
125803
125866
  this.lifecycleState = "stopped";
125804
125867
  return;
125805
125868
  }
125869
+ this.reachedRunningState = false;
125806
125870
  this.lifecycleState = "stopping";
125807
125871
  this.stopRequested = true;
125808
125872
  await processManager.stop();
@@ -125815,9 +125879,18 @@ class ModelManager extends EventEmitter {
125815
125879
  this.lifecycleState === "starting" ||
125816
125880
  this.lifecycleState === "errored");
125817
125881
  }
125882
+ get lastExitCode() {
125883
+ return this.lastEngineExitCode;
125884
+ }
125885
+ get lastExitSignal() {
125886
+ return this.lastEngineExitSignal;
125887
+ }
125818
125888
  get state() {
125819
125889
  return this.lifecycleState;
125820
125890
  }
125891
+ get wasRunning() {
125892
+ return this.reachedRunningState;
125893
+ }
125821
125894
  async checkEngineReadiness() {
125822
125895
  switch (this.engine) {
125823
125896
  case "llama.cpp": {
@@ -125931,6 +126004,7 @@ class ModelManager extends EventEmitter {
125931
126004
  if (readiness === "ready") {
125932
126005
  this.clearHealthPoll();
125933
126006
  this.lifecycleState = "running";
126007
+ this.reachedRunningState = true;
125934
126008
  this.emit("engineReady");
125935
126009
  }
125936
126010
  })
@@ -125993,6 +126067,8 @@ class ModelManager extends EventEmitter {
125993
126067
  }));
125994
126068
  });
125995
126069
  processManager.on("stopped", (code, signal) => {
126070
+ this.lastEngineExitCode = code;
126071
+ this.lastEngineExitSignal = signal;
125996
126072
  if (hasTerminated) {
125997
126073
  return;
125998
126074
  }
@@ -126060,6 +126136,69 @@ class ModelManager extends EventEmitter {
126060
126136
  }
126061
126137
  }
126062
126138
 
126139
+ // Ordered most-specific first: a message mentioning a prompt-size failure should classify as
126140
+ // "prompt" even if it also touches memory text; memory outranks config because OOM kills frequently
126141
+ // emit sparse stderr. These are heuristics, not exhaustively enumerated engine vocabularies.
126142
+ const OOM_PATTERNS = [
126143
+ /CUDA out of memory/i,
126144
+ /No available memory for the cache blocks/i,
126145
+ /\bOOMKilled\b/,
126146
+ /out of memory/i,
126147
+ /MemoryError/i,
126148
+ /Cannot allocate memory/i
126149
+ ];
126150
+ const PROMPT_PATTERNS = [
126151
+ /prompt is too long/i,
126152
+ /maximum context length exceeded/i,
126153
+ /too many tokens/i
126154
+ ];
126155
+ const CONFIG_PATTERNS = [
126156
+ /unrecognized argument/i,
126157
+ /invalid argument/i,
126158
+ /error while loading state_dict/i,
126159
+ /Architecture not understood/i,
126160
+ /No such file or directory/i
126161
+ ];
126162
+ /**
126163
+ * Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
126164
+ * terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
126165
+ * how much stderr the engine produced); the message text then refines the category. Anything
126166
+ * unrecognized is "unknown".
126167
+ */
126168
+ function classifyEngineFailure({ error, exitCode, signal }) {
126169
+ // 137 = SIGKILL (kernel OOM-killer), 139 = SIGSEGV (illegal memory access). Treat both as OOM
126170
+ // so that memory-pressure deaths do not degrade to "unknown" when stderr is truncated.
126171
+ if (exitCode === 137 || exitCode === 139) {
126172
+ return "oom";
126173
+ }
126174
+ // Direct OS kills (SIGKILL/SIGSEGV) leave no meaningful exit code; honor them like 137/139.
126175
+ if (signal === "SIGKILL" || signal === "SIGSEGV") {
126176
+ return "oom";
126177
+ }
126178
+ const text = `${error.message}`.slice(0, 4000);
126179
+ for (const pattern of PROMPT_PATTERNS) {
126180
+ if (pattern.test(text)) {
126181
+ return "prompt";
126182
+ }
126183
+ }
126184
+ for (const pattern of OOM_PATTERNS) {
126185
+ if (pattern.test(text)) {
126186
+ return "oom";
126187
+ }
126188
+ }
126189
+ for (const pattern of CONFIG_PATTERNS) {
126190
+ if (pattern.test(text)) {
126191
+ return "config";
126192
+ }
126193
+ }
126194
+ // A process that exited abnormally with no recognizable diagnostic: treat as a crash rather
126195
+ // than "unknown" so the two buckets distinguish "we saw nothing" from "it died badly".
126196
+ if (exitCode !== null && exitCode !== 0) {
126197
+ return "crash";
126198
+ }
126199
+ return "unknown";
126200
+ }
126201
+
126063
126202
  const EXCEPTION_LINE_PATTERN = /([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*(.+)/;
126064
126203
  const FALLBACK_DETAIL_MAX_LENGTH = 300;
126065
126204
  const FALLBACK_RAW_MAX_LENGTH = 500;
@@ -137936,12 +138075,20 @@ async function detectVRAMViaSysfs(pciBusSuffix) {
137936
138075
  const deviceDir = path$1.join(DRM_PATH, entry, "device");
137937
138076
  const totalStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_vram_total"));
137938
138077
  const usedStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_vram_used"));
138078
+ const gttTotalStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_gtt_total"));
138079
+ const gttUsedStr = await readSysfsFile(path$1.join(deviceDir, "mem_info_gtt_used"));
137939
138080
  const totalBytes = totalStr !== null ? parseInt(totalStr, 10) : null;
137940
138081
  const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
138082
+ const gttTotalBytes = gttTotalStr !== null ? parseInt(gttTotalStr, 10) : null;
138083
+ const gttUsedBytes = gttUsedStr !== null ? parseInt(gttUsedStr, 10) : null;
137941
138084
  const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
137942
138085
  const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
138086
+ const validGttTotal = gttTotalBytes !== null && Number.isFinite(gttTotalBytes) && gttTotalBytes >= 0;
138087
+ const validGttUsed = gttUsedBytes !== null && Number.isFinite(gttUsedBytes) && gttUsedBytes >= 0;
137943
138088
  if (validTotal) {
137944
138089
  return {
138090
+ gttTotalBytes: validGttTotal ? gttTotalBytes : null,
138091
+ gttUsedBytes: validGttUsed ? gttUsedBytes : null,
137945
138092
  memoryTotalBytes: totalBytes,
137946
138093
  memoryUsedBytes: validUsed ? usedBytes : null
137947
138094
  };
@@ -137951,36 +138098,74 @@ async function detectVRAMViaSysfs(pciBusSuffix) {
137951
138098
  catch {
137952
138099
  // sysfs not available
137953
138100
  }
137954
- return { memoryTotalBytes: null, memoryUsedBytes: null };
138101
+ return {
138102
+ gttTotalBytes: null,
138103
+ gttUsedBytes: null,
138104
+ memoryTotalBytes: null,
138105
+ memoryUsedBytes: null
138106
+ };
138107
+ }
138108
+ // rocm-smi accepts a single --showmeminfo type per invocation, so VRAM and GTT
138109
+ // arrive from separate calls; both share this per-card parser.
138110
+ function parseRocmSmiMemory({ keyPrefix, stdout }) {
138111
+ const parsed = JSON.parse(stdout);
138112
+ const results = [];
138113
+ const cards = Object.entries(parsed).filter(([key]) => key.startsWith("card"));
138114
+ for (const [, data] of cards) {
138115
+ const bus = data["PCI Bus"] ?? null;
138116
+ const totalStr = data[`${keyPrefix} Total Memory (B)`] ?? null;
138117
+ const usedStr = data[`${keyPrefix} Total Used Memory (B)`] ?? null;
138118
+ const totalBytes = totalStr !== null ? parseInt(totalStr, 10) : null;
138119
+ const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
138120
+ const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
138121
+ const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
138122
+ if (bus) {
138123
+ results.push({
138124
+ bus,
138125
+ memoryTotalBytes: validTotal ? totalBytes : null,
138126
+ memoryUsedBytes: validUsed ? usedBytes : null
138127
+ });
138128
+ }
138129
+ }
138130
+ return results;
137955
138131
  }
137956
- async function detectVRAMViaRocmSmi() {
138132
+ const ROCM_SMI_TIMEOUT_MS = 10_000;
138133
+ async function detectVRAMViaRocmSmi({ logger }) {
137957
138134
  try {
137958
- const { stdout } = await execa("rocm-smi", [
137959
- "--showbus",
137960
- "--showmeminfo",
137961
- "vram",
137962
- "--json"
138135
+ const [vramResult, gttResult] = await Promise.allSettled([
138136
+ execa("rocm-smi", ["--showbus", "--showmeminfo", "vram", "--json"], {
138137
+ timeout: ROCM_SMI_TIMEOUT_MS
138138
+ }),
138139
+ execa("rocm-smi", ["--showbus", "--showmeminfo", "gtt", "--json"], {
138140
+ timeout: ROCM_SMI_TIMEOUT_MS
138141
+ })
137963
138142
  ]);
137964
- const parsed = JSON.parse(stdout);
137965
- const results = [];
137966
- const cards = Object.entries(parsed).filter(([key]) => key.startsWith("card"));
137967
- for (const [, data] of cards) {
137968
- const bus = data["PCI Bus"] ?? null;
137969
- const totalStr = data["VRAM Total Memory (B)"] ?? null;
137970
- const usedStr = data["VRAM Total Used Memory (B)"] ?? null;
137971
- const totalBytes = totalStr !== null ? parseInt(totalStr, 10) : null;
137972
- const usedBytes = usedStr !== null ? parseInt(usedStr, 10) : null;
137973
- const validTotal = totalBytes !== null && Number.isFinite(totalBytes) && totalBytes >= 0;
137974
- const validUsed = usedBytes !== null && Number.isFinite(usedBytes) && usedBytes >= 0;
137975
- if (bus) {
137976
- results.push({
137977
- bus,
137978
- memoryTotalBytes: validTotal ? totalBytes : null,
137979
- memoryUsedBytes: validUsed ? usedBytes : null
138143
+ if (vramResult.status !== "fulfilled")
138144
+ return [];
138145
+ let gttEntries = [];
138146
+ if (gttResult.status === "fulfilled") {
138147
+ try {
138148
+ gttEntries = parseRocmSmiMemory({
138149
+ keyPrefix: "GTT",
138150
+ stdout: gttResult.value.stdout
137980
138151
  });
137981
138152
  }
138153
+ catch (error) {
138154
+ // Unusable GTT output (e.g. older rocm-smi) degrades to VRAM-only
138155
+ logger.warn("rocm-smi GTT output parse failed", { error: asError(error) });
138156
+ }
137982
138157
  }
137983
- return results;
138158
+ const gttByBus = new Map(gttEntries.map(entry => [entry.bus, entry]));
138159
+ return parseRocmSmiMemory({ keyPrefix: "VRAM", stdout: vramResult.value.stdout }).map(vram => {
138160
+ const gtt = gttByBus.get(vram.bus);
138161
+ return {
138162
+ bus: vram.bus,
138163
+ gttTotalBytes: gtt?.memoryTotalBytes ?? null,
138164
+ gttUsedBytes: gtt?.memoryUsedBytes ?? null,
138165
+ memoryTotalBytes: vram.memoryTotalBytes,
138166
+ memoryUsedBytes: vram.memoryUsedBytes
138167
+ };
138168
+ });
137984
138169
  }
137985
138170
  catch {
137986
138171
  return [];
@@ -138045,8 +138230,9 @@ async function detectGPUsViaNvidiaSmi({ logger }) {
138045
138230
  vendor: "NVIDIA"
138046
138231
  });
138047
138232
  }
138048
- // Unified-memory devices (e.g. NVIDIA GB10 on DGX Spark) report "[N/A]" for
138049
- // memory. Their compute pool is system RAM, so fall back to /proc/meminfo.
138233
+ // Shared/unified-memory devices (e.g. NVIDIA GB10 on DGX Spark) report
138234
+ // "[N/A]" for memory. Their compute pool is system RAM, so fall back to
138235
+ // /proc/meminfo. (AMD iGPUs get the same treatment via GTT merging.)
138050
138236
  if (gpus.some(gpu => gpu.memoryTotalBytes === null)) {
138051
138237
  const systemMemory = await readSystemMemoryBytes({ logger });
138052
138238
  const total = systemMemory.totalBytes;
@@ -138070,7 +138256,7 @@ async function detectGPUsViaNvidiaSmi({ logger }) {
138070
138256
  }
138071
138257
  }
138072
138258
  function buildMergedGPUs(options) {
138073
- const { lspciGPUs, nvidiaGPUs, rocmVRAM, siGPUs, sysfsVRAMMap } = options;
138259
+ const { lspciGPUs, nvidiaGPUs, rocmVRAM, siGPUs, systemTotalBytes, sysfsVRAMMap } = options;
138074
138260
  const rocmByBus = new Map();
138075
138261
  for (const entry of rocmVRAM) {
138076
138262
  const key = normalizeBusAddress(entry.bus);
@@ -138111,6 +138297,7 @@ function buildMergedGPUs(options) {
138111
138297
  gpu: existing,
138112
138298
  key,
138113
138299
  rocmByBus,
138300
+ systemTotalBytes,
138114
138301
  sysfsVRAMMap
138115
138302
  });
138116
138303
  }
@@ -138130,30 +138317,73 @@ function buildMergedGPUs(options) {
138130
138317
  gpu,
138131
138318
  key,
138132
138319
  rocmByBus,
138320
+ systemTotalBytes,
138133
138321
  sysfsVRAMMap
138134
138322
  });
138135
138323
  byBus.set(key, gpu);
138136
138324
  }
138137
138325
  return [...byBus.values()];
138138
138326
  }
138139
- function applySysfsOrRocmVRAM({ gpu, key, rocmByBus, sysfsVRAMMap }) {
138327
+ // Shared-memory GPUs (AMD iGPUs) expose a small dedicated VRAM carve-out via
138328
+ // mem_info_vram_total while the real usable pool - GTT - is carved dynamically
138329
+ // from system RAM. When GTT exceeds VRAM the device is treated as integrated
138330
+ // and the two pools combine (capped by installed RAM). Note that GTT defaults
138331
+ // to half of system RAM even on discrete GPUs, so a dGPU with less VRAM than
138332
+ // that also merges - acceptable, since GTT remains real addressable memory
138333
+ // for amdgpu compute (albeit slower over PCIe).
138334
+ function mergeGTTMemory({ gttTotalBytes, gttUsedBytes, systemTotalBytes, vramTotalBytes, vramUsedBytes }) {
138335
+ if (vramTotalBytes === null || !Number.isFinite(vramTotalBytes)) {
138336
+ return { memoryTotalBytes: null, memoryUsedBytes: null };
138337
+ }
138338
+ const isIntegrated = gttTotalBytes !== null && gttTotalBytes > vramTotalBytes;
138339
+ if (!isIntegrated) {
138340
+ return { memoryTotalBytes: vramTotalBytes, memoryUsedBytes: vramUsedBytes };
138341
+ }
138342
+ const combined = vramTotalBytes + gttTotalBytes;
138343
+ const capped = systemTotalBytes !== null && systemTotalBytes > 0
138344
+ ? Math.min(combined, systemTotalBytes)
138345
+ : combined;
138346
+ const hasCompleteUsage = vramUsedBytes !== null &&
138347
+ Number.isFinite(vramUsedBytes) &&
138348
+ gttUsedBytes !== null &&
138349
+ Number.isFinite(gttUsedBytes);
138350
+ const memoryUsedBytes = hasCompleteUsage
138351
+ ? Math.min(vramUsedBytes + gttUsedBytes, capped)
138352
+ : null;
138353
+ return { memoryTotalBytes: capped, memoryUsedBytes };
138354
+ }
138355
+ function applySysfsOrRocmVRAM({ gpu, key, rocmByBus, systemTotalBytes, sysfsVRAMMap }) {
138140
138356
  const sysfs = sysfsVRAMMap.get(key);
138141
- let totalBytes = sysfs?.memoryTotalBytes ?? null;
138142
- let usedBytes = sysfs?.memoryUsedBytes ?? null;
138143
- if (totalBytes === null) {
138357
+ let vramTotalBytes = sysfs?.memoryTotalBytes ?? null;
138358
+ let vramUsedBytes = sysfs?.memoryUsedBytes ?? null;
138359
+ let gttTotalBytes = sysfs?.gttTotalBytes ?? null;
138360
+ let gttUsedBytes = sysfs?.gttUsedBytes ?? null;
138361
+ if (vramTotalBytes === null) {
138144
138362
  const rocm = rocmByBus.get(key);
138145
138363
  if (rocm) {
138146
- totalBytes = rocm.memoryTotalBytes;
138147
- usedBytes = rocm.memoryUsedBytes;
138148
- }
138149
- }
138150
- if (totalBytes === null || !Number.isFinite(totalBytes))
138364
+ vramTotalBytes = rocm.memoryTotalBytes;
138365
+ vramUsedBytes = rocm.memoryUsedBytes;
138366
+ gttTotalBytes = rocm.gttTotalBytes;
138367
+ gttUsedBytes = rocm.gttUsedBytes;
138368
+ }
138369
+ }
138370
+ const merged = mergeGTTMemory({
138371
+ gttTotalBytes,
138372
+ gttUsedBytes,
138373
+ systemTotalBytes,
138374
+ vramTotalBytes,
138375
+ vramUsedBytes
138376
+ });
138377
+ if (merged.memoryTotalBytes === null || !Number.isFinite(merged.memoryTotalBytes))
138151
138378
  return;
138152
- gpu.memoryTotalBytes = totalBytes;
138153
- gpu.memoryUsedBytes = usedBytes !== null && Number.isFinite(usedBytes) ? usedBytes : null;
138379
+ gpu.memoryTotalBytes = merged.memoryTotalBytes;
138380
+ gpu.memoryUsedBytes =
138381
+ merged.memoryUsedBytes !== null && Number.isFinite(merged.memoryUsedBytes)
138382
+ ? merged.memoryUsedBytes
138383
+ : null;
138154
138384
  gpu.memoryFreeBytes =
138155
- usedBytes !== null && Number.isFinite(usedBytes)
138156
- ? Math.max(totalBytes - usedBytes, 0)
138385
+ gpu.memoryUsedBytes !== null
138386
+ ? Math.max(gpu.memoryTotalBytes - gpu.memoryUsedBytes, 0)
138157
138387
  : null;
138158
138388
  }
138159
138389
  async function collectMachineMetadata({ logger }) {
@@ -138164,7 +138394,7 @@ async function collectMachineMetadata({ logger }) {
138164
138394
  si.graphics(),
138165
138395
  detectGPUsViaLspci(),
138166
138396
  detectGPUsViaNvidiaSmi({ logger }),
138167
- detectVRAMViaRocmSmi()
138397
+ detectVRAMViaRocmSmi({ logger })
138168
138398
  ]);
138169
138399
  const cpuInfo = cpuResult.status === "fulfilled" ? cpuResult.value : null;
138170
138400
  const memInfo = memResult.status === "fulfilled" ? memResult.value : null;
@@ -138219,6 +138449,7 @@ async function collectMachineMetadata({ logger }) {
138219
138449
  nvidiaGPUs: resolvedNvidiaGPUs,
138220
138450
  rocmVRAM: resolvedRocmVRAM,
138221
138451
  siGPUs,
138452
+ systemTotalBytes: memInfo?.total ?? null,
138222
138453
  sysfsVRAMMap
138223
138454
  });
138224
138455
  const machineMetadata = {
@@ -138279,6 +138510,119 @@ async function detectDockerVersion() {
138279
138510
  }
138280
138511
  }
138281
138512
 
138513
+ /**
138514
+ * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
138515
+ * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
138516
+ * as its value when that token does not start with "-" (classic CLI convention); anything else
138517
+ * (flags, non-strings) is dropped.
138518
+ */
138519
+ function pairExtraArgs(tokens) {
138520
+ if (!Array.isArray(tokens)) {
138521
+ return [];
138522
+ }
138523
+ const list = tokens;
138524
+ const pairs = [];
138525
+ let index = 0;
138526
+ while (index < list.length) {
138527
+ const token = list[index];
138528
+ if (typeof token !== "string" || token.length === 0 || !token.startsWith("-")) {
138529
+ index++;
138530
+ continue;
138531
+ }
138532
+ const separator = token.indexOf("=");
138533
+ if (separator > -1) {
138534
+ const arg = token.slice(0, separator);
138535
+ if (arg.length > 0) {
138536
+ pairs.push([arg, token.slice(separator + 1)]);
138537
+ }
138538
+ index++;
138539
+ continue;
138540
+ }
138541
+ const next = list[index + 1];
138542
+ const consumesNext = typeof next === "string" && next.length > 0 && !next.startsWith("-");
138543
+ if (consumesNext) {
138544
+ pairs.push([token, next]);
138545
+ index += 2;
138546
+ }
138547
+ else {
138548
+ pairs.push([token, ""]);
138549
+ index++;
138550
+ }
138551
+ }
138552
+ return pairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0);
138553
+ }
138554
+ /**
138555
+ * Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
138556
+ * latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
138557
+ * row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
138558
+ * crash, or first successful full prompt completion.
138559
+ */
138560
+ class EngineExecutionReporter {
138561
+ options;
138562
+ currentStartupAt = null;
138563
+ reportedForCurrentStartup = false;
138564
+ constructor(options) {
138565
+ this.options = options;
138566
+ }
138567
+ /** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
138568
+ beginStartup(runAt) {
138569
+ this.currentStartupAt = runAt;
138570
+ this.reportedForCurrentStartup = false;
138571
+ }
138572
+ /** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
138573
+ async reportStartupFailure(report) {
138574
+ await this.file(report, false);
138575
+ }
138576
+ /** Reports a spontaneous crash of an engine that had reached the running state. */
138577
+ async reportRuntimeCrash(report) {
138578
+ await this.file(report, false);
138579
+ }
138580
+ /** Reports the first fully-responded, token-bearing prompt completion since startup. */
138581
+ async reportSuccess(report) {
138582
+ await this.file(report, true);
138583
+ }
138584
+ async file(report, success) {
138585
+ if (this.reportedForCurrentStartup || this.currentStartupAt === null) {
138586
+ return;
138587
+ }
138588
+ // Latch BEFORE the network call: a thrown POST cannot double-file for this startup.
138589
+ this.reportedForCurrentStartup = true;
138590
+ const context = this.options.buildContext();
138591
+ const payload = {
138592
+ avgTps: report.throughput.avgTps,
138593
+ completionTokens: report.usage.completionTokens,
138594
+ durationMs: report.durationMs,
138595
+ engineType: context.engineType,
138596
+ engineVersion: context.engineVersion,
138597
+ errorDetail: success ? null : report.errorDetail,
138598
+ errorType: success ? null : report.errorType,
138599
+ extraArgs: context.extraArgsPairs,
138600
+ finishedAtISO: new Date().toISOString(),
138601
+ peakTps: report.throughput.peakTps,
138602
+ promptTokens: report.usage.promptTokens,
138603
+ runAtISO: this.currentStartupAt.toISOString(),
138604
+ success,
138605
+ ttftMs: report.ttftMs,
138606
+ totalTokens: report.usage.totalTokens
138607
+ };
138608
+ try {
138609
+ await this.options.report(payload);
138610
+ this.options.logger.info("Engine execution outcome reported", {
138611
+ inferenceSourceID: this.options.sourceLabel,
138612
+ success
138613
+ });
138614
+ }
138615
+ catch (error) {
138616
+ // Losing one report is preferable to filing two; the latch stays latched.
138617
+ this.options.logger.warn("Failed to report engine execution outcome", {
138618
+ error: asError(error),
138619
+ inferenceSourceID: this.options.sourceLabel,
138620
+ success
138621
+ });
138622
+ }
138623
+ }
138624
+ }
138625
+
138282
138626
  async function createApplication({ abortController, apiClient, configuration, logger }) {
138283
138627
  ensureDockerValidEnv();
138284
138628
  logger.info("Fetching conduit configuration");
@@ -138310,6 +138654,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
138310
138654
  error: asError(error)
138311
138655
  });
138312
138656
  }
138657
+ const reporter = new EngineExecutionReporter({
138658
+ buildContext: () => {
138659
+ const engineType = (conduitConfiguration.engineConfig?.type ??
138660
+ "llama.cpp");
138661
+ const versions = {
138662
+ exllamav3: machine?.exllamav3Version ?? null,
138663
+ "llama.cpp": machine?.llamaCppVersion ?? null,
138664
+ "mlx-lm": machine?.mlxlmVersion ?? null,
138665
+ sglang: machine?.sglangVersion ?? null,
138666
+ "tensorrt-llm": machine?.tensorrtLlmVersion ?? null,
138667
+ vllm: machine?.vllmVersion ?? null
138668
+ };
138669
+ return {
138670
+ engineType,
138671
+ engineVersion: versions[engineType] ?? null,
138672
+ extraArgsPairs: pairExtraArgs(conduitConfiguration.engineConfig?.extraArgs)
138673
+ };
138674
+ },
138675
+ logger,
138676
+ report: payload => apiClient.reportEngineExecution(payload),
138677
+ sourceLabel: configuration.inferenceSourceID
138678
+ });
138679
+ // Intercept the prompt-metrics chokepoint so the first fully-responded, token-bearing prompt of
138680
+ // each fresh startup files the one-shot engine_execution success report. Handlers close over the
138681
+ // SAME `apiClient` object and read `reportPromptMetrics` at request-dispatch time (which always
138682
+ // follows this point), so the wrapped method is what they invoke.
138683
+ const rawReportPromptMetrics = apiClient.reportPromptMetrics;
138684
+ apiClient.reportPromptMetrics = async (payload) => {
138685
+ if (payload.successful && payload.completionTokens > 0 && payload.latencyMs > 0) {
138686
+ // The one-shot report is kicked off and its settlement attached HERE (before any await): if the
138687
+ // metrics path throws below, the report promise must still be able to log its own rejection.
138688
+ const successReport = reporter
138689
+ .reportSuccess({
138690
+ durationMs: payload.latencyMs,
138691
+ errorDetail: null,
138692
+ errorType: null,
138693
+ throughput: {
138694
+ avgTps: payload.tokensPerSecond,
138695
+ peakTps: null
138696
+ },
138697
+ ttftMs: payload.timeToFirstTokenMs ?? 0,
138698
+ usage: {
138699
+ completionTokens: payload.completionTokens,
138700
+ promptTokens: payload.promptTokens,
138701
+ totalTokens: payload.totalTokens
138702
+ }
138703
+ })
138704
+ .catch(error => {
138705
+ logger.warn("Engine execution success report failed", {
138706
+ error: asError(error)
138707
+ });
138708
+ });
138709
+ await rawReportPromptMetrics(payload);
138710
+ await successReport;
138711
+ return;
138712
+ }
138713
+ await rawReportPromptMetrics(payload);
138714
+ };
138715
+ const SECRET_ARG_MASK_PATTERN = /(-{1,2}[A-Za-z0-9_.]*(?:api[-_]?key|hf[-_]?token|token)(?:\s+|[=:]))\S+/gi;
138716
+ // Assembles the payload shared by the startup-failure and runtime-crash report paths. Stderr
138717
+ // may echo secrets, so mask `--api-key`/token-looking args before they reach the DB.
138718
+ function buildCrashReport(error, exitCode, signal) {
138719
+ const classification = classifyEngineFailure({ error, exitCode, signal });
138720
+ const raw = normalizeEngineError(error.message);
138721
+ const masked = raw.replace(SECRET_ARG_MASK_PATTERN, "$1***");
138722
+ return {
138723
+ durationMs: 0,
138724
+ errorDetail: masked.slice(0, 2048),
138725
+ errorType: classification,
138726
+ ttftMs: 0,
138727
+ throughput: {
138728
+ avgTps: 0,
138729
+ peakTps: null
138730
+ },
138731
+ usage: {
138732
+ completionTokens: 0,
138733
+ promptTokens: 0,
138734
+ totalTokens: 0
138735
+ }
138736
+ };
138737
+ }
138313
138738
  const conduitStateManager = new ConduitStateManager({
138314
138739
  initialState: {
138315
138740
  state: "initialising"
@@ -138361,6 +138786,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
138361
138786
  });
138362
138787
  stopRequestedByControl = false;
138363
138788
  setErrorState({ error: normalizeEngineError(err.message) });
138789
+ // Spontaneous death of a SERVING engine → crash report, suppressed by the latch if the
138790
+ // startup's one-shot outcome was already filed. Startup-path failures report from
138791
+ // `startEngine`'s catch; this listener is the RUNTIME-crash path only.
138792
+ if (modelManager.wasRunning && !err.message.includes("interrupted by stop request")) {
138793
+ const crashReport = buildCrashReport(err, modelManager.lastExitCode, modelManager.lastExitSignal);
138794
+ reporter.reportRuntimeCrash(crashReport).catch(crashReportError => {
138795
+ logger.warn("Engine execution crash report failed", {
138796
+ error: asError(crashReportError)
138797
+ });
138798
+ });
138799
+ }
138364
138800
  });
138365
138801
  modelManager.on("engineReady", () => {
138366
138802
  setOnlineState();
@@ -138420,24 +138856,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
138420
138856
  };
138421
138857
  async function startEngine() {
138422
138858
  logger.info("Engine start requested");
138423
- conduitStateManager.setState({
138424
- modelFileName,
138425
- modelName,
138426
- state: "downloadingModelFiles",
138427
- totalProgress: {
138428
- file: 0,
138429
- total: 0
138859
+ reporter.beginStartup(new Date());
138860
+ try {
138861
+ conduitStateManager.setState({
138862
+ modelFileName,
138863
+ modelName,
138864
+ state: "downloadingModelFiles",
138865
+ totalProgress: {
138866
+ file: 0,
138867
+ total: 0
138868
+ }
138869
+ });
138870
+ await conduitStateReportManager.reportNow();
138871
+ await modelManager.prepare({
138872
+ onDownloadProgress: reportDownloadProgress
138873
+ });
138874
+ conduitStateManager.setState({
138875
+ state: "bootingEngine"
138876
+ });
138877
+ await conduitStateReportManager.reportNow();
138878
+ await modelManager.start();
138879
+ }
138880
+ catch (error) {
138881
+ const parsedError = asError(error);
138882
+ // Operator-initiated aborts are not startup failures worth reporting.
138883
+ if (!parsedError.message.includes("interrupted by stop request")) {
138884
+ const startupReport = buildCrashReport(parsedError, modelManager.lastExitCode, modelManager.lastExitSignal);
138885
+ reporter.reportStartupFailure(startupReport).catch(startupReportError => {
138886
+ logger.warn("Engine execution startup report failed", {
138887
+ error: asError(startupReportError)
138888
+ });
138889
+ });
138430
138890
  }
138431
- });
138432
- await conduitStateReportManager.reportNow();
138433
- await modelManager.prepare({
138434
- onDownloadProgress: reportDownloadProgress
138435
- });
138436
- conduitStateManager.setState({
138437
- state: "bootingEngine"
138438
- });
138439
- await conduitStateReportManager.reportNow();
138440
- await modelManager.start();
138891
+ throw error;
138892
+ }
138441
138893
  }
138442
138894
  async function stopEngine({ reason }) {
138443
138895
  if (!modelManager.canStop) {
@@ -342892,12 +343344,13 @@ async function runModelFit(options) {
342892
343344
  console.log();
342893
343345
  const feasible = filterFeasibleModels({ detection, models: recommendedModels });
342894
343346
  if (feasible.length === 0) {
343347
+ const smallestTierGB = Math.min(...recommendedModels.map(model => model.vramTierGB));
342895
343348
  console.error("No recommended models fit this hardware. " +
342896
343349
  `Budget: ${formatBytes$1(detection.gpus.some(gpu => gpu.memoryTotalBytes)
342897
343350
  ? Math.max(...detection.gpus
342898
343351
  .map(gpu => gpu.memoryTotalBytes ?? 0)
342899
343352
  .filter(bytes => bytes > 0))
342900
- : detection.memory.totalBytes)}. Smallest tier starts at 2 GB.`);
343353
+ : detection.memory.totalBytes)}. Smallest tier starts at ${smallestTierGB} GB.`);
342901
343354
  process.exitCode = 1;
342902
343355
  return;
342903
343356
  }