@infersec/conduit 1.113.0 → 1.114.1

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,
@@ -21105,7 +21127,8 @@ const CreateModelResponseSchema = object$5({
21105
21127
  const CreateSourceBodySchema = object$5({
21106
21128
  contextLength: number$1().int().positive().max(1048576).optional(),
21107
21129
  engineId: ULIDSchema,
21108
- modelID: ULIDSchema,
21130
+ // Optional for custom engines, which serve externally managed models
21131
+ modelID: ULIDSchema.nullable().optional(),
21109
21132
  name: ResourceNameSchema,
21110
21133
  quantizationLabel: string$2().min(1).max(128).optional()
21111
21134
  });
@@ -21154,7 +21177,7 @@ const SourceDetailResponseSchema = object$5({
21154
21177
  const UpdateSourceBodySchema = object$5({
21155
21178
  contextLength: number$1().int().positive().nullable().optional(),
21156
21179
  engineId: ULIDSchema.nullable().optional(),
21157
- modelID: ULIDSchema.optional(),
21180
+ modelID: ULIDSchema.nullable().optional(),
21158
21181
  name: ResourceNameSchema.optional(),
21159
21182
  quantizationLabel: string$2().min(1).max(128).nullable().optional()
21160
21183
  });
@@ -21278,6 +21301,7 @@ const CreateEndpointResponseSchema = object$5({
21278
21301
  id: ULIDSchema
21279
21302
  });
21280
21303
  const EngineOutputSchema = object$5({
21304
+ baseUrl: string$2().nullable(),
21281
21305
  created: string$2(),
21282
21306
  extraArgs: array$1(string$2()),
21283
21307
  id: ULIDSchema,
@@ -21286,11 +21310,29 @@ const EngineOutputSchema = object$5({
21286
21310
  updated: string$2()
21287
21311
  });
21288
21312
  const CreateEngineBodySchema = object$5({
21313
+ baseUrl: string$2().url().nullable().optional(),
21289
21314
  extraArgs: array$1(string$2()).optional(),
21290
21315
  name: ResourceNameSchema,
21291
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
+ }
21292
21333
  });
21293
21334
  const UpdateEngineBodySchema = object$5({
21335
+ baseUrl: string$2().url().nullable().optional(),
21294
21336
  extraArgs: array$1(string$2()).optional(),
21295
21337
  name: ResourceNameSchema.optional(),
21296
21338
  type: LLMEngineSchema.optional()
@@ -22096,6 +22138,11 @@ const RecommendedModelSchema = object$5({
22096
22138
  const recommendedModels = RecommendedModelSchema.array().parse(modelsData);
22097
22139
 
22098
22140
  const ENGINE_API_COMPATIBILITY = {
22141
+ custom: {
22142
+ nativeAnthropicMessages: false,
22143
+ supportsEmbeddings: true,
22144
+ supportsVision: true
22145
+ },
22099
22146
  exllamav3: {
22100
22147
  nativeAnthropicMessages: false,
22101
22148
  supportsEmbeddings: false,
@@ -111745,6 +111792,27 @@ function registerEndpointCommands({ program }) {
111745
111792
  }
111746
111793
 
111747
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
+ }
111748
111816
  function buildEngineCreateBody(options) {
111749
111817
  if (!options.name) {
111750
111818
  throw new Error("--name is required");
@@ -111755,7 +111823,12 @@ function buildEngineCreateBody(options) {
111755
111823
  if (!ENGINE_TYPES.includes(options.type)) {
111756
111824
  throw new Error(`Invalid engine type: ${options.type} (expected one of: ${ENGINE_TYPES.join(", ")})`);
111757
111825
  }
111826
+ const baseUrl = validateEngineBaseURL({
111827
+ baseUrl: options.baseUrl,
111828
+ type: options.type
111829
+ });
111758
111830
  return {
111831
+ baseUrl,
111759
111832
  extraArgs: options.arg ?? [],
111760
111833
  name: options.name,
111761
111834
  type: options.type
@@ -111773,6 +111846,24 @@ function buildEngineUpdateBody(options) {
111773
111846
  }
111774
111847
  if (options.arg !== undefined)
111775
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
+ }
111776
111867
  return body;
111777
111868
  }
111778
111869
 
@@ -111786,10 +111877,11 @@ function registerEngineCommands({ program }) {
111786
111877
  .description("Create or update an inference engine resource")
111787
111878
  .option("--api-url <url>", "API base URL (required, no environment variable fallback)")
111788
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)")
111789
111881
  .option("--id <ulid>", "Target an existing engine by ID (requires --update)")
111790
111882
  .option("--key <value>", "API key (required, no environment variable fallback)")
111791
111883
  .option("--name <name>", "Engine name (matched by --update)")
111792
- .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")
111793
111885
  .option("--update", "Update an existing engine matched by name (or --id) instead of erroring")
111794
111886
  .action(async (options) => {
111795
111887
  const { apiURL, apiKey } = resolveManagementConnection(options);
@@ -117789,6 +117881,9 @@ async function getChatTemplateEngineArgs({ engine, model, targetDirectory }) {
117789
117881
  return [];
117790
117882
  const flag = FLAG_BASED_ENGINE_ARGS[engine];
117791
117883
  if (!flag) {
117884
+ if (engine === "custom") {
117885
+ console.warn("[chatTemplate] Custom engines manage their own serving; ignoring chat template override");
117886
+ }
117792
117887
  if (engine === "tensorrt-llm") {
117793
117888
  console.warn("[chatTemplate] TensorRT-LLM does not support chat template overrides; ignoring");
117794
117889
  }
@@ -124686,7 +124781,7 @@ function matchesQuantizationVariant({ filePath, variant }) {
124686
124781
  return segments.slice(0, -1).some(segment => matcher.test(segment));
124687
124782
  }
124688
124783
  async function findQuantizedModelTarget({ model, path }) {
124689
- if (model.source.type === "storage") {
124784
+ if (model.source.type !== "huggingface") {
124690
124785
  throw new Error("Model storage not supported yet");
124691
124786
  }
124692
124787
  if (model.format !== "gguf") {
@@ -125601,7 +125696,11 @@ function sanitizeSegment(value) {
125601
125696
  .replace(new RegExp(`${SEPARATOR}{2,}`, "g"), SEPARATOR);
125602
125697
  }
125603
125698
  function createModelStorageKey(model) {
125604
- 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;
125605
125704
  return `${model.source.type}${SEPARATOR}${sanitizeSegment(identifier)}`;
125606
125705
  }
125607
125706
 
@@ -125677,8 +125776,11 @@ class ModelManager extends EventEmitter {
125677
125776
  uniqueName;
125678
125777
  contextLength;
125679
125778
  logger;
125779
+ discoveredModelNames = [];
125780
+ customProbeCount = 0;
125680
125781
  engineProcess = null;
125681
125782
  healthPollInterval = null;
125783
+ lastCustomReadinessReport = null;
125682
125784
  lastEngineError = null;
125683
125785
  lifecycleState = "stopped";
125684
125786
  downloadLockHandle = null;
@@ -125709,6 +125811,7 @@ class ModelManager extends EventEmitter {
125709
125811
  }
125710
125812
  async fetchOpenAI(path, opts) {
125711
125813
  switch (this.engine) {
125814
+ case "custom":
125712
125815
  case "exllamav3":
125713
125816
  case "llama.cpp":
125714
125817
  case "mlx-lm":
@@ -125716,6 +125819,9 @@ class ModelManager extends EventEmitter {
125716
125819
  case "tensorrt-llm":
125717
125820
  case "vllm": {
125718
125821
  this.logger.debug(`Fetching from engine: ${path}`);
125822
+ const baseURL = this.engine === "custom"
125823
+ ? this.requireCustomBaseURL()
125824
+ : `http://localhost:${this.enginePort}`;
125719
125825
  const callerSignal = opts?.signal;
125720
125826
  const controller = new AbortController();
125721
125827
  const timeout = setTimeout(() => {
@@ -125726,7 +125832,7 @@ class ModelManager extends EventEmitter {
125726
125832
  : controller.signal;
125727
125833
  try {
125728
125834
  const fetchStartedAt = Date.now();
125729
- const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, path), {
125835
+ const response = await undiciExports.fetch(joinURL(baseURL, path), {
125730
125836
  ...opts,
125731
125837
  dispatcher: ENGINE_AGENT,
125732
125838
  headers: {
@@ -125761,6 +125867,11 @@ class ModelManager extends EventEmitter {
125761
125867
  modelID: this.model.id
125762
125868
  });
125763
125869
  switch (this.engine) {
125870
+ case "custom":
125871
+ if (this.model.chatTemplate) {
125872
+ this.logger.warn("Chat template overrides are ignored for custom engines: the remote server manages its own serving");
125873
+ }
125874
+ break;
125764
125875
  case "exllamav3":
125765
125876
  case "llama.cpp":
125766
125877
  case "mlx-lm":
@@ -125816,7 +125927,9 @@ class ModelManager extends EventEmitter {
125816
125927
  });
125817
125928
  try {
125818
125929
  this.engineProcess = await this.startEngineProcess();
125819
- this.bindEngineProcessEvents(this.engineProcess);
125930
+ if (this.engineProcess) {
125931
+ this.bindEngineProcessEvents(this.engineProcess);
125932
+ }
125820
125933
  this.logger.info("Started LLM engine", {
125821
125934
  agentEngineType: this.engine
125822
125935
  });
@@ -125835,7 +125948,7 @@ class ModelManager extends EventEmitter {
125835
125948
  if (!alreadyEmitted) {
125836
125949
  this.emit("engineError", err);
125837
125950
  }
125838
- if (this.engineProcess) {
125951
+ if (this.engineProcess || this.engine === "custom") {
125839
125952
  this.startHealthPoll();
125840
125953
  }
125841
125954
  throw err;
@@ -125843,6 +125956,9 @@ class ModelManager extends EventEmitter {
125843
125956
  this.lifecycleState = "running";
125844
125957
  this.reachedRunningState = true;
125845
125958
  this.emit("engineReady");
125959
+ if (this.engine === "custom") {
125960
+ this.startHealthPoll();
125961
+ }
125846
125962
  }
125847
125963
  async stop() {
125848
125964
  if (this.lifecycleState === "stopping") {
@@ -125863,6 +125979,8 @@ class ModelManager extends EventEmitter {
125863
125979
  this.clearHealthPoll();
125864
125980
  const processManager = this.engineProcess;
125865
125981
  if (!processManager) {
125982
+ this.stopRequested = true;
125983
+ this.reachedRunningState = false;
125866
125984
  this.lifecycleState = "stopped";
125867
125985
  return;
125868
125986
  }
@@ -125888,11 +126006,27 @@ class ModelManager extends EventEmitter {
125888
126006
  get state() {
125889
126007
  return this.lifecycleState;
125890
126008
  }
126009
+ get resolvedServedModelName() {
126010
+ if (this.engine !== "custom")
126011
+ return null;
126012
+ return this.discoveredModelNames[0] ?? null;
126013
+ }
125891
126014
  get wasRunning() {
125892
126015
  return this.reachedRunningState;
125893
126016
  }
126017
+ get customBaseURL() {
126018
+ if (this.engine !== "custom")
126019
+ return null;
126020
+ const baseUrl = this.engineConfig?.baseUrl;
126021
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
126022
+ return null;
126023
+ return baseUrl.replace(/\/+$/, "");
126024
+ }
125894
126025
  async checkEngineReadiness() {
125895
126026
  switch (this.engine) {
126027
+ case "custom": {
126028
+ return this.checkCustomReadiness();
126029
+ }
125896
126030
  case "llama.cpp": {
125897
126031
  return this.checkLlamacppReadiness();
125898
126032
  }
@@ -125909,6 +126043,98 @@ class ModelManager extends EventEmitter {
125909
126043
  return "ready";
125910
126044
  }
125911
126045
  }
126046
+ async checkCustomReadiness() {
126047
+ const baseURL = this.customBaseURL;
126048
+ if (!baseURL) {
126049
+ this.reportCustomReadinessChange("unreachable", "", "no base URL configured");
126050
+ return "unreachable";
126051
+ }
126052
+ const probeURL = joinURL(baseURL, "/v1/models");
126053
+ this.customProbeCount++;
126054
+ try {
126055
+ const response = await undiciExports.fetch(probeURL, {
126056
+ method: "GET",
126057
+ signal: AbortSignal.timeout(5000)
126058
+ });
126059
+ if (response.status === 503) {
126060
+ this.logger.debug("Custom engine probe: server loading", {
126061
+ attempt: this.customProbeCount,
126062
+ probeURL
126063
+ });
126064
+ this.reportCustomReadinessChange("loading", baseURL, "server loading (HTTP 503)");
126065
+ return "loading";
126066
+ }
126067
+ if (!response.ok) {
126068
+ const reason = `HTTP ${response.status} from /v1/models`;
126069
+ this.logger.debug("Custom engine probe: not ready", {
126070
+ attempt: this.customProbeCount,
126071
+ probeURL,
126072
+ reason
126073
+ });
126074
+ this.reportCustomReadinessChange("unreachable", baseURL, reason);
126075
+ return "unreachable";
126076
+ }
126077
+ const payload = (await response.json());
126078
+ const models = Array.isArray(payload.data) ? payload.data : [];
126079
+ const modelIDs = models
126080
+ .map(model => {
126081
+ if (model === null || typeof model !== "object")
126082
+ return null;
126083
+ const id = model.id;
126084
+ return typeof id === "string" ? id : null;
126085
+ })
126086
+ .filter((id) => id !== null);
126087
+ if (modelIDs.length === 0) {
126088
+ this.logger.debug("Custom engine probe: no models exposed yet", {
126089
+ attempt: this.customProbeCount,
126090
+ probeURL
126091
+ });
126092
+ this.reportCustomReadinessChange("loading", baseURL, "/v1/models returned no models");
126093
+ return "loading";
126094
+ }
126095
+ if (modelIDs.join("\n") !== this.discoveredModelNames.join("\n")) {
126096
+ this.discoveredModelNames = modelIDs;
126097
+ this.logger.info("Discovered models on custom engine endpoint", {
126098
+ engineBaseURL: baseURL,
126099
+ models: modelIDs,
126100
+ selectedModel: modelIDs[0]
126101
+ });
126102
+ }
126103
+ return "ready";
126104
+ }
126105
+ catch (error) {
126106
+ const reason = asError(error).message;
126107
+ this.logger.debug("Custom engine probe: request failed", {
126108
+ attempt: this.customProbeCount,
126109
+ probeURL,
126110
+ reason
126111
+ });
126112
+ this.reportCustomReadinessChange("unreachable", baseURL, reason);
126113
+ return "unreachable";
126114
+ }
126115
+ }
126116
+ reportCustomReadinessChange(readiness, baseURL, reason) {
126117
+ const key = `${readiness}:${reason}`;
126118
+ if (key === this.lastCustomReadinessReport) {
126119
+ // Same outcome as the last report: surface a heartbeat every 15
126120
+ // attempts (~30s while booting) so progress stays visible.
126121
+ if (this.customProbeCount % 15 === 0) {
126122
+ this.logger.info("Still waiting for custom engine endpoint", {
126123
+ attempt: this.customProbeCount,
126124
+ engineBaseURL: baseURL,
126125
+ reason
126126
+ });
126127
+ }
126128
+ return;
126129
+ }
126130
+ this.lastCustomReadinessReport = key;
126131
+ this.logger.warn(readiness === "loading"
126132
+ ? "Custom engine endpoint is loading — waiting for /v1/models to expose a model"
126133
+ : "Custom engine endpoint unreachable — verify the server is running and reachable from conduit", {
126134
+ engineBaseURL: baseURL,
126135
+ reason
126136
+ });
126137
+ }
125912
126138
  async checkGenericHealthReadiness() {
125913
126139
  try {
125914
126140
  const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, "/health"), {
@@ -125964,23 +126190,41 @@ class ModelManager extends EventEmitter {
125964
126190
  }
125965
126191
  }
125966
126192
  async waitForEngineReady() {
125967
- const maxWaitMs = 15 * 60 * 1000;
126193
+ const maxWaitMs = Number.parseInt(process.env.ENGINE_STARTUP_TIMEOUT_MS ?? "", 10) || 15 * 60 * 1000;
125968
126194
  const pollIntervalMs = 2000;
125969
126195
  const start = Date.now();
126196
+ if (this.engine === "custom") {
126197
+ this.logger.info(`Connecting to external OpenAI-compatible server at ${this.customBaseURL ?? "(no base URL configured)"} — serving begins once /v1/models exposes a model`, {
126198
+ engineBaseURL: this.customBaseURL ?? ""
126199
+ });
126200
+ }
125970
126201
  while (Date.now() - start < maxWaitMs) {
125971
- if (this.lifecycleState === "stopping") {
126202
+ if (this.lifecycleState === "stopping" || this.stopRequested) {
125972
126203
  throw new Error("LLM engine startup interrupted by stop request");
125973
126204
  }
125974
- if (!this.engineProcess) {
126205
+ if (!this.engineProcess && this.engine !== "custom") {
125975
126206
  throw new Error("LLM engine process exited before readiness checks completed");
125976
126207
  }
125977
126208
  const readiness = await this.checkEngineReadiness();
125978
126209
  if (readiness === "ready") {
126210
+ // A stop() may have landed while the readiness request was
126211
+ // in flight; re-check before declaring ready.
126212
+ const lifecycleState = this.lifecycleState;
126213
+ if (lifecycleState === "stopping" || this.stopRequested) {
126214
+ throw new Error("LLM engine startup interrupted by stop request");
126215
+ }
125979
126216
  return;
125980
126217
  }
125981
126218
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
125982
126219
  }
125983
126220
  const stderrTail = this.engineProcess?.stderr?.slice(-1e3);
126221
+ if (this.engine === "custom") {
126222
+ const baseURL = this.customBaseURL ?? "(missing)";
126223
+ const discovered = this.discoveredModelNames;
126224
+ throw new Error(discovered.length > 0
126225
+ ? `Custom engine endpoint at ${baseURL} exposed models (${discovered.join(", ")}) but never reported ready within ${Math.round(maxWaitMs / 1000)}s`
126226
+ : `Custom engine endpoint at ${baseURL} did not expose a model via /v1/models within ${Math.round(maxWaitMs / 1000)}s. Verify the server is running and reachable from conduit (if conduit runs in a container, use the host address instead of localhost)`);
126227
+ }
125984
126228
  throw new Error(stderrTail
125985
126229
  ? `LLM engine failed readiness checks within timeout. Last engine output:\n${stderrTail}`
125986
126230
  : "LLM engine failed readiness checks within timeout");
@@ -125993,19 +126237,32 @@ class ModelManager extends EventEmitter {
125993
126237
  }
125994
126238
  startHealthPoll() {
125995
126239
  this.clearHealthPoll();
125996
- this.logger.info("Starting background health poll for errored engine");
126240
+ this.logger.info("Starting background engine health poll", {
126241
+ agentEngineType: this.engine
126242
+ });
125997
126243
  this.healthPollInterval = setInterval(() => {
125998
- if (!this.engineProcess) {
126244
+ if (!this.engineProcess && this.engine !== "custom") {
125999
126245
  this.clearHealthPoll();
126000
126246
  return;
126001
126247
  }
126002
126248
  this.checkEngineReadiness()
126003
126249
  .then(readiness => {
126004
126250
  if (readiness === "ready") {
126005
- this.clearHealthPoll();
126006
- this.lifecycleState = "running";
126007
- this.reachedRunningState = true;
126008
- this.emit("engineReady");
126251
+ if (this.lifecycleState === "errored" ||
126252
+ this.lifecycleState === "starting") {
126253
+ this.lifecycleState = "running";
126254
+ this.reachedRunningState = true;
126255
+ this.emit("engineReady");
126256
+ }
126257
+ if (this.engine !== "custom") {
126258
+ this.clearHealthPoll();
126259
+ }
126260
+ return;
126261
+ }
126262
+ if (this.engine === "custom" &&
126263
+ readiness === "unreachable" &&
126264
+ this.lifecycleState === "running") {
126265
+ this.recordEngineError(new Error(`Custom engine endpoint unreachable: ${this.customBaseURL}`));
126009
126266
  }
126010
126267
  })
126011
126268
  .catch(() => {
@@ -126030,6 +126287,15 @@ class ModelManager extends EventEmitter {
126030
126287
  this.lastEngineError = err;
126031
126288
  this.emit("engineError", err);
126032
126289
  }
126290
+ requireCustomBaseURL() {
126291
+ const baseURL = this.customBaseURL;
126292
+ if (!baseURL) {
126293
+ throw new ConfigurationInvalidError({
126294
+ message: "Custom engine requires a base URL"
126295
+ });
126296
+ }
126297
+ return baseURL;
126298
+ }
126033
126299
  async releaseDownloadLock() {
126034
126300
  const handle = this.downloadLockHandle;
126035
126301
  if (!handle)
@@ -126096,6 +126362,8 @@ class ModelManager extends EventEmitter {
126096
126362
  async startEngineProcess() {
126097
126363
  const targetDir = join(this.modelsDirectory, this.uniqueName);
126098
126364
  switch (this.engine) {
126365
+ case "custom":
126366
+ return null;
126099
126367
  case "exllamav3":
126100
126368
  return startExllamav3.call(this, {
126101
126369
  enginePort: this.enginePort,
@@ -126407,8 +126675,9 @@ function isEngineUsageChunk(value) {
126407
126675
  }
126408
126676
  return true;
126409
126677
  }
126410
- function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126678
+ function monitorEngineResponseStream({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126411
126679
  const startedAt = requestStartedAt ?? Date.now();
126680
+ const clientModelName = responseModelName ?? null;
126412
126681
  const passThrough = new PassThrough();
126413
126682
  passThrough.on("error", (error) => {
126414
126683
  logger.error("Engine response stream error", {
@@ -126420,53 +126689,61 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126420
126689
  let firstChunkAt = null;
126421
126690
  let usage = null;
126422
126691
  let buffer = "";
126692
+ let pendingFragment = "";
126423
126693
  let completed = false;
126424
- function modifyChunkWithUsage(chunk) {
126425
- const text = chunk.toString("utf8");
126426
- const lines = text.split("\n");
126427
- const modifiedLines = [];
126428
- for (const rawLine of lines) {
126429
- const line = rawLine.trim();
126430
- if (!line.startsWith("data:")) {
126431
- modifiedLines.push(rawLine);
126432
- continue;
126694
+ function rewriteDataLine(rawLine) {
126695
+ const line = rawLine.trim();
126696
+ if (!line.startsWith("data:")) {
126697
+ return rawLine;
126698
+ }
126699
+ const payload = line.slice(5).trim();
126700
+ if (!payload || payload === "[DONE]") {
126701
+ return rawLine;
126702
+ }
126703
+ try {
126704
+ const parsed = JSON.parse(payload);
126705
+ let modified = false;
126706
+ if (coerceToolCallArguments(parsed)) {
126707
+ modified = true;
126433
126708
  }
126434
- const payload = line.slice(5).trim();
126435
- if (!payload || payload === "[DONE]") {
126436
- modifiedLines.push(rawLine);
126437
- continue;
126709
+ if (clientModelName !== null &&
126710
+ typeof parsed.model === "string" &&
126711
+ parsed.model !== clientModelName) {
126712
+ parsed.model = clientModelName;
126713
+ modified = true;
126438
126714
  }
126439
- try {
126440
- const parsed = JSON.parse(payload);
126441
- let modified = false;
126442
- if (coerceToolCallArguments(parsed)) {
126715
+ if (parsed.usage) {
126716
+ const usageChunk = parsed.usage;
126717
+ const effectiveContext = getEffectiveContextLength({
126718
+ contextLength,
126719
+ engineConfig,
126720
+ engineType
126721
+ });
126722
+ if (usageChunk.context_usage === undefined &&
126723
+ usageChunk.prompt_tokens !== undefined &&
126724
+ effectiveContext !== null) {
126725
+ usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126443
126726
  modified = true;
126444
126727
  }
126445
- if (parsed.usage) {
126446
- const usageChunk = parsed.usage;
126447
- const effectiveContext = getEffectiveContextLength({
126448
- contextLength,
126449
- engineConfig,
126450
- engineType
126451
- });
126452
- if (usageChunk.context_usage === undefined &&
126453
- usageChunk.prompt_tokens !== undefined &&
126454
- effectiveContext !== null) {
126455
- usageChunk.context_usage = usageChunk.prompt_tokens / effectiveContext;
126456
- modified = true;
126457
- }
126458
- }
126459
- if (modified) {
126460
- modifiedLines.push("data: " + JSON.stringify(parsed));
126461
- continue;
126462
- }
126463
126728
  }
126464
- catch (_error) {
126465
- // Ignore malformed chunks
126729
+ if (modified) {
126730
+ return "data: " + JSON.stringify(parsed);
126466
126731
  }
126467
- modifiedLines.push(rawLine);
126468
126732
  }
126469
- return Buffer.from(modifiedLines.join("\n"), "utf8");
126733
+ catch (_error) {
126734
+ // Ignore malformed chunks
126735
+ }
126736
+ return rawLine;
126737
+ }
126738
+ // SSE events can split across transport chunks: hold back the trailing
126739
+ // (newline-less) fragment and only rewrite complete data lines, so a
126740
+ // partial JSON event is never forwarded with its upstream model name.
126741
+ function modifyChunkWithUsage(chunk, flush = false) {
126742
+ const combined = pendingFragment + chunk.toString("utf8");
126743
+ const lines = combined.split("\n");
126744
+ pendingFragment = flush ? "" : (lines.pop() ?? "");
126745
+ const modifiedLines = lines.map(rewriteDataLine);
126746
+ return Buffer.from(modifiedLines.length > 0 ? modifiedLines.join("\n") + (flush ? "" : "\n") : "", "utf8");
126470
126747
  }
126471
126748
  function parseUsageFromBuffer() {
126472
126749
  const lines = buffer.split("\n");
@@ -126562,6 +126839,9 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126562
126839
  if (buffer.length > 0) {
126563
126840
  parseUsageFromBuffer();
126564
126841
  }
126842
+ if (pendingFragment.length > 0) {
126843
+ passThrough.write(modifyChunkWithUsage(Buffer.from("", "utf8"), true));
126844
+ }
126565
126845
  logEngineMetrics({
126566
126846
  agentEngineType,
126567
126847
  level: "info",
@@ -126604,9 +126884,11 @@ function monitorEngineResponseStream({ agentEngineType, body, contextLength, eng
126604
126884
  stream: passThrough
126605
126885
  };
126606
126886
  }
126607
- function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt }) {
126887
+ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, engineConfig, engineType, logger, onComplete, requestBodyBytes, requestPath, requestStartedAt, responseModelName }) {
126608
126888
  const maxUsageCaptureBytes = 1024 * 1024;
126609
126889
  const startedAt = requestStartedAt ?? Date.now();
126890
+ const clientModelName = responseModelName ?? null;
126891
+ const rewriteBuffer = clientModelName !== null ? [] : null;
126610
126892
  const passThrough = new PassThrough();
126611
126893
  passThrough.on("error", (error) => {
126612
126894
  logger.error("Engine response stream error", {
@@ -126662,7 +126944,12 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126662
126944
  usageChunks.length = 0;
126663
126945
  }
126664
126946
  }
126665
- passThrough.write(chunkBuffer);
126947
+ if (rewriteBuffer) {
126948
+ rewriteBuffer.push(chunkBuffer);
126949
+ }
126950
+ else {
126951
+ passThrough.write(chunkBuffer);
126952
+ }
126666
126953
  });
126667
126954
  body.once("error", err => {
126668
126955
  logEngineMetrics({
@@ -126722,6 +127009,24 @@ function monitorEngineResponseSingle({ agentEngineType, body, contextLength, eng
126722
127009
  responseBytes,
126723
127010
  usage
126724
127011
  });
127012
+ if (rewriteBuffer) {
127013
+ const original = Buffer.concat(rewriteBuffer);
127014
+ let output = original;
127015
+ try {
127016
+ const parsed = JSON.parse(original.toString("utf8"));
127017
+ if (parsed !== null &&
127018
+ typeof parsed === "object" &&
127019
+ typeof parsed.model === "string" &&
127020
+ parsed.model !== clientModelName) {
127021
+ parsed.model = clientModelName;
127022
+ output = Buffer.from(JSON.stringify(parsed), "utf8");
127023
+ }
127024
+ }
127025
+ catch (_error) {
127026
+ // Non-JSON body: pass through untouched
127027
+ }
127028
+ passThrough.write(output);
127029
+ }
126725
127030
  finalize(null);
126726
127031
  passThrough.end();
126727
127032
  });
@@ -126858,7 +127163,7 @@ function applyChatTemplateKwargs({ body, model }) {
126858
127163
  }
126859
127164
  return payload;
126860
127165
  }
126861
- function serializeRequestBody$1(body, { model, path } = {}) {
127166
+ function serializeRequestBody$1(body, { model, path, servedModelName } = {}) {
126862
127167
  if (!isPlainObject$a(body)) {
126863
127168
  const payload = typeof body === "string" ? body : JSON.stringify(body);
126864
127169
  return {
@@ -126867,6 +127172,9 @@ function serializeRequestBody$1(body, { model, path } = {}) {
126867
127172
  };
126868
127173
  }
126869
127174
  let requestPayload = { ...body };
127175
+ if (servedModelName) {
127176
+ requestPayload.model = servedModelName;
127177
+ }
126870
127178
  if (path === "/v1/chat/completions" && model) {
126871
127179
  requestPayload = applyChatTemplateKwargs({ body: requestPayload, model });
126872
127180
  }
@@ -126923,8 +127231,15 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
126923
127231
  }
126924
127232
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
126925
127233
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127234
+ const servedModelName = modelManager.resolvedServedModelName;
127235
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
126926
127236
  const serializedBody = isPlainObject$a(body)
126927
- ? JSON.stringify(body)
127237
+ ? JSON.stringify(servedModelName
127238
+ ? {
127239
+ ...body,
127240
+ model: servedModelName
127241
+ }
127242
+ : body)
126928
127243
  : typeof body === "string"
126929
127244
  ? body
126930
127245
  : JSON.stringify(body);
@@ -127042,14 +127357,27 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
127042
127357
  onComplete: onMonitoringComplete,
127043
127358
  requestBodyBytes,
127044
127359
  requestPath: "/v1/embeddings",
127045
- requestStartedAt
127360
+ requestStartedAt,
127361
+ responseModelName: servedModelName ? clientModelName : null
127046
127362
  });
127047
127363
  return {
127048
127364
  body: monitoredResponse.stream,
127049
- headers: Object.fromEntries(response.headers.entries()),
127365
+ headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
127050
127366
  status: response.status
127051
127367
  };
127052
127368
  }
127369
+ /**
127370
+ * Forwards upstream headers, dropping content-length when the response body
127371
+ * is rewritten (model-name substitution changes its length; a stale
127372
+ * content-length desyncs the stream and aborts downstream readers).
127373
+ */
127374
+ function buildProxyResponseHeaders(headers, rewriteActive) {
127375
+ const forwarded = Object.fromEntries(headers.entries());
127376
+ if (rewriteActive) {
127377
+ delete forwarded["content-length"];
127378
+ }
127379
+ return forwarded;
127380
+ }
127053
127381
  async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointId, logger, modelID, modelManager, path, reportMetrics, signal }) {
127054
127382
  function normalizeTokenCount(value) {
127055
127383
  if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
@@ -127067,8 +127395,10 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127067
127395
  }
127068
127396
  const engineType = conduitConfiguration.engineConfig?.type ?? null;
127069
127397
  const engineConfig = conduitConfiguration.engineConfig ?? null;
127398
+ const servedModelName = modelManager.resolvedServedModelName;
127399
+ const clientModelName = isPlainObject$a(body) && typeof body.model === "string" ? body.model : null;
127070
127400
  const effectiveBody = modelManager.model.multimodalEnabled ? body : stripImagesFromBody(body);
127071
- const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path });
127401
+ const { bytes: requestBodyBytes, payload: serializedBody } = serializeRequestBody$1(effectiveBody, { model: modelManager.model, path, servedModelName });
127072
127402
  const requestStartedAt = Date.now();
127073
127403
  const requestBody = JSON.parse(serializedBody);
127074
127404
  const streamRequested = requestBody.stream === true;
@@ -127208,7 +127538,8 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127208
127538
  onComplete: onMonitoringComplete,
127209
127539
  requestBodyBytes,
127210
127540
  requestPath: path,
127211
- requestStartedAt
127541
+ requestStartedAt,
127542
+ responseModelName: servedModelName ? clientModelName : null
127212
127543
  })
127213
127544
  : monitorEngineResponseSingle({
127214
127545
  agentEngineType: engineType ?? "unknown",
@@ -127220,11 +127551,12 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
127220
127551
  onComplete: onMonitoringComplete,
127221
127552
  requestBodyBytes,
127222
127553
  requestPath: path,
127223
- requestStartedAt
127554
+ requestStartedAt,
127555
+ responseModelName: servedModelName ? clientModelName : null
127224
127556
  });
127225
127557
  return {
127226
127558
  body: monitoredResponse.stream,
127227
- headers: Object.fromEntries(response.headers.entries()),
127559
+ headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
127228
127560
  status: response.status
127229
127561
  };
127230
127562
  }
@@ -138514,13 +138846,16 @@ async function detectDockerVersion() {
138514
138846
  * Flattens flat CLI extra-arg tokens into [arg, value] pairs, sorted by ARG NAME (ascending, ties by
138515
138847
  * value). `--flag=value` pairs split on the first `=`; a bare `--flag` consumes the following token
138516
138848
  * as its value when that token does not start with "-" (classic CLI convention); anything else
138517
- * (flags, non-strings) is dropped.
138849
+ * (flags, non-strings) is dropped. Secret-like option values are masked before pairing so they
138850
+ * never reach execution reports.
138518
138851
  */
138519
138852
  function pairExtraArgs(tokens) {
138520
138853
  if (!Array.isArray(tokens)) {
138521
138854
  return [];
138522
138855
  }
138523
- const list = tokens;
138856
+ // Non-string tokens become empty strings: they keep their position as a
138857
+ // non-consumable barrier while allowing secret masking over string tokens.
138858
+ const list = redactSecretArgs(tokens.map(token => (typeof token === "string" ? token : "")));
138524
138859
  const pairs = [];
138525
138860
  let index = 0;
138526
138861
  while (index < list.length) {
@@ -138656,9 +138991,9 @@ async function createApplication({ abortController, apiClient, configuration, lo
138656
138991
  }
138657
138992
  const reporter = new EngineExecutionReporter({
138658
138993
  buildContext: () => {
138659
- const engineType = (conduitConfiguration.engineConfig?.type ??
138660
- "llama.cpp");
138994
+ const engineType = conduitConfiguration.engineConfig?.type ?? "llama.cpp";
138661
138995
  const versions = {
138996
+ custom: null,
138662
138997
  exllamav3: machine?.exllamav3Version ?? null,
138663
138998
  "llama.cpp": machine?.llamaCppVersion ?? null,
138664
138999
  "mlx-lm": machine?.mlxlmVersion ?? null,
@@ -138858,23 +139193,33 @@ async function createApplication({ abortController, apiClient, configuration, lo
138858
139193
  logger.info("Engine start requested");
138859
139194
  reporter.beginStartup(new Date());
138860
139195
  try {
138861
- conduitStateManager.setState({
138862
- modelFileName,
138863
- modelName,
138864
- state: "downloadingModelFiles",
138865
- totalProgress: {
138866
- file: 0,
138867
- total: 0
138868
- }
138869
- });
138870
- await conduitStateReportManager.reportNow();
139196
+ if (modelManager.engine === "custom") {
139197
+ conduitStateManager.setState({
139198
+ state: "bootingEngine"
139199
+ });
139200
+ await conduitStateReportManager.reportNow();
139201
+ }
139202
+ else {
139203
+ conduitStateManager.setState({
139204
+ modelFileName,
139205
+ modelName,
139206
+ state: "downloadingModelFiles",
139207
+ totalProgress: {
139208
+ file: 0,
139209
+ total: 0
139210
+ }
139211
+ });
139212
+ await conduitStateReportManager.reportNow();
139213
+ }
138871
139214
  await modelManager.prepare({
138872
139215
  onDownloadProgress: reportDownloadProgress
138873
139216
  });
138874
- conduitStateManager.setState({
138875
- state: "bootingEngine"
138876
- });
138877
- await conduitStateReportManager.reportNow();
139217
+ if (modelManager.engine !== "custom") {
139218
+ conduitStateManager.setState({
139219
+ state: "bootingEngine"
139220
+ });
139221
+ await conduitStateReportManager.reportNow();
139222
+ }
138878
139223
  await modelManager.start();
138879
139224
  }
138880
139225
  catch (error) {
@@ -139145,7 +139490,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
139145
139490
  return new ModelManager({
139146
139491
  contextLength: conduitConfiguration.contextLength ?? null,
139147
139492
  engineConfig: engineConfig
139148
- ? { extraArgs: engineConfig.extraArgs, type: engineConfig.type }
139493
+ ? {
139494
+ baseUrl: engineConfig.baseUrl ?? null,
139495
+ extraArgs: engineConfig.extraArgs,
139496
+ type: engineConfig.type
139497
+ }
139149
139498
  : null,
139150
139499
  enginePort: configuration.enginePort,
139151
139500
  engineType: engineConfig?.type ?? "llama.cpp",
@@ -139156,7 +139505,11 @@ function createModelManagerFromConfig(conduitConfiguration, configuration, logge
139156
139505
  }
139157
139506
  function getConduitModelFileName(configuration) {
139158
139507
  const { source } = configuration.targetModel;
139159
- return source.type === "huggingface" ? source.slug : source.irid;
139508
+ if (source.type === "huggingface")
139509
+ return source.slug;
139510
+ if (source.type === "storage")
139511
+ return source.irid;
139512
+ return configuration.targetModel.id;
139160
139513
  }
139161
139514
  function getConduitModelName(configuration) {
139162
139515
  return configuration.targetModel.id;
@@ -343824,16 +344177,15 @@ function buildSourceCreateBody(options) {
343824
344177
  if (!options.engine) {
343825
344178
  throw new Error("--engine is required (engine ID from `engine create`)");
343826
344179
  }
343827
- if (!options.model) {
343828
- throw new Error("--model is required (model ID from `models create`)");
343829
- }
343830
344180
  validateULID({ flagName: "--engine", value: options.engine });
343831
- validateULID({ flagName: "--model", value: options.model });
343832
344181
  const body = {
343833
344182
  engineId: options.engine,
343834
- modelID: options.model,
344183
+ modelID: options.model ?? null,
343835
344184
  name: options.name
343836
344185
  };
344186
+ if (options.model !== undefined) {
344187
+ validateULID({ flagName: "--model", value: options.model });
344188
+ }
343837
344189
  if (options.quant !== undefined) {
343838
344190
  validateQuant(options.quant);
343839
344191
  body.quantizationLabel = options.quant;
@@ -343875,7 +344227,7 @@ function registerSourceCommands({ program }) {
343875
344227
  .option("--engine <id>", "Engine ID to run this source with (required)")
343876
344228
  .option("--id <ulid>", "Target an existing source by ID (requires --update)")
343877
344229
  .option("--key <value>", "API key (required, no environment variable fallback)")
343878
- .option("--model <id>", "Model ID to serve (required)")
344230
+ .option("--model <id>", "Model ID to serve (required unless the engine type is custom)")
343879
344231
  .option("--name <name>", "Source name (matched by --update)")
343880
344232
  .option("--quant <label>", "Quantization variant label (eg Q4_K_M)")
343881
344233
  .option("--update", "Update an existing source matched by name (or --id) instead of erroring")