@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.
- package/dist/apiClient/index.d.ts +2 -1
- package/dist/cli.js +377 -17
- package/dist/cli.sea.cjs +377 -17
- package/dist/modelManagement/ModelManager.d.ts +6 -0
- package/dist/modelManagement/classifyEngineError.d.ts +12 -0
- package/dist/reporting/engineExecutionReporter.d.ts +54 -0
- package/dist/reporting/index.d.ts +1 -0
- package/dist/requestHandlers/createConduitAnthropicAPIReferenceHandlers.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
138516
|
-
|
|
138517
|
-
|
|
138518
|
-
|
|
138519
|
-
|
|
138520
|
-
|
|
138521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158745
|
-
|
|
158746
|
-
|
|
158747
|
-
|
|
158748
|
-
|
|
158749
|
-
|
|
158750
|
-
|
|
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
|
-
|
|
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?: "
|
|
89
|
+
tool_choice?: "any" | "none" | "auto" | {
|
|
90
90
|
type: "auto";
|
|
91
91
|
disable_parallel_tool_use?: boolean | undefined;
|
|
92
92
|
} | {
|