@infersec/conduit 1.112.1 → 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.
@@ -1,9 +1,10 @@
1
- import { InferenceAgentConfiguration, InferenceAgentLLMMetricsPayload, InferenceAgentMachineReportPayload, ULID, type ConduitState } from "@infersec/definitions";
1
+ import { EngineExecutionReportPayload, InferenceAgentConfiguration, InferenceAgentLLMMetricsPayload, InferenceAgentMachineReportPayload, ULID, type ConduitState } from "@infersec/definitions";
2
2
  import { Logger } from "@infersec/logger";
3
3
  export interface APIClient {
4
4
  cycleInferenceSourceEngine: () => Promise<void>;
5
5
  getConduitConfiguration: () => Promise<InferenceAgentConfiguration>;
6
6
  reportConduitState: (state: ConduitState) => Promise<void>;
7
+ reportEngineExecution: (payload: EngineExecutionReportPayload) => Promise<void>;
7
8
  reportMachineMetadata: (payload: InferenceAgentMachineReportPayload) => Promise<void>;
8
9
  reportPromptMetrics: (payload: InferenceAgentLLMMetricsPayload) => Promise<void>;
9
10
  updateMachineStatus: (status: {
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;
@@ -138371,6 +138510,119 @@ async function detectDockerVersion() {
138371
138510
  }
138372
138511
  }
138373
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
+
138374
138626
  async function createApplication({ abortController, apiClient, configuration, logger }) {
138375
138627
  ensureDockerValidEnv();
138376
138628
  logger.info("Fetching conduit configuration");
@@ -138402,6 +138654,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
138402
138654
  error: asError(error)
138403
138655
  });
138404
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
+ }
138405
138738
  const conduitStateManager = new ConduitStateManager({
138406
138739
  initialState: {
138407
138740
  state: "initialising"
@@ -138453,6 +138786,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
138453
138786
  });
138454
138787
  stopRequestedByControl = false;
138455
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
+ }
138456
138800
  });
138457
138801
  modelManager.on("engineReady", () => {
138458
138802
  setOnlineState();
@@ -138512,24 +138856,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
138512
138856
  };
138513
138857
  async function startEngine() {
138514
138858
  logger.info("Engine start requested");
138515
- conduitStateManager.setState({
138516
- modelFileName,
138517
- modelName,
138518
- state: "downloadingModelFiles",
138519
- totalProgress: {
138520
- file: 0,
138521
- 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
+ });
138522
138890
  }
138523
- });
138524
- await conduitStateReportManager.reportNow();
138525
- await modelManager.prepare({
138526
- onDownloadProgress: reportDownloadProgress
138527
- });
138528
- conduitStateManager.setState({
138529
- state: "bootingEngine"
138530
- });
138531
- await conduitStateReportManager.reportNow();
138532
- await modelManager.start();
138891
+ throw error;
138892
+ }
138533
138893
  }
138534
138894
  async function stopEngine({ reason }) {
138535
138895
  if (!modelManager.canStop) {
package/dist/cli.sea.cjs CHANGED
@@ -19986,6 +19986,32 @@ object$5({
19986
19986
  sizeBytes: number$1().int().nonnegative().nullable()
19987
19987
  });
19988
19988
 
19989
+ const EngineExecutionErrorTypeSchema = _enum$1([
19990
+ "config",
19991
+ "crash",
19992
+ "oom",
19993
+ "prompt",
19994
+ "unknown"
19995
+ ]);
19996
+ const EngineExecutionReportPayloadSchema = object$5({
19997
+ avgTps: number$1().nonnegative().finite().default(0),
19998
+ completionTokens: number$1().int().nonnegative().default(0),
19999
+ durationMs: number$1().int().nonnegative().default(0),
20000
+ engineType: LLMEngineSchema.nullable(),
20001
+ engineVersion: string$2().max(64).nullable().default(null),
20002
+ errorDetail: string$2().max(2048).nullable().default(null),
20003
+ errorType: EngineExecutionErrorTypeSchema.nullable(),
20004
+ extraArgs: array$1(tuple([string$2().min(1).max(128), string$2().max(512)]))
20005
+ .max(256)
20006
+ .default([]),
20007
+ finishedAtISO: string$2().datetime({ offset: true }),
20008
+ peakTps: number$1().nonnegative().finite().nullable().default(null),
20009
+ promptTokens: number$1().int().nonnegative().default(0),
20010
+ runAtISO: string$2().datetime({ offset: true }),
20011
+ success: boolean$1(),
20012
+ ttftMs: number$1().int().nonnegative().default(0),
20013
+ totalTokens: number$1().int().nonnegative().default(0)
20014
+ });
19989
20015
  const InferenceAgentLLMMetricsPayloadSchema = object$5({
19990
20016
  bytes: number$1().int().nonnegative(),
19991
20017
  completionTokens: number$1().int().nonnegative(),
@@ -20324,6 +20350,23 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
20324
20350
  }
20325
20351
  }
20326
20352
  },
20353
+ "/conduit/api/v1/source/:sourceID/engine/execution": {
20354
+ POST: {
20355
+ auth: {
20356
+ type: "api-key"
20357
+ },
20358
+ body: EngineExecutionReportPayloadSchema,
20359
+ parameters: {
20360
+ sourceID: ULIDSchema
20361
+ },
20362
+ response: {
20363
+ schema: object$5({
20364
+ acknowledged: literal(true)
20365
+ }),
20366
+ type: "rest"
20367
+ }
20368
+ }
20369
+ },
20327
20370
  "/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
20328
20371
  POST: {
20329
20372
  auth: {
@@ -113781,6 +113824,19 @@ function createAPIClient({ apiKey, apiURL, inferenceSourceID, logger }) {
113781
113824
  route: "/conduit/api/v1/source/:sourceID/state"
113782
113825
  });
113783
113826
  },
113827
+ reportEngineExecution: async (payload) => {
113828
+ await fetchByReference({
113829
+ baseURL: apiURL,
113830
+ body: payload,
113831
+ fetch: fetchWithAPIKey,
113832
+ method: "POST",
113833
+ parameters: {
113834
+ sourceID: inferenceSourceID
113835
+ },
113836
+ reference: API_SERVICE_CONDUIT_API_REFERENCE,
113837
+ route: "/conduit/api/v1/source/:sourceID/engine/execution"
113838
+ });
113839
+ },
113784
113840
  reportPromptMetrics: async (payload) => {
113785
113841
  await fetchByReference({
113786
113842
  baseURL: apiURL,
@@ -125641,6 +125697,9 @@ class ModelManager extends EventEmitter {
125641
125697
  lifecycleState = "stopped";
125642
125698
  downloadLockHandle = null;
125643
125699
  stopRequested = false;
125700
+ lastEngineExitCode = null;
125701
+ lastEngineExitSignal = null;
125702
+ reachedRunningState = false;
125644
125703
  modelsDirectory;
125645
125704
  constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
125646
125705
  super();
@@ -125763,6 +125822,9 @@ class ModelManager extends EventEmitter {
125763
125822
  this.lifecycleState = "starting";
125764
125823
  this.lastEngineError = null;
125765
125824
  this.stopRequested = false;
125825
+ this.lastEngineExitCode = null;
125826
+ this.lastEngineExitSignal = null;
125827
+ this.reachedRunningState = false;
125766
125828
  this.logger.info("Starting LLM engine", {
125767
125829
  agentEngineType: this.engine
125768
125830
  });
@@ -125793,6 +125855,7 @@ class ModelManager extends EventEmitter {
125793
125855
  throw err;
125794
125856
  }
125795
125857
  this.lifecycleState = "running";
125858
+ this.reachedRunningState = true;
125796
125859
  this.emit("engineReady");
125797
125860
  }
125798
125861
  async stop() {
@@ -125817,6 +125880,7 @@ class ModelManager extends EventEmitter {
125817
125880
  this.lifecycleState = "stopped";
125818
125881
  return;
125819
125882
  }
125883
+ this.reachedRunningState = false;
125820
125884
  this.lifecycleState = "stopping";
125821
125885
  this.stopRequested = true;
125822
125886
  await processManager.stop();
@@ -125829,9 +125893,18 @@ class ModelManager extends EventEmitter {
125829
125893
  this.lifecycleState === "starting" ||
125830
125894
  this.lifecycleState === "errored");
125831
125895
  }
125896
+ get lastExitCode() {
125897
+ return this.lastEngineExitCode;
125898
+ }
125899
+ get lastExitSignal() {
125900
+ return this.lastEngineExitSignal;
125901
+ }
125832
125902
  get state() {
125833
125903
  return this.lifecycleState;
125834
125904
  }
125905
+ get wasRunning() {
125906
+ return this.reachedRunningState;
125907
+ }
125835
125908
  async checkEngineReadiness() {
125836
125909
  switch (this.engine) {
125837
125910
  case "llama.cpp": {
@@ -125945,6 +126018,7 @@ class ModelManager extends EventEmitter {
125945
126018
  if (readiness === "ready") {
125946
126019
  this.clearHealthPoll();
125947
126020
  this.lifecycleState = "running";
126021
+ this.reachedRunningState = true;
125948
126022
  this.emit("engineReady");
125949
126023
  }
125950
126024
  })
@@ -126007,6 +126081,8 @@ class ModelManager extends EventEmitter {
126007
126081
  }));
126008
126082
  });
126009
126083
  processManager.on("stopped", (code, signal) => {
126084
+ this.lastEngineExitCode = code;
126085
+ this.lastEngineExitSignal = signal;
126010
126086
  if (hasTerminated) {
126011
126087
  return;
126012
126088
  }
@@ -126074,6 +126150,69 @@ class ModelManager extends EventEmitter {
126074
126150
  }
126075
126151
  }
126076
126152
 
126153
+ // Ordered most-specific first: a message mentioning a prompt-size failure should classify as
126154
+ // "prompt" even if it also touches memory text; memory outranks config because OOM kills frequently
126155
+ // emit sparse stderr. These are heuristics, not exhaustively enumerated engine vocabularies.
126156
+ const OOM_PATTERNS = [
126157
+ /CUDA out of memory/i,
126158
+ /No available memory for the cache blocks/i,
126159
+ /\bOOMKilled\b/,
126160
+ /out of memory/i,
126161
+ /MemoryError/i,
126162
+ /Cannot allocate memory/i
126163
+ ];
126164
+ const PROMPT_PATTERNS = [
126165
+ /prompt is too long/i,
126166
+ /maximum context length exceeded/i,
126167
+ /too many tokens/i
126168
+ ];
126169
+ const CONFIG_PATTERNS = [
126170
+ /unrecognized argument/i,
126171
+ /invalid argument/i,
126172
+ /error while loading state_dict/i,
126173
+ /Architecture not understood/i,
126174
+ /No such file or directory/i
126175
+ ];
126176
+ /**
126177
+ * Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
126178
+ * terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
126179
+ * how much stderr the engine produced); the message text then refines the category. Anything
126180
+ * unrecognized is "unknown".
126181
+ */
126182
+ function classifyEngineFailure({ error, exitCode, signal }) {
126183
+ // 137 = SIGKILL (kernel OOM-killer), 139 = SIGSEGV (illegal memory access). Treat both as OOM
126184
+ // so that memory-pressure deaths do not degrade to "unknown" when stderr is truncated.
126185
+ if (exitCode === 137 || exitCode === 139) {
126186
+ return "oom";
126187
+ }
126188
+ // Direct OS kills (SIGKILL/SIGSEGV) leave no meaningful exit code; honor them like 137/139.
126189
+ if (signal === "SIGKILL" || signal === "SIGSEGV") {
126190
+ return "oom";
126191
+ }
126192
+ const text = `${error.message}`.slice(0, 4000);
126193
+ for (const pattern of PROMPT_PATTERNS) {
126194
+ if (pattern.test(text)) {
126195
+ return "prompt";
126196
+ }
126197
+ }
126198
+ for (const pattern of OOM_PATTERNS) {
126199
+ if (pattern.test(text)) {
126200
+ return "oom";
126201
+ }
126202
+ }
126203
+ for (const pattern of CONFIG_PATTERNS) {
126204
+ if (pattern.test(text)) {
126205
+ return "config";
126206
+ }
126207
+ }
126208
+ // A process that exited abnormally with no recognizable diagnostic: treat as a crash rather
126209
+ // than "unknown" so the two buckets distinguish "we saw nothing" from "it died badly".
126210
+ if (exitCode !== null && exitCode !== 0) {
126211
+ return "crash";
126212
+ }
126213
+ return "unknown";
126214
+ }
126215
+
126077
126216
  const EXCEPTION_LINE_PATTERN = /([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*(.+)/;
126078
126217
  const FALLBACK_DETAIL_MAX_LENGTH = 300;
126079
126218
  const FALLBACK_RAW_MAX_LENGTH = 500;
@@ -158600,6 +158739,119 @@ async function detectDockerVersion() {
158600
158739
  }
158601
158740
  }
158602
158741
 
158742
+ /**
158743
+ * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
158744
+ * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
158745
+ * as its value when that token does not start with "-" (classic CLI convention); anything else
158746
+ * (flags, non-strings) is dropped.
158747
+ */
158748
+ function pairExtraArgs(tokens) {
158749
+ if (!Array.isArray(tokens)) {
158750
+ return [];
158751
+ }
158752
+ const list = tokens;
158753
+ const pairs = [];
158754
+ let index = 0;
158755
+ while (index < list.length) {
158756
+ const token = list[index];
158757
+ if (typeof token !== "string" || token.length === 0 || !token.startsWith("-")) {
158758
+ index++;
158759
+ continue;
158760
+ }
158761
+ const separator = token.indexOf("=");
158762
+ if (separator > -1) {
158763
+ const arg = token.slice(0, separator);
158764
+ if (arg.length > 0) {
158765
+ pairs.push([arg, token.slice(separator + 1)]);
158766
+ }
158767
+ index++;
158768
+ continue;
158769
+ }
158770
+ const next = list[index + 1];
158771
+ const consumesNext = typeof next === "string" && next.length > 0 && !next.startsWith("-");
158772
+ if (consumesNext) {
158773
+ pairs.push([token, next]);
158774
+ index += 2;
158775
+ }
158776
+ else {
158777
+ pairs.push([token, ""]);
158778
+ index++;
158779
+ }
158780
+ }
158781
+ 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);
158782
+ }
158783
+ /**
158784
+ * Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
158785
+ * latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
158786
+ * row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
158787
+ * crash, or first successful full prompt completion.
158788
+ */
158789
+ class EngineExecutionReporter {
158790
+ options;
158791
+ currentStartupAt = null;
158792
+ reportedForCurrentStartup = false;
158793
+ constructor(options) {
158794
+ this.options = options;
158795
+ }
158796
+ /** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
158797
+ beginStartup(runAt) {
158798
+ this.currentStartupAt = runAt;
158799
+ this.reportedForCurrentStartup = false;
158800
+ }
158801
+ /** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
158802
+ async reportStartupFailure(report) {
158803
+ await this.file(report, false);
158804
+ }
158805
+ /** Reports a spontaneous crash of an engine that had reached the running state. */
158806
+ async reportRuntimeCrash(report) {
158807
+ await this.file(report, false);
158808
+ }
158809
+ /** Reports the first fully-responded, token-bearing prompt completion since startup. */
158810
+ async reportSuccess(report) {
158811
+ await this.file(report, true);
158812
+ }
158813
+ async file(report, success) {
158814
+ if (this.reportedForCurrentStartup || this.currentStartupAt === null) {
158815
+ return;
158816
+ }
158817
+ // Latch BEFORE the network call: a thrown POST cannot double-file for this startup.
158818
+ this.reportedForCurrentStartup = true;
158819
+ const context = this.options.buildContext();
158820
+ const payload = {
158821
+ avgTps: report.throughput.avgTps,
158822
+ completionTokens: report.usage.completionTokens,
158823
+ durationMs: report.durationMs,
158824
+ engineType: context.engineType,
158825
+ engineVersion: context.engineVersion,
158826
+ errorDetail: success ? null : report.errorDetail,
158827
+ errorType: success ? null : report.errorType,
158828
+ extraArgs: context.extraArgsPairs,
158829
+ finishedAtISO: new Date().toISOString(),
158830
+ peakTps: report.throughput.peakTps,
158831
+ promptTokens: report.usage.promptTokens,
158832
+ runAtISO: this.currentStartupAt.toISOString(),
158833
+ success,
158834
+ ttftMs: report.ttftMs,
158835
+ totalTokens: report.usage.totalTokens
158836
+ };
158837
+ try {
158838
+ await this.options.report(payload);
158839
+ this.options.logger.info("Engine execution outcome reported", {
158840
+ inferenceSourceID: this.options.sourceLabel,
158841
+ success
158842
+ });
158843
+ }
158844
+ catch (error) {
158845
+ // Losing one report is preferable to filing two; the latch stays latched.
158846
+ this.options.logger.warn("Failed to report engine execution outcome", {
158847
+ error: asError(error),
158848
+ inferenceSourceID: this.options.sourceLabel,
158849
+ success
158850
+ });
158851
+ }
158852
+ }
158853
+ }
158854
+
158603
158855
  async function createApplication({ abortController, apiClient, configuration, logger }) {
158604
158856
  ensureDockerValidEnv();
158605
158857
  logger.info("Fetching conduit configuration");
@@ -158631,6 +158883,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
158631
158883
  error: asError(error)
158632
158884
  });
158633
158885
  }
158886
+ const reporter = new EngineExecutionReporter({
158887
+ buildContext: () => {
158888
+ const engineType = (conduitConfiguration.engineConfig?.type ??
158889
+ "llama.cpp");
158890
+ const versions = {
158891
+ exllamav3: machine?.exllamav3Version ?? null,
158892
+ "llama.cpp": machine?.llamaCppVersion ?? null,
158893
+ "mlx-lm": machine?.mlxlmVersion ?? null,
158894
+ sglang: machine?.sglangVersion ?? null,
158895
+ "tensorrt-llm": machine?.tensorrtLlmVersion ?? null,
158896
+ vllm: machine?.vllmVersion ?? null
158897
+ };
158898
+ return {
158899
+ engineType,
158900
+ engineVersion: versions[engineType] ?? null,
158901
+ extraArgsPairs: pairExtraArgs(conduitConfiguration.engineConfig?.extraArgs)
158902
+ };
158903
+ },
158904
+ logger,
158905
+ report: payload => apiClient.reportEngineExecution(payload),
158906
+ sourceLabel: configuration.inferenceSourceID
158907
+ });
158908
+ // Intercept the prompt-metrics chokepoint so the first fully-responded, token-bearing prompt of
158909
+ // each fresh startup files the one-shot engine_execution success report. Handlers close over the
158910
+ // SAME `apiClient` object and read `reportPromptMetrics` at request-dispatch time (which always
158911
+ // follows this point), so the wrapped method is what they invoke.
158912
+ const rawReportPromptMetrics = apiClient.reportPromptMetrics;
158913
+ apiClient.reportPromptMetrics = async (payload) => {
158914
+ if (payload.successful && payload.completionTokens > 0 && payload.latencyMs > 0) {
158915
+ // The one-shot report is kicked off and its settlement attached HERE (before any await): if the
158916
+ // metrics path throws below, the report promise must still be able to log its own rejection.
158917
+ const successReport = reporter
158918
+ .reportSuccess({
158919
+ durationMs: payload.latencyMs,
158920
+ errorDetail: null,
158921
+ errorType: null,
158922
+ throughput: {
158923
+ avgTps: payload.tokensPerSecond,
158924
+ peakTps: null
158925
+ },
158926
+ ttftMs: payload.timeToFirstTokenMs ?? 0,
158927
+ usage: {
158928
+ completionTokens: payload.completionTokens,
158929
+ promptTokens: payload.promptTokens,
158930
+ totalTokens: payload.totalTokens
158931
+ }
158932
+ })
158933
+ .catch(error => {
158934
+ logger.warn("Engine execution success report failed", {
158935
+ error: asError(error)
158936
+ });
158937
+ });
158938
+ await rawReportPromptMetrics(payload);
158939
+ await successReport;
158940
+ return;
158941
+ }
158942
+ await rawReportPromptMetrics(payload);
158943
+ };
158944
+ const SECRET_ARG_MASK_PATTERN = /(-{1,2}[A-Za-z0-9_.]*(?:api[-_]?key|hf[-_]?token|token)(?:\s+|[=:]))\S+/gi;
158945
+ // Assembles the payload shared by the startup-failure and runtime-crash report paths. Stderr
158946
+ // may echo secrets, so mask `--api-key`/token-looking args before they reach the DB.
158947
+ function buildCrashReport(error, exitCode, signal) {
158948
+ const classification = classifyEngineFailure({ error, exitCode, signal });
158949
+ const raw = normalizeEngineError(error.message);
158950
+ const masked = raw.replace(SECRET_ARG_MASK_PATTERN, "$1***");
158951
+ return {
158952
+ durationMs: 0,
158953
+ errorDetail: masked.slice(0, 2048),
158954
+ errorType: classification,
158955
+ ttftMs: 0,
158956
+ throughput: {
158957
+ avgTps: 0,
158958
+ peakTps: null
158959
+ },
158960
+ usage: {
158961
+ completionTokens: 0,
158962
+ promptTokens: 0,
158963
+ totalTokens: 0
158964
+ }
158965
+ };
158966
+ }
158634
158967
  const conduitStateManager = new ConduitStateManager({
158635
158968
  initialState: {
158636
158969
  state: "initialising"
@@ -158682,6 +159015,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
158682
159015
  });
158683
159016
  stopRequestedByControl = false;
158684
159017
  setErrorState({ error: normalizeEngineError(err.message) });
159018
+ // Spontaneous death of a SERVING engine → crash report, suppressed by the latch if the
159019
+ // startup's one-shot outcome was already filed. Startup-path failures report from
159020
+ // `startEngine`'s catch; this listener is the RUNTIME-crash path only.
159021
+ if (modelManager.wasRunning && !err.message.includes("interrupted by stop request")) {
159022
+ const crashReport = buildCrashReport(err, modelManager.lastExitCode, modelManager.lastExitSignal);
159023
+ reporter.reportRuntimeCrash(crashReport).catch(crashReportError => {
159024
+ logger.warn("Engine execution crash report failed", {
159025
+ error: asError(crashReportError)
159026
+ });
159027
+ });
159028
+ }
158685
159029
  });
158686
159030
  modelManager.on("engineReady", () => {
158687
159031
  setOnlineState();
@@ -158741,24 +159085,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
158741
159085
  };
158742
159086
  async function startEngine() {
158743
159087
  logger.info("Engine start requested");
158744
- conduitStateManager.setState({
158745
- modelFileName,
158746
- modelName,
158747
- state: "downloadingModelFiles",
158748
- totalProgress: {
158749
- file: 0,
158750
- total: 0
159088
+ reporter.beginStartup(new Date());
159089
+ try {
159090
+ conduitStateManager.setState({
159091
+ modelFileName,
159092
+ modelName,
159093
+ state: "downloadingModelFiles",
159094
+ totalProgress: {
159095
+ file: 0,
159096
+ total: 0
159097
+ }
159098
+ });
159099
+ await conduitStateReportManager.reportNow();
159100
+ await modelManager.prepare({
159101
+ onDownloadProgress: reportDownloadProgress
159102
+ });
159103
+ conduitStateManager.setState({
159104
+ state: "bootingEngine"
159105
+ });
159106
+ await conduitStateReportManager.reportNow();
159107
+ await modelManager.start();
159108
+ }
159109
+ catch (error) {
159110
+ const parsedError = asError(error);
159111
+ // Operator-initiated aborts are not startup failures worth reporting.
159112
+ if (!parsedError.message.includes("interrupted by stop request")) {
159113
+ const startupReport = buildCrashReport(parsedError, modelManager.lastExitCode, modelManager.lastExitSignal);
159114
+ reporter.reportStartupFailure(startupReport).catch(startupReportError => {
159115
+ logger.warn("Engine execution startup report failed", {
159116
+ error: asError(startupReportError)
159117
+ });
159118
+ });
158751
159119
  }
158752
- });
158753
- await conduitStateReportManager.reportNow();
158754
- await modelManager.prepare({
158755
- onDownloadProgress: reportDownloadProgress
158756
- });
158757
- conduitStateManager.setState({
158758
- state: "bootingEngine"
158759
- });
158760
- await conduitStateReportManager.reportNow();
158761
- await modelManager.start();
159120
+ throw error;
159121
+ }
158762
159122
  }
158763
159123
  async function stopEngine({ reason }) {
158764
159124
  if (!modelManager.canStop) {
@@ -24,6 +24,9 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
24
24
  private lifecycleState;
25
25
  private downloadLockHandle;
26
26
  private stopRequested;
27
+ private lastEngineExitCode;
28
+ private lastEngineExitSignal;
29
+ private reachedRunningState;
27
30
  protected readonly modelsDirectory: string;
28
31
  constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }: {
29
32
  contextLength?: number | null;
@@ -43,7 +46,10 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
43
46
  stop(): Promise<void>;
44
47
  get canStart(): boolean;
45
48
  get canStop(): boolean;
49
+ get lastExitCode(): number | null;
50
+ get lastExitSignal(): NodeJS.Signals | null;
46
51
  get state(): EngineLifecycleState;
52
+ get wasRunning(): boolean;
47
53
  private checkEngineReadiness;
48
54
  private checkGenericHealthReadiness;
49
55
  private checkLlamacppReadiness;
@@ -0,0 +1,12 @@
1
+ import { EngineExecutionErrorType } from "@infersec/definitions";
2
+ /**
3
+ * Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
4
+ * terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
5
+ * how much stderr the engine produced); the message text then refines the category. Anything
6
+ * unrecognized is "unknown".
7
+ */
8
+ export declare function classifyEngineFailure({ error, exitCode, signal }: {
9
+ error: Error;
10
+ exitCode: number | null;
11
+ signal?: NodeJS.Signals | null;
12
+ }): EngineExecutionErrorType;
@@ -0,0 +1,54 @@
1
+ import { EngineExecutionErrorType, EngineExecutionReportPayload, LLMEngine } from "@infersec/definitions";
2
+ import { Logger } from "@infersec/logger";
3
+ export interface EngineExecutionReportSource {
4
+ durationMs: number;
5
+ errorDetail: string | null;
6
+ errorType: EngineExecutionErrorType | null;
7
+ ttftMs: number;
8
+ throughput: {
9
+ avgTps: number;
10
+ peakTps: number | null;
11
+ };
12
+ usage: {
13
+ completionTokens: number;
14
+ promptTokens: number;
15
+ totalTokens: number;
16
+ };
17
+ }
18
+ /**
19
+ * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
20
+ * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
21
+ * as its value when that token does not start with "-" (classic CLI convention); anything else
22
+ * (flags, non-strings) is dropped.
23
+ */
24
+ export declare function pairExtraArgs(tokens: unknown): Array<[string, string]>;
25
+ /**
26
+ * Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
27
+ * latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
28
+ * row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
29
+ * crash, or first successful full prompt completion.
30
+ */
31
+ export declare class EngineExecutionReporter {
32
+ private readonly options;
33
+ private currentStartupAt;
34
+ private reportedForCurrentStartup;
35
+ constructor(options: {
36
+ buildContext: () => {
37
+ engineType: LLMEngine;
38
+ engineVersion: string | null;
39
+ extraArgsPairs: Array<[string, string]>;
40
+ };
41
+ logger: Logger;
42
+ report: (payload: EngineExecutionReportPayload) => Promise<void>;
43
+ sourceLabel: string;
44
+ });
45
+ /** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
46
+ beginStartup(runAt: Date): void;
47
+ /** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
48
+ reportStartupFailure(report: EngineExecutionReportSource): Promise<void>;
49
+ /** Reports a spontaneous crash of an engine that had reached the running state. */
50
+ reportRuntimeCrash(report: EngineExecutionReportSource): Promise<void>;
51
+ /** Reports the first fully-responded, token-bearing prompt completion since startup. */
52
+ reportSuccess(report: EngineExecutionReportSource): Promise<void>;
53
+ private file;
54
+ }
@@ -0,0 +1 @@
1
+ export { EngineExecutionReporter, type EngineExecutionReportSource, pairExtraArgs } from "./engineExecutionReporter.js";
@@ -86,7 +86,7 @@ export declare function createPostMessagesHandler(options: {
86
86
  budget_tokens: number;
87
87
  type: "enabled";
88
88
  } | undefined;
89
- tool_choice?: "none" | "any" | "auto" | {
89
+ tool_choice?: "any" | "none" | "auto" | {
90
90
  type: "auto";
91
91
  disable_parallel_tool_use?: boolean | undefined;
92
92
  } | {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@infersec/conduit",
3
3
  "description": "End user conduit agent for connecting local LLMs to the cloud.",
4
- "version": "1.112.1",
4
+ "version": "1.113.0",
5
5
  "bin": {
6
6
  "infersec-conduit": "./dist/cli.js"
7
7
  },