@infersec/conduit 1.112.1 → 1.114.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19886,6 +19886,7 @@ object$5({
19886
19886
  });
19887
19887
 
19888
19888
  const LLMEngineSchema = _enum$1([
19889
+ "custom",
19889
19890
  "exllamav3",
19890
19891
  "llama.cpp",
19891
19892
  "mlx-lm",
@@ -19894,8 +19895,25 @@ const LLMEngineSchema = _enum$1([
19894
19895
  "vllm"
19895
19896
  ]);
19896
19897
  const EngineConfigSchema = object$5({
19898
+ baseUrl: string$2().url().nullable().default(null),
19897
19899
  extraArgs: array$1(string$2()),
19898
19900
  type: LLMEngineSchema
19901
+ })
19902
+ .superRefine((config, ctx) => {
19903
+ if (config.type === "custom" && config.baseUrl === null) {
19904
+ ctx.addIssue({
19905
+ code: "custom",
19906
+ message: "Engine type 'custom' requires a base URL",
19907
+ path: ["baseUrl"]
19908
+ });
19909
+ }
19910
+ if (config.type !== "custom" && config.baseUrl !== null) {
19911
+ ctx.addIssue({
19912
+ code: "custom",
19913
+ message: "Base URL is only valid for engine type 'custom'",
19914
+ path: ["baseUrl"]
19915
+ });
19916
+ }
19899
19917
  });
19900
19918
  const LLMModelFormatSchema = _enum$1([
19901
19919
  // VLLM / SGLang / TensorRT-LLM
@@ -19954,14 +19972,18 @@ const LLMModelSchema = object$5({
19954
19972
  id: string$2().min(1),
19955
19973
  multimodalEnabled: boolean$1(),
19956
19974
  source: discriminatedUnion("type", [
19975
+ // Custom engines: no local model record, serving is external
19957
19976
  object$5({
19958
- irid: IRIDSchema,
19959
- type: literal("storage")
19977
+ type: literal("external")
19960
19978
  }),
19961
19979
  object$5({
19962
19980
  modelSecret: string$2().min(1).nullable(),
19963
19981
  slug: string$2().min(1),
19964
19982
  type: literal("huggingface")
19983
+ }),
19984
+ object$5({
19985
+ irid: IRIDSchema,
19986
+ type: literal("storage")
19965
19987
  })
19966
19988
  ]),
19967
19989
  taskType: LLMModelTaskTypeSchema,
@@ -19972,6 +19994,32 @@ object$5({
19972
19994
  sizeBytes: number$1().int().nonnegative().nullable()
19973
19995
  });
19974
19996
 
19997
+ const EngineExecutionErrorTypeSchema = _enum$1([
19998
+ "config",
19999
+ "crash",
20000
+ "oom",
20001
+ "prompt",
20002
+ "unknown"
20003
+ ]);
20004
+ const EngineExecutionReportPayloadSchema = object$5({
20005
+ avgTps: number$1().nonnegative().finite().default(0),
20006
+ completionTokens: number$1().int().nonnegative().default(0),
20007
+ durationMs: number$1().int().nonnegative().default(0),
20008
+ engineType: LLMEngineSchema.nullable(),
20009
+ engineVersion: string$2().max(64).nullable().default(null),
20010
+ errorDetail: string$2().max(2048).nullable().default(null),
20011
+ errorType: EngineExecutionErrorTypeSchema.nullable(),
20012
+ extraArgs: array$1(tuple([string$2().min(1).max(128), string$2().max(512)]))
20013
+ .max(256)
20014
+ .default([]),
20015
+ finishedAtISO: string$2().datetime({ offset: true }),
20016
+ peakTps: number$1().nonnegative().finite().nullable().default(null),
20017
+ promptTokens: number$1().int().nonnegative().default(0),
20018
+ runAtISO: string$2().datetime({ offset: true }),
20019
+ success: boolean$1(),
20020
+ ttftMs: number$1().int().nonnegative().default(0),
20021
+ totalTokens: number$1().int().nonnegative().default(0)
20022
+ });
19975
20023
  const InferenceAgentLLMMetricsPayloadSchema = object$5({
19976
20024
  bytes: number$1().int().nonnegative(),
19977
20025
  completionTokens: number$1().int().nonnegative(),
@@ -20310,6 +20358,23 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
20310
20358
  }
20311
20359
  }
20312
20360
  },
20361
+ "/conduit/api/v1/source/:sourceID/engine/execution": {
20362
+ POST: {
20363
+ auth: {
20364
+ type: "api-key"
20365
+ },
20366
+ body: EngineExecutionReportPayloadSchema,
20367
+ parameters: {
20368
+ sourceID: ULIDSchema
20369
+ },
20370
+ response: {
20371
+ schema: object$5({
20372
+ acknowledged: literal(true)
20373
+ }),
20374
+ type: "rest"
20375
+ }
20376
+ }
20377
+ },
20313
20378
  "/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
20314
20379
  POST: {
20315
20380
  auth: {
@@ -21062,7 +21127,8 @@ const CreateModelResponseSchema = object$5({
21062
21127
  const CreateSourceBodySchema = object$5({
21063
21128
  contextLength: number$1().int().positive().max(1048576).optional(),
21064
21129
  engineId: ULIDSchema,
21065
- modelID: ULIDSchema,
21130
+ // Optional for custom engines, which serve externally managed models
21131
+ modelID: ULIDSchema.nullable().optional(),
21066
21132
  name: ResourceNameSchema,
21067
21133
  quantizationLabel: string$2().min(1).max(128).optional()
21068
21134
  });
@@ -21111,7 +21177,7 @@ const SourceDetailResponseSchema = object$5({
21111
21177
  const UpdateSourceBodySchema = object$5({
21112
21178
  contextLength: number$1().int().positive().nullable().optional(),
21113
21179
  engineId: ULIDSchema.nullable().optional(),
21114
- modelID: ULIDSchema.optional(),
21180
+ modelID: ULIDSchema.nullable().optional(),
21115
21181
  name: ResourceNameSchema.optional(),
21116
21182
  quantizationLabel: string$2().min(1).max(128).nullable().optional()
21117
21183
  });
@@ -21235,6 +21301,7 @@ const CreateEndpointResponseSchema = object$5({
21235
21301
  id: ULIDSchema
21236
21302
  });
21237
21303
  const EngineOutputSchema = object$5({
21304
+ baseUrl: string$2().nullable(),
21238
21305
  created: string$2(),
21239
21306
  extraArgs: array$1(string$2()),
21240
21307
  id: ULIDSchema,
@@ -21243,11 +21310,29 @@ const EngineOutputSchema = object$5({
21243
21310
  updated: string$2()
21244
21311
  });
21245
21312
  const CreateEngineBodySchema = object$5({
21313
+ baseUrl: string$2().url().nullable().optional(),
21246
21314
  extraArgs: array$1(string$2()).optional(),
21247
21315
  name: ResourceNameSchema,
21248
21316
  type: LLMEngineSchema
21317
+ })
21318
+ .superRefine((body, ctx) => {
21319
+ if (body.type === "custom" && !body.baseUrl) {
21320
+ ctx.addIssue({
21321
+ code: "custom",
21322
+ message: "Engine type 'custom' requires a base URL",
21323
+ path: ["baseUrl"]
21324
+ });
21325
+ }
21326
+ if (body.type !== "custom" && body.baseUrl) {
21327
+ ctx.addIssue({
21328
+ code: "custom",
21329
+ message: "Base URL is only valid for engine type 'custom'",
21330
+ path: ["baseUrl"]
21331
+ });
21332
+ }
21249
21333
  });
21250
21334
  const UpdateEngineBodySchema = object$5({
21335
+ baseUrl: string$2().url().nullable().optional(),
21251
21336
  extraArgs: array$1(string$2()).optional(),
21252
21337
  name: ResourceNameSchema.optional(),
21253
21338
  type: LLMEngineSchema.optional()
@@ -22053,6 +22138,11 @@ const RecommendedModelSchema = object$5({
22053
22138
  const recommendedModels = RecommendedModelSchema.array().parse(modelsData);
22054
22139
 
22055
22140
  const ENGINE_API_COMPATIBILITY = {
22141
+ custom: {
22142
+ nativeAnthropicMessages: false,
22143
+ supportsEmbeddings: true,
22144
+ supportsVision: true
22145
+ },
22056
22146
  exllamav3: {
22057
22147
  nativeAnthropicMessages: false,
22058
22148
  supportsEmbeddings: false,
@@ -111702,6 +111792,27 @@ function registerEndpointCommands({ program }) {
111702
111792
  }
111703
111793
 
111704
111794
  const ENGINE_TYPES = LLMEngineSchema.options;
111795
+ function isValidURL(value) {
111796
+ try {
111797
+ const url = new URL(value);
111798
+ return url.protocol === "http:" || url.protocol === "https:";
111799
+ }
111800
+ catch (_error) {
111801
+ return false;
111802
+ }
111803
+ }
111804
+ function validateEngineBaseURL({ baseUrl, type }) {
111805
+ if (type === "custom" && !baseUrl) {
111806
+ throw new Error("Engine type 'custom' requires --base-url");
111807
+ }
111808
+ if (baseUrl !== undefined && type !== "custom") {
111809
+ throw new Error("--base-url is only valid for engine type 'custom'");
111810
+ }
111811
+ if (baseUrl !== undefined && baseUrl !== "" && !isValidURL(baseUrl)) {
111812
+ throw new Error(`Invalid --base-url value: ${baseUrl}`);
111813
+ }
111814
+ return baseUrl === "" ? null : (baseUrl ?? null);
111815
+ }
111705
111816
  function buildEngineCreateBody(options) {
111706
111817
  if (!options.name) {
111707
111818
  throw new Error("--name is required");
@@ -111712,7 +111823,12 @@ function buildEngineCreateBody(options) {
111712
111823
  if (!ENGINE_TYPES.includes(options.type)) {
111713
111824
  throw new Error(`Invalid engine type: ${options.type} (expected one of: ${ENGINE_TYPES.join(", ")})`);
111714
111825
  }
111826
+ const baseUrl = validateEngineBaseURL({
111827
+ baseUrl: options.baseUrl,
111828
+ type: options.type
111829
+ });
111715
111830
  return {
111831
+ baseUrl,
111716
111832
  extraArgs: options.arg ?? [],
111717
111833
  name: options.name,
111718
111834
  type: options.type
@@ -111730,6 +111846,24 @@ function buildEngineUpdateBody(options) {
111730
111846
  }
111731
111847
  if (options.arg !== undefined)
111732
111848
  body.extraArgs = options.arg;
111849
+ if (options.baseUrl !== undefined) {
111850
+ const targetType = body.type;
111851
+ if (targetType !== undefined) {
111852
+ body.baseUrl = validateEngineBaseURL({
111853
+ baseUrl: options.baseUrl,
111854
+ type: targetType
111855
+ });
111856
+ }
111857
+ else if (options.baseUrl === "") {
111858
+ body.baseUrl = null;
111859
+ }
111860
+ else if (!isValidURL(options.baseUrl)) {
111861
+ throw new Error(`Invalid --base-url value: ${options.baseUrl}`);
111862
+ }
111863
+ else {
111864
+ body.baseUrl = options.baseUrl;
111865
+ }
111866
+ }
111733
111867
  return body;
111734
111868
  }
111735
111869
 
@@ -111743,10 +111877,11 @@ function registerEngineCommands({ program }) {
111743
111877
  .description("Create or update an inference engine resource")
111744
111878
  .option("--api-url <url>", "API base URL (required, no environment variable fallback)")
111745
111879
  .option("--arg <flag>", 'Raw engine CLI flag, repeatable (eg --arg "--flash-attn on")', collect, [])
111880
+ .option("--base-url <url>", "Base URL of an external OpenAI-compatible server (engine type 'custom' only)")
111746
111881
  .option("--id <ulid>", "Target an existing engine by ID (requires --update)")
111747
111882
  .option("--key <value>", "API key (required, no environment variable fallback)")
111748
111883
  .option("--name <name>", "Engine name (matched by --update)")
111749
- .option("--type <type>", "Engine type: llama.cpp|vllm|sglang|tensorrt-llm|mlx-lm|exllamav3")
111884
+ .option("--type <type>", "Engine type: llama.cpp|vllm|sglang|tensorrt-llm|mlx-lm|exllamav3|custom")
111750
111885
  .option("--update", "Update an existing engine matched by name (or --id) instead of erroring")
111751
111886
  .action(async (options) => {
111752
111887
  const { apiURL, apiKey } = resolveManagementConnection(options);
@@ -113767,6 +113902,19 @@ function createAPIClient({ apiKey, apiURL, inferenceSourceID, logger }) {
113767
113902
  route: "/conduit/api/v1/source/:sourceID/state"
113768
113903
  });
113769
113904
  },
113905
+ reportEngineExecution: async (payload) => {
113906
+ await fetchByReference({
113907
+ baseURL: apiURL,
113908
+ body: payload,
113909
+ fetch: fetchWithAPIKey,
113910
+ method: "POST",
113911
+ parameters: {
113912
+ sourceID: inferenceSourceID
113913
+ },
113914
+ reference: API_SERVICE_CONDUIT_API_REFERENCE,
113915
+ route: "/conduit/api/v1/source/:sourceID/engine/execution"
113916
+ });
113917
+ },
113770
113918
  reportPromptMetrics: async (payload) => {
113771
113919
  await fetchByReference({
113772
113920
  baseURL: apiURL,
@@ -117733,6 +117881,9 @@ async function getChatTemplateEngineArgs({ engine, model, targetDirectory }) {
117733
117881
  return [];
117734
117882
  const flag = FLAG_BASED_ENGINE_ARGS[engine];
117735
117883
  if (!flag) {
117884
+ if (engine === "custom") {
117885
+ console.warn("[chatTemplate] Custom engines manage their own serving; ignoring chat template override");
117886
+ }
117736
117887
  if (engine === "tensorrt-llm") {
117737
117888
  console.warn("[chatTemplate] TensorRT-LLM does not support chat template overrides; ignoring");
117738
117889
  }
@@ -124630,7 +124781,7 @@ function matchesQuantizationVariant({ filePath, variant }) {
124630
124781
  return segments.slice(0, -1).some(segment => matcher.test(segment));
124631
124782
  }
124632
124783
  async function findQuantizedModelTarget({ model, path }) {
124633
- if (model.source.type === "storage") {
124784
+ if (model.source.type !== "huggingface") {
124634
124785
  throw new Error("Model storage not supported yet");
124635
124786
  }
124636
124787
  if (model.format !== "gguf") {
@@ -125545,7 +125696,11 @@ function sanitizeSegment(value) {
125545
125696
  .replace(new RegExp(`${SEPARATOR}{2,}`, "g"), SEPARATOR);
125546
125697
  }
125547
125698
  function createModelStorageKey(model) {
125548
- const identifier = model.source.type === "huggingface" ? model.source.slug : model.source.irid;
125699
+ const identifier = model.source.type === "huggingface"
125700
+ ? model.source.slug
125701
+ : model.source.type === "storage"
125702
+ ? model.source.irid
125703
+ : model.id;
125549
125704
  return `${model.source.type}${SEPARATOR}${sanitizeSegment(identifier)}`;
125550
125705
  }
125551
125706
 
@@ -125621,12 +125776,16 @@ class ModelManager extends EventEmitter {
125621
125776
  uniqueName;
125622
125777
  contextLength;
125623
125778
  logger;
125779
+ discoveredModelNames = [];
125624
125780
  engineProcess = null;
125625
125781
  healthPollInterval = null;
125626
125782
  lastEngineError = null;
125627
125783
  lifecycleState = "stopped";
125628
125784
  downloadLockHandle = null;
125629
125785
  stopRequested = false;
125786
+ lastEngineExitCode = null;
125787
+ lastEngineExitSignal = null;
125788
+ reachedRunningState = false;
125630
125789
  modelsDirectory;
125631
125790
  constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
125632
125791
  super();
@@ -125650,6 +125809,7 @@ class ModelManager extends EventEmitter {
125650
125809
  }
125651
125810
  async fetchOpenAI(path, opts) {
125652
125811
  switch (this.engine) {
125812
+ case "custom":
125653
125813
  case "exllamav3":
125654
125814
  case "llama.cpp":
125655
125815
  case "mlx-lm":
@@ -125657,6 +125817,9 @@ class ModelManager extends EventEmitter {
125657
125817
  case "tensorrt-llm":
125658
125818
  case "vllm": {
125659
125819
  this.logger.debug(`Fetching from engine: ${path}`);
125820
+ const baseURL = this.engine === "custom"
125821
+ ? this.requireCustomBaseURL()
125822
+ : `http://localhost:${this.enginePort}`;
125660
125823
  const callerSignal = opts?.signal;
125661
125824
  const controller = new AbortController();
125662
125825
  const timeout = setTimeout(() => {
@@ -125667,7 +125830,7 @@ class ModelManager extends EventEmitter {
125667
125830
  : controller.signal;
125668
125831
  try {
125669
125832
  const fetchStartedAt = Date.now();
125670
- const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, path), {
125833
+ const response = await undiciExports.fetch(joinURL(baseURL, path), {
125671
125834
  ...opts,
125672
125835
  dispatcher: ENGINE_AGENT,
125673
125836
  headers: {
@@ -125702,6 +125865,11 @@ class ModelManager extends EventEmitter {
125702
125865
  modelID: this.model.id
125703
125866
  });
125704
125867
  switch (this.engine) {
125868
+ case "custom":
125869
+ if (this.model.chatTemplate) {
125870
+ this.logger.warn("Chat template overrides are ignored for custom engines: the remote server manages its own serving");
125871
+ }
125872
+ break;
125705
125873
  case "exllamav3":
125706
125874
  case "llama.cpp":
125707
125875
  case "mlx-lm":
@@ -125749,12 +125917,17 @@ class ModelManager extends EventEmitter {
125749
125917
  this.lifecycleState = "starting";
125750
125918
  this.lastEngineError = null;
125751
125919
  this.stopRequested = false;
125920
+ this.lastEngineExitCode = null;
125921
+ this.lastEngineExitSignal = null;
125922
+ this.reachedRunningState = false;
125752
125923
  this.logger.info("Starting LLM engine", {
125753
125924
  agentEngineType: this.engine
125754
125925
  });
125755
125926
  try {
125756
125927
  this.engineProcess = await this.startEngineProcess();
125757
- this.bindEngineProcessEvents(this.engineProcess);
125928
+ if (this.engineProcess) {
125929
+ this.bindEngineProcessEvents(this.engineProcess);
125930
+ }
125758
125931
  this.logger.info("Started LLM engine", {
125759
125932
  agentEngineType: this.engine
125760
125933
  });
@@ -125773,13 +125946,17 @@ class ModelManager extends EventEmitter {
125773
125946
  if (!alreadyEmitted) {
125774
125947
  this.emit("engineError", err);
125775
125948
  }
125776
- if (this.engineProcess) {
125949
+ if (this.engineProcess || this.engine === "custom") {
125777
125950
  this.startHealthPoll();
125778
125951
  }
125779
125952
  throw err;
125780
125953
  }
125781
125954
  this.lifecycleState = "running";
125955
+ this.reachedRunningState = true;
125782
125956
  this.emit("engineReady");
125957
+ if (this.engine === "custom") {
125958
+ this.startHealthPoll();
125959
+ }
125783
125960
  }
125784
125961
  async stop() {
125785
125962
  if (this.lifecycleState === "stopping") {
@@ -125800,9 +125977,12 @@ class ModelManager extends EventEmitter {
125800
125977
  this.clearHealthPoll();
125801
125978
  const processManager = this.engineProcess;
125802
125979
  if (!processManager) {
125980
+ this.stopRequested = true;
125981
+ this.reachedRunningState = false;
125803
125982
  this.lifecycleState = "stopped";
125804
125983
  return;
125805
125984
  }
125985
+ this.reachedRunningState = false;
125806
125986
  this.lifecycleState = "stopping";
125807
125987
  this.stopRequested = true;
125808
125988
  await processManager.stop();
@@ -125815,11 +125995,34 @@ class ModelManager extends EventEmitter {
125815
125995
  this.lifecycleState === "starting" ||
125816
125996
  this.lifecycleState === "errored");
125817
125997
  }
125998
+ get lastExitCode() {
125999
+ return this.lastEngineExitCode;
126000
+ }
126001
+ get lastExitSignal() {
126002
+ return this.lastEngineExitSignal;
126003
+ }
125818
126004
  get state() {
125819
126005
  return this.lifecycleState;
125820
126006
  }
126007
+ get resolvedServedModelName() {
126008
+ if (this.engine !== "custom")
126009
+ return null;
126010
+ return this.discoveredModelNames[0] ?? null;
126011
+ }
126012
+ get wasRunning() {
126013
+ return this.reachedRunningState;
126014
+ }
126015
+ get customBaseURL() {
126016
+ if (this.engine !== "custom")
126017
+ return null;
126018
+ const baseUrl = this.engineConfig?.baseUrl;
126019
+ return typeof baseUrl === "string" && baseUrl.length > 0 ? baseUrl : null;
126020
+ }
125821
126021
  async checkEngineReadiness() {
125822
126022
  switch (this.engine) {
126023
+ case "custom": {
126024
+ return this.checkCustomReadiness();
126025
+ }
125823
126026
  case "llama.cpp": {
125824
126027
  return this.checkLlamacppReadiness();
125825
126028
  }
@@ -125836,6 +126039,51 @@ class ModelManager extends EventEmitter {
125836
126039
  return "ready";
125837
126040
  }
125838
126041
  }
126042
+ async checkCustomReadiness() {
126043
+ const baseURL = this.customBaseURL;
126044
+ if (!baseURL) {
126045
+ return "unreachable";
126046
+ }
126047
+ try {
126048
+ const response = await undiciExports.fetch(joinURL(baseURL, "/v1/models"), {
126049
+ method: "GET",
126050
+ signal: AbortSignal.timeout(5000)
126051
+ });
126052
+ if (response.status === 503) {
126053
+ return "loading";
126054
+ }
126055
+ if (!response.ok) {
126056
+ return "unreachable";
126057
+ }
126058
+ const payload = (await response.json());
126059
+ const models = Array.isArray(payload.data) ? payload.data : [];
126060
+ const modelIDs = models
126061
+ .map(model => {
126062
+ if (model === null || typeof model !== "object")
126063
+ return null;
126064
+ const id = model.id;
126065
+ return typeof id === "string" ? id : null;
126066
+ })
126067
+ .filter((id) => id !== null);
126068
+ if (modelIDs.length === 0) {
126069
+ this.logger.warn("Custom engine endpoint exposed no models via /v1/models", {
126070
+ engineBaseURL: baseURL
126071
+ });
126072
+ return "loading";
126073
+ }
126074
+ if (modelIDs.join("\n") !== this.discoveredModelNames.join("\n")) {
126075
+ this.logger.info("Discovered models on custom engine endpoint", {
126076
+ engineBaseURL: baseURL,
126077
+ models: modelIDs
126078
+ });
126079
+ this.discoveredModelNames = modelIDs;
126080
+ }
126081
+ return "ready";
126082
+ }
126083
+ catch (_error) {
126084
+ return "unreachable";
126085
+ }
126086
+ }
125839
126087
  async checkGenericHealthReadiness() {
125840
126088
  try {
125841
126089
  const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, "/health"), {
@@ -125891,18 +126139,24 @@ class ModelManager extends EventEmitter {
125891
126139
  }
125892
126140
  }
125893
126141
  async waitForEngineReady() {
125894
- const maxWaitMs = 15 * 60 * 1000;
126142
+ const maxWaitMs = Number.parseInt(process.env.ENGINE_STARTUP_TIMEOUT_MS ?? "", 10) || 15 * 60 * 1000;
125895
126143
  const pollIntervalMs = 2000;
125896
126144
  const start = Date.now();
125897
126145
  while (Date.now() - start < maxWaitMs) {
125898
- if (this.lifecycleState === "stopping") {
126146
+ if (this.lifecycleState === "stopping" || this.stopRequested) {
125899
126147
  throw new Error("LLM engine startup interrupted by stop request");
125900
126148
  }
125901
- if (!this.engineProcess) {
126149
+ if (!this.engineProcess && this.engine !== "custom") {
125902
126150
  throw new Error("LLM engine process exited before readiness checks completed");
125903
126151
  }
125904
126152
  const readiness = await this.checkEngineReadiness();
125905
126153
  if (readiness === "ready") {
126154
+ // A stop() may have landed while the readiness request was
126155
+ // in flight; re-check before declaring ready.
126156
+ const lifecycleState = this.lifecycleState;
126157
+ if (lifecycleState === "stopping" || this.stopRequested) {
126158
+ throw new Error("LLM engine startup interrupted by stop request");
126159
+ }
125906
126160
  return;
125907
126161
  }
125908
126162
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
@@ -125920,18 +126174,32 @@ class ModelManager extends EventEmitter {
125920
126174
  }
125921
126175
  startHealthPoll() {
125922
126176
  this.clearHealthPoll();
125923
- this.logger.info("Starting background health poll for errored engine");
126177
+ this.logger.info("Starting background engine health poll", {
126178
+ agentEngineType: this.engine
126179
+ });
125924
126180
  this.healthPollInterval = setInterval(() => {
125925
- if (!this.engineProcess) {
126181
+ if (!this.engineProcess && this.engine !== "custom") {
125926
126182
  this.clearHealthPoll();
125927
126183
  return;
125928
126184
  }
125929
126185
  this.checkEngineReadiness()
125930
126186
  .then(readiness => {
125931
126187
  if (readiness === "ready") {
125932
- this.clearHealthPoll();
125933
- this.lifecycleState = "running";
125934
- this.emit("engineReady");
126188
+ if (this.lifecycleState === "errored" ||
126189
+ this.lifecycleState === "starting") {
126190
+ this.lifecycleState = "running";
126191
+ this.reachedRunningState = true;
126192
+ this.emit("engineReady");
126193
+ }
126194
+ if (this.engine !== "custom") {
126195
+ this.clearHealthPoll();
126196
+ }
126197
+ return;
126198
+ }
126199
+ if (this.engine === "custom" &&
126200
+ readiness === "unreachable" &&
126201
+ this.lifecycleState === "running") {
126202
+ this.recordEngineError(new Error(`Custom engine endpoint unreachable: ${this.customBaseURL}`));
125935
126203
  }
125936
126204
  })
125937
126205
  .catch(() => {
@@ -125956,6 +126224,15 @@ class ModelManager extends EventEmitter {
125956
126224
  this.lastEngineError = err;
125957
126225
  this.emit("engineError", err);
125958
126226
  }
126227
+ requireCustomBaseURL() {
126228
+ const baseURL = this.customBaseURL;
126229
+ if (!baseURL) {
126230
+ throw new ConfigurationInvalidError({
126231
+ message: "Custom engine requires a base URL"
126232
+ });
126233
+ }
126234
+ return baseURL;
126235
+ }
125959
126236
  async releaseDownloadLock() {
125960
126237
  const handle = this.downloadLockHandle;
125961
126238
  if (!handle)
@@ -125993,6 +126270,8 @@ class ModelManager extends EventEmitter {
125993
126270
  }));
125994
126271
  });
125995
126272
  processManager.on("stopped", (code, signal) => {
126273
+ this.lastEngineExitCode = code;
126274
+ this.lastEngineExitSignal = signal;
125996
126275
  if (hasTerminated) {
125997
126276
  return;
125998
126277
  }
@@ -126020,6 +126299,8 @@ class ModelManager extends EventEmitter {
126020
126299
  async startEngineProcess() {
126021
126300
  const targetDir = join(this.modelsDirectory, this.uniqueName);
126022
126301
  switch (this.engine) {
126302
+ case "custom":
126303
+ return null;
126023
126304
  case "exllamav3":
126024
126305
  return startExllamav3.call(this, {
126025
126306
  enginePort: this.enginePort,
@@ -126060,6 +126341,69 @@ class ModelManager extends EventEmitter {
126060
126341
  }
126061
126342
  }
126062
126343
 
126344
+ // Ordered most-specific first: a message mentioning a prompt-size failure should classify as
126345
+ // "prompt" even if it also touches memory text; memory outranks config because OOM kills frequently
126346
+ // emit sparse stderr. These are heuristics, not exhaustively enumerated engine vocabularies.
126347
+ const OOM_PATTERNS = [
126348
+ /CUDA out of memory/i,
126349
+ /No available memory for the cache blocks/i,
126350
+ /\bOOMKilled\b/,
126351
+ /out of memory/i,
126352
+ /MemoryError/i,
126353
+ /Cannot allocate memory/i
126354
+ ];
126355
+ const PROMPT_PATTERNS = [
126356
+ /prompt is too long/i,
126357
+ /maximum context length exceeded/i,
126358
+ /too many tokens/i
126359
+ ];
126360
+ const CONFIG_PATTERNS = [
126361
+ /unrecognized argument/i,
126362
+ /invalid argument/i,
126363
+ /error while loading state_dict/i,
126364
+ /Architecture not understood/i,
126365
+ /No such file or directory/i
126366
+ ];
126367
+ /**
126368
+ * Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
126369
+ * terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
126370
+ * how much stderr the engine produced); the message text then refines the category. Anything
126371
+ * unrecognized is "unknown".
126372
+ */
126373
+ function classifyEngineFailure({ error, exitCode, signal }) {
126374
+ // 137 = SIGKILL (kernel OOM-killer), 139 = SIGSEGV (illegal memory access). Treat both as OOM
126375
+ // so that memory-pressure deaths do not degrade to "unknown" when stderr is truncated.
126376
+ if (exitCode === 137 || exitCode === 139) {
126377
+ return "oom";
126378
+ }
126379
+ // Direct OS kills (SIGKILL/SIGSEGV) leave no meaningful exit code; honor them like 137/139.
126380
+ if (signal === "SIGKILL" || signal === "SIGSEGV") {
126381
+ return "oom";
126382
+ }
126383
+ const text = `${error.message}`.slice(0, 4000);
126384
+ for (const pattern of PROMPT_PATTERNS) {
126385
+ if (pattern.test(text)) {
126386
+ return "prompt";
126387
+ }
126388
+ }
126389
+ for (const pattern of OOM_PATTERNS) {
126390
+ if (pattern.test(text)) {
126391
+ return "oom";
126392
+ }
126393
+ }
126394
+ for (const pattern of CONFIG_PATTERNS) {
126395
+ if (pattern.test(text)) {
126396
+ return "config";
126397
+ }
126398
+ }
126399
+ // A process that exited abnormally with no recognizable diagnostic: treat as a crash rather
126400
+ // than "unknown" so the two buckets distinguish "we saw nothing" from "it died badly".
126401
+ if (exitCode !== null && exitCode !== 0) {
126402
+ return "crash";
126403
+ }
126404
+ return "unknown";
126405
+ }
126406
+
126063
126407
  const EXCEPTION_LINE_PATTERN = /([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*(.+)/;
126064
126408
  const FALLBACK_DETAIL_MAX_LENGTH = 300;
126065
126409
  const FALLBACK_RAW_MAX_LENGTH = 500;
@@ -126268,8 +126612,9 @@ function isEngineUsageChunk(value) {
126268
126612
  }
126269
126613
  return true;
126270
126614
  }
126271
- function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126615
+ function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126272
126616
  const startedAt = requestStartedAt ?? Date.now();
126617
+ const clientModelName = responseModelName ?? null;
126273
126618
  const passThrough = new PassThrough();
126274
126619
  passThrough.on("error", (error) => {
126275
126620
  logger.error("Engine response stream error", {
@@ -126281,53 +126626,61 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126281
126626
  let firstChunkAt = null;
126282
126627
  let usage = null;
126283
126628
  let buffer = "";
126629
+ let pendingFragment = "";
126284
126630
  let completed = false;
126285
- function modifyChunkWithUsage(chunk) {
126286
- const text = chunk.toString("utf8");
126287
- const lines = text.split("\n");
126288
- const modifiedLines = [];
126289
- for (const rawLine of lines) {
126290
- const line = rawLine.trim();
126291
- if (!line.startsWith("data:")) {
126292
- modifiedLines.push(rawLine);
126293
- continue;
126631
+ function rewriteDataLine(rawLine) {
126632
+ const line = rawLine.trim();
126633
+ if (!line.startsWith("data:")) {
126634
+ return rawLine;
126635
+ }
126636
+ const payload = line.slice(5).trim();
126637
+ if (!payload || payload === "[DONE]") {
126638
+ return rawLine;
126639
+ }
126640
+ try {
126641
+ const parsed = JSON.parse(payload);
126642
+ let modified = false;
126643
+ if (coerceToolCallArguments(parsed)) {
126644
+ modified = true;
126294
126645
  }
126295
- const payload = line.slice(5).trim();
126296
- if (!payload || payload === "[DONE]") {
126297
- modifiedLines.push(rawLine);
126298
- continue;
126646
+ if (clientModelName !== null &&
126647
+ typeof parsed.model === "string" &&
126648
+ parsed.model !== clientModelName) {
126649
+ parsed.model = clientModelName;
126650
+ modified = true;
126299
126651
  }
126300
- try {
126301
- const parsed = JSON.parse(payload);
126302
- let modified = false;
126303
- if (coerceToolCallArguments(parsed)) {
126652
+ if (parsed.usage) {
126653
+ const usageChunk = parsed.usage;
126654
+ const effectiveContext = getEffectiveContextLength({
126655
+ contextLength,
126656
+ engineConfig,
126657
+ engineType
126658
+ });
126659
+ if (usageChunk.context_usage === undefined &&
126660
+ usageChunk.prompt_tokens !== undefined &&
126661
+ effectiveContext !== null) {
126662
+ usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126304
126663
  modified = true;
126305
126664
  }
126306
- if (parsed.usage) {
126307
- const usageChunk = parsed.usage;
126308
- const effectiveContext = getEffectiveContextLength({
126309
- contextLength,
126310
- engineConfig,
126311
- engineType
126312
- });
126313
- if (usageChunk.context_usage === undefined &&
126314
- usageChunk.prompt_tokens !== undefined &&
126315
- effectiveContext !== null) {
126316
- usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126317
- modified = true;
126318
- }
126319
- }
126320
- if (modified) {
126321
- modifiedLines.push("data: " + JSON.stringify(parsed));
126322
- continue;
126323
- }
126324
126665
  }
126325
- catch (_error) {
126326
- // Ignore malformed chunks
126666
+ if (modified) {
126667
+ return "data: " + JSON.stringify(parsed);
126327
126668
  }
126328
- modifiedLines.push(rawLine);
126329
126669
  }
126330
- return Buffer.from(modifiedLines.join("\n"), "utf8");
126670
+ catch (_error) {
126671
+ // Ignore malformed chunks
126672
+ }
126673
+ return rawLine;
126674
+ }
126675
+ // SSE events can split across transport chunks: hold back the trailing
126676
+ // (newline-less) fragment and only rewrite complete data lines, so a
126677
+ // partial JSON event is never forwarded with its upstream model name.
126678
+ function modifyChunkWithUsage(chunk, flush = false) {
126679
+ const combined = pendingFragment + chunk.toString("utf8");
126680
+ const lines = combined.split("\n");
126681
+ pendingFragment = flush ? "" : (lines.pop() ?? "");
126682
+ const modifiedLines = lines.map(rewriteDataLine);
126683
+ return Buffer.from(modifiedLines.length > 0 ? modifiedLines.join("\n") + (flush ? "" : "\n") : "", "utf8");
126331
126684
  }
126332
126685
  function parseUsageFromBuffer() {
126333
126686
  const lines = buffer.split("\n");
@@ -126423,6 +126776,9 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126423
126776
  if (buffer.length > 0) {
126424
126777
  parseUsageFromBuffer();
126425
126778
  }
126779
+ if (pendingFragment.length > 0) {
126780
+ passThrough.write(modifyChunkWithUsage(Buffer.from("", "utf8"), true));
126781
+ }
126426
126782
  logEngineMetrics({
126427
126783
  agentEngineType,
126428
126784
  level: "info",
@@ -126465,9 +126821,11 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126465
126821
  stream: passThrough
126466
126822
  };
126467
126823
  }
126468
- function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126824
+ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126469
126825
  const maxUsageCaptureBytes = 1024 * 1024;
126470
126826
  const startedAt = requestStartedAt ?? Date.now();
126827
+ const clientModelName = responseModelName ?? null;
126828
+ const rewriteBuffer = clientModelName !== null ? [] : null;
126471
126829
  const passThrough = new PassThrough();
126472
126830
  passThrough.on("error", (error) => {
126473
126831
  logger.error("Engine response stream error", {
@@ -126523,7 +126881,12 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126523
126881
  usageChunks.length = 0;
126524
126882
  }
126525
126883
  }
126526
- passThrough.write(chunkBuffer);
126884
+ if (rewriteBuffer) {
126885
+ rewriteBuffer.push(chunkBuffer);
126886
+ }
126887
+ else {
126888
+ passThrough.write(chunkBuffer);
126889
+ }
126527
126890
  });
126528
126891
  body.once("error", err => {
126529
126892
  logEngineMetrics({
@@ -126583,6 +126946,24 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126583
126946
  responseBytes,
126584
126947
  usage
126585
126948
  });
126949
+ if (rewriteBuffer) {
126950
+ const original = Buffer.concat(rewriteBuffer);
126951
+ let output = original;
126952
+ try {
126953
+ const parsed = JSON.parse(original.toString("utf8"));
126954
+ if (parsed !== null &&
126955
+ typeof parsed === "object" &&
126956
+ typeof parsed.model === "string" &&
126957
+ parsed.model !== clientModelName) {
126958
+ parsed.model = clientModelName;
126959
+ output = Buffer.from(JSON.stringify(parsed), "utf8");
126960
+ }
126961
+ }
126962
+ catch (_error) {
126963
+ // Non-JSON body: pass through untouched
126964
+ }
126965
+ passThrough.write(output);
126966
+ }
126586
126967
  finalize(null);
126587
126968
  passThrough.end();
126588
126969
  });
@@ -126719,7 +127100,7 @@ function applyChatTemplateKwargs({ body, model }) {
126719
127100
  }
126720
127101
  return payload;
126721
127102
  }
126722
- function serializeRequestBody$1(body, { model, path } = {}) {
127103
+ function serializeRequestBody$1(body, { model, path, servedModelName } = {}) {
126723
127104
  if (!isPlainObject$a(body)) {
126724
127105
  const payload = typeof body === "string" ? body : JSON.stringify(body);
126725
127106
  return {
@@ -126728,6 +127109,9 @@ function serializeRequestBody$1(body, { model, path } = {}) {
126728
127109
  };
126729
127110
  }
126730
127111
  let requestPayload = { ...body };
127112
+ if (servedModelName) {
127113
+ requestPayload.model = servedModelName;
127114
+ }
126731
127115
  if (path === "/v1/chat/completions" && model) {
126732
127116
  requestPayload = applyChatTemplateKwargs({ body: requestPayload, model });
126733
127117
  }
@@ -126784,8 +127168,15 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
126784
127168
  }
126785
127169
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
126786
127170
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127171
+ const servedModelName = modelManager.resolvedServedModelName;
127172
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
126787
127173
  const serializedBody = isPlainObject$a(body)
126788
- ? JSON.stringify(body)
127174
+ ? JSON.stringify(servedModelName
127175
+ ? {
127176
+ ...body,
127177
+ model: servedModelName
127178
+ }
127179
+ : body)
126789
127180
  : typeof body === "string"
126790
127181
  ? body
126791
127182
  : JSON.stringify(body);
@@ -126903,7 +127294,8 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
126903
127294
  onComplete: onMonitoringComplete,
126904
127295
  requestBodyBytes,
126905
127296
  requestPath: "/v1/embeddings",
126906
- requestStartedAt
127297
+ requestStartedAt,
127298
+ responseModelName: servedModelName ? clientModelName : null
126907
127299
  });
126908
127300
  return {
126909
127301
  body: monitoredResponse.stream,
@@ -126928,8 +127320,10 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
126928
127320
  }
126929
127321
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
126930
127322
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127323
+ const servedModelName = modelManager.resolvedServedModelName;
127324
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
126931
127325
  const effectiveBody = modelManager.model.multimodalEnabled ? body : stripImagesFromBody(body);
126932
- const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path });
127326
+ const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path, servedModelName });
126933
127327
  const requestStartedAt = Date.now();
126934
127328
  const requestBody = JSON.parse(serializedBody);
126935
127329
  const streamRequested = requestBody.stream === true;
@@ -127069,7 +127463,8 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127069
127463
  onComplete: onMonitoringComplete,
127070
127464
  requestBodyBytes,
127071
127465
  requestPath: path,
127072
- requestStartedAt
127466
+ requestStartedAt,
127467
+ responseModelName: servedModelName ? clientModelName : null
127073
127468
  })
127074
127469
  : monitorEngineResponseSingle({
127075
127470
  agentEngineType: engineType ?? "unknown",
@@ -127081,7 +127476,8 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127081
127476
  onComplete: onMonitoringComplete,
127082
127477
  requestBodyBytes,
127083
127478
  requestPath: path,
127084
- requestStartedAt
127479
+ requestStartedAt,
127480
+ responseModelName: servedModelName ? clientModelName : null
127085
127481
  });
127086
127482
  return {
127087
127483
  body: monitoredResponse.stream,
@@ -138371,6 +138767,122 @@ async function detectDockerVersion() {
138371
138767
  }
138372
138768
  }
138373
138769
 
138770
+ /**
138771
+ * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
138772
+ * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
138773
+ * as its value when that token does not start with "-" (classic CLI convention); anything else
138774
+ * (flags, non-strings) is dropped. Secret-like option values are masked before pairing so they
138775
+ * never reach execution reports.
138776
+ */
138777
+ function pairExtraArgs(tokens) {
138778
+ if (!Array.isArray(tokens)) {
138779
+ return [];
138780
+ }
138781
+ // Non-string tokens become empty strings: they keep their position as a
138782
+ // non-consumable barrier while allowing secret masking over string tokens.
138783
+ const list = redactSecretArgs(tokens.map(token => (typeof token === "string" ? token : "")));
138784
+ const pairs = [];
138785
+ let index = 0;
138786
+ while (index < list.length) {
138787
+ const token = list[index];
138788
+ if (typeof token !== "string" || token.length === 0 || !token.startsWith("-")) {
138789
+ index++;
138790
+ continue;
138791
+ }
138792
+ const separator = token.indexOf("=");
138793
+ if (separator > -1) {
138794
+ const arg = token.slice(0, separator);
138795
+ if (arg.length > 0) {
138796
+ pairs.push([arg, token.slice(separator + 1)]);
138797
+ }
138798
+ index++;
138799
+ continue;
138800
+ }
138801
+ const next = list[index + 1];
138802
+ const consumesNext = typeof next === "string" && next.length > 0 && !next.startsWith("-");
138803
+ if (consumesNext) {
138804
+ pairs.push([token, next]);
138805
+ index += 2;
138806
+ }
138807
+ else {
138808
+ pairs.push([token, ""]);
138809
+ index++;
138810
+ }
138811
+ }
138812
+ 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);
138813
+ }
138814
+ /**
138815
+ * Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
138816
+ * latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
138817
+ * row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
138818
+ * crash, or first successful full prompt completion.
138819
+ */
138820
+ class EngineExecutionReporter {
138821
+ options;
138822
+ currentStartupAt = null;
138823
+ reportedForCurrentStartup = false;
138824
+ constructor(options) {
138825
+ this.options = options;
138826
+ }
138827
+ /** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
138828
+ beginStartup(runAt) {
138829
+ this.currentStartupAt = runAt;
138830
+ this.reportedForCurrentStartup = false;
138831
+ }
138832
+ /** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
138833
+ async reportStartupFailure(report) {
138834
+ await this.file(report, false);
138835
+ }
138836
+ /** Reports a spontaneous crash of an engine that had reached the running state. */
138837
+ async reportRuntimeCrash(report) {
138838
+ await this.file(report, false);
138839
+ }
138840
+ /** Reports the first fully-responded, token-bearing prompt completion since startup. */
138841
+ async reportSuccess(report) {
138842
+ await this.file(report, true);
138843
+ }
138844
+ async file(report, success) {
138845
+ if (this.reportedForCurrentStartup || this.currentStartupAt === null) {
138846
+ return;
138847
+ }
138848
+ // Latch BEFORE the network call: a thrown POST cannot double-file for this startup.
138849
+ this.reportedForCurrentStartup = true;
138850
+ const context = this.options.buildContext();
138851
+ const payload = {
138852
+ avgTps: report.throughput.avgTps,
138853
+ completionTokens: report.usage.completionTokens,
138854
+ durationMs: report.durationMs,
138855
+ engineType: context.engineType,
138856
+ engineVersion: context.engineVersion,
138857
+ errorDetail: success ? null : report.errorDetail,
138858
+ errorType: success ? null : report.errorType,
138859
+ extraArgs: context.extraArgsPairs,
138860
+ finishedAtISO: new Date().toISOString(),
138861
+ peakTps: report.throughput.peakTps,
138862
+ promptTokens: report.usage.promptTokens,
138863
+ runAtISO: this.currentStartupAt.toISOString(),
138864
+ success,
138865
+ ttftMs: report.ttftMs,
138866
+ totalTokens: report.usage.totalTokens
138867
+ };
138868
+ try {
138869
+ await this.options.report(payload);
138870
+ this.options.logger.info("Engine execution outcome reported", {
138871
+ inferenceSourceID: this.options.sourceLabel,
138872
+ success
138873
+ });
138874
+ }
138875
+ catch (error) {
138876
+ // Losing one report is preferable to filing two; the latch stays latched.
138877
+ this.options.logger.warn("Failed to report engine execution outcome", {
138878
+ error: asError(error),
138879
+ inferenceSourceID: this.options.sourceLabel,
138880
+ success
138881
+ });
138882
+ }
138883
+ }
138884
+ }
138885
+
138374
138886
  async function createApplication({ abortController, apiClient, configuration, logger }) {
138375
138887
  ensureDockerValidEnv();
138376
138888
  logger.info("Fetching conduit configuration");
@@ -138402,6 +138914,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
138402
138914
  error: asError(error)
138403
138915
  });
138404
138916
  }
138917
+ const reporter = new EngineExecutionReporter({
138918
+ buildContext: () => {
138919
+ const engineType = conduitConfiguration.engineConfig?.type ?? "llama.cpp";
138920
+ const versions = {
138921
+ custom: null,
138922
+ exllamav3: machine?.exllamav3Version ?? null,
138923
+ "llama.cpp": machine?.llamaCppVersion ?? null,
138924
+ "mlx-lm": machine?.mlxlmVersion ?? null,
138925
+ sglang: machine?.sglangVersion ?? null,
138926
+ "tensorrt-llm": machine?.tensorrtLlmVersion ?? null,
138927
+ vllm: machine?.vllmVersion ?? null
138928
+ };
138929
+ return {
138930
+ engineType,
138931
+ engineVersion: versions[engineType] ?? null,
138932
+ extraArgsPairs: pairExtraArgs(conduitConfiguration.engineConfig?.extraArgs)
138933
+ };
138934
+ },
138935
+ logger,
138936
+ report: payload => apiClient.reportEngineExecution(payload),
138937
+ sourceLabel: configuration.inferenceSourceID
138938
+ });
138939
+ // Intercept the prompt-metrics chokepoint so the first fully-responded, token-bearing prompt of
138940
+ // each fresh startup files the one-shot engine_execution success report. Handlers close over the
138941
+ // SAME `apiClient` object and read `reportPromptMetrics` at request-dispatch time (which always
138942
+ // follows this point), so the wrapped method is what they invoke.
138943
+ const rawReportPromptMetrics = apiClient.reportPromptMetrics;
138944
+ apiClient.reportPromptMetrics = async (payload) => {
138945
+ if (payload.successful && payload.completionTokens > 0 && payload.latencyMs > 0) {
138946
+ // The one-shot report is kicked off and its settlement attached HERE (before any await): if the
138947
+ // metrics path throws below, the report promise must still be able to log its own rejection.
138948
+ const successReport = reporter
138949
+ .reportSuccess({
138950
+ durationMs: payload.latencyMs,
138951
+ errorDetail: null,
138952
+ errorType: null,
138953
+ throughput: {
138954
+ avgTps: payload.tokensPerSecond,
138955
+ peakTps: null
138956
+ },
138957
+ ttftMs: payload.timeToFirstTokenMs ?? 0,
138958
+ usage: {
138959
+ completionTokens: payload.completionTokens,
138960
+ promptTokens: payload.promptTokens,
138961
+ totalTokens: payload.totalTokens
138962
+ }
138963
+ })
138964
+ .catch(error => {
138965
+ logger.warn("Engine execution success report failed", {
138966
+ error: asError(error)
138967
+ });
138968
+ });
138969
+ await rawReportPromptMetrics(payload);
138970
+ await successReport;
138971
+ return;
138972
+ }
138973
+ await rawReportPromptMetrics(payload);
138974
+ };
138975
+ const SECRET_ARG_MASK_PATTERN = /(-{1,2}[A-Za-z0-9_.]*(?:api[-_]?key|hf[-_]?token|token)(?:\s+|[=:]))\S+/gi;
138976
+ // Assembles the payload shared by the startup-failure and runtime-crash report paths. Stderr
138977
+ // may echo secrets, so mask `--api-key`/token-looking args before they reach the DB.
138978
+ function buildCrashReport(error, exitCode, signal) {
138979
+ const classification = classifyEngineFailure({ error, exitCode, signal });
138980
+ const raw = normalizeEngineError(error.message);
138981
+ const masked = raw.replace(SECRET_ARG_MASK_PATTERN, "$1***");
138982
+ return {
138983
+ durationMs: 0,
138984
+ errorDetail: masked.slice(0, 2048),
138985
+ errorType: classification,
138986
+ ttftMs: 0,
138987
+ throughput: {
138988
+ avgTps: 0,
138989
+ peakTps: null
138990
+ },
138991
+ usage: {
138992
+ completionTokens: 0,
138993
+ promptTokens: 0,
138994
+ totalTokens: 0
138995
+ }
138996
+ };
138997
+ }
138405
138998
  const conduitStateManager = new ConduitStateManager({
138406
138999
  initialState: {
138407
139000
  state: "initialising"
@@ -138453,6 +139046,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
138453
139046
  });
138454
139047
  stopRequestedByControl = false;
138455
139048
  setErrorState({ error: normalizeEngineError(err.message) });
139049
+ // Spontaneous death of a SERVING engine → crash report, suppressed by the latch if the
139050
+ // startup's one-shot outcome was already filed. Startup-path failures report from
139051
+ // `startEngine`'s catch; this listener is the RUNTIME-crash path only.
139052
+ if (modelManager.wasRunning && !err.message.includes("interrupted by stop request")) {
139053
+ const crashReport = buildCrashReport(err, modelManager.lastExitCode, modelManager.lastExitSignal);
139054
+ reporter.reportRuntimeCrash(crashReport).catch(crashReportError => {
139055
+ logger.warn("Engine execution crash report failed", {
139056
+ error: asError(crashReportError)
139057
+ });
139058
+ });
139059
+ }
138456
139060
  });
138457
139061
  modelManager.on("engineReady", () => {
138458
139062
  setOnlineState();
@@ -138512,24 +139116,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
138512
139116
  };
138513
139117
  async function startEngine() {
138514
139118
  logger.info("Engine start requested");
138515
- conduitStateManager.setState({
138516
- modelFileName,
138517
- modelName,
138518
- state: "downloadingModelFiles",
138519
- totalProgress: {
138520
- file: 0,
138521
- total: 0
139119
+ reporter.beginStartup(new Date());
139120
+ try {
139121
+ conduitStateManager.setState({
139122
+ modelFileName,
139123
+ modelName,
139124
+ state: "downloadingModelFiles",
139125
+ totalProgress: {
139126
+ file: 0,
139127
+ total: 0
139128
+ }
139129
+ });
139130
+ await conduitStateReportManager.reportNow();
139131
+ await modelManager.prepare({
139132
+ onDownloadProgress: reportDownloadProgress
139133
+ });
139134
+ conduitStateManager.setState({
139135
+ state: "bootingEngine"
139136
+ });
139137
+ await conduitStateReportManager.reportNow();
139138
+ await modelManager.start();
139139
+ }
139140
+ catch (error) {
139141
+ const parsedError = asError(error);
139142
+ // Operator-initiated aborts are not startup failures worth reporting.
139143
+ if (!parsedError.message.includes("interrupted by stop request")) {
139144
+ const startupReport = buildCrashReport(parsedError, modelManager.lastExitCode, modelManager.lastExitSignal);
139145
+ reporter.reportStartupFailure(startupReport).catch(startupReportError => {
139146
+ logger.warn("Engine execution startup report failed", {
139147
+ error: asError(startupReportError)
139148
+ });
139149
+ });
138522
139150
  }
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();
139151
+ throw error;
139152
+ }
138533
139153
  }
138534
139154
  async function stopEngine({ reason }) {
138535
139155
  if (!modelManager.canStop) {
@@ -138785,7 +139405,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
138785
139405
  return new ModelManager({
138786
139406
  contextLength: conduitConfiguration.contextLength ?? null,
138787
139407
  engineConfig: engineConfig
138788
- ? { extraArgs: engineConfig.extraArgs, type: engineConfig.type }
139408
+ ? {
139409
+ baseUrl: engineConfig.baseUrl ?? null,
139410
+ extraArgs: engineConfig.extraArgs,
139411
+ type: engineConfig.type
139412
+ }
138789
139413
  : null,
138790
139414
  enginePort: configuration.enginePort,
138791
139415
  engineType: engineConfig?.type ?? "llama.cpp",
@@ -138796,7 +139420,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
138796
139420
  }
138797
139421
  function getConduitModelFileName(configuration) {
138798
139422
  const { source } = configuration.targetModel;
138799
- return source.type === "huggingface" ? source.slug : source.irid;
139423
+ if (source.type === "huggingface")
139424
+ return source.slug;
139425
+ if (source.type === "storage")
139426
+ return source.irid;
139427
+ return configuration.targetModel.id;
138800
139428
  }
138801
139429
  function getConduitModelName(configuration) {
138802
139430
  return configuration.targetModel.id;