@infersec/conduit 1.114.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 +113 -29
- package/dist/cli.sea.cjs +113 -29
- package/dist/modelManagement/ModelManager.d.ts +3 -0
- package/dist/utils/openai.d.ts +6 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -125777,8 +125777,10 @@ class ModelManager extends EventEmitter {
|
|
|
125777
125777
|
contextLength;
|
|
125778
125778
|
logger;
|
|
125779
125779
|
discoveredModelNames = [];
|
|
125780
|
+
customProbeCount = 0;
|
|
125780
125781
|
engineProcess = null;
|
|
125781
125782
|
healthPollInterval = null;
|
|
125783
|
+
lastCustomReadinessReport = null;
|
|
125782
125784
|
lastEngineError = null;
|
|
125783
125785
|
lifecycleState = "stopped";
|
|
125784
125786
|
downloadLockHandle = null;
|
|
@@ -126016,7 +126018,9 @@ class ModelManager extends EventEmitter {
|
|
|
126016
126018
|
if (this.engine !== "custom")
|
|
126017
126019
|
return null;
|
|
126018
126020
|
const baseUrl = this.engineConfig?.baseUrl;
|
|
126019
|
-
|
|
126021
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0)
|
|
126022
|
+
return null;
|
|
126023
|
+
return baseUrl.replace(/\/+$/, "");
|
|
126020
126024
|
}
|
|
126021
126025
|
async checkEngineReadiness() {
|
|
126022
126026
|
switch (this.engine) {
|
|
@@ -126042,17 +126046,32 @@ class ModelManager extends EventEmitter {
|
|
|
126042
126046
|
async checkCustomReadiness() {
|
|
126043
126047
|
const baseURL = this.customBaseURL;
|
|
126044
126048
|
if (!baseURL) {
|
|
126049
|
+
this.reportCustomReadinessChange("unreachable", "", "no base URL configured");
|
|
126045
126050
|
return "unreachable";
|
|
126046
126051
|
}
|
|
126052
|
+
const probeURL = joinURL(baseURL, "/v1/models");
|
|
126053
|
+
this.customProbeCount++;
|
|
126047
126054
|
try {
|
|
126048
|
-
const response = await undiciExports.fetch(
|
|
126055
|
+
const response = await undiciExports.fetch(probeURL, {
|
|
126049
126056
|
method: "GET",
|
|
126050
126057
|
signal: AbortSignal.timeout(5000)
|
|
126051
126058
|
});
|
|
126052
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)");
|
|
126053
126065
|
return "loading";
|
|
126054
126066
|
}
|
|
126055
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);
|
|
126056
126075
|
return "unreachable";
|
|
126057
126076
|
}
|
|
126058
126077
|
const payload = (await response.json());
|
|
@@ -126066,24 +126085,56 @@ class ModelManager extends EventEmitter {
|
|
|
126066
126085
|
})
|
|
126067
126086
|
.filter((id) => id !== null);
|
|
126068
126087
|
if (modelIDs.length === 0) {
|
|
126069
|
-
this.logger.
|
|
126070
|
-
|
|
126088
|
+
this.logger.debug("Custom engine probe: no models exposed yet", {
|
|
126089
|
+
attempt: this.customProbeCount,
|
|
126090
|
+
probeURL
|
|
126071
126091
|
});
|
|
126092
|
+
this.reportCustomReadinessChange("loading", baseURL, "/v1/models returned no models");
|
|
126072
126093
|
return "loading";
|
|
126073
126094
|
}
|
|
126074
126095
|
if (modelIDs.join("\n") !== this.discoveredModelNames.join("\n")) {
|
|
126096
|
+
this.discoveredModelNames = modelIDs;
|
|
126075
126097
|
this.logger.info("Discovered models on custom engine endpoint", {
|
|
126076
126098
|
engineBaseURL: baseURL,
|
|
126077
|
-
models: modelIDs
|
|
126099
|
+
models: modelIDs,
|
|
126100
|
+
selectedModel: modelIDs[0]
|
|
126078
126101
|
});
|
|
126079
|
-
this.discoveredModelNames = modelIDs;
|
|
126080
126102
|
}
|
|
126081
126103
|
return "ready";
|
|
126082
126104
|
}
|
|
126083
|
-
catch (
|
|
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);
|
|
126084
126113
|
return "unreachable";
|
|
126085
126114
|
}
|
|
126086
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
|
+
}
|
|
126087
126138
|
async checkGenericHealthReadiness() {
|
|
126088
126139
|
try {
|
|
126089
126140
|
const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, "/health"), {
|
|
@@ -126142,6 +126193,11 @@ class ModelManager extends EventEmitter {
|
|
|
126142
126193
|
const maxWaitMs = Number.parseInt(process.env.ENGINE_STARTUP_TIMEOUT_MS ?? "", 10) || 15 * 60 * 1000;
|
|
126143
126194
|
const pollIntervalMs = 2000;
|
|
126144
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
|
+
}
|
|
126145
126201
|
while (Date.now() - start < maxWaitMs) {
|
|
126146
126202
|
if (this.lifecycleState === "stopping" || this.stopRequested) {
|
|
126147
126203
|
throw new Error("LLM engine startup interrupted by stop request");
|
|
@@ -126162,6 +126218,13 @@ class ModelManager extends EventEmitter {
|
|
|
126162
126218
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
126163
126219
|
}
|
|
126164
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
|
+
}
|
|
126165
126228
|
throw new Error(stderrTail
|
|
126166
126229
|
? `LLM engine failed readiness checks within timeout. Last engine output:\n${stderrTail}`
|
|
126167
126230
|
: "LLM engine failed readiness checks within timeout");
|
|
@@ -127299,10 +127362,22 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
|
|
|
127299
127362
|
});
|
|
127300
127363
|
return {
|
|
127301
127364
|
body: monitoredResponse.stream,
|
|
127302
|
-
headers:
|
|
127365
|
+
headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
|
|
127303
127366
|
status: response.status
|
|
127304
127367
|
};
|
|
127305
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
|
+
}
|
|
127306
127381
|
async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointId, logger, modelID, modelManager, path, reportMetrics, signal }) {
|
|
127307
127382
|
function normalizeTokenCount(value) {
|
|
127308
127383
|
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
@@ -127481,7 +127556,7 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
|
|
|
127481
127556
|
});
|
|
127482
127557
|
return {
|
|
127483
127558
|
body: monitoredResponse.stream,
|
|
127484
|
-
headers:
|
|
127559
|
+
headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
|
|
127485
127560
|
status: response.status
|
|
127486
127561
|
};
|
|
127487
127562
|
}
|
|
@@ -139118,23 +139193,33 @@ async function createApplication({ abortController, apiClient, configuration, lo
|
|
|
139118
139193
|
logger.info("Engine start requested");
|
|
139119
139194
|
reporter.beginStartup(new Date());
|
|
139120
139195
|
try {
|
|
139121
|
-
|
|
139122
|
-
|
|
139123
|
-
|
|
139124
|
-
|
|
139125
|
-
|
|
139126
|
-
|
|
139127
|
-
|
|
139128
|
-
|
|
139129
|
-
|
|
139130
|
-
|
|
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
|
+
}
|
|
139131
139214
|
await modelManager.prepare({
|
|
139132
139215
|
onDownloadProgress: reportDownloadProgress
|
|
139133
139216
|
});
|
|
139134
|
-
|
|
139135
|
-
|
|
139136
|
-
|
|
139137
|
-
|
|
139217
|
+
if (modelManager.engine !== "custom") {
|
|
139218
|
+
conduitStateManager.setState({
|
|
139219
|
+
state: "bootingEngine"
|
|
139220
|
+
});
|
|
139221
|
+
await conduitStateReportManager.reportNow();
|
|
139222
|
+
}
|
|
139138
139223
|
await modelManager.start();
|
|
139139
139224
|
}
|
|
139140
139225
|
catch (error) {
|
|
@@ -344092,16 +344177,15 @@ function buildSourceCreateBody(options) {
|
|
|
344092
344177
|
if (!options.engine) {
|
|
344093
344178
|
throw new Error("--engine is required (engine ID from `engine create`)");
|
|
344094
344179
|
}
|
|
344095
|
-
if (!options.model) {
|
|
344096
|
-
throw new Error("--model is required (model ID from `models create`)");
|
|
344097
|
-
}
|
|
344098
344180
|
validateULID({ flagName: "--engine", value: options.engine });
|
|
344099
|
-
validateULID({ flagName: "--model", value: options.model });
|
|
344100
344181
|
const body = {
|
|
344101
344182
|
engineId: options.engine,
|
|
344102
|
-
modelID: options.model,
|
|
344183
|
+
modelID: options.model ?? null,
|
|
344103
344184
|
name: options.name
|
|
344104
344185
|
};
|
|
344186
|
+
if (options.model !== undefined) {
|
|
344187
|
+
validateULID({ flagName: "--model", value: options.model });
|
|
344188
|
+
}
|
|
344105
344189
|
if (options.quant !== undefined) {
|
|
344106
344190
|
validateQuant(options.quant);
|
|
344107
344191
|
body.quantizationLabel = options.quant;
|
|
@@ -344143,7 +344227,7 @@ function registerSourceCommands({ program }) {
|
|
|
344143
344227
|
.option("--engine <id>", "Engine ID to run this source with (required)")
|
|
344144
344228
|
.option("--id <ulid>", "Target an existing source by ID (requires --update)")
|
|
344145
344229
|
.option("--key <value>", "API key (required, no environment variable fallback)")
|
|
344146
|
-
.option("--model <id>", "Model ID to serve (required)")
|
|
344230
|
+
.option("--model <id>", "Model ID to serve (required unless the engine type is custom)")
|
|
344147
344231
|
.option("--name <name>", "Source name (matched by --update)")
|
|
344148
344232
|
.option("--quant <label>", "Quantization variant label (eg Q4_K_M)")
|
|
344149
344233
|
.option("--update", "Update an existing source matched by name (or --id) instead of erroring")
|
package/dist/cli.sea.cjs
CHANGED
|
@@ -125791,8 +125791,10 @@ class ModelManager extends EventEmitter {
|
|
|
125791
125791
|
contextLength;
|
|
125792
125792
|
logger;
|
|
125793
125793
|
discoveredModelNames = [];
|
|
125794
|
+
customProbeCount = 0;
|
|
125794
125795
|
engineProcess = null;
|
|
125795
125796
|
healthPollInterval = null;
|
|
125797
|
+
lastCustomReadinessReport = null;
|
|
125796
125798
|
lastEngineError = null;
|
|
125797
125799
|
lifecycleState = "stopped";
|
|
125798
125800
|
downloadLockHandle = null;
|
|
@@ -126030,7 +126032,9 @@ class ModelManager extends EventEmitter {
|
|
|
126030
126032
|
if (this.engine !== "custom")
|
|
126031
126033
|
return null;
|
|
126032
126034
|
const baseUrl = this.engineConfig?.baseUrl;
|
|
126033
|
-
|
|
126035
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0)
|
|
126036
|
+
return null;
|
|
126037
|
+
return baseUrl.replace(/\/+$/, "");
|
|
126034
126038
|
}
|
|
126035
126039
|
async checkEngineReadiness() {
|
|
126036
126040
|
switch (this.engine) {
|
|
@@ -126056,17 +126060,32 @@ class ModelManager extends EventEmitter {
|
|
|
126056
126060
|
async checkCustomReadiness() {
|
|
126057
126061
|
const baseURL = this.customBaseURL;
|
|
126058
126062
|
if (!baseURL) {
|
|
126063
|
+
this.reportCustomReadinessChange("unreachable", "", "no base URL configured");
|
|
126059
126064
|
return "unreachable";
|
|
126060
126065
|
}
|
|
126066
|
+
const probeURL = joinURL(baseURL, "/v1/models");
|
|
126067
|
+
this.customProbeCount++;
|
|
126061
126068
|
try {
|
|
126062
|
-
const response = await undiciExports.fetch(
|
|
126069
|
+
const response = await undiciExports.fetch(probeURL, {
|
|
126063
126070
|
method: "GET",
|
|
126064
126071
|
signal: AbortSignal.timeout(5000)
|
|
126065
126072
|
});
|
|
126066
126073
|
if (response.status === 503) {
|
|
126074
|
+
this.logger.debug("Custom engine probe: server loading", {
|
|
126075
|
+
attempt: this.customProbeCount,
|
|
126076
|
+
probeURL
|
|
126077
|
+
});
|
|
126078
|
+
this.reportCustomReadinessChange("loading", baseURL, "server loading (HTTP 503)");
|
|
126067
126079
|
return "loading";
|
|
126068
126080
|
}
|
|
126069
126081
|
if (!response.ok) {
|
|
126082
|
+
const reason = `HTTP ${response.status} from /v1/models`;
|
|
126083
|
+
this.logger.debug("Custom engine probe: not ready", {
|
|
126084
|
+
attempt: this.customProbeCount,
|
|
126085
|
+
probeURL,
|
|
126086
|
+
reason
|
|
126087
|
+
});
|
|
126088
|
+
this.reportCustomReadinessChange("unreachable", baseURL, reason);
|
|
126070
126089
|
return "unreachable";
|
|
126071
126090
|
}
|
|
126072
126091
|
const payload = (await response.json());
|
|
@@ -126080,24 +126099,56 @@ class ModelManager extends EventEmitter {
|
|
|
126080
126099
|
})
|
|
126081
126100
|
.filter((id) => id !== null);
|
|
126082
126101
|
if (modelIDs.length === 0) {
|
|
126083
|
-
this.logger.
|
|
126084
|
-
|
|
126102
|
+
this.logger.debug("Custom engine probe: no models exposed yet", {
|
|
126103
|
+
attempt: this.customProbeCount,
|
|
126104
|
+
probeURL
|
|
126085
126105
|
});
|
|
126106
|
+
this.reportCustomReadinessChange("loading", baseURL, "/v1/models returned no models");
|
|
126086
126107
|
return "loading";
|
|
126087
126108
|
}
|
|
126088
126109
|
if (modelIDs.join("\n") !== this.discoveredModelNames.join("\n")) {
|
|
126110
|
+
this.discoveredModelNames = modelIDs;
|
|
126089
126111
|
this.logger.info("Discovered models on custom engine endpoint", {
|
|
126090
126112
|
engineBaseURL: baseURL,
|
|
126091
|
-
models: modelIDs
|
|
126113
|
+
models: modelIDs,
|
|
126114
|
+
selectedModel: modelIDs[0]
|
|
126092
126115
|
});
|
|
126093
|
-
this.discoveredModelNames = modelIDs;
|
|
126094
126116
|
}
|
|
126095
126117
|
return "ready";
|
|
126096
126118
|
}
|
|
126097
|
-
catch (
|
|
126119
|
+
catch (error) {
|
|
126120
|
+
const reason = asError(error).message;
|
|
126121
|
+
this.logger.debug("Custom engine probe: request failed", {
|
|
126122
|
+
attempt: this.customProbeCount,
|
|
126123
|
+
probeURL,
|
|
126124
|
+
reason
|
|
126125
|
+
});
|
|
126126
|
+
this.reportCustomReadinessChange("unreachable", baseURL, reason);
|
|
126098
126127
|
return "unreachable";
|
|
126099
126128
|
}
|
|
126100
126129
|
}
|
|
126130
|
+
reportCustomReadinessChange(readiness, baseURL, reason) {
|
|
126131
|
+
const key = `${readiness}:${reason}`;
|
|
126132
|
+
if (key === this.lastCustomReadinessReport) {
|
|
126133
|
+
// Same outcome as the last report: surface a heartbeat every 15
|
|
126134
|
+
// attempts (~30s while booting) so progress stays visible.
|
|
126135
|
+
if (this.customProbeCount % 15 === 0) {
|
|
126136
|
+
this.logger.info("Still waiting for custom engine endpoint", {
|
|
126137
|
+
attempt: this.customProbeCount,
|
|
126138
|
+
engineBaseURL: baseURL,
|
|
126139
|
+
reason
|
|
126140
|
+
});
|
|
126141
|
+
}
|
|
126142
|
+
return;
|
|
126143
|
+
}
|
|
126144
|
+
this.lastCustomReadinessReport = key;
|
|
126145
|
+
this.logger.warn(readiness === "loading"
|
|
126146
|
+
? "Custom engine endpoint is loading — waiting for /v1/models to expose a model"
|
|
126147
|
+
: "Custom engine endpoint unreachable — verify the server is running and reachable from conduit", {
|
|
126148
|
+
engineBaseURL: baseURL,
|
|
126149
|
+
reason
|
|
126150
|
+
});
|
|
126151
|
+
}
|
|
126101
126152
|
async checkGenericHealthReadiness() {
|
|
126102
126153
|
try {
|
|
126103
126154
|
const response = await undiciExports.fetch(joinURL(`http://localhost:${this.enginePort}`, "/health"), {
|
|
@@ -126156,6 +126207,11 @@ class ModelManager extends EventEmitter {
|
|
|
126156
126207
|
const maxWaitMs = Number.parseInt(process.env.ENGINE_STARTUP_TIMEOUT_MS ?? "", 10) || 15 * 60 * 1000;
|
|
126157
126208
|
const pollIntervalMs = 2000;
|
|
126158
126209
|
const start = Date.now();
|
|
126210
|
+
if (this.engine === "custom") {
|
|
126211
|
+
this.logger.info(`Connecting to external OpenAI-compatible server at ${this.customBaseURL ?? "(no base URL configured)"} — serving begins once /v1/models exposes a model`, {
|
|
126212
|
+
engineBaseURL: this.customBaseURL ?? ""
|
|
126213
|
+
});
|
|
126214
|
+
}
|
|
126159
126215
|
while (Date.now() - start < maxWaitMs) {
|
|
126160
126216
|
if (this.lifecycleState === "stopping" || this.stopRequested) {
|
|
126161
126217
|
throw new Error("LLM engine startup interrupted by stop request");
|
|
@@ -126176,6 +126232,13 @@ class ModelManager extends EventEmitter {
|
|
|
126176
126232
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
126177
126233
|
}
|
|
126178
126234
|
const stderrTail = this.engineProcess?.stderr?.slice(-1e3);
|
|
126235
|
+
if (this.engine === "custom") {
|
|
126236
|
+
const baseURL = this.customBaseURL ?? "(missing)";
|
|
126237
|
+
const discovered = this.discoveredModelNames;
|
|
126238
|
+
throw new Error(discovered.length > 0
|
|
126239
|
+
? `Custom engine endpoint at ${baseURL} exposed models (${discovered.join(", ")}) but never reported ready within ${Math.round(maxWaitMs / 1000)}s`
|
|
126240
|
+
: `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)`);
|
|
126241
|
+
}
|
|
126179
126242
|
throw new Error(stderrTail
|
|
126180
126243
|
? `LLM engine failed readiness checks within timeout. Last engine output:\n${stderrTail}`
|
|
126181
126244
|
: "LLM engine failed readiness checks within timeout");
|
|
@@ -127313,10 +127376,22 @@ async function proxyEmbeddingsRoute({ body, conduitConfiguration, endpointId, lo
|
|
|
127313
127376
|
});
|
|
127314
127377
|
return {
|
|
127315
127378
|
body: monitoredResponse.stream,
|
|
127316
|
-
headers:
|
|
127379
|
+
headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
|
|
127317
127380
|
status: response.status
|
|
127318
127381
|
};
|
|
127319
127382
|
}
|
|
127383
|
+
/**
|
|
127384
|
+
* Forwards upstream headers, dropping content-length when the response body
|
|
127385
|
+
* is rewritten (model-name substitution changes its length; a stale
|
|
127386
|
+
* content-length desyncs the stream and aborts downstream readers).
|
|
127387
|
+
*/
|
|
127388
|
+
function buildProxyResponseHeaders(headers, rewriteActive) {
|
|
127389
|
+
const forwarded = Object.fromEntries(headers.entries());
|
|
127390
|
+
if (rewriteActive) {
|
|
127391
|
+
delete forwarded["content-length"];
|
|
127392
|
+
}
|
|
127393
|
+
return forwarded;
|
|
127394
|
+
}
|
|
127320
127395
|
async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointId, logger, modelID, modelManager, path, reportMetrics, signal }) {
|
|
127321
127396
|
function normalizeTokenCount(value) {
|
|
127322
127397
|
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
@@ -127495,7 +127570,7 @@ async function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointI
|
|
|
127495
127570
|
});
|
|
127496
127571
|
return {
|
|
127497
127572
|
body: monitoredResponse.stream,
|
|
127498
|
-
headers:
|
|
127573
|
+
headers: buildProxyResponseHeaders(response.headers, Boolean(servedModelName)),
|
|
127499
127574
|
status: response.status
|
|
127500
127575
|
};
|
|
127501
127576
|
}
|
|
@@ -159347,23 +159422,33 @@ async function createApplication({ abortController, apiClient, configuration, lo
|
|
|
159347
159422
|
logger.info("Engine start requested");
|
|
159348
159423
|
reporter.beginStartup(new Date());
|
|
159349
159424
|
try {
|
|
159350
|
-
|
|
159351
|
-
|
|
159352
|
-
|
|
159353
|
-
|
|
159354
|
-
|
|
159355
|
-
|
|
159356
|
-
|
|
159357
|
-
|
|
159358
|
-
|
|
159359
|
-
|
|
159425
|
+
if (modelManager.engine === "custom") {
|
|
159426
|
+
conduitStateManager.setState({
|
|
159427
|
+
state: "bootingEngine"
|
|
159428
|
+
});
|
|
159429
|
+
await conduitStateReportManager.reportNow();
|
|
159430
|
+
}
|
|
159431
|
+
else {
|
|
159432
|
+
conduitStateManager.setState({
|
|
159433
|
+
modelFileName,
|
|
159434
|
+
modelName,
|
|
159435
|
+
state: "downloadingModelFiles",
|
|
159436
|
+
totalProgress: {
|
|
159437
|
+
file: 0,
|
|
159438
|
+
total: 0
|
|
159439
|
+
}
|
|
159440
|
+
});
|
|
159441
|
+
await conduitStateReportManager.reportNow();
|
|
159442
|
+
}
|
|
159360
159443
|
await modelManager.prepare({
|
|
159361
159444
|
onDownloadProgress: reportDownloadProgress
|
|
159362
159445
|
});
|
|
159363
|
-
|
|
159364
|
-
|
|
159365
|
-
|
|
159366
|
-
|
|
159446
|
+
if (modelManager.engine !== "custom") {
|
|
159447
|
+
conduitStateManager.setState({
|
|
159448
|
+
state: "bootingEngine"
|
|
159449
|
+
});
|
|
159450
|
+
await conduitStateReportManager.reportNow();
|
|
159451
|
+
}
|
|
159367
159452
|
await modelManager.start();
|
|
159368
159453
|
}
|
|
159369
159454
|
catch (error) {
|
|
@@ -364321,16 +364406,15 @@ function buildSourceCreateBody(options) {
|
|
|
364321
364406
|
if (!options.engine) {
|
|
364322
364407
|
throw new Error("--engine is required (engine ID from `engine create`)");
|
|
364323
364408
|
}
|
|
364324
|
-
if (!options.model) {
|
|
364325
|
-
throw new Error("--model is required (model ID from `models create`)");
|
|
364326
|
-
}
|
|
364327
364409
|
validateULID({ flagName: "--engine", value: options.engine });
|
|
364328
|
-
validateULID({ flagName: "--model", value: options.model });
|
|
364329
364410
|
const body = {
|
|
364330
364411
|
engineId: options.engine,
|
|
364331
|
-
modelID: options.model,
|
|
364412
|
+
modelID: options.model ?? null,
|
|
364332
364413
|
name: options.name
|
|
364333
364414
|
};
|
|
364415
|
+
if (options.model !== undefined) {
|
|
364416
|
+
validateULID({ flagName: "--model", value: options.model });
|
|
364417
|
+
}
|
|
364334
364418
|
if (options.quant !== undefined) {
|
|
364335
364419
|
validateQuant(options.quant);
|
|
364336
364420
|
body.quantizationLabel = options.quant;
|
|
@@ -364372,7 +364456,7 @@ function registerSourceCommands({ program }) {
|
|
|
364372
364456
|
.option("--engine <id>", "Engine ID to run this source with (required)")
|
|
364373
364457
|
.option("--id <ulid>", "Target an existing source by ID (requires --update)")
|
|
364374
364458
|
.option("--key <value>", "API key (required, no environment variable fallback)")
|
|
364375
|
-
.option("--model <id>", "Model ID to serve (required)")
|
|
364459
|
+
.option("--model <id>", "Model ID to serve (required unless the engine type is custom)")
|
|
364376
364460
|
.option("--name <name>", "Source name (matched by --update)")
|
|
364377
364461
|
.option("--quant <label>", "Quantization variant label (eg Q4_K_M)")
|
|
364378
364462
|
.option("--update", "Update an existing source matched by name (or --id) instead of erroring")
|
|
@@ -19,8 +19,10 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
|
|
|
19
19
|
readonly contextLength: number | null;
|
|
20
20
|
protected readonly logger: Logger;
|
|
21
21
|
private discoveredModelNames;
|
|
22
|
+
private customProbeCount;
|
|
22
23
|
private engineProcess;
|
|
23
24
|
private healthPollInterval;
|
|
25
|
+
private lastCustomReadinessReport;
|
|
24
26
|
private lastEngineError;
|
|
25
27
|
private lifecycleState;
|
|
26
28
|
private downloadLockHandle;
|
|
@@ -55,6 +57,7 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
|
|
|
55
57
|
private get customBaseURL();
|
|
56
58
|
private checkEngineReadiness;
|
|
57
59
|
private checkCustomReadiness;
|
|
60
|
+
private reportCustomReadinessChange;
|
|
58
61
|
private checkGenericHealthReadiness;
|
|
59
62
|
private checkLlamacppReadiness;
|
|
60
63
|
private checkVLLMReadiness;
|
package/dist/utils/openai.d.ts
CHANGED
|
@@ -44,6 +44,12 @@ export declare function proxyEmbeddingsRoute({ body, conduitConfiguration, endpo
|
|
|
44
44
|
status: number;
|
|
45
45
|
statusText: string;
|
|
46
46
|
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Forwards upstream headers, dropping content-length when the response body
|
|
49
|
+
* is rewritten (model-name substitution changes its length; a stale
|
|
50
|
+
* content-length desyncs the stream and aborts downstream readers).
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildProxyResponseHeaders(headers: Headers, rewriteActive: boolean): Record<string, string>;
|
|
47
53
|
export declare function proxyOpenAIStreamingRoute({ body, conduitConfiguration, endpointId, logger, modelID, modelManager, path, reportMetrics, signal }: {
|
|
48
54
|
body: unknown;
|
|
49
55
|
conduitConfiguration: InferenceAgentConfiguration;
|