@infersec/conduit 1.60.1 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import path$1, { join, dirname, win32, posix, resolve } from 'node:path';
10
10
  import * as require$$3$1 from 'node:fs';
11
11
  import require$$3__default, { existsSync, createWriteStream, statSync, readFileSync, appendFileSync, writeFileSync, createReadStream } from 'node:fs';
12
12
  import process$5, { platform, hrtime, execPath, execArgv } from 'node:process';
13
- import crypto, { createHash } from 'node:crypto';
13
+ import crypto from 'node:crypto';
14
14
  import require$$0$8, { Readable, Transform, PassThrough, getDefaultHighWaterMark, Duplex, Writable } from 'node:stream';
15
15
  import 'argon2';
16
16
  import require$$1$7, { setDefaultResultOrder } from 'node:dns';
@@ -120764,8 +120764,36 @@ async function startLlamacpp({ enginePort, targetDirectory }) {
120764
120764
  return processManager;
120765
120765
  }
120766
120766
 
120767
+ const SAFE_CHARS = /[a-zA-Z0-9\-_.]/;
120768
+ const SEPARATOR = "__";
120769
+ function sanitizeSegment(value) {
120770
+ return value
120771
+ .split("")
120772
+ .map(ch => {
120773
+ if (ch === "/")
120774
+ return SEPARATOR;
120775
+ if (ch === "@")
120776
+ return "_rev_";
120777
+ if (ch === ":")
120778
+ return SEPARATOR;
120779
+ if (SAFE_CHARS.test(ch))
120780
+ return ch;
120781
+ return "";
120782
+ })
120783
+ .join("")
120784
+ .replace(new RegExp(`${SEPARATOR}{2,}`, "g"), SEPARATOR);
120785
+ }
120786
+ function createModelStorageKey(model) {
120787
+ const identifier = model.source.type === "huggingface" ? model.source.slug : model.source.irid;
120788
+ return `${model.source.type}${SEPARATOR}${sanitizeSegment(identifier)}`;
120789
+ }
120790
+
120767
120791
  // 2 hours
120768
120792
  const ENGINE_FETCH_TIMEOUT_MS$1 = 7200000;
120793
+ // 20 minutes
120794
+ const DOWNLOAD_LOCK_TIMEOUT_MS = 20 * 60 * 1000;
120795
+ // 5 seconds
120796
+ const DOWNLOAD_LOCK_POLL_INTERVAL_MS = 5000;
120769
120797
  const ENGINE_AGENT = new undiciExports.Agent({
120770
120798
  bodyTimeout: ENGINE_FETCH_TIMEOUT_MS$1,
120771
120799
  headersTimeout: ENGINE_FETCH_TIMEOUT_MS$1
@@ -120805,12 +120833,7 @@ class ModelManager extends EventEmitter {
120805
120833
  this.parallelism = typeof parallelism === "number" ? parallelism : null;
120806
120834
  // this.providerSlugentifier = source.identifier;
120807
120835
  this.logger = logger;
120808
- const uniqueHash = createHash("sha1")
120809
- .update(this.model.source.type === "huggingface"
120810
- ? this.model.source.slug
120811
- : this.model.source.irid)
120812
- .digest("hex");
120813
- this.uniqueName = `${this.model.id}-${uniqueHash}`;
120836
+ this.uniqueName = createModelStorageKey(this.model);
120814
120837
  this.modelsDirectory = join(root, "models");
120815
120838
  }
120816
120839
  async fetchOpenAI(path, opts) {
@@ -120874,14 +120897,20 @@ class ModelManager extends EventEmitter {
120874
120897
  if (this.model.source.type !== "huggingface") {
120875
120898
  throw new Error(`Model source not implemented: ${this.model.source.type}`);
120876
120899
  }
120877
- await downloadModelViaHuggingFace({
120878
- format: this.model.format,
120879
- huggingFaceToken: this.model.source.modelSecret,
120880
- modelSlug: this.model.source.slug,
120881
- onProgress: onDownloadProgress,
120882
- progressFilePath: join(this.modelsDirectory, `${this.uniqueName}.progress.json`),
120883
- targetDirectory: join(this.modelsDirectory, this.uniqueName)
120884
- });
120900
+ await this.acquireDownloadLock();
120901
+ try {
120902
+ await downloadModelViaHuggingFace({
120903
+ format: this.model.format,
120904
+ huggingFaceToken: this.model.source.modelSecret,
120905
+ modelSlug: this.model.source.slug,
120906
+ onProgress: onDownloadProgress,
120907
+ progressFilePath: join(this.modelsDirectory, `${this.uniqueName}.progress.json`),
120908
+ targetDirectory: join(this.modelsDirectory, this.uniqueName)
120909
+ });
120910
+ }
120911
+ finally {
120912
+ await this.releaseDownloadLock();
120913
+ }
120885
120914
  break;
120886
120915
  // case "ollama":
120887
120916
  // this.logger.info("Loading model", {
@@ -121065,6 +121094,63 @@ class ModelManager extends EventEmitter {
121065
121094
  });
121066
121095
  }, 15_000);
121067
121096
  }
121097
+ get lockFilePath() {
121098
+ return join(this.modelsDirectory, `${this.uniqueName}.lock`);
121099
+ }
121100
+ async acquireDownloadLock() {
121101
+ await mkdir(this.modelsDirectory, { recursive: true });
121102
+ if (existsSync(this.lockFilePath)) {
121103
+ const age = await this.getLockAge();
121104
+ if (age !== null && age < DOWNLOAD_LOCK_TIMEOUT_MS) {
121105
+ this.logger.info("Download lock held by another process, waiting", {
121106
+ lockAgeMs: age,
121107
+ lockFilePath: this.lockFilePath
121108
+ });
121109
+ await this.waitForDownloadLock();
121110
+ }
121111
+ else {
121112
+ this.logger.info("Stale download lock found, breaking", {
121113
+ lockFilePath: this.lockFilePath
121114
+ });
121115
+ await this.releaseDownloadLock();
121116
+ }
121117
+ }
121118
+ await writeFile(this.lockFilePath, JSON.stringify({ acquiredAt: Date.now() }));
121119
+ this.logger.info("Acquired download lock", { lockFilePath: this.lockFilePath });
121120
+ }
121121
+ async getLockAge() {
121122
+ try {
121123
+ const raw = await readFile(this.lockFilePath, "utf-8");
121124
+ const data = JSON.parse(raw);
121125
+ return Date.now() - data.acquiredAt;
121126
+ }
121127
+ catch {
121128
+ return null;
121129
+ }
121130
+ }
121131
+ async releaseDownloadLock() {
121132
+ try {
121133
+ await unlink(this.lockFilePath);
121134
+ }
121135
+ catch {
121136
+ // already released
121137
+ }
121138
+ }
121139
+ async waitForDownloadLock() {
121140
+ const start = Date.now();
121141
+ while (Date.now() - start < DOWNLOAD_LOCK_TIMEOUT_MS) {
121142
+ await new Promise(resolve => setTimeout(resolve, DOWNLOAD_LOCK_POLL_INTERVAL_MS));
121143
+ if (!existsSync(this.lockFilePath)) {
121144
+ return;
121145
+ }
121146
+ const age = await this.getLockAge();
121147
+ if (age === null || age >= DOWNLOAD_LOCK_TIMEOUT_MS) {
121148
+ await this.releaseDownloadLock();
121149
+ return;
121150
+ }
121151
+ }
121152
+ await this.releaseDownloadLock();
121153
+ }
121068
121154
  bindEngineProcessEvents(processManager) {
121069
121155
  let hasTerminated = false;
121070
121156
  processManager.on("stderr", line => {
@@ -47,6 +47,11 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
47
47
  private waitForEngineReady;
48
48
  private clearHealthPoll;
49
49
  private startHealthPoll;
50
+ private get lockFilePath();
51
+ private acquireDownloadLock;
52
+ private getLockAge;
53
+ private releaseDownloadLock;
54
+ private waitForDownloadLock;
50
55
  private bindEngineProcessEvents;
51
56
  private startEngineProcess;
52
57
  }
@@ -0,0 +1,2 @@
1
+ import type { LLMModel } from "@infersec/definitions";
2
+ export declare function createModelStorageKey(model: LLMModel): string;
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.60.1",
4
+ "version": "1.61.0",
5
5
  "bin": {
6
6
  "infersec-conduit": "./dist/cli.js"
7
7
  },