@mcoda/mswarm 0.1.101 → 0.1.102
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/codali-executor.d.ts +1 -1
- package/dist/codali-executor.d.ts.map +1 -1
- package/dist/codali-executor.js.map +1 -1
- package/dist/invocation-token.d.ts +2 -0
- package/dist/invocation-token.d.ts.map +1 -1
- package/dist/invocation-token.js +9 -0
- package/dist/invocation-token.js.map +1 -1
- package/dist/runtime.d.ts +49 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +656 -28
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/package.json +4 -3
- package/scripts/stable-audio-3-openai-server.py +738 -0
package/dist/runtime.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
|
6
6
|
import { isIP } from "node:net";
|
|
7
7
|
import { GlobalRepository } from "@mcoda/db";
|
|
8
8
|
import { MswarmCodaliExecutor } from "./codali-executor.js";
|
|
9
|
-
import { MSWARM_CAPABILITY_SCHEMA_VERSION, CryptoHelper, assertMswarmSafeRelativePath, validateMswarmArchiveEntry, buildMswarmCapabilityNames, buildMswarmPrivateCapabilityCatalogEntry, buildMswarmLocalArtifactUri, buildMswarmSandboxProfile, defaultMswarmArtifactAccessPolicy, defaultMswarmArtifactRetentionPolicy, projectMswarmPublicCapabilities, validateMswarmGenericJobRequest } from "@mcoda/shared";
|
|
9
|
+
import { GENERATIVE_OPERATION_DEFAULT_PATHS, GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS, MSWARM_CAPABILITY_SCHEMA_VERSION, CryptoHelper, assertMswarmSafeRelativePath, validateMswarmArchiveEntry, buildMswarmCapabilityNames, buildMswarmPrivateCapabilityCatalogEntry, buildMswarmLocalArtifactUri, buildMswarmSandboxProfile, defaultMswarmArtifactAccessPolicy, defaultMswarmArtifactRetentionPolicy, normalizeGenerativeModalities, normalizeSelfHostedGenerativeOperation, projectMswarmPublicCapabilities, validateMswarmGenericJobRequest } from "@mcoda/shared";
|
|
10
10
|
const REGISTRATION_POLL_INTERVAL_MS = 5000;
|
|
11
11
|
const REGISTRATION_POLL_TIMEOUT_MS = 30 * 60000;
|
|
12
12
|
/**
|
|
@@ -197,6 +197,14 @@ const SECRET_LOCAL_RUNNER_HEADER_KEYS = new Set(["authorization", "proxy-authori
|
|
|
197
197
|
const RESERVED_LOCAL_RUNNER_EXTRA_BODY_KEYS = new Set([
|
|
198
198
|
"model",
|
|
199
199
|
"messages",
|
|
200
|
+
"prompt",
|
|
201
|
+
"n",
|
|
202
|
+
"size",
|
|
203
|
+
"seed",
|
|
204
|
+
"steps",
|
|
205
|
+
"negative_prompt",
|
|
206
|
+
"duration_seconds",
|
|
207
|
+
"sample_rate",
|
|
200
208
|
"stream",
|
|
201
209
|
"tools",
|
|
202
210
|
"tool_choice",
|
|
@@ -204,6 +212,17 @@ const RESERVED_LOCAL_RUNNER_EXTRA_BODY_KEYS = new Set([
|
|
|
204
212
|
"max_tokens",
|
|
205
213
|
"temperature"
|
|
206
214
|
]);
|
|
215
|
+
const REQUIRED_GENERATIVE_OPERATION_REQUEST_PARAMETERS = {
|
|
216
|
+
"chat.completions": ["model", "messages"],
|
|
217
|
+
"images.generations": ["model", "prompt", "response_format"],
|
|
218
|
+
"audio.generations": ["model", "prompt", "response_format"]
|
|
219
|
+
};
|
|
220
|
+
const AUDIO_GENERATION_RESPONSE_FORMATS = new Set(["wav", "flac", "mp3", "ogg"]);
|
|
221
|
+
const STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_PATTERN = /<sd_cpp_extra_args\b[^>]*>[\s\S]*?<\/sd_cpp_extra_args\s*>/gi;
|
|
222
|
+
const STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_FRAGMENT_PATTERN = /<\/?sd_cpp_extra_args\b[^>]*>/gi;
|
|
223
|
+
const DEFAULT_IMAGE_GENERATIVE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
224
|
+
const DEFAULT_AUDIO_GENERATIVE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
225
|
+
const GENERATIVE_RESPONSE_JSON_OVERHEAD_BYTES = 64 * 1024;
|
|
207
226
|
const LOCAL_RUNNER_KIND_ALIASES = {
|
|
208
227
|
vllm: "vllm",
|
|
209
228
|
"llama-cpp": "llama-cpp",
|
|
@@ -226,6 +245,13 @@ const LOCAL_RUNNER_KIND_ALIASES = {
|
|
|
226
245
|
text_generation_inference: "tgi",
|
|
227
246
|
custom: "custom"
|
|
228
247
|
};
|
|
248
|
+
const GENERATIVE_RUNNER_KIND_ALIASES = {
|
|
249
|
+
"stable-diffusion-cpp": "stable-diffusion-cpp",
|
|
250
|
+
"stable-diffusion.cpp": "stable-diffusion-cpp",
|
|
251
|
+
stable_diffusion_cpp: "stable-diffusion-cpp",
|
|
252
|
+
"sd-cpp": "stable-diffusion-cpp",
|
|
253
|
+
"sd.cpp": "stable-diffusion-cpp"
|
|
254
|
+
};
|
|
229
255
|
const LOCAL_RUNNER_AUTH_MODE_ALIASES = {
|
|
230
256
|
none: "none",
|
|
231
257
|
bearer: "bearer",
|
|
@@ -254,6 +280,160 @@ function optionalText(value) {
|
|
|
254
280
|
function isRecord(value) {
|
|
255
281
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
256
282
|
}
|
|
283
|
+
export function normalizeSelfHostedGenerativeOperationName(value) {
|
|
284
|
+
const normalized = optionalText(value);
|
|
285
|
+
return normalized === "chat.completions" ||
|
|
286
|
+
normalized === "images.generations" ||
|
|
287
|
+
normalized === "audio.generations"
|
|
288
|
+
? normalized
|
|
289
|
+
: null;
|
|
290
|
+
}
|
|
291
|
+
export function resolveSelfHostedInvocationOperation(value) {
|
|
292
|
+
const raw = optionalText(value);
|
|
293
|
+
if (!raw) {
|
|
294
|
+
return "chat.completions";
|
|
295
|
+
}
|
|
296
|
+
const operation = normalizeSelfHostedGenerativeOperationName(raw);
|
|
297
|
+
if (!operation) {
|
|
298
|
+
throw new Error(`unsupported self-hosted operation ${raw}`);
|
|
299
|
+
}
|
|
300
|
+
return operation;
|
|
301
|
+
}
|
|
302
|
+
function normalizeGenerativeOperationDescriptor(value) {
|
|
303
|
+
const record = isRecord(value)
|
|
304
|
+
? {
|
|
305
|
+
...value,
|
|
306
|
+
operation: value.operation ?? value.type,
|
|
307
|
+
requestParameterAllowlist: value.requestParameterAllowlist ??
|
|
308
|
+
value.request_parameter_allowlist ??
|
|
309
|
+
value.supported_parameters,
|
|
310
|
+
responseFormats: value.responseFormats ?? value.response_formats,
|
|
311
|
+
outputMimeTypes: value.outputMimeTypes ?? value.output_mime_types
|
|
312
|
+
}
|
|
313
|
+
: value;
|
|
314
|
+
const normalized = normalizeSelfHostedGenerativeOperation(record);
|
|
315
|
+
if (!normalized)
|
|
316
|
+
return null;
|
|
317
|
+
const operation = normalized.operation;
|
|
318
|
+
const requestParameterAllowlist = Array.from(new Set([
|
|
319
|
+
...REQUIRED_GENERATIVE_OPERATION_REQUEST_PARAMETERS[operation],
|
|
320
|
+
...(normalized.requestParameterAllowlist ??
|
|
321
|
+
GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS[operation])
|
|
322
|
+
]));
|
|
323
|
+
const responseFormats = normalized.responseFormats?.length
|
|
324
|
+
? normalized.responseFormats
|
|
325
|
+
: operation === "chat.completions"
|
|
326
|
+
? undefined
|
|
327
|
+
: operation === "images.generations"
|
|
328
|
+
? ["b64_json"]
|
|
329
|
+
: ["wav", "flac", "mp3", "ogg"];
|
|
330
|
+
return {
|
|
331
|
+
operation,
|
|
332
|
+
path: normalized.path,
|
|
333
|
+
method: "POST",
|
|
334
|
+
requestParameterAllowlist,
|
|
335
|
+
...(responseFormats?.length ? { responseFormats } : {}),
|
|
336
|
+
...(normalized.outputMimeTypes?.length ? { outputMimeTypes: normalized.outputMimeTypes } : {}),
|
|
337
|
+
...(normalized.limits ? { limits: normalized.limits } : {})
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function defaultGenerativeOperationDescriptor(operation, path = GENERATIVE_OPERATION_DEFAULT_PATHS[operation]) {
|
|
341
|
+
return {
|
|
342
|
+
operation,
|
|
343
|
+
path,
|
|
344
|
+
method: "POST",
|
|
345
|
+
requestParameterAllowlist: [...GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS[operation]],
|
|
346
|
+
...(operation === "images.generations"
|
|
347
|
+
? { responseFormats: ["b64_json"] }
|
|
348
|
+
: operation === "audio.generations"
|
|
349
|
+
? { responseFormats: ["wav", "flac", "mp3", "ogg"] }
|
|
350
|
+
: {})
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function normalizeGenerativeOperations(value) {
|
|
354
|
+
if (value === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
if (!Array.isArray(value))
|
|
357
|
+
return [];
|
|
358
|
+
const operations = value
|
|
359
|
+
.map((entry) => normalizeGenerativeOperationDescriptor(entry))
|
|
360
|
+
.filter((entry) => Boolean(entry));
|
|
361
|
+
const deduplicated = new Map();
|
|
362
|
+
for (const operation of operations) {
|
|
363
|
+
if (!deduplicated.has(operation.operation)) {
|
|
364
|
+
deduplicated.set(operation.operation, operation);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return Array.from(deduplicated.values());
|
|
368
|
+
}
|
|
369
|
+
function inferGenerativeModalities(operations, direction) {
|
|
370
|
+
const modalities = new Set();
|
|
371
|
+
for (const descriptor of operations) {
|
|
372
|
+
if (direction === "input" || descriptor.operation === "chat.completions") {
|
|
373
|
+
modalities.add("text");
|
|
374
|
+
}
|
|
375
|
+
if (direction === "output" && descriptor.operation === "images.generations") {
|
|
376
|
+
modalities.add("image");
|
|
377
|
+
}
|
|
378
|
+
if (direction === "output" && descriptor.operation === "audio.generations") {
|
|
379
|
+
modalities.add("audio");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return Array.from(modalities);
|
|
383
|
+
}
|
|
384
|
+
function generativeOperationCatalogInput(descriptor) {
|
|
385
|
+
const limits = descriptor.limits
|
|
386
|
+
? {
|
|
387
|
+
...(descriptor.limits.maxRequestBytes !== undefined
|
|
388
|
+
? { max_request_bytes: descriptor.limits.maxRequestBytes }
|
|
389
|
+
: {}),
|
|
390
|
+
...(descriptor.limits.maxOutputBytes !== undefined
|
|
391
|
+
? { max_output_bytes: descriptor.limits.maxOutputBytes }
|
|
392
|
+
: {}),
|
|
393
|
+
...(descriptor.limits.maxPromptChars !== undefined
|
|
394
|
+
? { max_prompt_chars: descriptor.limits.maxPromptChars }
|
|
395
|
+
: {}),
|
|
396
|
+
...(descriptor.limits.maxNegativePromptChars !== undefined
|
|
397
|
+
? { max_negative_prompt_chars: descriptor.limits.maxNegativePromptChars }
|
|
398
|
+
: {}),
|
|
399
|
+
...(descriptor.limits.maxN !== undefined ? { max_n: descriptor.limits.maxN } : {}),
|
|
400
|
+
...(descriptor.limits.maxWidth !== undefined
|
|
401
|
+
? { max_width: descriptor.limits.maxWidth }
|
|
402
|
+
: {}),
|
|
403
|
+
...(descriptor.limits.maxHeight !== undefined
|
|
404
|
+
? { max_height: descriptor.limits.maxHeight }
|
|
405
|
+
: {}),
|
|
406
|
+
...(descriptor.limits.maxPixels !== undefined
|
|
407
|
+
? { max_pixels: descriptor.limits.maxPixels }
|
|
408
|
+
: {}),
|
|
409
|
+
...(descriptor.limits.minDurationSeconds !== undefined
|
|
410
|
+
? { min_duration_seconds: descriptor.limits.minDurationSeconds }
|
|
411
|
+
: {}),
|
|
412
|
+
...(descriptor.limits.maxDurationSeconds !== undefined
|
|
413
|
+
? { max_duration_seconds: descriptor.limits.maxDurationSeconds }
|
|
414
|
+
: {}),
|
|
415
|
+
...(descriptor.limits.maxSampleRate !== undefined
|
|
416
|
+
? { max_sample_rate: descriptor.limits.maxSampleRate }
|
|
417
|
+
: {}),
|
|
418
|
+
...(descriptor.limits.maxSteps !== undefined
|
|
419
|
+
? { max_steps: descriptor.limits.maxSteps }
|
|
420
|
+
: {})
|
|
421
|
+
}
|
|
422
|
+
: undefined;
|
|
423
|
+
return {
|
|
424
|
+
type: descriptor.operation,
|
|
425
|
+
path: descriptor.path,
|
|
426
|
+
method: descriptor.method,
|
|
427
|
+
supported_parameters: [...descriptor.requestParameterAllowlist],
|
|
428
|
+
...(descriptor.responseFormats?.length
|
|
429
|
+
? { response_formats: [...descriptor.responseFormats] }
|
|
430
|
+
: {}),
|
|
431
|
+
...(descriptor.outputMimeTypes?.length
|
|
432
|
+
? { output_mime_types: [...descriptor.outputMimeTypes] }
|
|
433
|
+
: {}),
|
|
434
|
+
...(limits ? { limits } : {})
|
|
435
|
+
};
|
|
436
|
+
}
|
|
257
437
|
function normalizeLookupKey(value) {
|
|
258
438
|
return value.trim().toLowerCase();
|
|
259
439
|
}
|
|
@@ -316,6 +496,10 @@ function normalizeLocalRunnerKind(value) {
|
|
|
316
496
|
const normalized = optionalText(value);
|
|
317
497
|
return normalized ? LOCAL_RUNNER_KIND_ALIASES[normalizeLookupKey(normalized)] : undefined;
|
|
318
498
|
}
|
|
499
|
+
function normalizeGenerativeRunnerKind(value) {
|
|
500
|
+
const normalized = optionalText(value);
|
|
501
|
+
return normalized ? GENERATIVE_RUNNER_KIND_ALIASES[normalizeLookupKey(normalized)] : undefined;
|
|
502
|
+
}
|
|
319
503
|
function normalizeLocalRunnerAuthMode(value) {
|
|
320
504
|
const normalized = optionalText(value);
|
|
321
505
|
return normalized ? LOCAL_RUNNER_AUTH_MODE_ALIASES[normalizeLookupKey(normalized)] : undefined;
|
|
@@ -332,11 +516,14 @@ function normalizeMcodaLocalRunnerConfig(adapter, config) {
|
|
|
332
516
|
const merged = { ...localRunnerRecord, ...configRecord };
|
|
333
517
|
const authMode = normalizeLocalRunnerAuthMode(merged.authMode) ?? "none";
|
|
334
518
|
const dummyBearerToken = readConfigString(merged, ["dummyBearerToken", "dummyApiKey"]);
|
|
519
|
+
const generativeRunnerKind = normalizeGenerativeRunnerKind(merged.runnerKind ?? merged.runner_kind);
|
|
335
520
|
return {
|
|
336
521
|
baseUrl: readConfigString(merged, ["baseUrl", "base_url", "endpoint", "apiBaseUrl", "api_base_url"]),
|
|
337
522
|
endpoint: readConfigString(merged, ["endpoint"]),
|
|
338
523
|
apiBaseUrl: readConfigString(merged, ["apiBaseUrl", "api_base_url"]),
|
|
339
|
-
runnerKind: normalizeLocalRunnerKind(merged.runnerKind
|
|
524
|
+
runnerKind: normalizeLocalRunnerKind(merged.runnerKind ?? merged.runner_kind) ??
|
|
525
|
+
(generativeRunnerKind ? "custom" : defaultLocalRunnerKindForAdapter(adapter)),
|
|
526
|
+
generativeRunnerKind,
|
|
340
527
|
authMode,
|
|
341
528
|
dummyBearerToken: authMode === "dummy-bearer" ? dummyBearerToken ?? "local" : dummyBearerToken,
|
|
342
529
|
headers: normalizeStringMap(merged.headers),
|
|
@@ -348,7 +535,11 @@ function normalizeMcodaLocalRunnerConfig(adapter, config) {
|
|
|
348
535
|
supportsStreaming: readConfigBoolean(merged, ["supportsStreaming", "supports_streaming"]),
|
|
349
536
|
supportsTools: readConfigBoolean(merged, ["supportsTools", "supports_tools"]),
|
|
350
537
|
supportsJsonSchema: readConfigBoolean(merged, ["supportsJsonSchema", "supports_json_schema"]),
|
|
351
|
-
supportsGbnf: readConfigBoolean(merged, ["supportsGbnf", "supports_gbnf"])
|
|
538
|
+
supportsGbnf: readConfigBoolean(merged, ["supportsGbnf", "supports_gbnf"]),
|
|
539
|
+
inputModalities: normalizeGenerativeModalities(merged.inputModalities ?? merged.input_modalities),
|
|
540
|
+
outputModalities: normalizeGenerativeModalities(merged.outputModalities ?? merged.output_modalities),
|
|
541
|
+
operations: normalizeGenerativeOperations(merged.operations),
|
|
542
|
+
publicModelId: readConfigString(merged, ["publicModelId", "public_model_id"])
|
|
352
543
|
};
|
|
353
544
|
}
|
|
354
545
|
function parsePositiveInteger(value, fallback) {
|
|
@@ -681,8 +872,23 @@ function buildCatalogFingerprint(models) {
|
|
|
681
872
|
adapter: optionalText(model.adapter) || null,
|
|
682
873
|
source_agent_slug: optionalText(model.source_agent_slug) || null,
|
|
683
874
|
model_id: optionalText(model.model_id) || optionalText(model.model) || null,
|
|
875
|
+
public_model_id: optionalText(model.public_model_id) || null,
|
|
876
|
+
upstream_model: optionalText(model.upstream_model) || optionalText(model.model) || null,
|
|
684
877
|
exposed: model.exposed !== false,
|
|
685
878
|
capabilities: normalizeCapabilities(model.capabilities).sort(),
|
|
879
|
+
input_modalities: [...(model.input_modalities ?? [])].sort(),
|
|
880
|
+
output_modalities: [...(model.output_modalities ?? [])].sort(),
|
|
881
|
+
operations: (model.operations ?? [])
|
|
882
|
+
.map((operation) => ({
|
|
883
|
+
type: operation.type,
|
|
884
|
+
path: operation.path,
|
|
885
|
+
method: operation.method,
|
|
886
|
+
supported_parameters: [...operation.supported_parameters].sort(),
|
|
887
|
+
response_formats: [...(operation.response_formats ?? [])].sort(),
|
|
888
|
+
output_mime_types: [...(operation.output_mime_types ?? [])].sort(),
|
|
889
|
+
limits: operation.limits ?? null
|
|
890
|
+
}))
|
|
891
|
+
.sort((left, right) => left.type.localeCompare(right.type)),
|
|
686
892
|
health_status: normalizeHealthStatus(model.health_status)
|
|
687
893
|
}))
|
|
688
894
|
.sort((left, right) => `${left.provider || ""}:${left.name}`.localeCompare(`${right.provider || ""}:${right.name}`));
|
|
@@ -860,6 +1066,67 @@ async function fetchJson(fetchImpl, url, init, timeoutMs) {
|
|
|
860
1066
|
clearTimeout(timeout);
|
|
861
1067
|
}
|
|
862
1068
|
}
|
|
1069
|
+
function generativeOutputByteLimit(operation, configuredLimit) {
|
|
1070
|
+
if (typeof configuredLimit === "number" &&
|
|
1071
|
+
Number.isSafeInteger(configuredLimit) &&
|
|
1072
|
+
configuredLimit > 0) {
|
|
1073
|
+
return configuredLimit;
|
|
1074
|
+
}
|
|
1075
|
+
return operation === "images.generations"
|
|
1076
|
+
? DEFAULT_IMAGE_GENERATIVE_OUTPUT_BYTES
|
|
1077
|
+
: DEFAULT_AUDIO_GENERATIVE_OUTPUT_BYTES;
|
|
1078
|
+
}
|
|
1079
|
+
function generativeJsonResponseByteLimit(outputByteLimit) {
|
|
1080
|
+
const encodedLimit = Math.ceil(outputByteLimit / 3) * 4;
|
|
1081
|
+
return Math.min(Number.MAX_SAFE_INTEGER, encodedLimit + GENERATIVE_RESPONSE_JSON_OVERHEAD_BYTES);
|
|
1082
|
+
}
|
|
1083
|
+
async function readBoundedResponseText(response, maxBytes, overflowMessage) {
|
|
1084
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
1085
|
+
if (Number.isFinite(declaredLength) &&
|
|
1086
|
+
declaredLength >= 0 &&
|
|
1087
|
+
declaredLength > maxBytes) {
|
|
1088
|
+
await response.body?.cancel().catch(() => undefined);
|
|
1089
|
+
throw new Error(overflowMessage);
|
|
1090
|
+
}
|
|
1091
|
+
if (!response.body) {
|
|
1092
|
+
return "";
|
|
1093
|
+
}
|
|
1094
|
+
const reader = response.body.getReader();
|
|
1095
|
+
const chunks = [];
|
|
1096
|
+
let totalBytes = 0;
|
|
1097
|
+
try {
|
|
1098
|
+
while (true) {
|
|
1099
|
+
const { done, value } = await reader.read();
|
|
1100
|
+
if (done)
|
|
1101
|
+
break;
|
|
1102
|
+
totalBytes += value.byteLength;
|
|
1103
|
+
if (totalBytes > maxBytes) {
|
|
1104
|
+
await reader.cancel().catch(() => undefined);
|
|
1105
|
+
throw new Error(overflowMessage);
|
|
1106
|
+
}
|
|
1107
|
+
chunks.push(Buffer.from(value));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
finally {
|
|
1111
|
+
reader.releaseLock();
|
|
1112
|
+
}
|
|
1113
|
+
return Buffer.concat(chunks, totalBytes).toString("utf8");
|
|
1114
|
+
}
|
|
1115
|
+
function base64DecodedByteLength(value) {
|
|
1116
|
+
if (value.length === 0 ||
|
|
1117
|
+
value.length % 4 !== 0 ||
|
|
1118
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
1122
|
+
return (value.length / 4) * 3 - padding;
|
|
1123
|
+
}
|
|
1124
|
+
function sanitizeStableDiffusionCppExtraArgsTags(value) {
|
|
1125
|
+
return value
|
|
1126
|
+
.replace(STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_PATTERN, "")
|
|
1127
|
+
.replace(STABLE_DIFFUSION_CPP_EXTRA_ARGS_TAG_FRAGMENT_PATTERN, "")
|
|
1128
|
+
.trim();
|
|
1129
|
+
}
|
|
863
1130
|
export async function readSelfHostedNodeState(statePath) {
|
|
864
1131
|
try {
|
|
865
1132
|
const content = await readFile(statePath, "utf8");
|
|
@@ -1522,17 +1789,20 @@ export function mapOllamaModelToSelfHostedModel(model, config) {
|
|
|
1522
1789
|
const parameterSize = optionalText(model.details?.parameter_size);
|
|
1523
1790
|
const quantizationLevel = optionalText(model.details?.quantization_level);
|
|
1524
1791
|
const embeddingOnly = isEmbeddingModel(name, family);
|
|
1792
|
+
const operations = [defaultGenerativeOperationDescriptor("chat.completions", "/api/chat")];
|
|
1793
|
+
const supportsVision = isVisionModel(name, family);
|
|
1525
1794
|
return {
|
|
1526
1795
|
name,
|
|
1527
1796
|
provider: "ollama",
|
|
1528
1797
|
adapter: "ollama",
|
|
1529
1798
|
model_id: name,
|
|
1799
|
+
upstream_model: name,
|
|
1530
1800
|
digest: optionalText(model.digest),
|
|
1531
1801
|
family,
|
|
1532
1802
|
parameter_size: parameterSize,
|
|
1533
1803
|
quantization_level: quantizationLevel,
|
|
1534
1804
|
supports_tools: false,
|
|
1535
|
-
supports_vision:
|
|
1805
|
+
supports_vision: supportsVision,
|
|
1536
1806
|
openai_compatible: false,
|
|
1537
1807
|
exposed: isModelExposed(name, family, config),
|
|
1538
1808
|
best_usage: embeddingOnly
|
|
@@ -1544,7 +1814,10 @@ export function mapOllamaModelToSelfHostedModel(model, config) {
|
|
|
1544
1814
|
reasoning_rating: 4,
|
|
1545
1815
|
max_complexity: 3,
|
|
1546
1816
|
health_status: "healthy",
|
|
1547
|
-
metadata_quality: model.details ? "discovered" : "unknown"
|
|
1817
|
+
metadata_quality: model.details ? "discovered" : "unknown",
|
|
1818
|
+
input_modalities: supportsVision ? ["text", "image"] : ["text"],
|
|
1819
|
+
output_modalities: ["text"],
|
|
1820
|
+
operations: operations.map(generativeOperationCatalogInput)
|
|
1548
1821
|
};
|
|
1549
1822
|
}
|
|
1550
1823
|
export function mapMcodaAgentToSelfHostedModel(agent, config) {
|
|
@@ -1568,6 +1841,17 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
|
|
|
1568
1841
|
const supportsTools = optionalBoolean(agent.supportsTools, agent.supports_tools, localRunner?.supportsTools) === true;
|
|
1569
1842
|
const openaiCompatible = localRunner !== undefined || optionalBoolean(agent.openaiCompatible, agent.openai_compatible) === true;
|
|
1570
1843
|
const model = defaultModel || slug;
|
|
1844
|
+
const operations = localRunner?.operations ??
|
|
1845
|
+
[defaultGenerativeOperationDescriptor("chat.completions", localRunner ? "/chat/completions" : undefined)];
|
|
1846
|
+
const inferredInputModalities = inferGenerativeModalities(operations, "input");
|
|
1847
|
+
const supportsVision = capabilities.some((capability) => capability.toLowerCase().includes("vision")) ||
|
|
1848
|
+
capabilities.some((capability) => capability.toLowerCase().includes("visual"));
|
|
1849
|
+
const inputModalities = localRunner?.inputModalities ??
|
|
1850
|
+
(supportsVision && operations.some((entry) => entry.operation === "chat.completions")
|
|
1851
|
+
? Array.from(new Set([...inferredInputModalities, "image"]))
|
|
1852
|
+
: inferredInputModalities);
|
|
1853
|
+
const outputModalities = localRunner?.outputModalities ?? inferGenerativeModalities(operations, "output");
|
|
1854
|
+
const publicModelId = localRunner?.publicModelId;
|
|
1571
1855
|
return {
|
|
1572
1856
|
name: slug,
|
|
1573
1857
|
provider: "mcoda",
|
|
@@ -1575,9 +1859,11 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
|
|
|
1575
1859
|
source_agent_id: optionalText(agent.id),
|
|
1576
1860
|
source_agent_slug: slug,
|
|
1577
1861
|
model,
|
|
1578
|
-
model_id: model,
|
|
1862
|
+
model_id: publicModelId || model,
|
|
1863
|
+
public_model_id: publicModelId,
|
|
1864
|
+
upstream_model: model,
|
|
1579
1865
|
base_url: localRunner?.baseUrl,
|
|
1580
|
-
runner_kind: localRunner?.runnerKind,
|
|
1866
|
+
runner_kind: localRunner?.generativeRunnerKind ?? localRunner?.runnerKind,
|
|
1581
1867
|
auth_mode: localRunner?.authMode,
|
|
1582
1868
|
response_format_strategy: localRunner?.responseFormatStrategy,
|
|
1583
1869
|
health_path: localRunner?.healthPath,
|
|
@@ -1587,8 +1873,7 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
|
|
|
1587
1873
|
max_output_tokens: optionalNumber(agent.maxOutputTokens, agent.max_output_tokens),
|
|
1588
1874
|
supports_tools: supportsTools,
|
|
1589
1875
|
supports_streaming: localRunner?.supportsStreaming,
|
|
1590
|
-
supports_vision:
|
|
1591
|
-
capabilities.some((capability) => capability.toLowerCase().includes("visual")),
|
|
1876
|
+
supports_vision: supportsVision,
|
|
1592
1877
|
supports_json_schema: localRunner?.supportsJsonSchema,
|
|
1593
1878
|
supports_gbnf: localRunner?.supportsGbnf,
|
|
1594
1879
|
openai_compatible: openaiCompatible,
|
|
@@ -1601,7 +1886,10 @@ export function mapMcodaAgentToSelfHostedModel(agent, config) {
|
|
|
1601
1886
|
reasoning_rating: optionalNumber(agent.reasoningRating, agent.reasoning_rating),
|
|
1602
1887
|
max_complexity: optionalNumber(agent.maxComplexity, agent.max_complexity),
|
|
1603
1888
|
health_status: healthStatus,
|
|
1604
|
-
metadata_quality: "discovered"
|
|
1889
|
+
metadata_quality: "discovered",
|
|
1890
|
+
input_modalities: inputModalities,
|
|
1891
|
+
output_modalities: outputModalities,
|
|
1892
|
+
operations: operations.map(generativeOperationCatalogInput)
|
|
1605
1893
|
};
|
|
1606
1894
|
}
|
|
1607
1895
|
function parseMcodaAgentListOutput(stdout) {
|
|
@@ -1905,6 +2193,8 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
|
|
|
1905
2193
|
const localRunner = normalizeMcodaLocalRunnerConfig(adapter, config);
|
|
1906
2194
|
const configAuthMode = normalizeLocalRunnerAuthMode(config?.authMode);
|
|
1907
2195
|
const supportsTools = optionalBoolean(agent.supportsTools, agent.supports_tools, localRunner?.supportsTools) === true;
|
|
2196
|
+
const generativeOperations = localRunner?.operations ??
|
|
2197
|
+
[defaultGenerativeOperationDescriptor("chat.completions", localRunner ? "/chat/completions" : undefined)];
|
|
1908
2198
|
return {
|
|
1909
2199
|
slug: optionalText(agent.slug) || fallbackSlug,
|
|
1910
2200
|
adapter,
|
|
@@ -1918,6 +2208,7 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
|
|
|
1918
2208
|
: configText(config, "apiKey", "api_key"),
|
|
1919
2209
|
localRunner,
|
|
1920
2210
|
runnerKind: localRunner?.runnerKind,
|
|
2211
|
+
generativeRunnerKind: localRunner?.generativeRunnerKind,
|
|
1921
2212
|
authMode: localRunner?.authMode ?? configAuthMode,
|
|
1922
2213
|
dummyBearerToken: localRunner?.dummyBearerToken,
|
|
1923
2214
|
headers: localRunner?.headers,
|
|
@@ -1933,6 +2224,10 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
|
|
|
1933
2224
|
capabilities: normalizeCapabilities(agent.capabilities),
|
|
1934
2225
|
contextWindow: optionalNumber(agent.contextWindow, agent.context_window) ?? undefined,
|
|
1935
2226
|
maxOutputTokens: optionalNumber(agent.maxOutputTokens, agent.max_output_tokens) ?? undefined,
|
|
2227
|
+
generativeOperations,
|
|
2228
|
+
inputModalities: localRunner?.inputModalities ?? inferGenerativeModalities(generativeOperations, "input"),
|
|
2229
|
+
outputModalities: localRunner?.outputModalities ?? inferGenerativeModalities(generativeOperations, "output"),
|
|
2230
|
+
publicModelId: localRunner?.publicModelId
|
|
1936
2231
|
};
|
|
1937
2232
|
}
|
|
1938
2233
|
function buildCodaliWorkspace(job) {
|
|
@@ -1958,6 +2253,7 @@ const PRE_START_JOB_ERROR_CODES = new Set([
|
|
|
1958
2253
|
"selected_agent_unavailable",
|
|
1959
2254
|
"selected_agent_auth_unavailable",
|
|
1960
2255
|
"selected_agent_unhealthy",
|
|
2256
|
+
"unsupported_operation",
|
|
1961
2257
|
"validation_failed",
|
|
1962
2258
|
"docdex_context_missing",
|
|
1963
2259
|
"docdex_api_key_missing",
|
|
@@ -2148,7 +2444,7 @@ function buildCodaliJob(job) {
|
|
|
2148
2444
|
return {
|
|
2149
2445
|
id: optionalText(payload.id) || job.job_id,
|
|
2150
2446
|
jobType,
|
|
2151
|
-
input: payload.input ?? messagesToPrompt(job.openai_request.messages),
|
|
2447
|
+
input: payload.input ?? messagesToPrompt(job.openai_request.messages ?? []),
|
|
2152
2448
|
context: objectRecord(payload.context) || undefined,
|
|
2153
2449
|
tenant: objectRecord(payload.tenant) || undefined,
|
|
2154
2450
|
requester: objectRecord(payload.requester) || undefined,
|
|
@@ -2182,7 +2478,7 @@ function buildCodaliPolicy(job) {
|
|
|
2182
2478
|
}
|
|
2183
2479
|
function normalizeGatewayConversation(job, value) {
|
|
2184
2480
|
const record = objectRecord(value);
|
|
2185
|
-
const rawMessages = Array.isArray(record?.messages) ? record.messages : job.openai_request.messages;
|
|
2481
|
+
const rawMessages = Array.isArray(record?.messages) ? record.messages : (job.openai_request.messages ?? []);
|
|
2186
2482
|
const messages = rawMessages
|
|
2187
2483
|
.map((entry) => {
|
|
2188
2484
|
const message = objectRecord(entry);
|
|
@@ -2279,7 +2575,7 @@ function buildCodaliGateway(job) {
|
|
|
2279
2575
|
const payload = job.codali_gateway;
|
|
2280
2576
|
if (!payload)
|
|
2281
2577
|
return undefined;
|
|
2282
|
-
const query = optionalText(payload.query) || messagesToPrompt(job.openai_request.messages);
|
|
2578
|
+
const query = optionalText(payload.query) || messagesToPrompt(job.openai_request.messages ?? []);
|
|
2283
2579
|
return {
|
|
2284
2580
|
id: optionalText(payload.id) || job.job_id,
|
|
2285
2581
|
query,
|
|
@@ -3903,6 +4199,7 @@ export class SelfHostedNodeRuntime {
|
|
|
3903
4199
|
this.recentFailures = [];
|
|
3904
4200
|
this.lifecyclePollingDisabled = false;
|
|
3905
4201
|
this.config = config;
|
|
4202
|
+
this.fetchImpl = deps?.fetchImpl || fetch;
|
|
3906
4203
|
this.gateway =
|
|
3907
4204
|
deps?.gateway ||
|
|
3908
4205
|
new MswarmSelfHostedNodeClient({
|
|
@@ -3911,7 +4208,7 @@ export class SelfHostedNodeRuntime {
|
|
|
3911
4208
|
jobsStartPathTemplate: config.jobsStartPathTemplate,
|
|
3912
4209
|
jobsEventsPathTemplate: config.jobsEventsPathTemplate,
|
|
3913
4210
|
jobsResultPathTemplate: config.jobsResultPathTemplate,
|
|
3914
|
-
fetchImpl:
|
|
4211
|
+
fetchImpl: this.fetchImpl,
|
|
3915
4212
|
timeoutMs: config.requestTimeoutMs
|
|
3916
4213
|
});
|
|
3917
4214
|
this.mcoda =
|
|
@@ -3933,14 +4230,14 @@ export class SelfHostedNodeRuntime {
|
|
|
3933
4230
|
deps?.ollama ||
|
|
3934
4231
|
new OllamaClient({
|
|
3935
4232
|
baseUrl: config.ollamaBaseUrl,
|
|
3936
|
-
fetchImpl:
|
|
4233
|
+
fetchImpl: this.fetchImpl,
|
|
3937
4234
|
timeoutMs: config.requestTimeoutMs
|
|
3938
4235
|
});
|
|
3939
4236
|
this.jobOllama =
|
|
3940
4237
|
deps?.ollama ||
|
|
3941
4238
|
new OllamaClient({
|
|
3942
4239
|
baseUrl: config.ollamaBaseUrl,
|
|
3943
|
-
fetchImpl:
|
|
4240
|
+
fetchImpl: this.fetchImpl,
|
|
3944
4241
|
timeoutMs: config.jobTimeoutMs
|
|
3945
4242
|
});
|
|
3946
4243
|
this.capabilityRunner = deps?.capabilityRunner || defaultCommandRunner;
|
|
@@ -4025,11 +4322,16 @@ export class SelfHostedNodeRuntime {
|
|
|
4025
4322
|
});
|
|
4026
4323
|
}
|
|
4027
4324
|
jobResultPayload(result) {
|
|
4325
|
+
const operation = normalizeSelfHostedGenerativeOperationName(result.operation) ?? "chat.completions";
|
|
4028
4326
|
return {
|
|
4029
4327
|
...result,
|
|
4030
4328
|
node_id: this.config.nodeId,
|
|
4031
4329
|
...(result.status === "success"
|
|
4032
|
-
?
|
|
4330
|
+
? result.usage
|
|
4331
|
+
? { usage: result.usage }
|
|
4332
|
+
: operation === "chat.completions"
|
|
4333
|
+
? { usage: openAiUsage(result.openai_response) }
|
|
4334
|
+
: {}
|
|
4033
4335
|
: {})
|
|
4034
4336
|
};
|
|
4035
4337
|
}
|
|
@@ -4397,7 +4699,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4397
4699
|
}
|
|
4398
4700
|
const body = {
|
|
4399
4701
|
model: optionalText(agent.model) || job.openai_request.model,
|
|
4400
|
-
messages: job.openai_request.messages,
|
|
4702
|
+
messages: job.openai_request.messages ?? [],
|
|
4401
4703
|
tools: job.openai_request.tools,
|
|
4402
4704
|
stream: false
|
|
4403
4705
|
};
|
|
@@ -4412,7 +4714,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4412
4714
|
if (job.openai_request.stop !== undefined)
|
|
4413
4715
|
body.stop = job.openai_request.stop;
|
|
4414
4716
|
const url = `${baseUrl.replace(/\/+$/, "")}/chat/completions`;
|
|
4415
|
-
const response = await
|
|
4717
|
+
const response = await this.fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
|
|
4416
4718
|
const text = await response.text();
|
|
4417
4719
|
let payload = null;
|
|
4418
4720
|
try {
|
|
@@ -4426,6 +4728,276 @@ export class SelfHostedNodeRuntime {
|
|
|
4426
4728
|
}
|
|
4427
4729
|
return payload;
|
|
4428
4730
|
}
|
|
4731
|
+
generativeOperationForAgent(agent, operation) {
|
|
4732
|
+
const descriptor = agent.generativeOperations.find((entry) => entry.operation === operation);
|
|
4733
|
+
if (!descriptor) {
|
|
4734
|
+
throw new SelfHostedPreStartJobError("unsupported_operation", `selected local mcoda agent ${agent.slug} does not expose ${operation}`);
|
|
4735
|
+
}
|
|
4736
|
+
return descriptor;
|
|
4737
|
+
}
|
|
4738
|
+
prepareOpenAiGenerativePassthrough(job, agent, operation) {
|
|
4739
|
+
const descriptor = this.generativeOperationForAgent(agent, operation);
|
|
4740
|
+
if (job.openai_request.stream === true) {
|
|
4741
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} does not support streaming through the self-hosted relay`);
|
|
4742
|
+
}
|
|
4743
|
+
const baseUrl = optionalText(agent.baseUrl);
|
|
4744
|
+
if (!baseUrl) {
|
|
4745
|
+
throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent has no local runner base URL for ${operation}`);
|
|
4746
|
+
}
|
|
4747
|
+
let parsedBaseUrl;
|
|
4748
|
+
try {
|
|
4749
|
+
parsedBaseUrl = new URL(baseUrl);
|
|
4750
|
+
}
|
|
4751
|
+
catch {
|
|
4752
|
+
throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent has an invalid local runner base URL for ${operation}`);
|
|
4753
|
+
}
|
|
4754
|
+
if ((parsedBaseUrl.protocol !== "http:" && parsedBaseUrl.protocol !== "https:") ||
|
|
4755
|
+
parsedBaseUrl.username ||
|
|
4756
|
+
parsedBaseUrl.password ||
|
|
4757
|
+
parsedBaseUrl.search ||
|
|
4758
|
+
parsedBaseUrl.hash) {
|
|
4759
|
+
throw new SelfHostedPreStartJobError("selected_agent_unavailable", `agent local runner base URL is not valid for ${operation}`);
|
|
4760
|
+
}
|
|
4761
|
+
const prompt = optionalText(job.openai_request.prompt);
|
|
4762
|
+
if (!prompt) {
|
|
4763
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} requires a non-empty prompt`);
|
|
4764
|
+
}
|
|
4765
|
+
const rawResponseFormat = job.openai_request.response_format;
|
|
4766
|
+
if (rawResponseFormat !== undefined && rawResponseFormat !== null && typeof rawResponseFormat !== "string") {
|
|
4767
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} response_format must be a string`);
|
|
4768
|
+
}
|
|
4769
|
+
const responseFormats = descriptor.responseFormats?.map((entry) => entry.toLowerCase()) ??
|
|
4770
|
+
(operation === "images.generations" ? ["b64_json"] : ["wav", "flac", "mp3", "ogg"]);
|
|
4771
|
+
const requestedResponseFormat = (optionalText(rawResponseFormat) ||
|
|
4772
|
+
(operation === "images.generations" ? "b64_json" : responseFormats[0] || "wav")).toLowerCase();
|
|
4773
|
+
if (!responseFormats.includes(requestedResponseFormat) ||
|
|
4774
|
+
(operation === "images.generations"
|
|
4775
|
+
? requestedResponseFormat !== "b64_json"
|
|
4776
|
+
: !AUDIO_GENERATION_RESPONSE_FORMATS.has(requestedResponseFormat))) {
|
|
4777
|
+
throw new SelfHostedPreStartJobError("validation_failed", operation === "images.generations"
|
|
4778
|
+
? `${operation} requires response_format b64_json through the self-hosted relay`
|
|
4779
|
+
: `${operation} response_format must be one of wav, flac, mp3, or ogg`);
|
|
4780
|
+
}
|
|
4781
|
+
const body = {};
|
|
4782
|
+
for (const parameter of descriptor.requestParameterAllowlist) {
|
|
4783
|
+
if (parameter === "model") {
|
|
4784
|
+
body.model = optionalText(agent.model) || job.openai_request.model;
|
|
4785
|
+
}
|
|
4786
|
+
else if (parameter === "response_format") {
|
|
4787
|
+
body.response_format = requestedResponseFormat;
|
|
4788
|
+
}
|
|
4789
|
+
else if (job.openai_request[parameter] !== undefined) {
|
|
4790
|
+
body[parameter] = job.openai_request[parameter];
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
body.model = optionalText(agent.model) || job.openai_request.model;
|
|
4794
|
+
body.prompt = prompt;
|
|
4795
|
+
body.response_format = requestedResponseFormat;
|
|
4796
|
+
const n = body.n;
|
|
4797
|
+
if (n !== undefined && (!Number.isSafeInteger(n) || n <= 0)) {
|
|
4798
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} n must be a positive integer`);
|
|
4799
|
+
}
|
|
4800
|
+
const durationSeconds = body.duration_seconds;
|
|
4801
|
+
if (durationSeconds !== undefined &&
|
|
4802
|
+
(typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || durationSeconds <= 0)) {
|
|
4803
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds must be a positive number`);
|
|
4804
|
+
}
|
|
4805
|
+
const sampleRate = body.sample_rate;
|
|
4806
|
+
if (sampleRate !== undefined &&
|
|
4807
|
+
(!Number.isSafeInteger(sampleRate) || sampleRate <= 0)) {
|
|
4808
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} sample_rate must be a positive integer`);
|
|
4809
|
+
}
|
|
4810
|
+
const steps = body.steps;
|
|
4811
|
+
if (steps !== undefined && (!Number.isSafeInteger(steps) || steps <= 0)) {
|
|
4812
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} steps must be a positive integer`);
|
|
4813
|
+
}
|
|
4814
|
+
const seed = body.seed;
|
|
4815
|
+
if (seed !== undefined && !Number.isSafeInteger(seed)) {
|
|
4816
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} seed must be an integer`);
|
|
4817
|
+
}
|
|
4818
|
+
if (body.size !== undefined && typeof body.size !== "string") {
|
|
4819
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} size must be a string`);
|
|
4820
|
+
}
|
|
4821
|
+
if (body.negative_prompt !== undefined && typeof body.negative_prompt !== "string") {
|
|
4822
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} negative_prompt must be a string`);
|
|
4823
|
+
}
|
|
4824
|
+
const limits = descriptor.limits;
|
|
4825
|
+
if (limits?.maxPromptChars && prompt.length > limits.maxPromptChars) {
|
|
4826
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} prompt exceeds the configured limit`);
|
|
4827
|
+
}
|
|
4828
|
+
if (limits?.maxNegativePromptChars &&
|
|
4829
|
+
typeof body.negative_prompt === "string" &&
|
|
4830
|
+
body.negative_prompt.length > limits.maxNegativePromptChars) {
|
|
4831
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} negative_prompt exceeds the configured limit`);
|
|
4832
|
+
}
|
|
4833
|
+
if (limits?.maxN && typeof n === "number" && n > limits.maxN) {
|
|
4834
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} n exceeds the configured limit`);
|
|
4835
|
+
}
|
|
4836
|
+
if (limits?.minDurationSeconds &&
|
|
4837
|
+
typeof durationSeconds === "number" &&
|
|
4838
|
+
durationSeconds < limits.minDurationSeconds) {
|
|
4839
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds is below the configured minimum`);
|
|
4840
|
+
}
|
|
4841
|
+
if (limits?.maxDurationSeconds &&
|
|
4842
|
+
typeof durationSeconds === "number" &&
|
|
4843
|
+
durationSeconds > limits.maxDurationSeconds) {
|
|
4844
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} duration_seconds exceeds the configured limit`);
|
|
4845
|
+
}
|
|
4846
|
+
if (limits?.maxSampleRate &&
|
|
4847
|
+
typeof sampleRate === "number" &&
|
|
4848
|
+
sampleRate > limits.maxSampleRate) {
|
|
4849
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} sample_rate exceeds the configured limit`);
|
|
4850
|
+
}
|
|
4851
|
+
if (limits?.maxSteps && typeof steps === "number" && steps > limits.maxSteps) {
|
|
4852
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} steps exceeds the configured limit`);
|
|
4853
|
+
}
|
|
4854
|
+
if (typeof body.size === "string") {
|
|
4855
|
+
const sizeMatch = /^([1-9][0-9]*)x([1-9][0-9]*)$/.exec(body.size);
|
|
4856
|
+
if (sizeMatch) {
|
|
4857
|
+
const width = Number(sizeMatch[1]);
|
|
4858
|
+
const height = Number(sizeMatch[2]);
|
|
4859
|
+
if ((limits?.maxWidth && width > limits.maxWidth) ||
|
|
4860
|
+
(limits?.maxHeight && height > limits.maxHeight) ||
|
|
4861
|
+
(limits?.maxPixels && width * height > limits.maxPixels)) {
|
|
4862
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} size exceeds the configured limit`);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
if (operation === "images.generations" &&
|
|
4867
|
+
agent.generativeRunnerKind === "stable-diffusion-cpp") {
|
|
4868
|
+
const sanitizedPrompt = sanitizeStableDiffusionCppExtraArgsTags(prompt);
|
|
4869
|
+
if (!sanitizedPrompt) {
|
|
4870
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} prompt is empty after removing reserved stable-diffusion.cpp tags`);
|
|
4871
|
+
}
|
|
4872
|
+
const controlledArgs = {};
|
|
4873
|
+
if (typeof body.negative_prompt === "string") {
|
|
4874
|
+
controlledArgs.negative_prompt =
|
|
4875
|
+
sanitizeStableDiffusionCppExtraArgsTags(body.negative_prompt);
|
|
4876
|
+
}
|
|
4877
|
+
if (typeof seed === "number") {
|
|
4878
|
+
controlledArgs.seed = seed;
|
|
4879
|
+
}
|
|
4880
|
+
if (typeof steps === "number") {
|
|
4881
|
+
controlledArgs.sample_params = { sample_steps: steps };
|
|
4882
|
+
}
|
|
4883
|
+
body.prompt =
|
|
4884
|
+
Object.keys(controlledArgs).length > 0
|
|
4885
|
+
? `${sanitizedPrompt}\n<sd_cpp_extra_args>${JSON.stringify(controlledArgs)}</sd_cpp_extra_args>`
|
|
4886
|
+
: sanitizedPrompt;
|
|
4887
|
+
delete body.negative_prompt;
|
|
4888
|
+
delete body.seed;
|
|
4889
|
+
delete body.steps;
|
|
4890
|
+
}
|
|
4891
|
+
const serializedBody = JSON.stringify(body);
|
|
4892
|
+
if (limits?.maxRequestBytes && Buffer.byteLength(serializedBody, "utf8") > limits.maxRequestBytes) {
|
|
4893
|
+
throw new SelfHostedPreStartJobError("validation_failed", `${operation} request exceeds the configured byte limit`);
|
|
4894
|
+
}
|
|
4895
|
+
const headers = {};
|
|
4896
|
+
const bearer = agent.authMode === "bearer"
|
|
4897
|
+
? optionalText(agent.apiKey)
|
|
4898
|
+
: agent.authMode === "dummy-bearer"
|
|
4899
|
+
? optionalText(agent.dummyBearerToken) || "local"
|
|
4900
|
+
: undefined;
|
|
4901
|
+
if (bearer) {
|
|
4902
|
+
headers.authorization = `Bearer ${bearer}`;
|
|
4903
|
+
}
|
|
4904
|
+
for (const [key, value] of Object.entries(agent.headers ?? {})) {
|
|
4905
|
+
if (typeof value === "string") {
|
|
4906
|
+
headers[key.toLowerCase()] = value;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
headers["content-type"] = "application/json";
|
|
4910
|
+
return {
|
|
4911
|
+
descriptor,
|
|
4912
|
+
url: `${baseUrl.replace(/\/+$/, "")}${descriptor.path}`,
|
|
4913
|
+
headers,
|
|
4914
|
+
body
|
|
4915
|
+
};
|
|
4916
|
+
}
|
|
4917
|
+
async executeOpenAiGenerativePassthrough(prepared, operation) {
|
|
4918
|
+
const outputByteLimit = generativeOutputByteLimit(operation, prepared.descriptor.limits?.maxOutputBytes);
|
|
4919
|
+
const jsonResponseByteLimit = generativeJsonResponseByteLimit(outputByteLimit);
|
|
4920
|
+
const controller = new AbortController();
|
|
4921
|
+
const timeout = setTimeout(() => controller.abort(), this.config.jobTimeoutMs);
|
|
4922
|
+
let payload;
|
|
4923
|
+
try {
|
|
4924
|
+
const response = await this.fetchImpl(prepared.url, {
|
|
4925
|
+
method: "POST",
|
|
4926
|
+
headers: prepared.headers,
|
|
4927
|
+
body: JSON.stringify(prepared.body),
|
|
4928
|
+
signal: controller.signal
|
|
4929
|
+
});
|
|
4930
|
+
if (!response.ok) {
|
|
4931
|
+
await response.body?.cancel().catch(() => undefined);
|
|
4932
|
+
throw new Error(`local ${operation} runner request failed with status ${response.status}`);
|
|
4933
|
+
}
|
|
4934
|
+
const responseText = await readBoundedResponseText(response, jsonResponseByteLimit, `local ${operation} runner response exceeds the configured byte limit`);
|
|
4935
|
+
try {
|
|
4936
|
+
payload = JSON.parse(responseText);
|
|
4937
|
+
}
|
|
4938
|
+
catch {
|
|
4939
|
+
throw new Error(`local ${operation} runner returned invalid JSON`);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
catch (error) {
|
|
4943
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4944
|
+
throw new Error(`local ${operation} runner timeout`);
|
|
4945
|
+
}
|
|
4946
|
+
throw error;
|
|
4947
|
+
}
|
|
4948
|
+
finally {
|
|
4949
|
+
clearTimeout(timeout);
|
|
4950
|
+
}
|
|
4951
|
+
if (!isRecord(payload) || !Array.isArray(payload.data) || payload.data.length === 0) {
|
|
4952
|
+
throw new Error(`local ${operation} runner returned an invalid base64 response`);
|
|
4953
|
+
}
|
|
4954
|
+
const limits = prepared.descriptor.limits;
|
|
4955
|
+
if (limits?.maxN && payload.data.length > limits.maxN) {
|
|
4956
|
+
throw new Error(`local ${operation} runner returned too many outputs`);
|
|
4957
|
+
}
|
|
4958
|
+
let decodedOutputBytes = 0;
|
|
4959
|
+
const normalizedData = [];
|
|
4960
|
+
for (const item of payload.data) {
|
|
4961
|
+
const encodedPayload = isRecord(item) && operation === "audio.generations"
|
|
4962
|
+
? optionalText(item.b64_audio) || optionalText(item.b64_json)
|
|
4963
|
+
: isRecord(item)
|
|
4964
|
+
? optionalText(item.b64_json)
|
|
4965
|
+
: null;
|
|
4966
|
+
if (!isRecord(item) || !encodedPayload) {
|
|
4967
|
+
throw new Error(`local ${operation} runner returned an invalid base64 response`);
|
|
4968
|
+
}
|
|
4969
|
+
const decodedBytes = base64DecodedByteLength(encodedPayload);
|
|
4970
|
+
if (decodedBytes === null) {
|
|
4971
|
+
throw new Error(`local ${operation} runner returned an invalid base64 response`);
|
|
4972
|
+
}
|
|
4973
|
+
decodedOutputBytes += decodedBytes;
|
|
4974
|
+
if (decodedOutputBytes > outputByteLimit) {
|
|
4975
|
+
throw new Error(`local ${operation} runner response exceeds the configured byte limit`);
|
|
4976
|
+
}
|
|
4977
|
+
const mimeType = optionalText(item.mime_type);
|
|
4978
|
+
if (mimeType &&
|
|
4979
|
+
prepared.descriptor.outputMimeTypes?.length &&
|
|
4980
|
+
!prepared.descriptor.outputMimeTypes.includes(mimeType.toLowerCase())) {
|
|
4981
|
+
throw new Error(`local ${operation} runner returned unsupported mime_type ${mimeType}`);
|
|
4982
|
+
}
|
|
4983
|
+
if (operation === "audio.generations" &&
|
|
4984
|
+
!optionalText(item.b64_audio) &&
|
|
4985
|
+
optionalText(item.b64_json)) {
|
|
4986
|
+
const normalizedItem = {
|
|
4987
|
+
...item,
|
|
4988
|
+
b64_audio: encodedPayload
|
|
4989
|
+
};
|
|
4990
|
+
delete normalizedItem.b64_json;
|
|
4991
|
+
normalizedData.push(normalizedItem);
|
|
4992
|
+
}
|
|
4993
|
+
else {
|
|
4994
|
+
normalizedData.push(item);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
return operation === "audio.generations"
|
|
4998
|
+
? { ...payload, data: normalizedData }
|
|
4999
|
+
: payload;
|
|
5000
|
+
}
|
|
4429
5001
|
async resolveMcodaAgentForJob(job) {
|
|
4430
5002
|
const selectedSourceAgentSlug = optionalText(job.source_agent_slug);
|
|
4431
5003
|
const selectedAgentSlug = optionalText(job.agent_slug);
|
|
@@ -4647,6 +5219,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4647
5219
|
const startedAt = Date.now();
|
|
4648
5220
|
this.beginExecutionTelemetry("llm");
|
|
4649
5221
|
let selectedAgent;
|
|
5222
|
+
let operation = "chat.completions";
|
|
4650
5223
|
let jobStarted = false;
|
|
4651
5224
|
const progressEvents = [];
|
|
4652
5225
|
const streamEvents = [];
|
|
@@ -4676,6 +5249,9 @@ export class SelfHostedNodeRuntime {
|
|
|
4676
5249
|
const result = {
|
|
4677
5250
|
job_id: job.job_id,
|
|
4678
5251
|
request_id: job.request_id,
|
|
5252
|
+
...(normalizeSelfHostedGenerativeOperationName(job.operation)
|
|
5253
|
+
? { operation: normalizeSelfHostedGenerativeOperationName(job.operation) }
|
|
5254
|
+
: {}),
|
|
4679
5255
|
status: "failed",
|
|
4680
5256
|
pre_start_failure: true,
|
|
4681
5257
|
error: { code: "validation_failed", message: "job node_id does not match this node" }
|
|
@@ -4689,7 +5265,20 @@ export class SelfHostedNodeRuntime {
|
|
|
4689
5265
|
return result;
|
|
4690
5266
|
}
|
|
4691
5267
|
try {
|
|
5268
|
+
try {
|
|
5269
|
+
operation = resolveSelfHostedInvocationOperation(job.operation);
|
|
5270
|
+
}
|
|
5271
|
+
catch {
|
|
5272
|
+
throw new SelfHostedPreStartJobError("unsupported_operation", `unsupported self-hosted operation ${optionalText(job.operation) || String(job.operation)}`);
|
|
5273
|
+
}
|
|
4692
5274
|
if (job.provider === "ollama") {
|
|
5275
|
+
if (operation !== "chat.completions") {
|
|
5276
|
+
throw new SelfHostedPreStartJobError("unsupported_operation", `Ollama relay jobs do not support ${operation}`);
|
|
5277
|
+
}
|
|
5278
|
+
const chatMessages = job.openai_request.messages;
|
|
5279
|
+
if (!Array.isArray(chatMessages)) {
|
|
5280
|
+
throw new SelfHostedPreStartJobError("validation_failed", "chat.completions requires messages");
|
|
5281
|
+
}
|
|
4693
5282
|
const options = {};
|
|
4694
5283
|
if (job.openai_request.temperature !== undefined)
|
|
4695
5284
|
options.temperature = job.openai_request.temperature;
|
|
@@ -4702,7 +5291,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4702
5291
|
await acknowledgeStarted();
|
|
4703
5292
|
const ollamaResult = await this.jobOllama.chat({
|
|
4704
5293
|
model: job.model || job.openai_request.model,
|
|
4705
|
-
messages:
|
|
5294
|
+
messages: chatMessages,
|
|
4706
5295
|
options,
|
|
4707
5296
|
format: resolveOllamaResponseFormat(job.openai_request.response_format)
|
|
4708
5297
|
});
|
|
@@ -4731,6 +5320,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4731
5320
|
const invocationResult = {
|
|
4732
5321
|
job_id: job.job_id,
|
|
4733
5322
|
request_id: job.request_id,
|
|
5323
|
+
operation,
|
|
4734
5324
|
status: "success",
|
|
4735
5325
|
openai_response: buildOpenAIChatCompletion({
|
|
4736
5326
|
requestId: job.request_id,
|
|
@@ -4752,12 +5342,41 @@ export class SelfHostedNodeRuntime {
|
|
|
4752
5342
|
});
|
|
4753
5343
|
return invocationResult;
|
|
4754
5344
|
}
|
|
4755
|
-
const taskPreview = messagesToPrompt(job.openai_request.messages);
|
|
4756
|
-
if (!taskPreview) {
|
|
4757
|
-
throw new Error("mcoda invocation prompt is empty");
|
|
4758
|
-
}
|
|
4759
5345
|
const agent = await this.resolveMcodaAgentForJob(job);
|
|
4760
5346
|
selectedAgent = agent;
|
|
5347
|
+
this.generativeOperationForAgent(agent, operation);
|
|
5348
|
+
if (operation === "images.generations" || operation === "audio.generations") {
|
|
5349
|
+
const prepared = this.prepareOpenAiGenerativePassthrough(job, agent, operation);
|
|
5350
|
+
await acknowledgeStarted(agent);
|
|
5351
|
+
await recordProgress({
|
|
5352
|
+
type: "agent_selected",
|
|
5353
|
+
operation,
|
|
5354
|
+
job_id: job.job_id,
|
|
5355
|
+
request_id: job.request_id,
|
|
5356
|
+
agent_slug: agent.slug,
|
|
5357
|
+
adapter: agent.adapter,
|
|
5358
|
+
local_model: optionalText(agent.model) || job.openai_request.model
|
|
5359
|
+
});
|
|
5360
|
+
const passthrough = await this.executeOpenAiGenerativePassthrough(prepared, operation);
|
|
5361
|
+
this.finishExecutionTelemetry({ executionClass: "llm", startedAt, ok: true });
|
|
5362
|
+
return {
|
|
5363
|
+
job_id: job.job_id,
|
|
5364
|
+
request_id: job.request_id,
|
|
5365
|
+
operation,
|
|
5366
|
+
status: "success",
|
|
5367
|
+
openai_response: passthrough,
|
|
5368
|
+
...(progressEvents.length ? { progress_events: progressEvents } : {}),
|
|
5369
|
+
timing: { local_latency_ms: Date.now() - startedAt }
|
|
5370
|
+
};
|
|
5371
|
+
}
|
|
5372
|
+
const chatMessages = job.openai_request.messages;
|
|
5373
|
+
if (!Array.isArray(chatMessages)) {
|
|
5374
|
+
throw new SelfHostedPreStartJobError("validation_failed", "chat.completions requires messages");
|
|
5375
|
+
}
|
|
5376
|
+
const taskPreview = messagesToPrompt(chatMessages);
|
|
5377
|
+
if (!taskPreview) {
|
|
5378
|
+
throw new SelfHostedPreStartJobError("validation_failed", "mcoda invocation prompt is empty");
|
|
5379
|
+
}
|
|
4761
5380
|
validateRequiredDocdexContext(job, options.attachedMswarmApiKey);
|
|
4762
5381
|
const attachedMswarmApiKey = attachedMswarmApiKeyForDocdex(job, options.attachedMswarmApiKey);
|
|
4763
5382
|
await acknowledgeStarted(agent);
|
|
@@ -4779,6 +5398,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4779
5398
|
const passthroughResult = {
|
|
4780
5399
|
job_id: job.job_id,
|
|
4781
5400
|
request_id: job.request_id,
|
|
5401
|
+
operation,
|
|
4782
5402
|
status: "success",
|
|
4783
5403
|
openai_response: passthrough,
|
|
4784
5404
|
...(progressEvents.length ? { progress_events: progressEvents } : {}),
|
|
@@ -4793,14 +5413,16 @@ export class SelfHostedNodeRuntime {
|
|
|
4793
5413
|
jobId: job.job_id,
|
|
4794
5414
|
requestId: job.request_id,
|
|
4795
5415
|
model: job.openai_request.model,
|
|
4796
|
-
messages:
|
|
5416
|
+
messages: chatMessages,
|
|
4797
5417
|
agent,
|
|
4798
5418
|
workspace: buildCodaliWorkspace(job),
|
|
4799
5419
|
docdex: buildCodaliDocdex(job),
|
|
4800
5420
|
attachedMswarmApiKey,
|
|
4801
5421
|
policy: buildCodaliPolicy(job),
|
|
4802
5422
|
temperature: job.openai_request.temperature,
|
|
4803
|
-
responseFormat: job.openai_request.response_format
|
|
5423
|
+
responseFormat: isRecord(job.openai_request.response_format)
|
|
5424
|
+
? job.openai_request.response_format
|
|
5425
|
+
: null,
|
|
4804
5426
|
stream: job.openai_request.stream === true,
|
|
4805
5427
|
codaliGateway,
|
|
4806
5428
|
codaliJob,
|
|
@@ -4860,6 +5482,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4860
5482
|
const result = {
|
|
4861
5483
|
job_id: job.job_id,
|
|
4862
5484
|
request_id: job.request_id,
|
|
5485
|
+
operation,
|
|
4863
5486
|
status: "success",
|
|
4864
5487
|
openai_response: buildOpenAIChatCompletion({
|
|
4865
5488
|
requestId: job.request_id,
|
|
@@ -4932,6 +5555,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4932
5555
|
const result = {
|
|
4933
5556
|
job_id: job.job_id,
|
|
4934
5557
|
request_id: job.request_id,
|
|
5558
|
+
operation,
|
|
4935
5559
|
status: "failed",
|
|
4936
5560
|
...(!jobStarted ? { pre_start_failure: true } : {}),
|
|
4937
5561
|
error: {
|
|
@@ -5147,6 +5771,9 @@ export class SelfHostedNodeRuntime {
|
|
|
5147
5771
|
if (!job) {
|
|
5148
5772
|
return { executed: false };
|
|
5149
5773
|
}
|
|
5774
|
+
const chatStreaming = (normalizeSelfHostedGenerativeOperationName(job.operation) ?? "chat.completions") ===
|
|
5775
|
+
"chat.completions" &&
|
|
5776
|
+
job.openai_request.stream === true;
|
|
5150
5777
|
const pendingStreamEvents = [];
|
|
5151
5778
|
let streamEventForwardingFailed = false;
|
|
5152
5779
|
let forwardedStreamEventCount = 0;
|
|
@@ -5193,7 +5820,7 @@ export class SelfHostedNodeRuntime {
|
|
|
5193
5820
|
}
|
|
5194
5821
|
},
|
|
5195
5822
|
onOpenAIChunk: async (chunk) => {
|
|
5196
|
-
if (
|
|
5823
|
+
if (!chatStreaming || streamEventForwardingFailed) {
|
|
5197
5824
|
return;
|
|
5198
5825
|
}
|
|
5199
5826
|
pendingStreamEvents.push(chunk);
|
|
@@ -5212,6 +5839,7 @@ export class SelfHostedNodeRuntime {
|
|
|
5212
5839
|
result = {
|
|
5213
5840
|
job_id: job.job_id,
|
|
5214
5841
|
request_id: job.request_id,
|
|
5842
|
+
operation: result.operation,
|
|
5215
5843
|
status: "failed",
|
|
5216
5844
|
error: { code: SELF_HOSTED_PROTOCOL_MISMATCH_CODE, message: error.message },
|
|
5217
5845
|
...(result.stream_events?.length ? { stream_events: result.stream_events } : {}),
|
|
@@ -5219,7 +5847,7 @@ export class SelfHostedNodeRuntime {
|
|
|
5219
5847
|
timing: result.timing
|
|
5220
5848
|
};
|
|
5221
5849
|
}
|
|
5222
|
-
const postedResult =
|
|
5850
|
+
const postedResult = chatStreaming
|
|
5223
5851
|
? streamEventForwardingFailed
|
|
5224
5852
|
? {
|
|
5225
5853
|
...result,
|