@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.sea.cjs CHANGED
@@ -19900,6 +19900,7 @@ object$5({
19900
19900
  });
19901
19901
 
19902
19902
  const LLMEngineSchema = _enum$1([
19903
+ "custom",
19903
19904
  "exllamav3",
19904
19905
  "llama.cpp",
19905
19906
  "mlx-lm",
@@ -19908,8 +19909,25 @@ const LLMEngineSchema = _enum$1([
19908
19909
  "vllm"
19909
19910
  ]);
19910
19911
  const EngineConfigSchema = object$5({
19912
+ baseUrl: string$2().url().nullable().default(null),
19911
19913
  extraArgs: array$1(string$2()),
19912
19914
  type: LLMEngineSchema
19915
+ })
19916
+ .superRefine((config, ctx) => {
19917
+ if (config.type === "custom" && config.baseUrl === null) {
19918
+ ctx.addIssue({
19919
+ code: "custom",
19920
+ message: "Engine type 'custom' requires a base URL",
19921
+ path: ["baseUrl"]
19922
+ });
19923
+ }
19924
+ if (config.type !== "custom" && config.baseUrl !== null) {
19925
+ ctx.addIssue({
19926
+ code: "custom",
19927
+ message: "Base URL is only valid for engine type 'custom'",
19928
+ path: ["baseUrl"]
19929
+ });
19930
+ }
19913
19931
  });
19914
19932
  const LLMModelFormatSchema = _enum$1([
19915
19933
  // VLLM / SGLang / TensorRT-LLM
@@ -19968,14 +19986,18 @@ const LLMModelSchema = object$5({
19968
19986
  id: string$2().min(1),
19969
19987
  multimodalEnabled: boolean$1(),
19970
19988
  source: discriminatedUnion("type", [
19989
+ // Custom engines: no local model record, serving is external
19971
19990
  object$5({
19972
- irid: IRIDSchema,
19973
- type: literal("storage")
19991
+ type: literal("external")
19974
19992
  }),
19975
19993
  object$5({
19976
19994
  modelSecret: string$2().min(1).nullable(),
19977
19995
  slug: string$2().min(1),
19978
19996
  type: literal("huggingface")
19997
+ }),
19998
+ object$5({
19999
+ irid: IRIDSchema,
20000
+ type: literal("storage")
19979
20001
  })
19980
20002
  ]),
19981
20003
  taskType: LLMModelTaskTypeSchema,
@@ -19986,6 +20008,32 @@ object$5({
19986
20008
  sizeBytes: number$1().int().nonnegative().nullable()
19987
20009
  });
19988
20010
 
20011
+ const EngineExecutionErrorTypeSchema = _enum$1([
20012
+ "config",
20013
+ "crash",
20014
+ "oom",
20015
+ "prompt",
20016
+ "unknown"
20017
+ ]);
20018
+ const EngineExecutionReportPayloadSchema = object$5({
20019
+ avgTps: number$1().nonnegative().finite().default(0),
20020
+ completionTokens: number$1().int().nonnegative().default(0),
20021
+ durationMs: number$1().int().nonnegative().default(0),
20022
+ engineType: LLMEngineSchema.nullable(),
20023
+ engineVersion: string$2().max(64).nullable().default(null),
20024
+ errorDetail: string$2().max(2048).nullable().default(null),
20025
+ errorType: EngineExecutionErrorTypeSchema.nullable(),
20026
+ extraArgs: array$1(tuple([string$2().min(1).max(128), string$2().max(512)]))
20027
+ .max(256)
20028
+ .default([]),
20029
+ finishedAtISO: string$2().datetime({ offset: true }),
20030
+ peakTps: number$1().nonnegative().finite().nullable().default(null),
20031
+ promptTokens: number$1().int().nonnegative().default(0),
20032
+ runAtISO: string$2().datetime({ offset: true }),
20033
+ success: boolean$1(),
20034
+ ttftMs: number$1().int().nonnegative().default(0),
20035
+ totalTokens: number$1().int().nonnegative().default(0)
20036
+ });
19989
20037
  const InferenceAgentLLMMetricsPayloadSchema = object$5({
19990
20038
  bytes: number$1().int().nonnegative(),
19991
20039
  completionTokens: number$1().int().nonnegative(),
@@ -20324,6 +20372,23 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
20324
20372
  }
20325
20373
  }
20326
20374
  },
20375
+ "/conduit/api/v1/source/:sourceID/engine/execution": {
20376
+ POST: {
20377
+ auth: {
20378
+ type: "api-key"
20379
+ },
20380
+ body: EngineExecutionReportPayloadSchema,
20381
+ parameters: {
20382
+ sourceID: ULIDSchema
20383
+ },
20384
+ response: {
20385
+ schema: object$5({
20386
+ acknowledged: literal(true)
20387
+ }),
20388
+ type: "rest"
20389
+ }
20390
+ }
20391
+ },
20327
20392
  "/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
20328
20393
  POST: {
20329
20394
  auth: {
@@ -21076,7 +21141,8 @@ const CreateModelResponseSchema = object$5({
21076
21141
  const CreateSourceBodySchema = object$5({
21077
21142
  contextLength: number$1().int().positive().max(1048576).optional(),
21078
21143
  engineId: ULIDSchema,
21079
- modelID: ULIDSchema,
21144
+ // Optional for custom engines, which serve externally managed models
21145
+ modelID: ULIDSchema.nullable().optional(),
21080
21146
  name: ResourceNameSchema,
21081
21147
  quantizationLabel: string$2().min(1).max(128).optional()
21082
21148
  });
@@ -21125,7 +21191,7 @@ const SourceDetailResponseSchema = object$5({
21125
21191
  const UpdateSourceBodySchema = object$5({
21126
21192
  contextLength: number$1().int().positive().nullable().optional(),
21127
21193
  engineId: ULIDSchema.nullable().optional(),
21128
- modelID: ULIDSchema.optional(),
21194
+ modelID: ULIDSchema.nullable().optional(),
21129
21195
  name: ResourceNameSchema.optional(),
21130
21196
  quantizationLabel: string$2().min(1).max(128).nullable().optional()
21131
21197
  });
@@ -21249,6 +21315,7 @@ const CreateEndpointResponseSchema = object$5({
21249
21315
  id: ULIDSchema
21250
21316
  });
21251
21317
  const EngineOutputSchema = object$5({
21318
+ baseUrl: string$2().nullable(),
21252
21319
  created: string$2(),
21253
21320
  extraArgs: array$1(string$2()),
21254
21321
  id: ULIDSchema,
@@ -21257,11 +21324,29 @@ const EngineOutputSchema = object$5({
21257
21324
  updated: string$2()
21258
21325
  });
21259
21326
  const CreateEngineBodySchema = object$5({
21327
+ baseUrl: string$2().url().nullable().optional(),
21260
21328
  extraArgs: array$1(string$2()).optional(),
21261
21329
  name: ResourceNameSchema,
21262
21330
  type: LLMEngineSchema
21331
+ })
21332
+ .superRefine((body, ctx) => {
21333
+ if (body.type === "custom" && !body.baseUrl) {
21334
+ ctx.addIssue({
21335
+ code: "custom",
21336
+ message: "Engine type 'custom' requires a base URL",
21337
+ path: ["baseUrl"]
21338
+ });
21339
+ }
21340
+ if (body.type !== "custom" && body.baseUrl) {
21341
+ ctx.addIssue({
21342
+ code: "custom",
21343
+ message: "Base URL is only valid for engine type 'custom'",
21344
+ path: ["baseUrl"]
21345
+ });
21346
+ }
21263
21347
  });
21264
21348
  const UpdateEngineBodySchema = object$5({
21349
+ baseUrl: string$2().url().nullable().optional(),
21265
21350
  extraArgs: array$1(string$2()).optional(),
21266
21351
  name: ResourceNameSchema.optional(),
21267
21352
  type: LLMEngineSchema.optional()
@@ -22067,6 +22152,11 @@ const RecommendedModelSchema = object$5({
22067
22152
  const recommendedModels = RecommendedModelSchema.array().parse(modelsData);
22068
22153
 
22069
22154
  const ENGINE_API_COMPATIBILITY = {
22155
+ custom: {
22156
+ nativeAnthropicMessages: false,
22157
+ supportsEmbeddings: true,
22158
+ supportsVision: true
22159
+ },
22070
22160
  exllamav3: {
22071
22161
  nativeAnthropicMessages: false,
22072
22162
  supportsEmbeddings: false,
@@ -111716,6 +111806,27 @@ function registerEndpointCommands({ program }) {
111716
111806
  }
111717
111807
 
111718
111808
  const ENGINE_TYPES = LLMEngineSchema.options;
111809
+ function isValidURL(value) {
111810
+ try {
111811
+ const url = new URL(value);
111812
+ return url.protocol === "http:" || url.protocol === "https:";
111813
+ }
111814
+ catch (_error) {
111815
+ return false;
111816
+ }
111817
+ }
111818
+ function validateEngineBaseURL({ baseUrl, type }) {
111819
+ if (type === "custom" && !baseUrl) {
111820
+ throw new Error("Engine type 'custom' requires --base-url");
111821
+ }
111822
+ if (baseUrl !== undefined && type !== "custom") {
111823
+ throw new Error("--base-url is only valid for engine type 'custom'");
111824
+ }
111825
+ if (baseUrl !== undefined && baseUrl !== "" && !isValidURL(baseUrl)) {
111826
+ throw new Error(`Invalid --base-url value: ${baseUrl}`);
111827
+ }
111828
+ return baseUrl === "" ? null : (baseUrl ?? null);
111829
+ }
111719
111830
  function buildEngineCreateBody(options) {
111720
111831
  if (!options.name) {
111721
111832
  throw new Error("--name is required");
@@ -111726,7 +111837,12 @@ function buildEngineCreateBody(options) {
111726
111837
  if (!ENGINE_TYPES.includes(options.type)) {
111727
111838
  throw new Error(`Invalid engine type: ${options.type} (expected one of: ${ENGINE_TYPES.join(", ")})`);
111728
111839
  }
111840
+ const baseUrl = validateEngineBaseURL({
111841
+ baseUrl: options.baseUrl,
111842
+ type: options.type
111843
+ });
111729
111844
  return {
111845
+ baseUrl,
111730
111846
  extraArgs: options.arg ?? [],
111731
111847
  name: options.name,
111732
111848
  type: options.type
@@ -111744,6 +111860,24 @@ function buildEngineUpdateBody(options) {
111744
111860
  }
111745
111861
  if (options.arg !== undefined)
111746
111862
  body.extraArgs = options.arg;
111863
+ if (options.baseUrl !== undefined) {
111864
+ const targetType = body.type;
111865
+ if (targetType !== undefined) {
111866
+ body.baseUrl = validateEngineBaseURL({
111867
+ baseUrl: options.baseUrl,
111868
+ type: targetType
111869
+ });
111870
+ }
111871
+ else if (options.baseUrl === "") {
111872
+ body.baseUrl = null;
111873
+ }
111874
+ else if (!isValidURL(options.baseUrl)) {
111875
+ throw new Error(`Invalid --base-url value: ${options.baseUrl}`);
111876
+ }
111877
+ else {
111878
+ body.baseUrl = options.baseUrl;
111879
+ }
111880
+ }
111747
111881
  return body;
111748
111882
  }
111749
111883
 
@@ -111757,10 +111891,11 @@ function registerEngineCommands({ program }) {
111757
111891
  .description("Create or update an inference engine resource")
111758
111892
  .option("--api-url <url>", "API base URL (required, no environment variable fallback)")
111759
111893
  .option("--arg <flag>", 'Raw engine CLI flag, repeatable (eg --arg "--flash-attn on")', collect, [])
111894
+ .option("--base-url <url>", "Base URL of an external OpenAI-compatible server (engine type 'custom' only)")
111760
111895
  .option("--id <ulid>", "Target an existing engine by ID (requires --update)")
111761
111896
  .option("--key <value>", "API key (required, no environment variable fallback)")
111762
111897
  .option("--name <name>", "Engine name (matched by --update)")
111763
- .option("--type <type>", "Engine type: llama.cpp|vllm|sglang|tensorrt-llm|mlx-lm|exllamav3")
111898
+ .option("--type <type>", "Engine type: llama.cpp|vllm|sglang|tensorrt-llm|mlx-lm|exllamav3|custom")
111764
111899
  .option("--update", "Update an existing engine matched by name (or --id) instead of erroring")
111765
111900
  .action(async (options) => {
111766
111901
  const { apiURL, apiKey } = resolveManagementConnection(options);
@@ -113781,6 +113916,19 @@ function createAPIClient({ apiKey, apiURL, inferenceSourceID, logger }) {
113781
113916
  route: "/conduit/api/v1/source/:sourceID/state"
113782
113917
  });
113783
113918
  },
113919
+ reportEngineExecution: async (payload) => {
113920
+ await fetchByReference({
113921
+ baseURL: apiURL,
113922
+ body: payload,
113923
+ fetch: fetchWithAPIKey,
113924
+ method: "POST",
113925
+ parameters: {
113926
+ sourceID: inferenceSourceID
113927
+ },
113928
+ reference: API_SERVICE_CONDUIT_API_REFERENCE,
113929
+ route: "/conduit/api/v1/source/:sourceID/engine/execution"
113930
+ });
113931
+ },
113784
113932
  reportPromptMetrics: async (payload) => {
113785
113933
  await fetchByReference({
113786
113934
  baseURL: apiURL,
@@ -117747,6 +117895,9 @@ async function getChatTemplateEngineArgs({ engine, model, targetDirectory }) {
117747
117895
  return [];
117748
117896
  const flag = FLAG_BASED_ENGINE_ARGS[engine];
117749
117897
  if (!flag) {
117898
+ if (engine === "custom") {
117899
+ console.warn("[chatTemplate] Custom engines manage their own serving; ignoring chat template override");
117900
+ }
117750
117901
  if (engine === "tensorrt-llm") {
117751
117902
  console.warn("[chatTemplate] TensorRT-LLM does not support chat template overrides; ignoring");
117752
117903
  }
@@ -124644,7 +124795,7 @@ function matchesQuantizationVariant({ filePath, variant }) {
124644
124795
  return segments.slice(0, -1).some(segment => matcher.test(segment));
124645
124796
  }
124646
124797
  async function findQuantizedModelTarget({ model, path }) {
124647
- if (model.source.type === "storage") {
124798
+ if (model.source.type !== "huggingface") {
124648
124799
  throw new Error("Model storage not supported yet");
124649
124800
  }
124650
124801
  if (model.format !== "gguf") {
@@ -125559,7 +125710,11 @@ function sanitizeSegment(value) {
125559
125710
  .replace(new RegExp(`${SEPARATOR}{2,}`, "g"), SEPARATOR);
125560
125711
  }
125561
125712
  function createModelStorageKey(model) {
125562
- const identifier = model.source.type === "huggingface" ? model.source.slug : model.source.irid;
125713
+ const identifier = model.source.type === "huggingface"
125714
+ ? model.source.slug
125715
+ : model.source.type === "storage"
125716
+ ? model.source.irid
125717
+ : model.id;
125563
125718
  return `${model.source.type}${SEPARATOR}${sanitizeSegment(identifier)}`;
125564
125719
  }
125565
125720
 
@@ -125635,12 +125790,16 @@ class ModelManager extends EventEmitter {
125635
125790
  uniqueName;
125636
125791
  contextLength;
125637
125792
  logger;
125793
+ discoveredModelNames = [];
125638
125794
  engineProcess = null;
125639
125795
  healthPollInterval = null;
125640
125796
  lastEngineError = null;
125641
125797
  lifecycleState = "stopped";
125642
125798
  downloadLockHandle = null;
125643
125799
  stopRequested = false;
125800
+ lastEngineExitCode = null;
125801
+ lastEngineExitSignal = null;
125802
+ reachedRunningState = false;
125644
125803
  modelsDirectory;
125645
125804
  constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
125646
125805
  super();
@@ -125664,6 +125823,7 @@ class ModelManager extends EventEmitter {
125664
125823
  }
125665
125824
  async fetchOpenAI(path, opts) {
125666
125825
  switch (this.engine) {
125826
+ case "custom":
125667
125827
  case "exllamav3":
125668
125828
  case "llama.cpp":
125669
125829
  case "mlx-lm":
@@ -125671,6 +125831,9 @@ class ModelManager extends EventEmitter {
125671
125831
  case "tensorrt-llm":
125672
125832
  case "vllm": {
125673
125833
  this.logger.debug(`Fetching from engine: ${path}`);
125834
+ const baseURL = this.engine === "custom"
125835
+ ? this.requireCustomBaseURL()
125836
+ : `http://localhost:${this.enginePort}`;
125674
125837
  const callerSignal = opts?.signal;
125675
125838
  const controller = new AbortController();
125676
125839
  const timeout = setTimeout(() => {
@@ -125681,7 +125844,7 @@ class ModelManager extends EventEmitter {
125681
125844
  : controller.signal;
125682
125845
  try {
125683
125846
  const fetchStartedAt = Date.now();
125684
- const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, path), {
125847
+ const response = await undiciExports.fetch(joinURL(baseURL, path), {
125685
125848
  ...opts,
125686
125849
  dispatcher: ENGINE_AGENT,
125687
125850
  headers: {
@@ -125716,6 +125879,11 @@ class ModelManager extends EventEmitter {
125716
125879
  modelID: this.model.id
125717
125880
  });
125718
125881
  switch (this.engine) {
125882
+ case "custom":
125883
+ if (this.model.chatTemplate) {
125884
+ this.logger.warn("Chat template overrides are ignored for custom engines: the remote server manages its own serving");
125885
+ }
125886
+ break;
125719
125887
  case "exllamav3":
125720
125888
  case "llama.cpp":
125721
125889
  case "mlx-lm":
@@ -125763,12 +125931,17 @@ class ModelManager extends EventEmitter {
125763
125931
  this.lifecycleState = "starting";
125764
125932
  this.lastEngineError = null;
125765
125933
  this.stopRequested = false;
125934
+ this.lastEngineExitCode = null;
125935
+ this.lastEngineExitSignal = null;
125936
+ this.reachedRunningState = false;
125766
125937
  this.logger.info("Starting LLM engine", {
125767
125938
  agentEngineType: this.engine
125768
125939
  });
125769
125940
  try {
125770
125941
  this.engineProcess = await this.startEngineProcess();
125771
- this.bindEngineProcessEvents(this.engineProcess);
125942
+ if (this.engineProcess) {
125943
+ this.bindEngineProcessEvents(this.engineProcess);
125944
+ }
125772
125945
  this.logger.info("Started LLM engine", {
125773
125946
  agentEngineType: this.engine
125774
125947
  });
@@ -125787,13 +125960,17 @@ class ModelManager extends EventEmitter {
125787
125960
  if (!alreadyEmitted) {
125788
125961
  this.emit("engineError", err);
125789
125962
  }
125790
- if (this.engineProcess) {
125963
+ if (this.engineProcess || this.engine === "custom") {
125791
125964
  this.startHealthPoll();
125792
125965
  }
125793
125966
  throw err;
125794
125967
  }
125795
125968
  this.lifecycleState = "running";
125969
+ this.reachedRunningState = true;
125796
125970
  this.emit("engineReady");
125971
+ if (this.engine === "custom") {
125972
+ this.startHealthPoll();
125973
+ }
125797
125974
  }
125798
125975
  async stop() {
125799
125976
  if (this.lifecycleState === "stopping") {
@@ -125814,9 +125991,12 @@ class ModelManager extends EventEmitter {
125814
125991
  this.clearHealthPoll();
125815
125992
  const processManager = this.engineProcess;
125816
125993
  if (!processManager) {
125994
+ this.stopRequested = true;
125995
+ this.reachedRunningState = false;
125817
125996
  this.lifecycleState = "stopped";
125818
125997
  return;
125819
125998
  }
125999
+ this.reachedRunningState = false;
125820
126000
  this.lifecycleState = "stopping";
125821
126001
  this.stopRequested = true;
125822
126002
  await processManager.stop();
@@ -125829,11 +126009,34 @@ class ModelManager extends EventEmitter {
125829
126009
  this.lifecycleState === "starting" ||
125830
126010
  this.lifecycleState === "errored");
125831
126011
  }
126012
+ get lastExitCode() {
126013
+ return this.lastEngineExitCode;
126014
+ }
126015
+ get lastExitSignal() {
126016
+ return this.lastEngineExitSignal;
126017
+ }
125832
126018
  get state() {
125833
126019
  return this.lifecycleState;
125834
126020
  }
126021
+ get resolvedServedModelName() {
126022
+ if (this.engine !== "custom")
126023
+ return null;
126024
+ return this.discoveredModelNames[0] ?? null;
126025
+ }
126026
+ get wasRunning() {
126027
+ return this.reachedRunningState;
126028
+ }
126029
+ get customBaseURL() {
126030
+ if (this.engine !== "custom")
126031
+ return null;
126032
+ const baseUrl = this.engineConfig?.baseUrl;
126033
+ return typeof baseUrl === "string" && baseUrl.length > 0 ? baseUrl : null;
126034
+ }
125835
126035
  async checkEngineReadiness() {
125836
126036
  switch (this.engine) {
126037
+ case "custom": {
126038
+ return this.checkCustomReadiness();
126039
+ }
125837
126040
  case "llama.cpp": {
125838
126041
  return this.checkLlamacppReadiness();
125839
126042
  }
@@ -125850,6 +126053,51 @@ class ModelManager extends EventEmitter {
125850
126053
  return "ready";
125851
126054
  }
125852
126055
  }
126056
+ async checkCustomReadiness() {
126057
+ const baseURL = this.customBaseURL;
126058
+ if (!baseURL) {
126059
+ return "unreachable";
126060
+ }
126061
+ try {
126062
+ const response = await undiciExports.fetch(joinURL(baseURL, "/v1/models"), {
126063
+ method: "GET",
126064
+ signal: AbortSignal.timeout(5000)
126065
+ });
126066
+ if (response.status === 503) {
126067
+ return "loading";
126068
+ }
126069
+ if (!response.ok) {
126070
+ return "unreachable";
126071
+ }
126072
+ const payload = (await response.json());
126073
+ const models = Array.isArray(payload.data) ? payload.data : [];
126074
+ const modelIDs = models
126075
+ .map(model => {
126076
+ if (model === null || typeof model !== "object")
126077
+ return null;
126078
+ const id = model.id;
126079
+ return typeof id === "string" ? id : null;
126080
+ })
126081
+ .filter((id) => id !== null);
126082
+ if (modelIDs.length === 0) {
126083
+ this.logger.warn("Custom engine endpoint exposed no models via /v1/models", {
126084
+ engineBaseURL: baseURL
126085
+ });
126086
+ return "loading";
126087
+ }
126088
+ if (modelIDs.join("\n") !== this.discoveredModelNames.join("\n")) {
126089
+ this.logger.info("Discovered models on custom engine endpoint", {
126090
+ engineBaseURL: baseURL,
126091
+ models: modelIDs
126092
+ });
126093
+ this.discoveredModelNames = modelIDs;
126094
+ }
126095
+ return "ready";
126096
+ }
126097
+ catch (_error) {
126098
+ return "unreachable";
126099
+ }
126100
+ }
125853
126101
  async checkGenericHealthReadiness() {
125854
126102
  try {
125855
126103
  const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, "/health"), {
@@ -125905,18 +126153,24 @@ class ModelManager extends EventEmitter {
125905
126153
  }
125906
126154
  }
125907
126155
  async waitForEngineReady() {
125908
- const maxWaitMs = 15 * 60 * 1000;
126156
+ const maxWaitMs = Number.parseInt(process.env.ENGINE_STARTUP_TIMEOUT_MS ?? "", 10) || 15 * 60 * 1000;
125909
126157
  const pollIntervalMs = 2000;
125910
126158
  const start = Date.now();
125911
126159
  while (Date.now() - start < maxWaitMs) {
125912
- if (this.lifecycleState === "stopping") {
126160
+ if (this.lifecycleState === "stopping" || this.stopRequested) {
125913
126161
  throw new Error("LLM engine startup interrupted by stop request");
125914
126162
  }
125915
- if (!this.engineProcess) {
126163
+ if (!this.engineProcess && this.engine !== "custom") {
125916
126164
  throw new Error("LLM engine process exited before readiness checks completed");
125917
126165
  }
125918
126166
  const readiness = await this.checkEngineReadiness();
125919
126167
  if (readiness === "ready") {
126168
+ // A stop() may have landed while the readiness request was
126169
+ // in flight; re-check before declaring ready.
126170
+ const lifecycleState = this.lifecycleState;
126171
+ if (lifecycleState === "stopping" || this.stopRequested) {
126172
+ throw new Error("LLM engine startup interrupted by stop request");
126173
+ }
125920
126174
  return;
125921
126175
  }
125922
126176
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
@@ -125934,18 +126188,32 @@ class ModelManager extends EventEmitter {
125934
126188
  }
125935
126189
  startHealthPoll() {
125936
126190
  this.clearHealthPoll();
125937
- this.logger.info("Starting background health poll for errored engine");
126191
+ this.logger.info("Starting background engine health poll", {
126192
+ agentEngineType: this.engine
126193
+ });
125938
126194
  this.healthPollInterval = setInterval(() => {
125939
- if (!this.engineProcess) {
126195
+ if (!this.engineProcess && this.engine !== "custom") {
125940
126196
  this.clearHealthPoll();
125941
126197
  return;
125942
126198
  }
125943
126199
  this.checkEngineReadiness()
125944
126200
  .then(readiness => {
125945
126201
  if (readiness === "ready") {
125946
- this.clearHealthPoll();
125947
- this.lifecycleState = "running";
125948
- this.emit("engineReady");
126202
+ if (this.lifecycleState === "errored" ||
126203
+ this.lifecycleState === "starting") {
126204
+ this.lifecycleState = "running";
126205
+ this.reachedRunningState = true;
126206
+ this.emit("engineReady");
126207
+ }
126208
+ if (this.engine !== "custom") {
126209
+ this.clearHealthPoll();
126210
+ }
126211
+ return;
126212
+ }
126213
+ if (this.engine === "custom" &&
126214
+ readiness === "unreachable" &&
126215
+ this.lifecycleState === "running") {
126216
+ this.recordEngineError(new Error(`Custom engine endpoint unreachable: ${this.customBaseURL}`));
125949
126217
  }
125950
126218
  })
125951
126219
  .catch(() => {
@@ -125970,6 +126238,15 @@ class ModelManager extends EventEmitter {
125970
126238
  this.lastEngineError = err;
125971
126239
  this.emit("engineError", err);
125972
126240
  }
126241
+ requireCustomBaseURL() {
126242
+ const baseURL = this.customBaseURL;
126243
+ if (!baseURL) {
126244
+ throw new ConfigurationInvalidError({
126245
+ message: "Custom engine requires a base URL"
126246
+ });
126247
+ }
126248
+ return baseURL;
126249
+ }
125973
126250
  async releaseDownloadLock() {
125974
126251
  const handle = this.downloadLockHandle;
125975
126252
  if (!handle)
@@ -126007,6 +126284,8 @@ class ModelManager extends EventEmitter {
126007
126284
  }));
126008
126285
  });
126009
126286
  processManager.on("stopped", (code, signal) => {
126287
+ this.lastEngineExitCode = code;
126288
+ this.lastEngineExitSignal = signal;
126010
126289
  if (hasTerminated) {
126011
126290
  return;
126012
126291
  }
@@ -126034,6 +126313,8 @@ class ModelManager extends EventEmitter {
126034
126313
  async startEngineProcess() {
126035
126314
  const targetDir = path$1.join(this.modelsDirectory, this.uniqueName);
126036
126315
  switch (this.engine) {
126316
+ case "custom":
126317
+ return null;
126037
126318
  case "exllamav3":
126038
126319
  return startExllamav3.call(this, {
126039
126320
  enginePort: this.enginePort,
@@ -126074,6 +126355,69 @@ class ModelManager extends EventEmitter {
126074
126355
  }
126075
126356
  }
126076
126357
 
126358
+ // Ordered most-specific first: a message mentioning a prompt-size failure should classify as
126359
+ // "prompt" even if it also touches memory text; memory outranks config because OOM kills frequently
126360
+ // emit sparse stderr. These are heuristics, not exhaustively enumerated engine vocabularies.
126361
+ const OOM_PATTERNS = [
126362
+ /CUDA out of memory/i,
126363
+ /No available memory for the cache blocks/i,
126364
+ /\bOOMKilled\b/,
126365
+ /out of memory/i,
126366
+ /MemoryError/i,
126367
+ /Cannot allocate memory/i
126368
+ ];
126369
+ const PROMPT_PATTERNS = [
126370
+ /prompt is too long/i,
126371
+ /maximum context length exceeded/i,
126372
+ /too many tokens/i
126373
+ ];
126374
+ const CONFIG_PATTERNS = [
126375
+ /unrecognized argument/i,
126376
+ /invalid argument/i,
126377
+ /error while loading state_dict/i,
126378
+ /Architecture not understood/i,
126379
+ /No such file or directory/i
126380
+ ];
126381
+ /**
126382
+ * Coarse engine-failure classification used only to fill `engine_execution.error_type`. Exit code and
126383
+ * terminating signal are consulted FIRST (SIGKILL/SIGSEGV are reliable OOM signals regardless of
126384
+ * how much stderr the engine produced); the message text then refines the category. Anything
126385
+ * unrecognized is "unknown".
126386
+ */
126387
+ function classifyEngineFailure({ error, exitCode, signal }) {
126388
+ // 137 = SIGKILL (kernel OOM-killer), 139 = SIGSEGV (illegal memory access). Treat both as OOM
126389
+ // so that memory-pressure deaths do not degrade to "unknown" when stderr is truncated.
126390
+ if (exitCode === 137 || exitCode === 139) {
126391
+ return "oom";
126392
+ }
126393
+ // Direct OS kills (SIGKILL/SIGSEGV) leave no meaningful exit code; honor them like 137/139.
126394
+ if (signal === "SIGKILL" || signal === "SIGSEGV") {
126395
+ return "oom";
126396
+ }
126397
+ const text = `${error.message}`.slice(0, 4000);
126398
+ for (const pattern of PROMPT_PATTERNS) {
126399
+ if (pattern.test(text)) {
126400
+ return "prompt";
126401
+ }
126402
+ }
126403
+ for (const pattern of OOM_PATTERNS) {
126404
+ if (pattern.test(text)) {
126405
+ return "oom";
126406
+ }
126407
+ }
126408
+ for (const pattern of CONFIG_PATTERNS) {
126409
+ if (pattern.test(text)) {
126410
+ return "config";
126411
+ }
126412
+ }
126413
+ // A process that exited abnormally with no recognizable diagnostic: treat as a crash rather
126414
+ // than "unknown" so the two buckets distinguish "we saw nothing" from "it died badly".
126415
+ if (exitCode !== null && exitCode !== 0) {
126416
+ return "crash";
126417
+ }
126418
+ return "unknown";
126419
+ }
126420
+
126077
126421
  const EXCEPTION_LINE_PATTERN = /([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):\s*(.+)/;
126078
126422
  const FALLBACK_DETAIL_MAX_LENGTH = 300;
126079
126423
  const FALLBACK_RAW_MAX_LENGTH = 500;
@@ -126282,8 +126626,9 @@ function isEngineUsageChunk(value) {
126282
126626
  }
126283
126627
  return true;
126284
126628
  }
126285
- function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126629
+ function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126286
126630
  const startedAt = requestStartedAt ?? Date.now();
126631
+ const clientModelName = responseModelName ?? null;
126287
126632
  const passThrough = new require$$0$8.PassThrough();
126288
126633
  passThrough.on("error", (error) => {
126289
126634
  logger.error("Engine response stream error", {
@@ -126295,53 +126640,61 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126295
126640
  let firstChunkAt = null;
126296
126641
  let usage = null;
126297
126642
  let buffer = "";
126643
+ let pendingFragment = "";
126298
126644
  let completed = false;
126299
- function modifyChunkWithUsage(chunk) {
126300
- const text = chunk.toString("utf8");
126301
- const lines = text.split("\n");
126302
- const modifiedLines = [];
126303
- for (const rawLine of lines) {
126304
- const line = rawLine.trim();
126305
- if (!line.startsWith("data:")) {
126306
- modifiedLines.push(rawLine);
126307
- continue;
126645
+ function rewriteDataLine(rawLine) {
126646
+ const line = rawLine.trim();
126647
+ if (!line.startsWith("data:")) {
126648
+ return rawLine;
126649
+ }
126650
+ const payload = line.slice(5).trim();
126651
+ if (!payload || payload === "[DONE]") {
126652
+ return rawLine;
126653
+ }
126654
+ try {
126655
+ const parsed = JSON.parse(payload);
126656
+ let modified = false;
126657
+ if (coerceToolCallArguments(parsed)) {
126658
+ modified = true;
126308
126659
  }
126309
- const payload = line.slice(5).trim();
126310
- if (!payload || payload === "[DONE]") {
126311
- modifiedLines.push(rawLine);
126312
- continue;
126660
+ if (clientModelName !== null &&
126661
+ typeof parsed.model === "string" &&
126662
+ parsed.model !== clientModelName) {
126663
+ parsed.model = clientModelName;
126664
+ modified = true;
126313
126665
  }
126314
- try {
126315
- const parsed = JSON.parse(payload);
126316
- let modified = false;
126317
- if (coerceToolCallArguments(parsed)) {
126666
+ if (parsed.usage) {
126667
+ const usageChunk = parsed.usage;
126668
+ const effectiveContext = getEffectiveContextLength({
126669
+ contextLength,
126670
+ engineConfig,
126671
+ engineType
126672
+ });
126673
+ if (usageChunk.context_usage === undefined &&
126674
+ usageChunk.prompt_tokens !== undefined &&
126675
+ effectiveContext !== null) {
126676
+ usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126318
126677
  modified = true;
126319
126678
  }
126320
- if (parsed.usage) {
126321
- const usageChunk = parsed.usage;
126322
- const effectiveContext = getEffectiveContextLength({
126323
- contextLength,
126324
- engineConfig,
126325
- engineType
126326
- });
126327
- if (usageChunk.context_usage === undefined &&
126328
- usageChunk.prompt_tokens !== undefined &&
126329
- effectiveContext !== null) {
126330
- usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126331
- modified = true;
126332
- }
126333
- }
126334
- if (modified) {
126335
- modifiedLines.push("data: " + JSON.stringify(parsed));
126336
- continue;
126337
- }
126338
126679
  }
126339
- catch (_error) {
126340
- // Ignore malformed chunks
126680
+ if (modified) {
126681
+ return "data: " + JSON.stringify(parsed);
126341
126682
  }
126342
- modifiedLines.push(rawLine);
126343
126683
  }
126344
- return Buffer.from(modifiedLines.join("\n"), "utf8");
126684
+ catch (_error) {
126685
+ // Ignore malformed chunks
126686
+ }
126687
+ return rawLine;
126688
+ }
126689
+ // SSE events can split across transport chunks: hold back the trailing
126690
+ // (newline-less) fragment and only rewrite complete data lines, so a
126691
+ // partial JSON event is never forwarded with its upstream model name.
126692
+ function modifyChunkWithUsage(chunk, flush = false) {
126693
+ const combined = pendingFragment + chunk.toString("utf8");
126694
+ const lines = combined.split("\n");
126695
+ pendingFragment = flush ? "" : (lines.pop() ?? "");
126696
+ const modifiedLines = lines.map(rewriteDataLine);
126697
+ return Buffer.from(modifiedLines.length > 0 ? modifiedLines.join("\n") + (flush ? "" : "\n") : "", "utf8");
126345
126698
  }
126346
126699
  function parseUsageFromBuffer() {
126347
126700
  const lines = buffer.split("\n");
@@ -126437,6 +126790,9 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126437
126790
  if (buffer.length > 0) {
126438
126791
  parseUsageFromBuffer();
126439
126792
  }
126793
+ if (pendingFragment.length > 0) {
126794
+ passThrough.write(modifyChunkWithUsage(Buffer.from("", "utf8"), true));
126795
+ }
126440
126796
  logEngineMetrics({
126441
126797
  agentEngineType,
126442
126798
  level: "info",
@@ -126479,9 +126835,11 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126479
126835
  stream: passThrough
126480
126836
  };
126481
126837
  }
126482
- function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126838
+ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126483
126839
  const maxUsageCaptureBytes = 1024 * 1024;
126484
126840
  const startedAt = requestStartedAt ?? Date.now();
126841
+ const clientModelName = responseModelName ?? null;
126842
+ const rewriteBuffer = clientModelName !== null ? [] : null;
126485
126843
  const passThrough = new require$$0$8.PassThrough();
126486
126844
  passThrough.on("error", (error) => {
126487
126845
  logger.error("Engine response stream error", {
@@ -126537,7 +126895,12 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126537
126895
  usageChunks.length = 0;
126538
126896
  }
126539
126897
  }
126540
- passThrough.write(chunkBuffer);
126898
+ if (rewriteBuffer) {
126899
+ rewriteBuffer.push(chunkBuffer);
126900
+ }
126901
+ else {
126902
+ passThrough.write(chunkBuffer);
126903
+ }
126541
126904
  });
126542
126905
  body.once("error", err => {
126543
126906
  logEngineMetrics({
@@ -126597,6 +126960,24 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126597
126960
  responseBytes,
126598
126961
  usage
126599
126962
  });
126963
+ if (rewriteBuffer) {
126964
+ const original = Buffer.concat(rewriteBuffer);
126965
+ let output = original;
126966
+ try {
126967
+ const parsed = JSON.parse(original.toString("utf8"));
126968
+ if (parsed !== null &&
126969
+ typeof parsed === "object" &&
126970
+ typeof parsed.model === "string" &&
126971
+ parsed.model !== clientModelName) {
126972
+ parsed.model = clientModelName;
126973
+ output = Buffer.from(JSON.stringify(parsed), "utf8");
126974
+ }
126975
+ }
126976
+ catch (_error) {
126977
+ // Non-JSON body: pass through untouched
126978
+ }
126979
+ passThrough.write(output);
126980
+ }
126600
126981
  finalize(null);
126601
126982
  passThrough.end();
126602
126983
  });
@@ -126733,7 +127114,7 @@ function applyChatTemplateKwargs({ body, model }) {
126733
127114
  }
126734
127115
  return payload;
126735
127116
  }
126736
- function serializeRequestBody$1(body, { model, path } = {}) {
127117
+ function serializeRequestBody$1(body, { model, path, servedModelName } = {}) {
126737
127118
  if (!isPlainObject$a(body)) {
126738
127119
  const payload = typeof body === "string" ? body : JSON.stringify(body);
126739
127120
  return {
@@ -126742,6 +127123,9 @@ function serializeRequestBody$1(body, { model, path } = {}) {
126742
127123
  };
126743
127124
  }
126744
127125
  let requestPayload = { ...body };
127126
+ if (servedModelName) {
127127
+ requestPayload.model = servedModelName;
127128
+ }
126745
127129
  if (path === "/v1/chat/completions" && model) {
126746
127130
  requestPayload = applyChatTemplateKwargs({ body: requestPayload, model });
126747
127131
  }
@@ -126798,8 +127182,15 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
126798
127182
  }
126799
127183
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
126800
127184
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127185
+ const servedModelName = modelManager.resolvedServedModelName;
127186
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
126801
127187
  const serializedBody = isPlainObject$a(body)
126802
- ? JSON.stringify(body)
127188
+ ? JSON.stringify(servedModelName
127189
+ ? {
127190
+ ...body,
127191
+ model: servedModelName
127192
+ }
127193
+ : body)
126803
127194
  : typeof body === "string"
126804
127195
  ? body
126805
127196
  : JSON.stringify(body);
@@ -126917,7 +127308,8 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
126917
127308
  onComplete: onMonitoringComplete,
126918
127309
  requestBodyBytes,
126919
127310
  requestPath: "/v1/embeddings",
126920
- requestStartedAt
127311
+ requestStartedAt,
127312
+ responseModelName: servedModelName ? clientModelName : null
126921
127313
  });
126922
127314
  return {
126923
127315
  body: monitoredResponse.stream,
@@ -126942,8 +127334,10 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
126942
127334
  }
126943
127335
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
126944
127336
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127337
+ const servedModelName = modelManager.resolvedServedModelName;
127338
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
126945
127339
  const effectiveBody = modelManager.model.multimodalEnabled ? body : stripImagesFromBody(body);
126946
- const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path });
127340
+ const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path, servedModelName });
126947
127341
  const requestStartedAt = Date.now();
126948
127342
  const requestBody = JSON.parse(serializedBody);
126949
127343
  const streamRequested = requestBody.stream === true;
@@ -127083,7 +127477,8 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127083
127477
  onComplete: onMonitoringComplete,
127084
127478
  requestBodyBytes,
127085
127479
  requestPath: path,
127086
- requestStartedAt
127480
+ requestStartedAt,
127481
+ responseModelName: servedModelName ? clientModelName : null
127087
127482
  })
127088
127483
  : monitorEngineResponseSingle({
127089
127484
  agentEngineType: engineType ?? "unknown",
@@ -127095,7 +127490,8 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127095
127490
  onComplete: onMonitoringComplete,
127096
127491
  requestBodyBytes,
127097
127492
  requestPath: path,
127098
- requestStartedAt
127493
+ requestStartedAt,
127494
+ responseModelName: servedModelName ? clientModelName : null
127099
127495
  });
127100
127496
  return {
127101
127497
  body: monitoredResponse.stream,
@@ -158600,6 +158996,122 @@ async function detectDockerVersion() {
158600
158996
  }
158601
158997
  }
158602
158998
 
158999
+ /**
159000
+ * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
159001
+ * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
159002
+ * as its value when that token does not start with "-" (classic CLI convention); anything else
159003
+ * (flags, non-strings) is dropped. Secret-like option values are masked before pairing so they
159004
+ * never reach execution reports.
159005
+ */
159006
+ function pairExtraArgs(tokens) {
159007
+ if (!Array.isArray(tokens)) {
159008
+ return [];
159009
+ }
159010
+ // Non-string tokens become empty strings: they keep their position as a
159011
+ // non-consumable barrier while allowing secret masking over string tokens.
159012
+ const list = redactSecretArgs(tokens.map(token => (typeof token === "string" ? token : "")));
159013
+ const pairs = [];
159014
+ let index = 0;
159015
+ while (index < list.length) {
159016
+ const token = list[index];
159017
+ if (typeof token !== "string" || token.length === 0 || !token.startsWith("-")) {
159018
+ index++;
159019
+ continue;
159020
+ }
159021
+ const separator = token.indexOf("=");
159022
+ if (separator > -1) {
159023
+ const arg = token.slice(0, separator);
159024
+ if (arg.length > 0) {
159025
+ pairs.push([arg, token.slice(separator + 1)]);
159026
+ }
159027
+ index++;
159028
+ continue;
159029
+ }
159030
+ const next = list[index + 1];
159031
+ const consumesNext = typeof next === "string" && next.length > 0 && !next.startsWith("-");
159032
+ if (consumesNext) {
159033
+ pairs.push([token, next]);
159034
+ index += 2;
159035
+ }
159036
+ else {
159037
+ pairs.push([token, ""]);
159038
+ index++;
159039
+ }
159040
+ }
159041
+ 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);
159042
+ }
159043
+ /**
159044
+ * Files AT MOST ONE engine_execution report per engine startup. `beginStartup(runAt)` re-arms the
159045
+ * latch on every fresh model start (initial boot or cycle) and stamps the startup epoch used as the
159046
+ * row's `run_at`. Whichever of the three report* paths fires first wins: startup failure, in-flight
159047
+ * crash, or first successful full prompt completion.
159048
+ */
159049
+ class EngineExecutionReporter {
159050
+ options;
159051
+ currentStartupAt = null;
159052
+ reportedForCurrentStartup = false;
159053
+ constructor(options) {
159054
+ this.options = options;
159055
+ }
159056
+ /** Re-arms the latch; the stamp becomes the row's `run_at` (moment startup began). */
159057
+ beginStartup(runAt) {
159058
+ this.currentStartupAt = runAt;
159059
+ this.reportedForCurrentStartup = false;
159060
+ }
159061
+ /** Reports a startup failure (rejected prepare/start, readiness timeout, pre-ready death). */
159062
+ async reportStartupFailure(report) {
159063
+ await this.file(report, false);
159064
+ }
159065
+ /** Reports a spontaneous crash of an engine that had reached the running state. */
159066
+ async reportRuntimeCrash(report) {
159067
+ await this.file(report, false);
159068
+ }
159069
+ /** Reports the first fully-responded, token-bearing prompt completion since startup. */
159070
+ async reportSuccess(report) {
159071
+ await this.file(report, true);
159072
+ }
159073
+ async file(report, success) {
159074
+ if (this.reportedForCurrentStartup || this.currentStartupAt === null) {
159075
+ return;
159076
+ }
159077
+ // Latch BEFORE the network call: a thrown POST cannot double-file for this startup.
159078
+ this.reportedForCurrentStartup = true;
159079
+ const context = this.options.buildContext();
159080
+ const payload = {
159081
+ avgTps: report.throughput.avgTps,
159082
+ completionTokens: report.usage.completionTokens,
159083
+ durationMs: report.durationMs,
159084
+ engineType: context.engineType,
159085
+ engineVersion: context.engineVersion,
159086
+ errorDetail: success ? null : report.errorDetail,
159087
+ errorType: success ? null : report.errorType,
159088
+ extraArgs: context.extraArgsPairs,
159089
+ finishedAtISO: new Date().toISOString(),
159090
+ peakTps: report.throughput.peakTps,
159091
+ promptTokens: report.usage.promptTokens,
159092
+ runAtISO: this.currentStartupAt.toISOString(),
159093
+ success,
159094
+ ttftMs: report.ttftMs,
159095
+ totalTokens: report.usage.totalTokens
159096
+ };
159097
+ try {
159098
+ await this.options.report(payload);
159099
+ this.options.logger.info("Engine execution outcome reported", {
159100
+ inferenceSourceID: this.options.sourceLabel,
159101
+ success
159102
+ });
159103
+ }
159104
+ catch (error) {
159105
+ // Losing one report is preferable to filing two; the latch stays latched.
159106
+ this.options.logger.warn("Failed to report engine execution outcome", {
159107
+ error: asError(error),
159108
+ inferenceSourceID: this.options.sourceLabel,
159109
+ success
159110
+ });
159111
+ }
159112
+ }
159113
+ }
159114
+
158603
159115
  async function createApplication({ abortController, apiClient, configuration, logger }) {
158604
159116
  ensureDockerValidEnv();
158605
159117
  logger.info("Fetching conduit configuration");
@@ -158631,6 +159143,87 @@ async function createApplication({ abortController, apiClient, configuration, lo
158631
159143
  error: asError(error)
158632
159144
  });
158633
159145
  }
159146
+ const reporter = new EngineExecutionReporter({
159147
+ buildContext: () => {
159148
+ const engineType = conduitConfiguration.engineConfig?.type ?? "llama.cpp";
159149
+ const versions = {
159150
+ custom: null,
159151
+ exllamav3: machine?.exllamav3Version ?? null,
159152
+ "llama.cpp": machine?.llamaCppVersion ?? null,
159153
+ "mlx-lm": machine?.mlxlmVersion ?? null,
159154
+ sglang: machine?.sglangVersion ?? null,
159155
+ "tensorrt-llm": machine?.tensorrtLlmVersion ?? null,
159156
+ vllm: machine?.vllmVersion ?? null
159157
+ };
159158
+ return {
159159
+ engineType,
159160
+ engineVersion: versions[engineType] ?? null,
159161
+ extraArgsPairs: pairExtraArgs(conduitConfiguration.engineConfig?.extraArgs)
159162
+ };
159163
+ },
159164
+ logger,
159165
+ report: payload => apiClient.reportEngineExecution(payload),
159166
+ sourceLabel: configuration.inferenceSourceID
159167
+ });
159168
+ // Intercept the prompt-metrics chokepoint so the first fully-responded, token-bearing prompt of
159169
+ // each fresh startup files the one-shot engine_execution success report. Handlers close over the
159170
+ // SAME `apiClient` object and read `reportPromptMetrics` at request-dispatch time (which always
159171
+ // follows this point), so the wrapped method is what they invoke.
159172
+ const rawReportPromptMetrics = apiClient.reportPromptMetrics;
159173
+ apiClient.reportPromptMetrics = async (payload) => {
159174
+ if (payload.successful && payload.completionTokens > 0 && payload.latencyMs > 0) {
159175
+ // The one-shot report is kicked off and its settlement attached HERE (before any await): if the
159176
+ // metrics path throws below, the report promise must still be able to log its own rejection.
159177
+ const successReport = reporter
159178
+ .reportSuccess({
159179
+ durationMs: payload.latencyMs,
159180
+ errorDetail: null,
159181
+ errorType: null,
159182
+ throughput: {
159183
+ avgTps: payload.tokensPerSecond,
159184
+ peakTps: null
159185
+ },
159186
+ ttftMs: payload.timeToFirstTokenMs ?? 0,
159187
+ usage: {
159188
+ completionTokens: payload.completionTokens,
159189
+ promptTokens: payload.promptTokens,
159190
+ totalTokens: payload.totalTokens
159191
+ }
159192
+ })
159193
+ .catch(error => {
159194
+ logger.warn("Engine execution success report failed", {
159195
+ error: asError(error)
159196
+ });
159197
+ });
159198
+ await rawReportPromptMetrics(payload);
159199
+ await successReport;
159200
+ return;
159201
+ }
159202
+ await rawReportPromptMetrics(payload);
159203
+ };
159204
+ const SECRET_ARG_MASK_PATTERN = /(-{1,2}[A-Za-z0-9_.]*(?:api[-_]?key|hf[-_]?token|token)(?:\s+|[=:]))\S+/gi;
159205
+ // Assembles the payload shared by the startup-failure and runtime-crash report paths. Stderr
159206
+ // may echo secrets, so mask `--api-key`/token-looking args before they reach the DB.
159207
+ function buildCrashReport(error, exitCode, signal) {
159208
+ const classification = classifyEngineFailure({ error, exitCode, signal });
159209
+ const raw = normalizeEngineError(error.message);
159210
+ const masked = raw.replace(SECRET_ARG_MASK_PATTERN, "$1***");
159211
+ return {
159212
+ durationMs: 0,
159213
+ errorDetail: masked.slice(0, 2048),
159214
+ errorType: classification,
159215
+ ttftMs: 0,
159216
+ throughput: {
159217
+ avgTps: 0,
159218
+ peakTps: null
159219
+ },
159220
+ usage: {
159221
+ completionTokens: 0,
159222
+ promptTokens: 0,
159223
+ totalTokens: 0
159224
+ }
159225
+ };
159226
+ }
158634
159227
  const conduitStateManager = new ConduitStateManager({
158635
159228
  initialState: {
158636
159229
  state: "initialising"
@@ -158682,6 +159275,17 @@ async function createApplication({ abortController, apiClient, configuration, lo
158682
159275
  });
158683
159276
  stopRequestedByControl = false;
158684
159277
  setErrorState({ error: normalizeEngineError(err.message) });
159278
+ // Spontaneous death of a SERVING engine → crash report, suppressed by the latch if the
159279
+ // startup's one-shot outcome was already filed. Startup-path failures report from
159280
+ // `startEngine`'s catch; this listener is the RUNTIME-crash path only.
159281
+ if (modelManager.wasRunning && !err.message.includes("interrupted by stop request")) {
159282
+ const crashReport = buildCrashReport(err, modelManager.lastExitCode, modelManager.lastExitSignal);
159283
+ reporter.reportRuntimeCrash(crashReport).catch(crashReportError => {
159284
+ logger.warn("Engine execution crash report failed", {
159285
+ error: asError(crashReportError)
159286
+ });
159287
+ });
159288
+ }
158685
159289
  });
158686
159290
  modelManager.on("engineReady", () => {
158687
159291
  setOnlineState();
@@ -158741,24 +159345,40 @@ async function createApplication({ abortController, apiClient, configuration, lo
158741
159345
  };
158742
159346
  async function startEngine() {
158743
159347
  logger.info("Engine start requested");
158744
- conduitStateManager.setState({
158745
- modelFileName,
158746
- modelName,
158747
- state: "downloadingModelFiles",
158748
- totalProgress: {
158749
- file: 0,
158750
- total: 0
159348
+ reporter.beginStartup(new Date());
159349
+ try {
159350
+ conduitStateManager.setState({
159351
+ modelFileName,
159352
+ modelName,
159353
+ state: "downloadingModelFiles",
159354
+ totalProgress: {
159355
+ file: 0,
159356
+ total: 0
159357
+ }
159358
+ });
159359
+ await conduitStateReportManager.reportNow();
159360
+ await modelManager.prepare({
159361
+ onDownloadProgress: reportDownloadProgress
159362
+ });
159363
+ conduitStateManager.setState({
159364
+ state: "bootingEngine"
159365
+ });
159366
+ await conduitStateReportManager.reportNow();
159367
+ await modelManager.start();
159368
+ }
159369
+ catch (error) {
159370
+ const parsedError = asError(error);
159371
+ // Operator-initiated aborts are not startup failures worth reporting.
159372
+ if (!parsedError.message.includes("interrupted by stop request")) {
159373
+ const startupReport = buildCrashReport(parsedError, modelManager.lastExitCode, modelManager.lastExitSignal);
159374
+ reporter.reportStartupFailure(startupReport).catch(startupReportError => {
159375
+ logger.warn("Engine execution startup report failed", {
159376
+ error: asError(startupReportError)
159377
+ });
159378
+ });
158751
159379
  }
158752
- });
158753
- await conduitStateReportManager.reportNow();
158754
- await modelManager.prepare({
158755
- onDownloadProgress: reportDownloadProgress
158756
- });
158757
- conduitStateManager.setState({
158758
- state: "bootingEngine"
158759
- });
158760
- await conduitStateReportManager.reportNow();
158761
- await modelManager.start();
159380
+ throw error;
159381
+ }
158762
159382
  }
158763
159383
  async function stopEngine({ reason }) {
158764
159384
  if (!modelManager.canStop) {
@@ -159014,7 +159634,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
159014
159634
  return new ModelManager({
159015
159635
  contextLength: conduitConfiguration.contextLength ?? null,
159016
159636
  engineConfig: engineConfig
159017
- ? { extraArgs: engineConfig.extraArgs, type: engineConfig.type }
159637
+ ? {
159638
+ baseUrl: engineConfig.baseUrl ?? null,
159639
+ extraArgs: engineConfig.extraArgs,
159640
+ type: engineConfig.type
159641
+ }
159018
159642
  : null,
159019
159643
  enginePort: configuration.enginePort,
159020
159644
  engineType: engineConfig?.type ?? "llama.cpp",
@@ -159025,7 +159649,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
159025
159649
  }
159026
159650
  function getConduitModelFileName(configuration) {
159027
159651
  const { source } = configuration.targetModel;
159028
- return source.type === "huggingface" ? source.slug : source.irid;
159652
+ if (source.type === "huggingface")
159653
+ return source.slug;
159654
+ if (source.type === "storage")
159655
+ return source.irid;
159656
+ return configuration.targetModel.id;
159029
159657
  }
159030
159658
  function getConduitModelName(configuration) {
159031
159659
  return configuration.targetModel.id;