@infersec/conduit 1.25.2 → 1.26.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/README.md CHANGED
@@ -10,14 +10,15 @@ npx @infersec/conduit --engine <type> --key <api-key> --source <source-id>
10
10
 
11
11
  ### Flags
12
12
 
13
- | Flag | Required | Default | Notes |
14
- | ----------- | -------- | ------------------------------ | ------------------------------------------ |
15
- | `--engine` | Yes | - | Engine type (matches `ENGINE`). |
16
- | `--key` | Yes | - | API key (matches `API_KEY`). |
17
- | `--source` | Yes | - | Inference source ID (matches `SOURCE`). |
18
- | `--api-url` | No | `https://api.infersec.ai` | API base URL (matches `API_URL`). |
19
- | `--port` | No | `9505` | Port to listen on (matches `PORT`). |
20
- | `--root` | No | `$HOME/.cache/infersec/iagent` | Root directory (matches `ROOT_DIRECTORY`). |
13
+ | Flag | Required | Default | Notes |
14
+ | -------------- | -------- | ------------------------------ | -------------------------------------------------------------------------------------- |
15
+ | `--engine` | Yes | - | Engine type (matches `ENGINE`). |
16
+ | `--key` | Yes | - | API key (matches `API_KEY`). |
17
+ | `--source` | Yes | - | Inference source ID (matches `SOURCE`). |
18
+ | `--api-url` | No | `https://api.infersec.ai` | API base URL (matches `API_URL`). |
19
+ | `--port` | No | `9505` | Port to listen on (matches `PORT`). |
20
+ | `--root` | No | `$HOME/.cache/infersec/iagent` | Root directory (matches `ROOT_DIRECTORY`). |
21
+ | `--start-mode` | No | `auto` | Startup mode (matches `START_MODE`): `auto` starts engine, `idle` leaves conduit idle. |
21
22
 
22
23
  ### Examples
23
24
 
@@ -36,5 +37,6 @@ npx @infersec/conduit --engine <type> --key <api-key> --source <source-id>
36
37
  | `API_URL` | No | `https://api.infersec.ai` | API base URL (matches `--api-url`). |
37
38
  | `PORT` | No | `9505` | Port to listen on (matches `--port`). |
38
39
  | `ROOT_DIRECTORY` | No | `$HOME/.cache/infersec/iagent` | Root directory (matches `--root`). |
40
+ | `START_MODE` | No | `auto` | Startup mode (matches `--start-mode`). |
39
41
 
40
42
  CLI flags override environment variables when both are provided.
@@ -7,4 +7,7 @@ export declare function createApplication({ abortController, apiClient, configur
7
7
  apiClient: APIClient;
8
8
  configuration: Configuration;
9
9
  logger: Logger;
10
- }): Promise<Application>;
10
+ }): Promise<{
11
+ app: Application;
12
+ shutdown: () => Promise<void>;
13
+ }>;
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ const __dirname = __pathDirname(__filename);
6
6
 
7
7
  import { parseArgs } from 'node:util';
8
8
  import 'node:crypto';
9
- import { a as asError, s as startInferenceAgent } from './start-BJ-o7I20.js';
9
+ import { a as asError, s as startInferenceAgent } from './start-PzV0cQI5.js';
10
10
  import 'argon2';
11
11
  import 'node:child_process';
12
12
  import 'node:stream';
@@ -68,6 +68,7 @@ Options:
68
68
  --key <value> API key (or API_KEY)
69
69
  --port <number> Port to listen on (or PORT)
70
70
  --root <path> Root directory (or ROOT_DIRECTORY)
71
+ --start-mode <mode> Startup mode: auto|idle (or START_MODE)
71
72
  --source <id> Inference source ID (or SOURCE)
72
73
  -h, --help Show this help message
73
74
  `;
@@ -94,6 +95,9 @@ async function run() {
94
95
  root: {
95
96
  type: "string"
96
97
  },
98
+ "start-mode": {
99
+ type: "string"
100
+ },
97
101
  source: {
98
102
  type: "string"
99
103
  }
@@ -119,6 +123,9 @@ async function run() {
119
123
  if (values.root) {
120
124
  configurationOverrides.rootDirectory = values.root;
121
125
  }
126
+ if (values["start-mode"]) {
127
+ configurationOverrides.startMode = values["start-mode"];
128
+ }
122
129
  if (values.port) {
123
130
  const port = Number.parseInt(values.port, 10);
124
131
  if (Number.isNaN(port)) {
@@ -1,4 +1,10 @@
1
1
  import { LLMEngine, ULID } from "@infersec/definitions";
2
+ import { z } from "zod";
3
+ declare const StartModeSchema: z.ZodEnum<{
4
+ idle: "idle";
5
+ auto: "auto";
6
+ }>;
7
+ export type StartMode = z.infer<typeof StartModeSchema>;
2
8
  export interface Configuration {
3
9
  agentEngineType: LLMEngine;
4
10
  apiKey: string;
@@ -6,6 +12,7 @@ export interface Configuration {
6
12
  inferenceSourceID: ULID;
7
13
  port: number;
8
14
  rootDirectory: string;
15
+ startMode: StartMode;
9
16
  }
10
17
  export interface ConfigurationOverrides {
11
18
  agentEngineType?: string;
@@ -14,7 +21,9 @@ export interface ConfigurationOverrides {
14
21
  inferenceSourceID?: ULID;
15
22
  port?: number;
16
23
  rootDirectory?: string;
24
+ startMode?: string;
17
25
  }
18
26
  export declare function getConfiguration({ overrides }?: {
19
27
  overrides?: ConfigurationOverrides;
20
28
  }): Configuration;
29
+ export {};
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ const __filename = __fileURLToPath(import.meta.url);
5
5
  const __dirname = __pathDirname(__filename);
6
6
 
7
7
  import 'node:crypto';
8
- import { s as startInferenceAgent, a as asError } from './start-BJ-o7I20.js';
8
+ import { s as startInferenceAgent, a as asError } from './start-PzV0cQI5.js';
9
9
  import 'argon2';
10
10
  import 'node:child_process';
11
11
  import 'node:stream';
@@ -8,6 +8,7 @@ interface ModelManagerEvents {
8
8
  engineReady: () => void;
9
9
  engineTerminated: () => void;
10
10
  }
11
+ type EngineLifecycleState = "errored" | "running" | "starting" | "stopped" | "stopping";
11
12
  export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
12
13
  readonly engine: LLMEngine;
13
14
  readonly model: LLMModel;
@@ -16,6 +17,8 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
16
17
  readonly contextLength: number | null;
17
18
  protected readonly logger: Logger;
18
19
  private engineProcess;
20
+ private lifecycleState;
21
+ private stopRequested;
19
22
  protected readonly modelsDirectory: string;
20
23
  constructor({ contextLength, engine, logger, model, parallelism, root }: {
21
24
  contextLength?: number | null;
@@ -30,7 +33,13 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
30
33
  onDownloadProgress?: (update: ModelDownloadProgressUpdate) => void;
31
34
  }): Promise<void>;
32
35
  start(): Promise<void>;
36
+ stop(): Promise<void>;
37
+ get canStart(): boolean;
38
+ get canStop(): boolean;
39
+ get state(): EngineLifecycleState;
33
40
  private isEngineReady;
34
41
  private waitForEngineReady;
42
+ private bindEngineProcessEvents;
43
+ private startEngineProcess;
35
44
  }
36
45
  export {};
@@ -14797,6 +14797,9 @@ const ConduitStateSchema = z
14797
14797
  z.object({
14798
14798
  state: z.literal("bootingEngine")
14799
14799
  }),
14800
+ z.object({
14801
+ state: z.literal("stoppingEngine")
14802
+ }),
14800
14803
  z.object({
14801
14804
  state: z.literal("offline")
14802
14805
  }),
@@ -14897,6 +14900,38 @@ const API_SERVICE_CONDUIT_API_REFERENCE = {
14897
14900
  }
14898
14901
  }
14899
14902
  },
14903
+ "/conduit/api/v1/source/:sourceID/engine/start": {
14904
+ POST: {
14905
+ auth: {
14906
+ type: "api-key"
14907
+ },
14908
+ parameters: {
14909
+ sourceID: ULIDSchema
14910
+ },
14911
+ response: {
14912
+ schema: object({
14913
+ acknowledged: literal(true)
14914
+ }),
14915
+ type: "rest"
14916
+ }
14917
+ }
14918
+ },
14919
+ "/conduit/api/v1/source/:sourceID/engine/stop": {
14920
+ POST: {
14921
+ auth: {
14922
+ type: "api-key"
14923
+ },
14924
+ parameters: {
14925
+ sourceID: ULIDSchema
14926
+ },
14927
+ response: {
14928
+ schema: object({
14929
+ acknowledged: literal(true)
14930
+ }),
14931
+ type: "rest"
14932
+ }
14933
+ }
14934
+ },
14900
14935
  "/conduit/api/v1/source/:sourceID/requests/:requestID/chunk": {
14901
14936
  POST: {
14902
14937
  auth: {
@@ -15176,7 +15211,35 @@ const CompletionCreateParamsSchema = object({
15176
15211
  user: string$1().optional()
15177
15212
  });
15178
15213
 
15179
- const API_CLIENT_INFERENCE_AGENT_API_REFERENCE = {
15214
+ const API_CLIENT_CONDUIT_GENERAL_REFERENCE = {
15215
+ "/conduit/engine/start": {
15216
+ POST: {
15217
+ auth: {
15218
+ type: "none"
15219
+ },
15220
+ response: {
15221
+ schema: object({
15222
+ acknowledged: literal(true)
15223
+ }),
15224
+ type: "rest"
15225
+ }
15226
+ }
15227
+ },
15228
+ "/conduit/engine/stop": {
15229
+ POST: {
15230
+ auth: {
15231
+ type: "none"
15232
+ },
15233
+ response: {
15234
+ schema: object({
15235
+ acknowledged: literal(true)
15236
+ }),
15237
+ type: "rest"
15238
+ }
15239
+ }
15240
+ }
15241
+ };
15242
+ const API_CLIENT_CONDUIT_OPENAI_REFERENCE = {
15180
15243
  "/v1/chat/completions": {
15181
15244
  POST: {
15182
15245
  auth: {
@@ -98567,26 +98630,6 @@ async function startVLLM({ targetDirectory }) {
98567
98630
  "1"
98568
98631
  ]
98569
98632
  });
98570
- processManager.on("error", err => {
98571
- this.emit("engineError", new ProcessExecutionError({
98572
- code: null,
98573
- error: err,
98574
- message: `Process error: ${VLLM_EXECUTABLE}: ${processManager.stderr}`,
98575
- signal: null
98576
- }));
98577
- });
98578
- processManager.on("stopped", (code, signal) => {
98579
- if (code === 0) {
98580
- this.emit("engineTerminated");
98581
- }
98582
- else {
98583
- this.emit("engineError", new ProcessExecutionError({
98584
- code,
98585
- message: `Process stopped: ${VLLM_EXECUTABLE}: ${processManager.stderr}`,
98586
- signal
98587
- }));
98588
- }
98589
- });
98590
98633
  await processManager.start();
98591
98634
  return processManager;
98592
98635
  }
@@ -108137,26 +108180,6 @@ async function startLlamacpp({ targetDirectory }) {
108137
108180
  command: LLAMACPP_EXECUTABLE,
108138
108181
  args
108139
108182
  });
108140
- processManager.on("error", err => {
108141
- this.emit("engineError", new ProcessExecutionError({
108142
- code: null,
108143
- error: err,
108144
- message: `Process error: ${LLAMACPP_EXECUTABLE}: ${processManager.stderr}`,
108145
- signal: null
108146
- }));
108147
- });
108148
- processManager.on("stopped", (code, signal) => {
108149
- if (code === 0) {
108150
- this.emit("engineTerminated");
108151
- }
108152
- else {
108153
- this.emit("engineError", new ProcessExecutionError({
108154
- code,
108155
- message: `Process stopped: ${LLAMACPP_EXECUTABLE}: ${processManager.stderr}`,
108156
- signal
108157
- }));
108158
- }
108159
- });
108160
108183
  await processManager.start();
108161
108184
  return processManager;
108162
108185
  }
@@ -108169,6 +108192,8 @@ class ModelManager extends EventEmitter {
108169
108192
  contextLength;
108170
108193
  logger;
108171
108194
  engineProcess = null;
108195
+ lifecycleState = "stopped";
108196
+ stopRequested = false;
108172
108197
  modelsDirectory;
108173
108198
  constructor({ contextLength, engine, logger, model, parallelism, root }) {
108174
108199
  super();
@@ -108257,57 +108282,66 @@ class ModelManager extends EventEmitter {
108257
108282
  }
108258
108283
  }
108259
108284
  async start() {
108260
- if (this.engineProcess) {
108285
+ if (this.lifecycleState !== "stopped") {
108261
108286
  throw new UnknownError({
108262
- message: "Cannot start LLM engine: engine process already running"
108287
+ message: `Cannot start LLM engine: Invalid state: ${this.lifecycleState}`
108263
108288
  });
108264
108289
  }
108290
+ this.lifecycleState = "starting";
108291
+ this.stopRequested = false;
108265
108292
  this.logger.info("Starting LLM engine", {
108266
108293
  agentEngineType: this.engine
108267
108294
  });
108268
- switch (this.engine) {
108269
- case "llama.cpp":
108270
- this.engineProcess = await startLlamacpp.call(this, {
108271
- targetDirectory: join(this.modelsDirectory, this.uniqueName)
108272
- });
108273
- break;
108274
- case "vllm":
108275
- this.engineProcess = await startVLLM.call(this, {
108276
- targetDirectory: join(this.modelsDirectory, this.uniqueName)
108277
- });
108278
- break;
108279
- // case "ollama":
108280
- // this.engineProcess = await startOllama.call(this);
108281
- // this.logger.info("Started LLM engine", {
108282
- // agentEngineType: this.engine
108283
- // });
108284
- // return;
108285
- default: {
108286
- const engineType = this.engine;
108287
- throw new ConfigurationInvalidError({
108288
- message: `Cannot load local model: Invalid or unsupported engine: ${engineType}`
108289
- });
108290
- }
108291
- }
108292
- this.engineProcess.on("stderr", line => {
108293
- console.error(`[${this.engine}]: ${line}`);
108294
- });
108295
- this.engineProcess.on("stdout", line => {
108296
- console.log(`[${this.engine}]: ${line}`);
108297
- });
108298
- this.logger.info("Started LLM engine", {
108299
- agentEngineType: this.engine
108300
- });
108301
108295
  try {
108296
+ this.engineProcess = await this.startEngineProcess();
108297
+ this.bindEngineProcessEvents(this.engineProcess);
108298
+ this.logger.info("Started LLM engine", {
108299
+ agentEngineType: this.engine
108300
+ });
108302
108301
  await this.waitForEngineReady();
108303
108302
  }
108304
108303
  catch (error) {
108305
108304
  const err = error instanceof Error ? error : new Error(String(error));
108305
+ if (this.stopRequested) {
108306
+ throw err;
108307
+ }
108308
+ this.lifecycleState = "errored";
108306
108309
  this.emit("engineError", err);
108307
108310
  throw err;
108308
108311
  }
108312
+ this.lifecycleState = "running";
108309
108313
  this.emit("engineReady");
108310
108314
  }
108315
+ async stop() {
108316
+ if (this.lifecycleState === "stopping") {
108317
+ throw new UnknownError({
108318
+ message: "Cannot stop LLM engine: already stopping"
108319
+ });
108320
+ }
108321
+ if (this.lifecycleState !== "running" && this.lifecycleState !== "starting") {
108322
+ throw new UnknownError({
108323
+ message: `Cannot stop LLM engine: Invalid state: ${this.lifecycleState}`
108324
+ });
108325
+ }
108326
+ const processManager = this.engineProcess;
108327
+ if (!processManager) {
108328
+ throw new UnknownError({
108329
+ message: "Cannot stop LLM engine: engine process not running"
108330
+ });
108331
+ }
108332
+ this.lifecycleState = "stopping";
108333
+ this.stopRequested = true;
108334
+ await processManager.stop();
108335
+ }
108336
+ get canStart() {
108337
+ return this.lifecycleState === "stopped";
108338
+ }
108339
+ get canStop() {
108340
+ return this.lifecycleState === "running" || this.lifecycleState === "starting";
108341
+ }
108342
+ get state() {
108343
+ return this.lifecycleState;
108344
+ }
108311
108345
  async isEngineReady() {
108312
108346
  switch (this.engine) {
108313
108347
  case "llama.cpp":
@@ -108332,6 +108366,12 @@ class ModelManager extends EventEmitter {
108332
108366
  const pollIntervalMs = 2000;
108333
108367
  const start = Date.now();
108334
108368
  while (Date.now() - start < maxWaitMs) {
108369
+ if (this.lifecycleState === "stopping") {
108370
+ throw new Error("LLM engine startup interrupted by stop request");
108371
+ }
108372
+ if (!this.engineProcess) {
108373
+ throw new Error("LLM engine process exited before readiness checks completed");
108374
+ }
108335
108375
  const ready = await this.isEngineReady();
108336
108376
  if (ready) {
108337
108377
  return;
@@ -108340,6 +108380,78 @@ class ModelManager extends EventEmitter {
108340
108380
  }
108341
108381
  throw new Error("LLM engine failed readiness checks within timeout");
108342
108382
  }
108383
+ bindEngineProcessEvents(processManager) {
108384
+ let hasTerminated = false;
108385
+ processManager.on("stderr", line => {
108386
+ console.error(`[${this.engine}]: ${line}`);
108387
+ });
108388
+ processManager.on("stdout", line => {
108389
+ console.log(`[${this.engine}]: ${line}`);
108390
+ });
108391
+ processManager.on("error", err => {
108392
+ if (hasTerminated) {
108393
+ return;
108394
+ }
108395
+ hasTerminated = true;
108396
+ if (this.stopRequested) {
108397
+ this.engineProcess = null;
108398
+ this.lifecycleState = "stopped";
108399
+ this.stopRequested = false;
108400
+ this.emit("engineTerminated");
108401
+ return;
108402
+ }
108403
+ this.engineProcess = null;
108404
+ this.lifecycleState = "errored";
108405
+ this.emit("engineError", new ProcessExecutionError({
108406
+ code: null,
108407
+ error: err,
108408
+ message: `Process error: ${this.engine}: ${processManager.stderr}`,
108409
+ signal: null
108410
+ }));
108411
+ });
108412
+ processManager.on("stopped", (code, signal) => {
108413
+ if (hasTerminated) {
108414
+ return;
108415
+ }
108416
+ hasTerminated = true;
108417
+ this.engineProcess = null;
108418
+ if (this.stopRequested) {
108419
+ this.lifecycleState = "stopped";
108420
+ this.stopRequested = false;
108421
+ this.emit("engineTerminated");
108422
+ return;
108423
+ }
108424
+ if (code === 0) {
108425
+ this.lifecycleState = "stopped";
108426
+ this.emit("engineTerminated");
108427
+ return;
108428
+ }
108429
+ this.lifecycleState = "errored";
108430
+ this.emit("engineError", new ProcessExecutionError({
108431
+ code,
108432
+ message: `Process stopped: ${this.engine}: ${processManager.stderr}`,
108433
+ signal
108434
+ }));
108435
+ });
108436
+ }
108437
+ async startEngineProcess() {
108438
+ switch (this.engine) {
108439
+ case "llama.cpp":
108440
+ return startLlamacpp.call(this, {
108441
+ targetDirectory: join(this.modelsDirectory, this.uniqueName)
108442
+ });
108443
+ case "vllm":
108444
+ return startVLLM.call(this, {
108445
+ targetDirectory: join(this.modelsDirectory, this.uniqueName)
108446
+ });
108447
+ default: {
108448
+ const engineType = this.engine;
108449
+ throw new ConfigurationInvalidError({
108450
+ message: `Cannot load local model: Invalid or unsupported engine: ${engineType}`
108451
+ });
108452
+ }
108453
+ }
108454
+ }
108343
108455
  }
108344
108456
 
108345
108457
  function sleep(ms) {
@@ -108774,6 +108886,13 @@ class ConduitStateReportManager {
108774
108886
  reportDownloadProgress() {
108775
108887
  this.scheduleConduitStateReport();
108776
108888
  }
108889
+ async reportNow() {
108890
+ if (this.pendingConduitStateReport) {
108891
+ clearTimeout(this.pendingConduitStateReport);
108892
+ this.pendingConduitStateReport = null;
108893
+ }
108894
+ await this.triggerConduitStateReport();
108895
+ }
108777
108896
  reportStateChange() {
108778
108897
  if (this.pendingConduitStateReport) {
108779
108898
  clearTimeout(this.pendingConduitStateReport);
@@ -118681,15 +118800,6 @@ async function createApplication({ abortController, apiClient, configuration, lo
118681
118800
  parallelism: conduitConfiguration.parallelism ?? null,
118682
118801
  root: configuration.rootDirectory
118683
118802
  });
118684
- conduitStateManager.setState({
118685
- modelFileName,
118686
- modelName,
118687
- state: "downloadingModelFiles",
118688
- totalProgress: {
118689
- file: 0,
118690
- total: 0
118691
- }
118692
- });
118693
118803
  const conduitStateReportManager = new ConduitStateReportManager({
118694
118804
  apiClient,
118695
118805
  conduitStateManager,
@@ -118698,6 +118808,30 @@ async function createApplication({ abortController, apiClient, configuration, lo
118698
118808
  stateIntervalMs: 30000
118699
118809
  });
118700
118810
  await conduitStateReportManager.start();
118811
+ const setIdleState = ({ reason }) => {
118812
+ conduitStateManager.setState({
118813
+ reason,
118814
+ state: "idle"
118815
+ });
118816
+ conduitStateReportManager.reportStateChange();
118817
+ };
118818
+ const setOnlineState = () => {
118819
+ if (conduitStateManager.getState().state === "online") {
118820
+ return;
118821
+ }
118822
+ conduitStateManager.setState({
118823
+ modelName,
118824
+ state: "online"
118825
+ });
118826
+ conduitStateReportManager.reportStateChange();
118827
+ };
118828
+ const setErrorState = ({ error }) => {
118829
+ conduitStateManager.setState({
118830
+ error,
118831
+ state: "error"
118832
+ });
118833
+ conduitStateReportManager.reportStateChange();
118834
+ };
118701
118835
  let lastDownloadKey = "";
118702
118836
  const reportDownloadProgress = (update) => {
118703
118837
  const filePercent = update.file.total > 0 ? Math.floor((update.file.bytes / update.file.total) * 100) : 0;
@@ -118721,14 +118855,75 @@ async function createApplication({ abortController, apiClient, configuration, lo
118721
118855
  });
118722
118856
  conduitStateReportManager.reportDownloadProgress();
118723
118857
  };
118724
- await modelManager.prepare({
118725
- onDownloadProgress: reportDownloadProgress
118858
+ let stopRequestedByControl = false;
118859
+ async function startEngine() {
118860
+ logger.info("Engine start requested");
118861
+ conduitStateManager.setState({
118862
+ modelFileName,
118863
+ modelName,
118864
+ state: "downloadingModelFiles",
118865
+ totalProgress: {
118866
+ file: 0,
118867
+ total: 0
118868
+ }
118869
+ });
118870
+ await conduitStateReportManager.reportNow();
118871
+ await modelManager.prepare({
118872
+ onDownloadProgress: reportDownloadProgress
118873
+ });
118874
+ conduitStateManager.setState({
118875
+ state: "bootingEngine"
118876
+ });
118877
+ await conduitStateReportManager.reportNow();
118878
+ await modelManager.start();
118879
+ }
118880
+ async function stopEngine({ reason }) {
118881
+ if (!modelManager.canStop) {
118882
+ throw new Error("Engine is not in a stoppable state");
118883
+ }
118884
+ stopRequestedByControl = true;
118885
+ conduitStateManager.setState({
118886
+ state: "stoppingEngine"
118887
+ });
118888
+ await conduitStateReportManager.reportNow();
118889
+ logger.info("Stopping engine process");
118890
+ await modelManager.stop();
118891
+ logger.info("Engine process stopped");
118892
+ setIdleState({ reason });
118893
+ }
118894
+ modelManager.on("engineError", err => {
118895
+ logger.error("LLM engine error", {
118896
+ error: err
118897
+ });
118898
+ stopRequestedByControl = false;
118899
+ setErrorState({ error: err.message });
118726
118900
  });
118727
- conduitStateManager.setState({
118728
- state: "bootingEngine"
118901
+ modelManager.on("engineTerminated", () => {
118902
+ if (stopRequestedByControl) {
118903
+ stopRequestedByControl = false;
118904
+ setIdleState({ reason: "Remote shutdown requested" });
118905
+ return;
118906
+ }
118907
+ conduitStateManager.setState({
118908
+ state: "offline"
118909
+ });
118910
+ conduitStateReportManager.reportStateChange();
118729
118911
  });
118730
- conduitStateReportManager.reportStateChange();
118731
- await modelManager.start();
118912
+ modelManager.on("engineReady", () => {
118913
+ setOnlineState();
118914
+ });
118915
+ if (configuration.startMode === "idle") {
118916
+ setIdleState({ reason: "Startup mode is idle" });
118917
+ }
118918
+ else {
118919
+ await startEngine().catch(error => {
118920
+ const parsedError = asError(error);
118921
+ logger.error("Failed starting LLM engine", {
118922
+ error: parsedError
118923
+ });
118924
+ setErrorState({ error: parsedError.message });
118925
+ });
118926
+ }
118732
118927
  // #region API routes
118733
118928
  const app = express();
118734
118929
  const publicRouter = createRouter();
@@ -118736,6 +118931,96 @@ async function createApplication({ abortController, apiClient, configuration, lo
118736
118931
  publicRouter.get("/health", (_req, res) => {
118737
118932
  res.status(200).send("OK");
118738
118933
  });
118934
+ implementAPIReference({
118935
+ api: {
118936
+ "/conduit/engine/start": {
118937
+ POST: async () => {
118938
+ if (conduitStateManager.getState().state !== "idle") {
118939
+ return {
118940
+ status: 409,
118941
+ statusText: "Engine can only be started from idle state"
118942
+ };
118943
+ }
118944
+ if (!modelManager.canStart) {
118945
+ return {
118946
+ status: 409,
118947
+ statusText: `Engine cannot be started from current state: ${modelManager.state}`
118948
+ };
118949
+ }
118950
+ try {
118951
+ logger.info("Received remote engine start request");
118952
+ await startEngine();
118953
+ return {
118954
+ body: {
118955
+ acknowledged: true
118956
+ },
118957
+ status: 202
118958
+ };
118959
+ }
118960
+ catch (error) {
118961
+ if (stopRequestedByControl || modelManager.state === "stopped") {
118962
+ return {
118963
+ status: 409,
118964
+ statusText: "Engine start was interrupted"
118965
+ };
118966
+ }
118967
+ const parsedError = asError(error);
118968
+ setErrorState({ error: parsedError.message });
118969
+ return {
118970
+ status: 500,
118971
+ statusText: parsedError.message
118972
+ };
118973
+ }
118974
+ }
118975
+ },
118976
+ "/conduit/engine/stop": {
118977
+ POST: async () => {
118978
+ const sourceState = conduitStateManager.getState().state;
118979
+ if (sourceState !== "bootingEngine" && sourceState !== "online") {
118980
+ return {
118981
+ status: 409,
118982
+ statusText: "Engine can only be stopped while booting or online"
118983
+ };
118984
+ }
118985
+ if (!modelManager.canStop) {
118986
+ return {
118987
+ status: 409,
118988
+ statusText: `Engine cannot be stopped from current state: ${modelManager.state}`
118989
+ };
118990
+ }
118991
+ try {
118992
+ logger.info("Received remote engine stop request");
118993
+ stopEngine({
118994
+ reason: "Remote shutdown requested"
118995
+ }).catch(error => {
118996
+ const parsedError = asError(error);
118997
+ logger.error("Remote engine stop request failed", {
118998
+ error: parsedError
118999
+ });
119000
+ setErrorState({ error: parsedError.message });
119001
+ });
119002
+ return {
119003
+ body: {
119004
+ acknowledged: true
119005
+ },
119006
+ status: 202
119007
+ };
119008
+ }
119009
+ catch (error) {
119010
+ const parsedError = asError(error);
119011
+ setErrorState({ error: parsedError.message });
119012
+ return {
119013
+ status: 500,
119014
+ statusText: parsedError.message
119015
+ };
119016
+ }
119017
+ }
119018
+ }
119019
+ },
119020
+ logger,
119021
+ mount: publicRouter,
119022
+ reference: API_CLIENT_CONDUIT_GENERAL_REFERENCE
119023
+ });
118739
119024
  implementAPIReference({
118740
119025
  api: {
118741
119026
  "/v1/chat/completions": {
@@ -118793,39 +119078,7 @@ async function createApplication({ abortController, apiClient, configuration, lo
118793
119078
  },
118794
119079
  logger,
118795
119080
  mount: publicRouter,
118796
- reference: API_CLIENT_INFERENCE_AGENT_API_REFERENCE
118797
- });
118798
- let activeRequests = 0;
118799
- const setOnlineState = () => {
118800
- if (conduitStateManager.getState().state === "online") {
118801
- return;
118802
- }
118803
- conduitStateManager.setState({
118804
- modelName,
118805
- state: "online"
118806
- });
118807
- conduitStateReportManager.reportStateChange();
118808
- };
118809
- modelManager.on("engineError", err => {
118810
- logger.error("LLM engine error", {
118811
- error: err
118812
- });
118813
- conduitStateManager.setState({
118814
- error: err.message,
118815
- state: "error"
118816
- });
118817
- conduitStateReportManager.reportStateChange();
118818
- abortController.abort(err);
118819
- });
118820
- modelManager.on("engineTerminated", () => {
118821
- conduitStateManager.setState({
118822
- state: "offline"
118823
- });
118824
- conduitStateReportManager.reportStateChange();
118825
- abortController.abort();
118826
- });
118827
- modelManager.on("engineReady", () => {
118828
- setOnlineState();
119081
+ reference: API_CLIENT_CONDUIT_OPENAI_REFERENCE
118829
119082
  });
118830
119083
  handleSSERequests({
118831
119084
  apiURL: configuration.apiURL,
@@ -118839,16 +119092,10 @@ async function createApplication({ abortController, apiClient, configuration, lo
118839
119092
  });
118840
119093
  },
118841
119094
  onRequestEnd: () => {
118842
- activeRequests = Math.max(0, activeRequests - 1);
118843
- if (activeRequests === 0) {
118844
- setOnlineState();
118845
- }
119095
+ return;
118846
119096
  },
118847
119097
  onRequestStart: () => {
118848
- activeRequests += 1;
118849
- if (activeRequests === 1) {
118850
- setOnlineState();
118851
- }
119098
+ return;
118852
119099
  },
118853
119100
  reportMetrics: apiClient.reportPromptMetrics,
118854
119101
  signal: abortController.signal
@@ -118857,7 +119104,55 @@ async function createApplication({ abortController, apiClient, configuration, lo
118857
119104
  error: asError(error)
118858
119105
  });
118859
119106
  });
118860
- return app;
119107
+ let shutdownPromise = null;
119108
+ async function shutdown() {
119109
+ if (shutdownPromise) {
119110
+ await shutdownPromise;
119111
+ return;
119112
+ }
119113
+ shutdownPromise = (async () => {
119114
+ logger.info("Conduit shutdown requested");
119115
+ if (modelManager.canStop) {
119116
+ conduitStateManager.setState({
119117
+ state: "stoppingEngine"
119118
+ });
119119
+ try {
119120
+ await conduitStateReportManager.reportNow();
119121
+ }
119122
+ catch (error) {
119123
+ logger.warn("Failed to report stopping conduit state", {
119124
+ error: asError(error)
119125
+ });
119126
+ }
119127
+ try {
119128
+ await modelManager.stop();
119129
+ }
119130
+ catch (error) {
119131
+ logger.warn("Failed to stop model process during shutdown", {
119132
+ error: asError(error)
119133
+ });
119134
+ }
119135
+ }
119136
+ conduitStateManager.setState({
119137
+ state: "offline"
119138
+ });
119139
+ try {
119140
+ await conduitStateReportManager.reportNow();
119141
+ }
119142
+ catch (error) {
119143
+ logger.warn("Failed to report offline conduit state", {
119144
+ error: asError(error)
119145
+ });
119146
+ }
119147
+ conduitStateReportManager.stop();
119148
+ abortController.abort();
119149
+ })();
119150
+ await shutdownPromise;
119151
+ }
119152
+ return {
119153
+ app,
119154
+ shutdown
119155
+ };
118861
119156
  // #endregion
118862
119157
  }
118863
119158
  function getConduitModelFileName(configuration) {
@@ -118868,6 +119163,7 @@ function getConduitModelName(configuration) {
118868
119163
  return configuration.targetModel.id;
118869
119164
  }
118870
119165
 
119166
+ const StartModeSchema = _enum(["auto", "idle"]);
118871
119167
  function getConfiguration({ overrides } = {}) {
118872
119168
  const agentEngineTypeValue = overrides?.agentEngineType ?? readEnvString("ENGINE");
118873
119169
  const agentEngineType = LLMEngineSchema.parse(agentEngineTypeValue);
@@ -118881,13 +119177,16 @@ function getConfiguration({ overrides } = {}) {
118881
119177
  }
118882
119178
  const defaultRootDirectory = join(process.env.HOME ?? "/tmp", ".cache", "infersec", "iagent");
118883
119179
  const rootDirectory = overrides?.rootDirectory ?? readEnvStringOptional("ROOT_DIRECTORY", defaultRootDirectory);
119180
+ const startModeValue = overrides?.startMode ?? readEnvStringOptional("START_MODE", "auto");
119181
+ const startMode = StartModeSchema.parse(startModeValue);
118884
119182
  return {
118885
119183
  agentEngineType,
118886
119184
  apiKey,
118887
119185
  apiURL,
118888
119186
  inferenceSourceID,
118889
119187
  port,
118890
- rootDirectory
119188
+ rootDirectory,
119189
+ startMode
118891
119190
  };
118892
119191
  }
118893
119192
 
@@ -118906,15 +119205,20 @@ async function startInferenceAgent({ configurationOverrides }) {
118906
119205
  inferenceSourceID: configuration.inferenceSourceID
118907
119206
  });
118908
119207
  logger.info("Starting web server");
118909
- const app = await createApplication({ abortController, apiClient, configuration, logger });
119208
+ const { app, shutdown } = await createApplication({
119209
+ abortController,
119210
+ apiClient,
119211
+ configuration,
119212
+ logger
119213
+ });
118910
119214
  await new Promise(resolve => {
118911
119215
  app.listen(configuration.port, () => {
118912
119216
  logger.info("Server listening", { port: configuration.port });
118913
119217
  resolve();
118914
119218
  });
118915
119219
  });
118916
- process.on("SIGINT", createSignalShutdown({ logger }));
118917
- process.on("SIGTERM", createSignalShutdown({ logger }));
119220
+ process.on("SIGINT", createSignalShutdown({ logger, shutdown }));
119221
+ process.on("SIGTERM", createSignalShutdown({ logger, shutdown }));
118918
119222
  abortController.signal.addEventListener("abort", () => {
118919
119223
  if (abortController.signal.reason instanceof Error) {
118920
119224
  process.exit(1);
@@ -118922,13 +119226,18 @@ async function startInferenceAgent({ configurationOverrides }) {
118922
119226
  process.exit(0);
118923
119227
  });
118924
119228
  }
118925
- function createSignalShutdown({ logger }) {
119229
+ function createSignalShutdown({ logger, shutdown }) {
119230
+ let shutdownPromise = null;
118926
119231
  return (signal) => {
118927
119232
  Promise.resolve()
118928
119233
  .then(async () => {
118929
119234
  logger.info("Received shutdown signal", {
118930
119235
  signal
118931
119236
  });
119237
+ if (!shutdownPromise) {
119238
+ shutdownPromise = shutdown();
119239
+ }
119240
+ await shutdownPromise;
118932
119241
  await sleep(500);
118933
119242
  process.exit(0);
118934
119243
  })
@@ -21,6 +21,7 @@ export declare class ConduitStateReportManager {
21
21
  start(): Promise<void>;
22
22
  stop(): void;
23
23
  reportDownloadProgress(): void;
24
+ reportNow(): Promise<void>;
24
25
  reportStateChange(): void;
25
26
  private sendConduitState;
26
27
  private triggerConduitStateReport;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@infersec/conduit",
3
3
  "description": "End user conduit agent for connecting local LLMs to the cloud.",
4
- "version": "1.25.2",
4
+ "version": "1.26.1",
5
5
  "bin": {
6
6
  "infersec-conduit": "./dist/cli.js"
7
7
  },